Skip to content

feat: 회원 탈퇴 API#127

Merged
chaechaepower merged 1 commit intomainfrom
feat/#126-user-delete
Apr 25, 2026
Merged

feat: 회원 탈퇴 API#127
chaechaepower merged 1 commit intomainfrom
feat/#126-user-delete

Conversation

@chaechaepower
Copy link
Copy Markdown
Member

@chaechaepower chaechaepower commented Apr 25, 2026

🧷 이슈

🔨 작업 내용

  • soft delete로 회원 탈퇴 기능 구현

👀 리뷰 요구사항

Summary by CodeRabbit

릴리스 노트

  • 새로운 기능
    • 사용자가 자신의 계정을 탈퇴할 수 있는 새로운 기능이 추가되었습니다.
    • 계정 탈퇴 시 데이터는 소프트 삭제(soft delete) 방식으로 처리되어 기록이 안전하게 보존됩니다.
    • 새로운 API 엔드포인트(DELETE /api/v1/users/me)가 제공됩니다.

@chaechaepower chaechaepower self-assigned this Apr 25, 2026
@chaechaepower chaechaepower added the ✨ Feature 기능 개발 label Apr 25, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 25, 2026

Warning

Rate limit exceeded

@chaechaepower has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 16 minutes and 18 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 16 minutes and 18 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 20a9f3c5-e8ab-4add-857e-457aa3b7302e

📥 Commits

Reviewing files that changed from the base of the PR and between 23ecdf8 and 6f5abd5.

📒 Files selected for processing (4)
  • src/main/java/kr/co/webee/application/user/service/UserService.java
  • src/main/java/kr/co/webee/domain/user/entity/User.java
  • src/main/java/kr/co/webee/presentation/user/api/UserApi.java
  • src/main/java/kr/co/webee/presentation/user/controller/UserController.java

Walkthrough

사용자 계정 삭제 기능을 구현합니다. Soft delete 패턴을 적용하여 User 엔티티에 deletedAt 타임스탬프를 추가하고, 서비스와 API 레이어에 withdraw 메서드를 구현했습니다.

Changes

Cohort / File(s) Summary
도메인 모델
src/main/java/kr/co/webee/domain/user/entity/User.java
Soft delete 패턴 적용: @SQLDelete@SQLRestriction 애노테이션을 통해 DELETE 작업을 UPDATE로 변환하고, 조회 시 deleted_at IS NULL 조건 추가. deletedAt 필드 신규 추가.
비즈니스 로직
src/main/java/kr/co/webee/application/user/service/UserService.java
withdraw(Long userId) 메서드 추가. 사용자 조회 후 EntityNotFoundException 처리 및 리포지토리를 통한 삭제 수행.
API 레이어
src/main/java/kr/co/webee/presentation/user/api/UserApi.java, src/main/java/kr/co/webee/presentation/user/controller/UserController.java
DELETE /api/v1/users/me 엔드포인트 정의 및 구현. Swagger 문서화(200, 401, 404 응답 코드) 포함. UserController에서 UserService 위임.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Poem

🐰 사용자 계정을 부드럽게 지우고,
Soft delete로 데이터를 보존하니,
DELETE가 UPDATE로 변신하는 마법!
삭제된 시간을 기록하며,
안녕히 가세요, 회원님! 🎉

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 제목은 '회원 탈퇴 API'로 변경 사항의 주요 내용인 사용자 탈퇴 기능 구현을 명확하게 설명하고 있습니다.
Linked Issues check ✅ Passed 풀 리퀘스트는 연결된 이슈 #126의 목표인 계정 삭제 API 구현을 완벽하게 충족합니다. 사용자 탈퇴 엔드포인트, 서비스 레이어, 소프트 삭제 구현이 모두 포함되어 있습니다.
Out of Scope Changes check ✅ Passed 모든 변경 사항은 회원 탈퇴 API 구현이라는 범위 내에 있습니다. 엔드포인트, 서비스, 도메인 엔티티 수정이 모두 이 기능 구현과 직접 관련됩니다.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/#126-user-delete

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.

