Skip to content

[Debug] 회원탈퇴 외래키 제약으로 인한 오류 & 프로필 이미지 url 반환 코드 수정#133

Merged
SungMinju merged 93 commits intodevelopfrom
debug/withdraw
Feb 12, 2026
Merged

[Debug] 회원탈퇴 외래키 제약으로 인한 오류 & 프로필 이미지 url 반환 코드 수정#133
SungMinju merged 93 commits intodevelopfrom
debug/withdraw

Conversation

@SungMinju
Copy link
Contributor

@SungMinju SungMinju commented Feb 11, 2026

💡 작업 개요

  • 무엇을 구현했는지 간단히 설명해주세요.
  • 회원 탈퇴 시 외래키 제약으로 인한 삭제 실패 수정
  • 잘못된 프로필 이미지 url 반환으로 인한 오류 수정

✅ 작업 내용

  • 기능 개발
  • 버그 수정
  • 리팩토링
  • 주석/포맷 정리
  • 기타 설정

🧪 테스트 내용

  • 테스트 방식이나 결과 요약 (예: Postman, Swagger로 테스트 완료 등)

📝 기타 참고 사항

  • 협업 관련 이슈, 주의 사항 등

Summary by CodeRabbit

  • 새로운 기능

    • 사용자 계정 탈퇴(회원 비활성화) 기능이 추가되었습니다.
    • 프로필 이미지 URL 처리 방식이 개선되어 외부 URL이 반영됩니다.
    • 보안 설정에 CORS 구성과 암호화(BCrypt) 설정이 추가되었습니다.
  • 버그 수정 / 오류 처리

    • 탈퇴한 회원 접근 시 관련 오류 응답이 추가되어 접근이 차단됩니다.

@coderabbitai
Copy link

coderabbitai bot commented Feb 11, 2026

📝 Walkthrough

Walkthrough

User 엔티티에 탈퇴 관련 필드와 메서드가 추가되고, 서비스 레이어에서 탈퇴 검증 및 S3 기반 프로필 이미지 URL을 조회해 UserConverter로 전달하도록 변경되었습니다. SecurityConfig에 CORS 및 PasswordEncoder 빈이 추가/수정되었습니다. (≤50단어)

Changes

Cohort / File(s) Summary
User Entity
src/main/java/com/eatsfine/eatsfine/domain/user/entity/User.java
deletedAt, isDeleted 필드 추가, withdraw()isDeleted() 메서드 추가, 보호된 no-arg 생성자(@NoArgsConstructor(access = AccessLevel.PROTECTED)) 추가.
Service Layer & Converter
src/main/java/com/eatsfine/eatsfine/domain/user/service/userService/UserServiceImpl.java, src/main/java/com/eatsfine/eatsfine/domain/user/converter/UserConverter.java
로그인/현재사용자 조회 시 탈퇴 여부 검사 추가, getMemberInfo()에서 S3로 프로필 URL 조회 후 toUserInfo(user, profileUrl) 호출로 변경, withdraw() 흐름을 soft-delete(엔티티 상태 변경)로 변경.
Error Status
src/main/java/com/eatsfine/eatsfine/domain/user/status/UserErrorStatus.java
WITHDRAWN_USER(HttpStatus.FORBIDDEN, "MEMBER4007", "탈퇴한 회원입니다.") 상수 추가.
Security Config
src/main/java/com/eatsfine/eatsfine/global/config/SecurityConfig.java
corsConfigurationSource()passwordEncoder() 빈 추가, OAuth cookie authorization request repository 빈 제거/정리.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Client
    participant UserService as UserServiceImpl
    participant S3 as S3Service
    participant Repo as UserRepository
    participant Converter as UserConverter

    Client->>UserService: getMemberInfo(request)
    UserService->>Repo: findById(userId)
    Repo-->>UserService: User entity
    UserService->>UserService: check isDeleted -> if true throw WITHDRAWN_USER
    UserService->>S3: toUrl(user.getProfileImage())
    S3-->>UserService: profileImageUrl
    UserService->>Converter: toUserInfo(user, profileImageUrl)
    Converter-->>UserService: UserInfoDto
    UserService-->>Client: UserInfoDto
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested labels

fix, refactor

Suggested reviewers

  • sonjunkyu
  • CokaNuri
  • twodo0

Poem