Copy link
Copy Markdown

@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: 3

Caution

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

⚠️ Outside diff range comments (1)
src/main/java/kr/co/webee/domain/user/entity/User.java (1)

21-43: ⚠️ Potential issue | 🟠 Major

개인정보 보존 및 연관 엔티티 처리 정책을 정의해 주세요.

현재 soft delete는 deleted_at만 기록하고 username, name, phoneNumber, password 등 개인정보를 그대로 보존합니다. 국내 개인정보보호법(PIPA)/GDPR 관점에서 탈퇴 회원의 개인정보 즉시 파기 또는 익명화가 일반적으로 요구됩니다. 또한 Post, Business, PostComment, ProductReviewUser를 참조하는 엔티티는 cascade 설정이 없어, soft delete 후 다음 두 가지 중 하나의 문제를 일으킵니다.

  1. @SQLRestriction 때문에 연관 User가 조회 결과에서 사라져, 해당 글/리뷰 조회 시 EntityNotFoundException 또는 N+1 시 NPE 발생.
  2. 자원에 대한 책임 주체(작성자)가 사라진 “고아” 레코드가 남아 비즈니스 로직이 모호해짐.

탈퇴 흐름에서 다음 정책을 명문화/구현해 주세요.

  • 개인정보 즉시 익명화(예: username = "withdrawn_" + id, phoneNumber/name = null, password 무효화)
  • 연관 엔티티에 대한 정책 결정: cascade soft delete / 작성자 표시 변경(“탈퇴한 사용자”) / 보존 기간 후 일괄 파기 등
  • 탈퇴 후 해당 사용자 토큰/세션 무효화(refresh 토큰 폐기 등)
🧹 Nitpick comments (3)
src/main/java/kr/co/webee/domain/user/entity/User.java (1)

43-43: deletedAt 필드에 @Column 명시 권장.

@SQLDelete SQL은 deleted_at 컬럼명을 가정하고 있는데, 매핑이 네이밍 전략(SpringPhysicalNamingStrategy 등)에 암묵적으로 의존합니다. 전략이 바뀌거나 다른 환경에서 다른 네이밍 전략이 활성화되면 SQL과 실제 컬럼명이 어긋나 soft delete가 실패할 수 있습니다.

♻️ 제안 변경
-    private LocalDateTime deletedAt;
+    `@Column`(name = "deleted_at")
+    private LocalDateTime deletedAt;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/kr/co/webee/domain/user/entity/User.java` at line 43, The User
entity's deletedAt field relies on implicit naming and `@SQLDelete` assumes the
column is named deleted_at; add an explicit `@Column` mapping on the deletedAt
field (e.g., `@Column`(name = "deleted_at")) in the User class to ensure the field
maps to the expected DB column regardless of naming strategy so soft-delete SQL
in `@SQLDelete` will target the correct column.
src/main/java/kr/co/webee/application/user/service/UserService.java (1)

45-45: 예외 처리는 정상 작동 중이나, 코드베이스의 예외 패턴 일관성 검토 권장.

현재 EntityNotFoundException은 글로벌 예외 핸들러에서 ErrorType.ENTITY_NOT_FOUND로 매핑되어 HTTP 404로 올바르게 응답하고 있습니다. 다만 코드베이스에서 예외 처리 패턴이 혼재되어 있습니다:

  • UserService, ProductService 등: EntityNotFoundException 사용
  • PostLikeService, PostCommentService 등: BusinessException(ErrorType.POST_NOT_FOUND) 사용

일관성을 위해 두 패턴 중 하나로 통일하면 좋습니다. 참고로 USER_NOT_FOUNDErrorType 열거형에 정의되지 않았으므로, ENTITY_NOT_FOUND 사용을 유지하거나 새로운 사용자 관련 에러 타입을 추가하는 방식으로 진행할 수 있습니다. uploadProfileImage도 동일한 패턴을 사용 중이므로 함께 정리하면 좋습니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/kr/co/webee/application/user/service/UserService.java` at line
45, The codebase mixes EntityNotFoundException and
BusinessException(ErrorType.*) patterns; pick one and make services consistent:
either convert services using BusinessException (e.g., PostLikeService,
PostCommentService) to throw EntityNotFoundException like
UserService/ProductService and uploadProfileImage, or add a new
ErrorType.USER_NOT_FOUND and refactor places using EntityNotFoundException to
throw BusinessException(ErrorType.USER_NOT_FOUND) (update global handler if
needed). Locate usages by searching for class/method names UserService,
ProductService, PostLikeService, PostCommentService, uploadProfileImage and
update all throw sites and tests to use the chosen pattern consistently,
ensuring ErrorType enum and global exception mapper are updated accordingly.
src/main/java/kr/co/webee/presentation/user/api/UserApi.java (1)

45-55: 응답 타입을 공통 래퍼로 통일하는 것을 권장합니다.

프로젝트에 ApiResponse<T> 공통 응답 래퍼가 있으므로, withdraw 메서드를 통일해야 합니다. 현재 String을 반환하고 컨트롤러에서 "OK"를 직접 반환하는 방식은 ApiResponseHandler에서 특수 처리(line 49-60)되고 있지만, uploadProfileImageUserProfileImageUploadResponse DTO를 반환하는 등 동일 컨트롤러 내에서 응답 모델이 비대칭적입니다. 다음 중 하나로 통일하세요.

  • 본문이 의미 있는 데이터가 없다면 void로 선언하고 HTTP 204 No Content 반환(@ResponseStatus(HttpStatus.NO_CONTENT)).
  • ApiResponse<Void> 형태로 통일 (기존 래퍼 활용).

또한 @SQLRestriction("deleted_at IS NULL")이 적용되어 있어 이미 탈퇴한 사용자가 재호출 시 EntityNotFoundException으로 404가 발생합니다. 클라이언트가 멱등성을 처리할 수 있도록 API 문서에 이 시나리오를 명시하세요.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/kr/co/webee/presentation/user/api/UserApi.java` around lines 45
- 55, The withdraw endpoint currently returns a raw String ("OK"), which breaks
the project's ApiResponse<T> convention and causes asymmetric responses in
UserApi (see withdraw and uploadProfileImage); change withdraw to use the common
wrapper or no-content semantics: either return ApiResponse<Void> or declare void
and add `@ResponseStatus`(HttpStatus.NO_CONTENT) so all controllers are consistent
with ApiResponseHandler behavior, and update the method signature and any
callers accordingly; also update the API docs for withdraw to state that
requests for already-soft-deleted users (SQLRestriction on deleted_at) result in
404 EntityNotFoundException so clients can handle idempotency.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/main/java/kr/co/webee/application/user/service/UserService.java`:
- Around line 42-48: The withdraw method in UserService currently only calls
userRepository.delete (which relies on `@SQLDelete` soft-delete) and misses
required side-effects; update UserService.withdraw to (1) invalidate all auth
tokens for the user (call the auth/token service, e.g.,
tokenService.invalidateTokens(userId) or equivalent), (2) remove or archive
profile images via the S3 helper (e.g.,
s3Service.deleteProfileImage(user.getProfileImageKey()) or document preservation
policy), (3) trigger anonymization or mark PII for later processing (either call
a new anonymizeUser(user) helper on User.java or publish a domain event), and
(4) publish a domain event for downstream handlers
(eventPublisher.publishEvent(new UserWithdrawnEvent(userId))). Also decide and
implement idempotency: handle repeated withdraw calls by checking soft-deleted
state (consider user.isDeleted or `@SQLRestriction` behavior) and return success
if already withdrawn instead of throwing EntityNotFoundException. Ensure all
calls occur inside the `@Transactional` withdraw method and log failures for
compensating actions.