🐰 탈퇴의 발자국 남기며,
deletedAt에 시간을 적고, isDeleted로 숨겨요.
S3에서 사진 불러와서 인사만 건네고,
withdraw() 한 번에 깔끔히 정리해요.
당근처럼 소소한 변화, 마음은 뿌듯하네 🥕

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 11.11% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive PR 설명이 템플릿의 필수 구조(작업 개요, 작업 내용, 테스트 내용, 기타 참고 사항)를 포함하고 있으나 테스트 내용 섹션이 구체적인 테스트 방식이나 결과 없이 비어있습니다. 테스트 내용 섹션에 구체적인 테스트 방식(예: Postman, Swagger)과 테스트 결과를 추가하여 완성해주세요.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed 제목은 PR의 주요 변경사항(회원탈퇴 외래키 제약 오류 수정과 프로필 이미지 URL 반환 코드 수정)을 명확하게 요약하고 있어 변경사항과 완벽하게 관련되어 있습니다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch debug/withdraw

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@SungMinju SungMinju self-assigned this Feb 11, 2026
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
src/main/java/com/eatsfine/eatsfine/domain/user/service/userService/UserServiceImpl.java (2)

195-211: ⚠️ Potential issue | 🟠 Major

S3 이미지 삭제를 트랜잭션 커밋 이후로 이동해야 합니다.

현재 S3 프로필 이미지 삭제(Line 202-206)가 user.withdraw()userRepository.save() 호출 전에 실행됩니다. DB 저장이 실패하면 이미지는 이미 삭제되었지만 사용자는 탈퇴 상태가 아닌 불일치가 발생합니다.

updateMemberInfo 메서드(Line 147-160)에서 이미 사용하고 있는 TransactionSynchronization.afterCommit() 패턴을 동일하게 적용해야 합니다.

🔧 수정 제안
     `@Override`
     `@Transactional`
     public void withdraw(HttpServletRequest request) {
         User user = getCurrentUser(request);
 
-        String profileImage = user.getProfileImage();
-        if (profileImage != null && !profileImage.isBlank()) {
-            try {
-                s3Service.deleteByKey(profileImage);
-            } catch (Exception e) {
-                log.warn("프로필 이미지 삭제 실패. key={}", profileImage, e);
-            }
-        }
-
         user.withdraw();
         userRepository.save(user);
+
+        String profileImage = user.getProfileImage();
+        if (profileImage != null && !profileImage.isBlank()) {
+            TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
+                `@Override`
+                public void afterCommit() {
+                    try {
+                        s3Service.deleteByKey(profileImage);
+                    } catch (Exception e) {
+                        log.warn("프로필 이미지 삭제 실패. key={}", profileImage, e);
+                    }
+                }
+            });
+        }
     }

As per coding guidelines, "DB 일관성, 롤백 정책이 올바른지 검토."


242-243: ⚠️ Potential issue | 🟡 Minor

로그에 사용자 이메일(user.getEmail())이 노출되고 있습니다.

이메일은 개인식별정보(PII)에 해당합니다. 로그에는 userId만 남기고 이메일은 제외하는 것이 바람직합니다.

As per coding guidelines, "비밀번호, 토큰 등 민감한 정보가 로깅되지 않는지."

src/main/java/com/eatsfine/eatsfine/domain/user/converter/UserConverter.java (1)

45-51: ⚠️ Potential issue | 🟡 Minor

toUpdateResponse는 현재 호출되지 않는 미사용 메서드입니다.

검증 결과, toUpdateResponse 메서드는 코드베이스 어디에서도 호출되고 있지 않습니다. 실제 흐름은 UserService.updateMemberInfo에서 String 메시지를 반환하며, UpdateResponseDtotoUpdateResponse 컨버터는 사용되지 않고 있습니다.

다만 코드에는 여전히 user.getProfileImage()를 직접 사용하여 S3 key가 반환될 수 있는 문제가 잠재되어 있습니다. 이 메서드를 향후 사용할 계획이 있다면, toUserInfo와 동일하게 profileImageUrl 파라미터를 받아 처리하도록 수정하거나, 현재 불필요한 메서드라면 제거하는 것을 권장합니다.

🤖 Fix all issues with AI agents
In `@src/main/java/com/eatsfine/eatsfine/domain/user/entity/User.java`:
- Around line 52-56: The isDeleted field is initialized to false but Lombok's
`@Builder` ignores field initializers, causing Builder-created Users (e.g. in
UserConverter.toUser()) to get a null isDeleted and nullable DB writes; add
Lombok's `@Builder.Default` to the isDeleted field (keep the "= false" assignment)
so Builder sets a default false value, and verify uses like isDeleted() remain
null-safe; alternatively ensure a DB column default via columnDefinition if you
prefer DB-level enforcement.
- Around line 99-108: The withdraw() soft-delete only nulls refreshToken and
sets isDeleted/deletedAt but leaves issued accessTokens valid and related Term
entities intact; update withdraw() (and/or add a UserService method called from
withdraw()) to 1) record/emit an immediate token invalidation (e.g., add user
token blacklist entry or increment a tokenVersion field tied to JWTs so issued
accessTokens are rejected), 2) explicitly remove or anonymize related Term data
instead of relying on CascadeType.REMOVE (deleteRelatedTerms() or
termRepository.deleteByUser(this) as part of withdrawal), and 3) enforce the
deletion check earlier by adding isDeleted() validation into
JwtAuthenticationFilter so requests with deleted users are rejected at
authentication time rather than only in getCurrentUser().

In
`@src/main/java/com/eatsfine/eatsfine/domain/user/service/userService/UserServiceImpl.java`:
- Around line 88-94: getMemberInfo in UserServiceImpl is read-only but annotated
with `@Transactional`; change it to use a read-only transaction by updating the
annotation to `@Transactional`(readOnly = true) on the getMemberInfo method so
Hibernate uses MANUAL flush mode for better performance; ensure the method
signature (getMemberInfo), any calls to getCurrentUser, s3Service.toUrl, and
UserConverter.toUserInfo remain unchanged.
- Around line 229-235: The login() method currently calls
userRepository.findByEmail(...) directly and therefore allows withdrawn users to
log back in; update login() to either call getCurrentUser(email) or perform the
same deletion check: after obtaining the User from
userRepository.findByEmail(...), call user.isDeleted() and if true throw new
UserException(UserErrorStatus.WITHDRAWN_USER) (same behavior as getCurrentUser),
ensuring the login flow blocks deleted accounts.

In `@src/main/java/com/eatsfine/eatsfine/domain/user/status/UserErrorStatus.java`:
- Around line 20-21: The enum UserErrorStatus has a duplicate semicolon after
the enum constant WITHDRAWN_USER; remove the redundant semicolon so the enum
constant list ends with a single semicolon (i.e., keep the semicolon that
terminates the enum constant list if needed, but delete the extra one following
WITHDRAWN_USER) in the UserErrorStatus enum to eliminate the obvious typo.

Comment on lines +52 to +56
@Column(name = "deleted_at")
private LocalDateTime deletedAt;

@Column(name = "is_deleted")
private Boolean isDeleted = false;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

isDeleted 필드에 @Builder.Default 누락 — Builder로 생성 시 null이 됩니다.

Lombok @Builder는 필드 초기화 값(= false)을 무시합니다. UserConverter.toUser() 등 Builder로 User를 생성하면 isDeletednull로 설정되어 DB에도 null이 저장됩니다.

isDeleted() 메서드에서 null-safe 처리를 하고 있어 런타임 오류는 없지만, DB 데이터 정합성을 위해 @Builder.Default를 추가하거나 columnDefinition으로 DB 기본값을 설정해야 합니다.

🔧 수정 제안
     `@Column`(name = "is_deleted")
+    `@Builder.Default`
     private Boolean isDeleted = false;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@Column(name = "deleted_at")
private LocalDateTime deletedAt;
@Column(name = "is_deleted")
private Boolean isDeleted = false;
`@Column`(name = "deleted_at")
private LocalDateTime deletedAt;
`@Column`(name = "is_deleted")
`@Builder.Default`
private Boolean isDeleted = false;
🤖 Prompt for AI Agents
In `@src/main/java/com/eatsfine/eatsfine/domain/user/entity/User.java` around
lines 52 - 56, The isDeleted field is initialized to false but Lombok's `@Builder`
ignores field initializers, causing Builder-created Users (e.g. in
UserConverter.toUser()) to get a null isDeleted and nullable DB writes; add
Lombok's `@Builder.Default` to the isDeleted field (keep the "= false" assignment)
so Builder sets a default false value, and verify uses like isDeleted() remain
null-safe; alternatively ensure a DB column default via columnDefinition if you
prefer DB-level enforcement.

Comment on lines +99 to +108
// 회원 탈퇴 메서드 추가
public void withdraw() {
this.isDeleted = true;
this.deletedAt = LocalDateTime.now();
this.refreshToken = null; // refresh token도 null 처리
}