In `@src/main/java/kr/co/webee/domain/user/entity/User.java`:
- Line 21: The User entity currently relies on Hibernate's default table name
while `@SQLDelete` uses a hardcoded "user" table; add an explicit `@Table`(name =
"user") annotation to the User class to make the table mapping explicit and keep
`@SQLDelete`(sql = "UPDATE user SET deleted_at = CURRENT_TIMESTAMP WHERE id = ?")
correct and maintainable (locate the User class and annotate it with `@Table`(name
= "user")).
- Line 22: The `@SQLRestriction`("deleted_at IS NULL") on User causes LAZY
`@ManyToOne` fetchers to throw EntityNotFoundException and makes
existsByUsername()/existsByPhoneNumber() ignore soft-deleted rows leading to DB
unique constraint errors; fix by removing or avoiding reliance on
`@SQLRestriction` and instead anonymize identifying fields on delete (implement in
User.delete()/UserService.deleteUser() to set username = username + "_deleted_"
+ id and phoneNumber = phoneNumber + "_deleted_" + id, and persist), and/or if
immediate fix needed change problematic relations (e.g., Post, PostComment,
PostLike, Business, UserCrop, ProductReview, InterestMarket,
InterestNewsKeyword, BeeDiagnosis, BeeRecommendation) from `@ManyToOne`(fetch =
FetchType.LAZY) to fetch = FetchType.EAGER to avoid lazy load failures; ensure
AuthService.signup() and repository methods
existsByUsername()/existsByPhoneNumber() will then behave consistently with DB
uniqueness.

---

Nitpick comments:
In `@src/main/java/kr/co/webee/application/user/service/UserService.java`:
- Line 45: The codebase mixes EntityNotFoundException and
BusinessException(ErrorType.*) patterns; pick one and make services consistent:
either convert services using BusinessException (e.g., PostLikeService,
PostCommentService) to throw EntityNotFoundException like
UserService/ProductService and uploadProfileImage, or add a new
ErrorType.USER_NOT_FOUND and refactor places using EntityNotFoundException to
throw BusinessException(ErrorType.USER_NOT_FOUND) (update global handler if
needed). Locate usages by searching for class/method names UserService,
ProductService, PostLikeService, PostCommentService, uploadProfileImage and
update all throw sites and tests to use the chosen pattern consistently,
ensuring ErrorType enum and global exception mapper are updated accordingly.

In `@src/main/java/kr/co/webee/domain/user/entity/User.java`:
- Line 43: The User entity's deletedAt field relies on implicit naming and
`@SQLDelete` assumes the column is named deleted_at; add an explicit `@Column`
mapping on the deletedAt field (e.g., `@Column`(name = "deleted_at")) in the User
class to ensure the field maps to the expected DB column regardless of naming
strategy so soft-delete SQL in `@SQLDelete` will target the correct column.

In `@src/main/java/kr/co/webee/presentation/user/api/UserApi.java`:
- Around line 45-55: The withdraw endpoint currently returns a raw String
("OK"), which breaks the project's ApiResponse<T> convention and causes
asymmetric responses in UserApi (see withdraw and uploadProfileImage); change
withdraw to use the common wrapper or no-content semantics: either return
ApiResponse<Void> or declare void and add `@ResponseStatus`(HttpStatus.NO_CONTENT)
so all controllers are consistent with ApiResponseHandler behavior, and update
the method signature and any callers accordingly; also update the API docs for
withdraw to state that requests for already-soft-deleted users (SQLRestriction
on deleted_at) result in 404 EntityNotFoundException so clients can handle
idempotency.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 39c029c4-2bf5-401a-bb38-40485200f7ab

📥 Commits

Reviewing files that changed from the base of the PR and between cd979f2 and 23ecdf8.

📒 Files selected for processing (4)
  • src/main/java/kr/co/webee/application/user/service/UserService.java
  • src/main/java/kr/co/webee/domain/user/entity/User.java
  • src/main/java/kr/co/webee/presentation/user/api/UserApi.java
  • src/main/java/kr/co/webee/presentation/user/controller/UserController.java