public boolean isDeleted() {
return this.isDeleted != null && this.isDeleted;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find User.java and related entity files
find . -type f -name "User.java" -o -name "*Term*.java" -o -name "*Entity.java" | head -20

Repository: Eatsfine/BE

Length of output: 337


🏁 Script executed:

#!/bin/bash
# Check repository structure
ls -la src/main/java/com/eatsfine/eatsfine/domain/ 2>/dev/null || ls -la . | head -20

Repository: Eatsfine/BE

Length of output: 1033


🏁 Script executed:

#!/bin/bash
# Find all entity files and examine User references
fd "\.java$" src/main/java --type f -x grep -l "@Entity\|@Table" {} \; | head -30

Repository: Eatsfine/BE

Length of output: 1225


🏁 Script executed:

#!/bin/bash
# Read User.java to see the withdraw() and isDeleted() methods and context
cat -n src/main/java/com/eatsfine/eatsfine/domain/user/entity/User.java | head -120

Repository: Eatsfine/BE

Length of output: 3686


🏁 Script executed:

#!/bin/bash
# Read Term.java to check User relationship and cascade config
cat -n src/main/java/com/eatsfine/eatsfine/domain/term/entity/Term.java

Repository: Eatsfine/BE

Length of output: 1166


🏁 Script executed:

#!/bin/bash
# Search for getCurrentUser implementation
rg -n "getCurrentUser" --type java -A 5

Repository: Eatsfine/BE

Length of output: 5362


🏁 Script executed:

#!/bin/bash
# Search for token invalidation or blacklist mechanism
rg -n "refreshToken|accessToken|token.*invalidat|blacklist|logout" --type java -i | head -40

Repository: Eatsfine/BE

Length of output: 6367


🏁 Script executed:

#!/bin/bash
# Search for where isDeleted() method is actually called
rg -n "\.isDeleted\(\)" --type java -B 3 -A 3

Repository: Eatsfine/BE

Length of output: 981


🏁 Script executed:

#!/bin/bash
# Check security configuration and filter/interceptor for user validation
rg -n "class.*Filter|class.*Interceptor|class.*Advice|SecurityContextHolder" --type java | head -20

Repository: Eatsfine/BE

Length of output: 708


🏁 Script executed:

#!/bin/bash
# Search for batch jobs, scheduled tasks, or cleanup logic
rg -n "@Scheduled|@Transactional.*void|batch|cleanup|purge|delete" --type java -i | grep -i "term\|orphan\|delete\|batch" | head -20

Repository: Eatsfine/BE

Length of output: 2488


🏁 Script executed:

#!/bin/bash
# Check if isDeleted filter exists in repository queries
rg -n "isDeleted|deleted_at" --type java src/main/java/com/eatsfine/eatsfine/domain/user/ -B 2 -A 2

Repository: Eatsfine/BE

Length of output: 2606


🏁 Script executed:

#!/bin/bash
# Check JwtAuthenticationFilter to see if isDeleted is checked there
cat -n src/main/java/com/eatsfine/eatsfine/global/config/jwt/JwtAuthenticationFilter.java | head -80

Repository: Eatsfine/BE

Length of output: 3602


🏁 Script executed:

#!/bin/bash
# Search for all entities that reference User to understand cascade impact scope
rg -n "User.*user|@ManyToOne.*User|@OneToOne.*User" --type java src/main/java/com/eatsfine/eatsfine/domain/ -B 1 -A 1 | head -60

Repository: Eatsfine/BE

Length of output: 6335


🏁 Script executed:

#!/bin/bash
# Check if there's a withdraw service method and how it's implemented
rg -n "withdraw" --type java -B 5 -A 10

Repository: Eatsfine/BE

Length of output: 9453


🏁 Script executed:

#!/bin/bash
# Check if there are any batch jobs or scheduled deletion tasks
fd ".*Batch.*\.java|.*Scheduler.*\.java|.*Job.*\.java" --type f | xargs cat 2>/dev/null | head -50

Repository: Eatsfine/BE

Length of output: 1548


🏁 Script executed:

#!/bin/bash
# Verify if there are any other User references or repositories that do isDeleted filtering
rg -n "findByEmail|findById" --type java src/main/java/com/eatsfine/eatsfine/domain/user/repository/ -A 3

Repository: Eatsfine/BE

Length of output: 559


🏁 Script executed:

#!/bin/bash
# Check if there's any aspect/annotation that globally checks isDeleted
rg -n "@Aspect|@Around|isDeleted" --type java src/main/java/com/eatsfine/eatsfine/global/ -B 2 -A 5

Repository: Eatsfine/BE

Length of output: 37


withdraw()isDeleted() — 소프트 딜리트 구현에 몇 가지 미흡한 점이 있습니다.

소프트 딜리트로 FK 제약을 해결한 점은 좋으나, 다음 사항들을 개선해야 합니다:

  1. 발급된 accessToken은 만료 시까지 유효합니다. refreshToken을 null 처리하면 토큰 재발급을 차단하지만, 탈퇴 전에 발급된 JWT accessToken은 만료 전까지 여전히 사용 가능합니다. 토큰 블랙리스트나 즉시 무효화 메커니즘이 필요할 수 있습니다.

  2. getCurrentUser()에서 isDeleted() 검사가 있으나, JwtAuthenticationFilter는 검사하지 않습니다. 현재 서비스 계층에서만 삭제 여부를 확인하므로, 필터 단계에서 추가 검증을 고려하세요.

  3. Term(약관 동의) 데이터가 영구 잔존합니다. withdraw() 메서드는 필드를 업데이트할 뿐 엔티티를 삭제하지 않으므로, User의 CascadeType.REMOVE(Line 110)가 작동하지 않습니다. GDPR/개인정보 보호 관점에서 탈퇴 시 관련 데이터(Term 등)를 명시적으로 삭제하는 로직이 필요합니다.

🤖 Prompt for AI Agents
In `@src/main/java/com/eatsfine/eatsfine/domain/user/entity/User.java` around
lines 99 - 108, The withdraw() soft-delete only nulls refreshToken and sets
isDeleted/deletedAt but leaves issued accessTokens valid and related Term
entities intact; update withdraw() (and/or add a UserService method called from
withdraw()) to 1) record/emit an immediate token invalidation (e.g., add user
token blacklist entry or increment a tokenVersion field tied to JWTs so issued
accessTokens are rejected), 2) explicitly remove or anonymize related Term data
instead of relying on CascadeType.REMOVE (deleteRelatedTerms() or
termRepository.deleteByUser(this) as part of withdrawal), and 3) enforce the
deletion check earlier by adding isDeleted() validation into
JwtAuthenticationFilter so requests with deleted users are rejected at
authentication time rather than only in getCurrentUser().

Comment on lines 88 to 94
@Override
@Transactional
public UserResponseDto.UserInfoDto getMemberInfo(HttpServletRequest request) {
User user = getCurrentUser(request);
return UserConverter.toUserInfo(user);
String profileUrl = s3Service.toUrl(user.getProfileImage());
return UserConverter.toUserInfo(user, profileUrl);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

getMemberInfo는 읽기 전용 — @Transactional(readOnly = true) 사용 권장.

이 메서드는 데이터를 조회만 하므로 readOnly = true를 설정하면 Hibernate flush 모드를 MANUAL로 전환하여 성능상 이점이 있습니다.

♻️ 수정 제안
     `@Override`
-    `@Transactional`
+    `@Transactional`(readOnly = true)
     public UserResponseDto.UserInfoDto getMemberInfo(HttpServletRequest request) {

As per coding guidelines, "읽기 전용 트랜잭션(readOnly = true)을 적절히 사용했는지."

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@Override
@Transactional
public UserResponseDto.UserInfoDto getMemberInfo(HttpServletRequest request) {
User user = getCurrentUser(request);
return UserConverter.toUserInfo(user);
String profileUrl = s3Service.toUrl(user.getProfileImage());
return UserConverter.toUserInfo(user, profileUrl);
}
`@Override`
`@Transactional`(readOnly = true)
public UserResponseDto.UserInfoDto getMemberInfo(HttpServletRequest request) {
User user = getCurrentUser(request);
String profileUrl = s3Service.toUrl(user.getProfileImage());
return UserConverter.toUserInfo(user, profileUrl);
}
🤖 Prompt for AI Agents
In
`@src/main/java/com/eatsfine/eatsfine/domain/user/service/userService/UserServiceImpl.java`
around lines 88 - 94, getMemberInfo in UserServiceImpl is read-only but
annotated with `@Transactional`; change it to use a read-only transaction by
updating the annotation to `@Transactional`(readOnly = true) on the getMemberInfo
method so Hibernate uses MANUAL flush mode for better performance; ensure the
method signature (getMemberInfo), any calls to getCurrentUser, s3Service.toUrl,
and UserConverter.toUserInfo remain unchanged.

@SungMinju SungMinju added the bug Something isn't working label Feb 11, 2026
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
src/main/java/com/eatsfine/eatsfine/domain/user/service/userService/UserServiceImpl.java (1)

199-215: ⚠️ Potential issue | 🟠 Major

withdraw() 메서드에서 S3 이미지 삭제 순서 및 refresh token 무효화 누락 문제.

두 가지 문제가 있습니다:

  1. S3 삭제가 트랜잭션 커밋 전에 실행됩니다. user.withdraw() 또는 userRepository.save() 실패 시 프로필 이미지는 이미 S3에서 삭제된 상태가 되어 데이터 불일치가 발생합니다. updateMemberInfo에서와 동일하게 TransactionSynchronization.afterCommit()을 사용하세요.

  2. Refresh token이 무효화되지 않습니다. logout()과 달리 withdraw() 시에는 user.updateRefreshToken(null)이 호출되지 않습니다. getCurrentUserisDeleted() 체크로 일부 보호되지만, refresh token으로 새 access token을 발급받는 별도의 경로가 있다면 보안 갭이 생길 수 있습니다.

🔒 수정 제안
 `@Override`
 `@Transactional`
 public void withdraw(HttpServletRequest request) {
     User user = getCurrentUser(request);
 
-    String profileImage = user.getProfileImage();
-    if (profileImage != null && !profileImage.isBlank()) {
-        try {
-            s3Service.deleteByKey(profileImage);
-        } catch (Exception e) {
-            log.warn("프로필 이미지 삭제 실패. key={}", profileImage, e);
-        }
-    }
-
+    user.updateRefreshToken(null);
     user.withdraw();
     userRepository.save(user);
+
+    String profileImage = user.getProfileImage();
+    if (profileImage != null && !profileImage.isBlank()) {
+        TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
+            `@Override`
+            public void afterCommit() {
+                try {
+                    s3Service.deleteByKey(profileImage);
+                } catch (Exception e) {
+                    log.warn("프로필 이미지 삭제 실패. key={}", profileImage, e);
+                }
+            }
+        });
+    }
 }