Comment on lines +42 to +48
@Transactional
public void withdraw(Long userId) {
User user = userRepository.findById(userId)
.orElseThrow(() -> new EntityNotFoundException("User not found: " + userId));

userRepository.delete(user);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

탈퇴 시 부수 작업 누락 점검.

withdraw는 단순히 userRepository.delete(user) 만 호출하여 @SQLDelete로 soft delete만 수행합니다. 다음 항목들은 탈퇴 API에서 통상적으로 함께 처리되어야 합니다.

  • 인증/인가 토큰 무효화: 탈퇴 후에도 발급된 JWT/Refresh 토큰이 만료 시점까지 유효하면, 탈퇴 회원이 보호 리소스에 계속 접근 가능. 이때 @SQLRestriction으로 findByUsername이 비어 다른 인증 흐름이 깨질 수도 있음.
  • S3 프로필 이미지 자원 정리(또는 보존 정책 정의)
  • 개인정보 익명화(코멘트는 User.java 측에 별도 작성)
  • 도메인 이벤트 발행으로 후속 처리(연관 엔티티 anonymize, 통계 차감 등) 트리거

또한 @SQLRestriction 적용 후에는 동일 사용자에 대한 두 번째 withdraw 호출이 findById 단계에서 EntityNotFoundException을 던지는데, 이 경로가 의도된 것인지(혹은 멱등하게 처리할지) 정의해 주세요.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/kr/co/webee/application/user/service/UserService.java` around
lines 42 - 48, The withdraw method in UserService currently only calls
userRepository.delete (which relies on `@SQLDelete` soft-delete) and misses
required side-effects; update UserService.withdraw to (1) invalidate all auth
tokens for the user (call the auth/token service, e.g.,
tokenService.invalidateTokens(userId) or equivalent), (2) remove or archive
profile images via the S3 helper (e.g.,
s3Service.deleteProfileImage(user.getProfileImageKey()) or document preservation
policy), (3) trigger anonymization or mark PII for later processing (either call
a new anonymizeUser(user) helper on User.java or publish a domain event), and
(4) publish a domain event for downstream handlers
(eventPublisher.publishEvent(new UserWithdrawnEvent(userId))). Also decide and
implement idempotency: handle repeated withdraw calls by checking soft-deleted
state (consider user.isDeleted or `@SQLRestriction` behavior) and return success
if already withdrawn instead of throwing EntityNotFoundException. Ensure all
calls occur inside the `@Transactional` withdraw method and log failures for
compensating actions.

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@SQLDelete(sql = "UPDATE user SET deleted_at = CURRENT_TIMESTAMP WHERE id = ?")
Copy link
Copy Markdown

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
# DB 엔진 확인
fd -e yml -e yaml -e properties . src/main/resources | xargs rg -n -i 'datasource|jdbc|dialect|hibernate'
# User 테이블 명시 여부 확인
rg -nP -C2 '@Table\s*\(' src/main/java/kr/co/webee/domain/user
# 마이그레이션/DDL에서 user 테이블명 확인
fd -e sql . | xargs rg -n -i 'create\s+table.*user'

Repository: BeeKeeprs/was

Length of output: 238


🏁 Script executed:

# User 엔티티 전체 확인
cat -n src/main/java/kr/co/webee/domain/user/entity/User.java

# 데이터베이스 마이그레이션 파일 찾기
fd -e sql . 

# Flyway 마이그레이션 폴더 확인
find . -path "*/db/migration*" -o -path "*/flyway*" 2>/dev/null | head -20

Repository: BeeKeeprs/was

Length of output: 2754


🏁 Script executed:

# 테이블 생성 DDL 찾기 (user 관련)
fd -e sql . -x grep -l -i "create.*table" {}

# application.yml/properties에서 JPA 설정 확인
cat -n src/main/resources/application.yml

Repository: BeeKeeprs/was

Length of output: 4029


🏁 Script executed:

# 마이그레이션 파일 찾기
find . -path "*/db/migration*" -o -path "*/flyway*" -o -name "*.sql" 2>/dev/null

# SQL 파일에서 user 테이블 정의 확인
fd -e sql . -x grep -i "create.*table.*user" {}

Repository: BeeKeeprs/was

Length of output: 39


🏁 Script executed:

# Flyway 기본 경로 확인
ls -la src/main/resources/db/ 2>/dev/null || echo "No db migration folder"

# 모든 SQL 파일 나열
find src -name "*.sql" -type f 2>/dev/null

Repository: BeeKeeprs/was

Length of output: 80


명시적 테이블명 지정으로 @SQLDelete 유지보수성 개선 권장

User 엔티티에 @Table 어노테이션이 없어 Hibernate가 기본값으로 테이블명 user를 사용하고 있으며, 현재 @SQLDelete의 하드코딩된 SQL이 이와 일치하고 있습니다. 또한 현재 데이터베이스가 MySQL이므로 user가 예약어로 취급되지 않습니다.

다만 코드의 견고성과 유지보수성을 위해, 엔티티에 명시적으로 @Table(name = "user")를 추가하여 테이블명 매핑을 명확히 하는 것을 권장합니다. 이렇게 하면:

  • 향후 누군가 테이블명을 변경할 때 두 곳을 동시에 수정하도록 강제
  • 다른 데이터베이스로 마이그레이션할 경우 대비
  • 코드의 의도가 명확해짐

현재는 기능상 문제가 없지만, 향후 스키마 변경 시 soft delete가 조용히 실패할 수 있는 위험을 사전에 방지하는 것이 좋습니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/kr/co/webee/domain/user/entity/User.java` at line 21, The User
entity currently relies on Hibernate's default table name while `@SQLDelete` uses
a hardcoded "user" table; add an explicit `@Table`(name = "user") annotation to
the User class to make the table mapping explicit and keep `@SQLDelete`(sql =
"UPDATE user SET deleted_at = CURRENT_TIMESTAMP WHERE id = ?") correct and
maintainable (locate the User class and annotate it with `@Table`(name = "user")).

@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@SQLDelete(sql = "UPDATE user SET deleted_at = CURRENT_TIMESTAMP WHERE id = ?")
@SQLRestriction("deleted_at IS NULL")
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# username/phoneNumber 유니크 제약 확인
fd -e sql . | xargs rg -ni 'unique.*(username|phone_number)|user.*unique'
rg -nP -C2 '@Column\s*\([^)]*unique' src/main/java/kr/co/webee/domain/user
# User에 대한 ManyToOne 사용처
rg -nP -C3 '@ManyToOne[\s\S]{0,120}User\b'

Repository: BeeKeeprs/was

Length of output: 39


🏁 Script executed:

cat -n src/main/java/kr/co/webee/domain/user/entity/User.java | head -100

Repository: BeeKeeprs/was

Length of output: 2754


🏁 Script executed:

# 유니크 제약 찾기 - 다양한 패턴으로 검색
rg -n '@Column.*unique\s*=' src/main/java/
rg -n 'unique\s*=\s*true' src/main/java/
# 마이그레이션 파일이나 스키마 정의 찾기
fd -e sql . | head -20
fd -e yml . | head -20
fd -e yaml . | head -20

Repository: BeeKeeprs/was

Length of output: 132


🏁 Script executed:

# User 엔티티에 대한 `@ManyToOne` 관계 찾기
rg -n '@ManyToOne' src/main/java/ -A 2

Repository: BeeKeeprs/was

Length of output: 4968


🏁 Script executed:

# SQLRestriction 실제 사용 확인
rg -n '@SQLRestriction' src/main/java/

Repository: BeeKeeprs/was

Length of output: 153


🏁 Script executed:

# UserRepository 찾기 및 검사
find src -name "*UserRepository*" -type f

Repository: BeeKeeprs/was

Length of output: 126


🏁 Script executed:

# 마이그레이션 파일 찾기
fd -e sql . src/main/resources/
fd -e sql . src/

Repository: BeeKeeprs/was

Length of output: 39


🏁 Script executed:

# findByUsername, existsByUsername 등의 메서드 검색
rg -n 'findByUsername|existsByUsername|existsByPhoneNumber|findByPhoneNumber' src/main/java/

Repository: BeeKeeprs/was

Length of output: 975


🏁 Script executed:

# 데이터베이스 스키마 정의 확인
rg -n 'CREATE TABLE.*user' src/ -i

Repository: BeeKeeprs/was

Length of output: 39


🏁 Script executed:

# 삭제 시 username/phoneNumber 처리 로직 확인
rg -n 'delete|soft.*delete' src/main/java/kr/co/webee/domain/user/ -i -A 3

Repository: BeeKeeprs/was

Length of output: 1345


🏁 Script executed:

cat src/main/java/kr/co/webee/domain/user/repository/UserRepository.java

Repository: BeeKeeprs/was

Length of output: 522


🏁 Script executed:

# application.yml 에서 데이터베이스 스키마 설정 확인
cat src/main/resources/application.yml | head -50

Repository: BeeKeeprs/was

Length of output: 1239


🏁 Script executed:

# 데이터베이스 제약 조건이 정의된 다른 엔티티 예 찾기
rg -n '@Column.*unique' src/main/java/ -C 1

Repository: BeeKeeprs/was

Length of output: 39


🏁 Script executed:

# AuthService 의 회원가입 로직 확인
cat src/main/java/kr/co/webee/application/auth/service/AuthService.java | head -70

Repository: BeeKeeprs/was

Length of output: 3168


@SQLRestriction의 부수 효과를 재검토하세요.

검증 결과, 다음 우려사항이 확인되었습니다:

  • @manytoone User 관계의 LAZY 로딩 위험: Post, PostComment, PostLike, Business, UserCrop, ProductReview, InterestMarket, InterestNewsKeyword, BeeDiagnosis, BeeRecommendation 등 10개 이상의 엔티티가 @ManyToOne(fetch = FetchType.LAZY) 로 User를 참조합니다. @SQLRestriction("deleted_at IS NULL")으로 인해 삭제된 User를 LAZY 로딩할 때 EntityNotFoundException이 발생할 수 있습니다.

  • 중복 검사와 유니크 제약의 불일치: AuthService의 signup() 메서드는 existsByUsername(), existsByPhoneNumber()를 통해 중복을 검사하며, 이 쿼리들은 @SQLRestriction으로 인해 삭제된 유저를 제외합니다. 따라서 삭제된 유저의 username/phoneNumber는 "사용 가능"한 것으로 인식되지만, DB 수준에서 unique 제약이 있다면 INSERT 시점에 DataIntegrityViolationException이 발생할 수 있습니다.

탈퇴 시 username과 phoneNumber를 익명화(예: username + "_deleted_" + id)하거나, @ManyToOne 관계에서 fetch = FetchType.EAGER로 변경하거나, 삭제 시 이러한 필드들의 관리 전략을 수립할 것을 권장합니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/kr/co/webee/domain/user/entity/User.java` at line 22, The
`@SQLRestriction`("deleted_at IS NULL") on User causes LAZY `@ManyToOne` fetchers to
throw EntityNotFoundException and makes existsByUsername()/existsByPhoneNumber()
ignore soft-deleted rows leading to DB unique constraint errors; fix by removing
or avoiding reliance on `@SQLRestriction` and instead anonymize identifying fields
on delete (implement in User.delete()/UserService.deleteUser() to set username =
username + "_deleted_" + id and phoneNumber = phoneNumber + "_deleted_" + id,
and persist), and/or if immediate fix needed change problematic relations (e.g.,
Post, PostComment, PostLike, Business, UserCrop, ProductReview, InterestMarket,
InterestNewsKeyword, BeeDiagnosis, BeeRecommendation) from `@ManyToOne`(fetch =
FetchType.LAZY) to fetch = FetchType.EAGER to avoid lazy load failures; ensure
AuthService.signup() and repository methods
existsByUsername()/existsByPhoneNumber() will then behave consistently with DB
uniqueness.

@chaechaepower chaechaepower force-pushed the feat/#126-user-delete branch from 23ecdf8 to 6f5abd5 Compare April 25, 2026 05:55
@chaechaepower chaechaepower merged commit 42cdfeb into main Apr 25, 2026
1 of 2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

✨ Feature 기능 개발

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

계정 삭제 API

1 participant