As per coding guidelines, "DB 일관성, 롤백 정책이 올바른지 검토."

src/main/java/com/eatsfine/eatsfine/global/config/SecurityConfig.java (2)

108-113: ⚠️ Potential issue | 🟠 Major

프로덕션 환경에 localhost origin이 포함되어 있습니다.

http://localhost:3000, http://localhost:5173이 프로덕션 CORS 허용 목록에 하드코딩되어 있으면, 공격자가 로컬에서 credentialed 요청을 보낼 수 있습니다. Spring Profile(@Profile 또는 @Value)을 활용하여 환경별로 분리하는 것을 권장합니다.

♻️ 환경별 분리 예시
-                config.setAllowedOriginPatterns(List.of(
-                                "https://www.eatsfine.co.kr",
-                                "https://eatsfine.co.kr",
-                                "http://localhost:3000",
-                                "http://localhost:5173"));
+                config.setAllowedOriginPatterns(allowedOrigins);

allowedOriginsapplication.yml의 프로파일별 설정(app.cors.allowed-origins)에서 @Value로 주입받도록 변경하세요.

As per coding guidelines, src/**: "매직 넘버, 하드코딩된 값이 존재하는지 점검" 및 "비밀번호, 토큰 등 민감한 정보가 로깅되지 않는지" (보안 관련).


126-129: ⚠️ Potential issue | 🟡 Minor

PasswordEncoder 빈 사용 현황 확인됨.

PasswordEncoder는 실제로 UserServiceImpl에서 주입되어 다음 기능들에 사용됩니다:

  • 회원가입 시 비밀번호 인코딩
  • 로그인 시 비밀번호 검증
  • 비밀번호 변경 시 현재/새 비밀번호 검증 및 인코딩

다만 이 PR의 명시된 목적(회원탈퇴, 프로필 이미지 URL 수정)과는 직접 연관이 없습니다. 이 빈이 이번 PR에서 새로 추가되는 것이라면, 추가 이유를 명확히 하거나 기존 인증 관련 기능 정리의 일환인지 확인이 필요합니다.

@SungMinju SungMinju merged commit 81940b9 into develop Feb 12, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants