Skip to content

Conversation

@whqtker
Copy link
Member

@whqtker whqtker commented Nov 20, 2025

관련 이슈

작업 내용

FK의 ON DELETE CASCADE 옵션을 사용하여 DB 레벨에서 연관 레코드를 삭제하도록 구현했습니다.
FK를 재설정하면서 컨벤션에 맞지 않았던 FK 제약조건 이름도 같이 수정했습니다.

deleteAllByXXX 메서드로 스케쥴러에서 사용자 관련 데이터를 삭제하도록 변경했습니다.

특이 사항

UserRemovalScheduler 에 의하면 탈퇴 후 30일이 지나면 DB에서 Hard Delete가 이루어집니다.

public void scheduledUserRemoval() {
	LocalDate cutoffDate = LocalDate.now().minusDays(ACCOUNT_RECOVER_DURATION);
	List<SiteUser> usersToRemove = siteUserRepository.findUsersToBeRemoved(cutoffDate);
	siteUserRepository.deleteAll(usersToRemove);
}

이전에 불필요한 연관 관계를 끊고 간접 참조로 리팩터링을 진행한 바 있습니다. 이전에는 JPA의 cascade 옵션으로 연관 객체들 또한 잘 삭제되었으나, 연관 관계를 끊은 현재는 SiteUser 을 삭제하기 전, 연관 레코드를 먼저 삭제해야 합니다.

interestedCountryRepository.deleteAllBySiteUserIdIn(userIdsToRemove);

다만, SiteUser 와 간접적으로 관계를 맺는 객체가 정말 많기에, 위처럼 직접 연관된 레코드를 삭제하는 경우 코드가 길어지게 됩니다. SiteUser 를 참조하는 새로운 엔티티를 생성하는 경우 스케쥴러에서 삭제하는 로직 또한 작성해주어야 하는데, 실수할 여지가 있다고 생각했습니다.

리뷰 요구사항 (선택)

  1. 빠뜨린 부분이 있는지 확인 부탁드립니다 !
  2. 현재는 모든 사용자 삭제가 하나의 트랜잭션으로 묶어 처리되는데(== 한 명 삭제 실패하면 모두 롤백), 별도의 서비스로 분리할지, 유지할지

@whqtker whqtker self-assigned this Nov 20, 2025
@whqtker whqtker added the 버그 Something isn't working label Nov 20, 2025
@coderabbitai
Copy link

coderabbitai bot commented Nov 20, 2025

Walkthrough

1.  저장소 메서드 추가: 여러 Repository 인터페이스에 deleteAllBySiteUserId(...) 및 유사한 파생 삭제 메서드들이 일괄적으로 추가되었습니다.
2.  채팅 관련 확장: ChatParticipant와 ChatReadStatus에 대해 ID 조회 및 컬렉션 기반 삭제 메서드가 추가되었습니다.
3.  차단·신고 연관 삭제: UserBlock과 ReportRepository 등에서 관련 식별자 기반의 대량 삭제 메서드가 도입되었습니다.
4.  성적·점수 도메인 변경: GpaScore, LanguageTestScore 같은 점수 저장소에 siteUserId 기반 삭제 메서드가 추가되었습니다.
5.  대학·뉴스·멘토링 등 확장: News, LikedUnivApplyInfo, Mentor, MentorApplication, Mentoring 등 여러 도메인 저장소에 일괄 삭제 메서드가 추가되었습니다.
6.  스케줄러 재구성: UserRemovalScheduler가 전체 일괄 삭제를 제거하고, 트랜잭션 내에서 사용자별로 연관 데이터를 순차 삭제하는 흐름으로 대체되었습니다.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested reviewers

  • wibaek
  • JAEHEE25
  • lsy1307

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 2.44% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목은 탈퇴 사용자의 물리적 삭제 문제를 해결한다는 주요 변경사항을 명확하게 나타냅니다.
Description check ✅ Passed PR 설명은 관련 이슈, 작업 내용, 특이사항, 리뷰 요구사항을 포함하여 템플릿을 충분히 따릅니다.
Linked Issues check ✅ Passed 코드 변경사항들이 #556 이슈의 목표인 SiteUser Hard Delete 문제 해결과 일치합니다: 여러 저장소에 deleteAllByXXX 메서드 추가 및 UserRemovalScheduler에서 연관 레코드 삭제 로직 구현.
Out of Scope Changes check ✅ Passed 모든 변경사항이 연관 레코드 삭제 메서드 추가 및 스케줄러 수정으로 #556 범위 내에 있습니다.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

📜 Recent review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 07b7851 and 2f5c555.

📒 Files selected for processing (17)
  • src/main/java/com/example/solidconnection/application/repository/ApplicationRepository.java
  • src/main/java/com/example/solidconnection/chat/repository/ChatParticipantRepository.java
  • src/main/java/com/example/solidconnection/chat/repository/ChatReadStatusRepository.java
  • src/main/java/com/example/solidconnection/community/comment/repository/CommentRepository.java
  • src/main/java/com/example/solidconnection/community/post/repository/PostLikeRepository.java
  • src/main/java/com/example/solidconnection/community/post/repository/PostRepository.java
  • src/main/java/com/example/solidconnection/mentor/repository/MentorApplicationRepository.java
  • src/main/java/com/example/solidconnection/mentor/repository/MentorRepository.java
  • src/main/java/com/example/solidconnection/mentor/repository/MentoringRepository.java
  • src/main/java/com/example/solidconnection/news/repository/LikedNewsRepository.java
  • src/main/java/com/example/solidconnection/news/repository/NewsRepository.java
  • src/main/java/com/example/solidconnection/report/repository/ReportRepository.java
  • src/main/java/com/example/solidconnection/scheduler/UserRemovalScheduler.java
  • src/main/java/com/example/solidconnection/score/repository/GpaScoreRepository.java
  • src/main/java/com/example/solidconnection/score/repository/LanguageTestScoreRepository.java
  • src/main/java/com/example/solidconnection/siteuser/repository/UserBlockRepository.java
  • src/main/java/com/example/solidconnection/university/repository/LikedUnivApplyInfoRepository.java
🚧 Files skipped from review as they are similar to previous changes (8)
  • src/main/java/com/example/solidconnection/application/repository/ApplicationRepository.java
  • src/main/java/com/example/solidconnection/siteuser/repository/UserBlockRepository.java
  • src/main/java/com/example/solidconnection/news/repository/LikedNewsRepository.java
  • src/main/java/com/example/solidconnection/community/post/repository/PostRepository.java
  • src/main/java/com/example/solidconnection/report/repository/ReportRepository.java
  • src/main/java/com/example/solidconnection/university/repository/LikedUnivApplyInfoRepository.java
  • src/main/java/com/example/solidconnection/score/repository/GpaScoreRepository.java
  • src/main/java/com/example/solidconnection/community/comment/repository/CommentRepository.java
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-07-28T18:36:56.462Z
Learnt from: nayonsoso
Repo: solid-connection/solid-connect-server PR: 408
File: src/main/java/com/example/solidconnection/chat/repository/ChatMessageRepository.java:21-27
Timestamp: 2025-07-28T18:36:56.462Z
Learning: MySQL + Hibernate 환경에서는 JPQL의 LIMIT 구문이 작동하지만, 이는 Hibernate 확장 문법이므로 다른 JPA 구현체나 데이터베이스에서는 동작하지 않을 수 있어 이식성 문제가 있다.

Applied to files:

  • src/main/java/com/example/solidconnection/chat/repository/ChatParticipantRepository.java
📚 Learning: 2025-07-28T03:14:33.849Z
Learnt from: whqtker
Repo: solid-connection/solid-connect-server PR: 408
File: src/main/java/com/example/solidconnection/chat/service/ChatService.java:46-54
Timestamp: 2025-07-28T03:14:33.849Z
Learning: 페치 조인을 사용한 N+1 문제 해결에 대해 사용자 whqtker는 JPQL 쿼리 접근법을 선호하며, SELECT 쿼리의 경우 영속성 컨텍스트 간 불일치 문제를 고려하지 않아도 된다고 인식하고 있습니다.

Applied to files:

  • src/main/java/com/example/solidconnection/chat/repository/ChatParticipantRepository.java
📚 Learning: 2025-07-29T17:26:08.811Z
Learnt from: whqtker
Repo: solid-connection/solid-connect-server PR: 408
File: src/main/java/com/example/solidconnection/chat/repository/ChatReadStatusRepository.java:11-17
Timestamp: 2025-07-29T17:26:08.811Z
Learning: 사용자 whqtker는 JPA save() 메서드를 사용한 upsert 연산에서 레이스 컨디션 문제를 우려하고 있으며, 특히 채팅 읽음 상태 업데이트와 같이 동시성이 중요한 연산에서는 네이티브 쿼리의 원자적 연산이 더 안전하다고 인식하고 있습니다.

Applied to files:

  • src/main/java/com/example/solidconnection/chat/repository/ChatParticipantRepository.java
🔇 Additional comments (7)
src/main/java/com/example/solidconnection/score/repository/LanguageTestScoreRepository.java (1)

18-18: 사용자 삭제 시 언어 점수 정리 메서드 추가 확인

파생 삭제 메서드가 Spring Data JPA 규칙에 맞게 잘 추가되었습니다.

src/main/java/com/example/solidconnection/chat/repository/ChatReadStatusRepository.java (1)

20-20: 채팅 읽음 상태 대량 삭제 메서드 추가 확인

ChatParticipant 삭제 시 연관된 ChatReadStatus를 정리하는 메서드가 올바르게 추가되었습니다. 과거 리뷰에서 지적된 누락 문제가 해결되었습니다.

src/main/java/com/example/solidconnection/community/post/repository/PostLikeRepository.java (1)

20-20: 게시글 좋아요 삭제 메서드 추가 확인

사용자 관련 PostLike 엔티티를 정리하는 메서드가 올바르게 추가되었습니다.

src/main/java/com/example/solidconnection/scheduler/UserRemovalScheduler.java (1)

74-100: 연관 데이터 삭제 순서 및 완결성 확인

사용자 관련 모든 엔티티를 올바른 순서로 삭제하고 있습니다:

  1. 뉴스 관련 데이터 (LikedNews, News)
  2. 게시글 관련 데이터 (PostLike, Comment, Post)
  3. 멘토링 관련 데이터 (Mentoring, Mentor, MentorApplication)
  4. 채팅 관련 데이터 (ChatReadStatus, ChatParticipant) - 이전 리뷰에서 지적된 ChatReadStatus 삭제 누락이 정상적으로 수정됨
  5. 신고 및 차단 데이터 (Report, UserBlock)
  6. 지원 및 점수 데이터 (Application, GpaScore, LanguageTestScore, LikedUnivApplyInfo)
  7. 관심 지역/국가 데이터 (InterestedCountry, InterestedRegion)

특히 Line 85-87의 ChatParticipant ID 조회 후 ChatReadStatus 삭제 로직이 올바르게 구현되어, 과거 리뷰에서 제기된 고아 레코드 문제가 해결되었습니다.

src/main/java/com/example/solidconnection/chat/repository/ChatParticipantRepository.java (1)

18-19: ID 조회 메서드 추가로 ChatReadStatus 삭제 문제 해결

ChatParticipant의 ID 목록을 조회하는 메서드가 추가되어, UserRemovalScheduler에서 ChatReadStatus를 먼저 삭제할 수 있게 되었습니다. 이는 과거 리뷰에서 지적된 고아 레코드 문제를 해결하는 핵심 메서드입니다.

src/main/java/com/example/solidconnection/mentor/repository/MentoringRepository.java (1)

43-43: 멘토링 내역 삭제 메서드 추가 확인

사용자가 멘티로 참여한 멘토링 내역을 삭제하는 메서드가 올바르게 추가되었습니다. deleteAllByMenteeId를 통해 멘티 역할의 관계를 정리합니다.

src/main/java/com/example/solidconnection/mentor/repository/MentorRepository.java (1)

31-31: 메서드명 수정과 트랜잭션 컨텍스트 모두 정상입니다!

  1. 이전 리뷰에서 지적된 네이밍이 이미 수정되어 있습니다.

    • deleteAllBySiteUserId로 변경되어 다른 리포지토리들과 일관성이 유지되고 있습니다.
  2. 트랜잭션 컨텍스트도 적절히 구성되어 있습니다.

    • UserRemovalSchedulerscheduledUserRemoval() 메서드(62-63줄)에 @Transactional 애노테이션이 있고, 이 메서드 내에서 mentorRepository.deleteAllBySiteUserId()가 호출되므로 트랜잭션 범위가 올바르게 설정되어 있습니다.

현재 코드는 Spring Data JPA 삭제 쿼리 패턴을 적절히 따르고 있습니다.


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.

@whqtker whqtker changed the title refactor: FK에 ON DELETE CASCADE 옵션 추가 refactor: 탈퇴한 사용자가 물리적 삭제가 되지 않았던 문제를 해결한다 Nov 20, 2025
@whqtker whqtker changed the title refactor: 탈퇴한 사용자가 물리적 삭제가 되지 않았던 문제를 해결한다 fix: 탈퇴한 사용자가 물리적 삭제가 되지 않았던 문제를 해결한다 Nov 20, 2025
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: 0

🧹 Nitpick comments (3)
src/main/resources/db/migration/V38__add_delete_cascade_option_on_fk.sql (3)

1-89: 마이그레이션 접근법은 명확하지만, 적용 범위와 롤백 전략 확인 필요합니다.

전반적으로 마이그레이션 구조는 건전합니다. site_user 테이블 삭제 시 관련 레코드를 연쇄적으로 삭제하는 방식으로 하드 딜리트 문제를 해결하는 것이 PR 목표와 일치합니다. 다음 몇 가지를 확인해 주세요:

  1. 외래 키 적용 범위 검증
    현재 마이그레이션이 site_user를 참조하는 모든 FK를 다루고 있는지 확인이 필요합니다. 마이그레이션 후 누락된 FK가 있으면 일부 연관 레코드가 여전히 삭제되지 않을 수 있습니다.

  2. 롤백 전략 부재
    역방향 마이그레이션(undo 스크립트)이 없으면 문제 발생 시 이전 상태로 복구하기 어렵습니다. Flyway는 일반적으로 단방향이지만, 이 구조적 변경에 대해 롤백 전략이 있는지 확인해 주세요.

  3. 프로덕션 배포 시 고려 사항
    DDL 작업은 영향을 받는 테이블을 잠금합니다. 특히 post, comment 같은 대형 테이블의 경우 마이그레이션 시간을 확인하고 적절한 점검 시간에 배포하세요.


1-89: 제약 조건 네이밍 컨벤션이 거의 일관성 있으나, 소수 불규칙성 정리 권장합니다.

대부분의 새 제약 조건 이름이 fk_[테이블]_[칼럼] 패턴을 따르고 있어 좋습니다. 단, 아래 두 가지 예외가 있습니다:

  1. Line 33-34 (fk_app_site_user): 테이블 약자와 칼럼명 생략 패턴으로 다른 네이밍과 톤이 다릅니다.
  2. Line 88-89 (fk_mentor_application_site_user): 다른 제약과 달리 _id 접미사가 없습니다.

장기 유지보수 관점에서 모든 FK 제약을 fk_[테이블]_[칼럼] 형식(예: fk_app_site_user_id, fk_mentor_application_site_user_id)으로 정렬하면 일관성이 높아집니다.


1-89: ON DELETE CASCADE 적용 시 의도하지 않은 연쇄 삭제 리스크를 모니터링하세요.

이 마이그레이션 후 site_user 레코드 삭제 시 위 18개 FK를 통해 관련된 모든 하위 레코드가 자동으로 삭제됩니다. 이는 의도된 동작이지만, 배포 후 몇 가지를 관찰해야 합니다:

  1. 의도하지 않은 데이터 손실 - 비즈니스 로직상 보존해야 할 데이터가 연쇄 삭제로 인해 손실되지 않는지 확인
  2. 애플리케이션 코드 정리 - PR 배경에 언급된 대로 UserRemovalScheduler의 연관 레코드 삭제 코드(siteUserRepository.deleteAll 전 개별 삭제 로직)가 이제 불필요할 가능성이 있으니 정리 검토 권장
  3. 데이터베이스 트리거/프로시저 - 기존에 수동으로 처리하던 삭제 로직이 있다면 중복 처리로 인한 오류를 피하도록 검토
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4da854b and 46df0f3.

📒 Files selected for processing (1)
  • src/main/resources/db/migration/V38__add_delete_cascade_option_on_fk.sql (1 hunks)
🔇 Additional comments (1)
src/main/resources/db/migration/V38__add_delete_cascade_option_on_fk.sql (1)

46-46: 일부 제약 조건에서 이미 같은 이름으로 존재하는 FK를 삭제 후 재생성하고 있습니다.

Line 46, 51, 56, 61, 71, 76, 81, 86에서 기존 제약 조건과 동일한 이름으로 FK를 재생성하고 있습니다. 이는 현재 스키마에서 이 제약들이 이미 사람이 정의한 이름으로 존재한다는 의미입니다.

확인 사항:

  • 이 제약 조건들이 정말로 같은 이름을 유지하는 것이 의도인지, 아니면 버전 관리 과정에서 일부는 이미 수정된 것인지 검증해 주세요.
  • 특히 application 테이블(Line 31)의 경우 이미 fk_app_site_user 이름으로 존재하는 것으로 보아, 이전 마이그레이션에서 수동으로 명명한 제약으로 추정됩니다.

Also applies to: 51-51, 56-56, 61-61, 71-71, 76-76, 81-81, 86-86

Copy link
Contributor

@Hexeong Hexeong left a comment

Choose a reason for hiding this comment

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

수고하십니다~ FK ID 관련하여 질문 있어서 comment 남겨뒀습니다~

@@ -0,0 +1,89 @@
ALTER TABLE interested_country DROP FOREIGN KEY FK26u5am55jefclcd7r5smk8ai7;
ALTER TABLE interested_country
Copy link
Contributor

Choose a reason for hiding this comment

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

FK ID가 로컬에서와 인스턴스에서가 다를 것 같은데, 하드 코딩된 이유가 따로 있을까요??

Copy link
Member Author

Choose a reason for hiding this comment

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

flyway가 stage, prod 환경에서만 동작하고, 확인한 결과 두 환경 모두 FK가 같아서 단순 하드 코딩했습니다

Copy link
Contributor

@sukangpunch sukangpunch left a comment

Choose a reason for hiding this comment

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

잘 작성해주신 것 같습니다!!
근데 고민이 하나 있는데, 엔티티 클래스 에서 jpa 연관관계를 다 끊어 놓은 상황에서 db 에서 casecade 구현하는게 괜찮을까? 라는 생각이 들었습니다.
제가 까먹어서 그런데, 연관관계 매핑을 끊었던 이유를 혹시 알고 계신가요? 들었던것 같은데 기억이 안나네요...

@Gyuhyeok99
Copy link
Contributor

음 이러면 코드와 디비간의 불일치가 발생할 거 같긴한데 문제는 없어보이네요!

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: 14

📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 46df0f3 and d17592f.

📒 Files selected for processing (16)
  • src/main/java/com/example/solidconnection/application/repository/ApplicationRepository.java
  • src/main/java/com/example/solidconnection/chat/repository/ChatParticipantRepository.java
  • src/main/java/com/example/solidconnection/community/comment/repository/CommentRepository.java
  • src/main/java/com/example/solidconnection/community/post/repository/PostLikeRepository.java
  • src/main/java/com/example/solidconnection/community/post/repository/PostRepository.java
  • src/main/java/com/example/solidconnection/mentor/repository/MentorApplicationRepository.java
  • src/main/java/com/example/solidconnection/mentor/repository/MentorRepository.java
  • src/main/java/com/example/solidconnection/mentor/repository/MentoringRepository.java
  • src/main/java/com/example/solidconnection/news/repository/LikedNewsRepository.java
  • src/main/java/com/example/solidconnection/news/repository/NewsRepository.java
  • src/main/java/com/example/solidconnection/report/repository/ReportRepository.java
  • src/main/java/com/example/solidconnection/scheduler/UserRemovalScheduler.java
  • src/main/java/com/example/solidconnection/score/repository/GpaScoreRepository.java
  • src/main/java/com/example/solidconnection/score/repository/LanguageTestScoreRepository.java
  • src/main/java/com/example/solidconnection/siteuser/repository/UserBlockRepository.java
  • src/main/java/com/example/solidconnection/university/repository/LikedUnivApplyInfoRepository.java
🔇 Additional comments (7)
src/main/java/com/example/solidconnection/community/comment/repository/CommentRepository.java (1)

45-46: 메서드 추가 확인 완료

deleteAllBySiteUserId 메서드가 올바르게 추가되었습니다. Spring Data JPA의 메서드 네이밍 규칙을 따르고 있어 Comment 엔티티에서 siteUserId 필드를 기준으로 삭제 쿼리가 자동 생성됩니다.

src/main/java/com/example/solidconnection/score/repository/LanguageTestScoreRepository.java (1)

17-18: 메서드 추가 확인 완료

deleteAllBySiteUserId 메서드가 다른 Repository들과 일관된 패턴으로 추가되었습니다.

src/main/java/com/example/solidconnection/report/repository/ReportRepository.java (1)

10-11: 메서드 추가 확인 완료

deleteAllByReporterId 메서드가 Report 도메인에 맞게 올바르게 추가되었습니다.

src/main/java/com/example/solidconnection/chat/repository/ChatParticipantRepository.java (1)

12-13: 메서드 추가 확인 완료

deleteAllBySiteUserId 메서드가 올바르게 추가되었습니다.

src/main/java/com/example/solidconnection/community/post/repository/PostLikeRepository.java (1)

19-20: 메서드 추가 확인 완료

deleteAllBySiteUserId 메서드가 올바르게 추가되었습니다.

src/main/java/com/example/solidconnection/mentor/repository/MentoringRepository.java (1)

42-43: 메서드 추가 확인 완료

deleteAllByMenteeId 메서드가 Mentoring 도메인에 맞게 올바르게 추가되었습니다.

src/main/java/com/example/solidconnection/siteuser/repository/UserBlockRepository.java (1)

27-28: 메서드 추가 확인 완료

deleteAllByBlockerIdOrBlockedId 메서드가 올바르게 추가되었습니다. 사용자가 차단한 경우와 차단당한 경우 모두를 처리하기 위해 OR 조건을 사용한 것이 적절합니다.

Comment on lines 60 to +69
@Scheduled(cron = EVERY_MIDNIGHT)
@Transactional
public void scheduledUserRemoval() {
LocalDate cutoffDate = LocalDate.now().minusDays(ACCOUNT_RECOVER_DURATION);
List<SiteUser> usersToRemove = siteUserRepository.findUsersToBeRemoved(cutoffDate);
siteUserRepository.deleteAll(usersToRemove);

usersToRemove.forEach(this::deleteUserAndRelatedData);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

2. 트랜잭션 경계 문제

@Transactional 애노테이션이 scheduledUserRemoval 메서드에 추가되어 모든 사용자 삭제가 하나의 트랜잭션으로 처리됩니다. 이는 심각한 문제를 초래할 수 있습니다:

  1. 대량 데이터 처리 시 트랜잭션 타임아웃: 삭제할 사용자가 많을 경우 트랜잭션이 너무 길어져 타임아웃이 발생할 수 있습니다.
  2. 부분 실패 시 전체 롤백: 100명 중 99번째 사용자 삭제 중 오류가 발생하면 앞의 98명도 모두 롤백됩니다.
  3. 테이블 락 장기 점유: 긴 트랜잭션으로 인해 다른 작업들이 대기하게 됩니다.
🔎 개별 사용자별 트랜잭션 처리로 개선
  @Scheduled(cron = EVERY_MIDNIGHT)
- @Transactional
  public void scheduledUserRemoval() {
      LocalDate cutoffDate = LocalDate.now().minusDays(ACCOUNT_RECOVER_DURATION);
      List<SiteUser> usersToRemove = siteUserRepository.findUsersToBeRemoved(cutoffDate);
      usersToRemove.forEach(this::deleteUserAndRelatedData);
  }

+ @Transactional
  private void deleteUserAndRelatedData(SiteUser user) {
+     try {
          long siteUserId = user.getId();
          
          likedNewsRepository.deleteAllBySiteUserId(siteUserId);
          // ... 나머지 삭제 로직
          
          siteUserRepository.delete(user);
+     } catch (Exception e) {
+         log.error("Failed to delete user and related data for userId: {}", user.getId(), e);
+         // 개별 사용자 삭제 실패 시에도 다음 사용자 처리 계속
+     }
  }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/main/java/com/example/solidconnection/scheduler/UserRemovalScheduler.java
around lines 60 to 67, the @Transactional on scheduledUserRemoval makes the
entire batch run in one transaction; remove the @Transactional from this method,
ensure each deletion runs in its own transaction (e.g., annotate
deleteUserAndRelatedData with @Transactional(propagation =
Propagation.REQUIRES_NEW) or execute each delete via TransactionTemplate), and
wrap each per-user delete call in a try/catch to log failures and continue so a
single failure does not roll back or stop the whole job.

interestedCountryRepository.deleteAllBySiteUserId(siteUserId);
interestedRegionRepository.deleteAllBySiteUserId(siteUserId);

s3Service.deleteExProfile(siteUserId);
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

4. S3 삭제 실패 처리

Line 94에서 S3 프로필 삭제 중 오류가 발생하면 현재 트랜잭션이 롤백되어 DB 삭제도 취소됩니다. S3 삭제는 외부 서비스 호출이므로:

  1. 네트워크 오류 가능성: S3 서비스가 일시적으로 불안정하면 전체 사용자 삭제가 실패합니다.
  2. 트랜잭션 일관성 문제: S3는 트랜잭션에 참여하지 않으므로, 롤백 시에도 S3에서는 파일이 삭제된 상태로 남을 수 있습니다.
🔎 S3 삭제 오류 격리 처리
+ try {
      s3Service.deleteExProfile(siteUserId);
+ } catch (Exception e) {
+     log.warn("Failed to delete S3 profile for userId: {}, continuing with user deletion", siteUserId, e);
+     // S3 삭제 실패해도 사용자 삭제는 진행
+ }
  
  siteUserRepository.delete(user);

또는 S3 삭제를 별도의 보상 트랜잭션으로 처리하는 방안을 고려하세요.

📝 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
s3Service.deleteExProfile(siteUserId);
try {
s3Service.deleteExProfile(siteUserId);
} catch (Exception e) {
log.warn("Failed to delete S3 profile for userId: {}, continuing with user deletion", siteUserId, e);
// S3 삭제 실패해도 사용자 삭제는 진행
}
🤖 Prompt for AI Agents
In src/main/java/com/example/solidconnection/scheduler/UserRemovalScheduler.java
around line 94, calling s3Service.deleteExProfile(siteUserId) directly can cause
DB transaction rollback when S3 fails; change to perform S3 deletion outside the
DB transaction (e.g., register an after-commit action via
TransactionSynchronizationManager or publish a transactional
event/@TransactionalEventListener that runs after commit), wrap the S3 call in
retry logic and exception handling so failures do not throw into the DB
transaction, log detailed errors, and if deletion still fails enqueue a
compensating job or mark the user record with a flag for async cleanup to ensure
idempotent retry and eventual consistency.

@whqtker whqtker force-pushed the fix/delete-siteuser-correctly branch from d17592f to 07b7851 Compare December 29, 2025 10:40
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: 1

♻️ Duplicate comments (8)
src/main/java/com/example/solidconnection/score/repository/GpaScoreRepository.java (1)

17-17: 구현이 올바릅니다.

Derived delete 메서드는 별도 애노테이션 없이 정상 동작합니다. 과거 리뷰 코멘트는 부정확한 정보였습니다.

src/main/java/com/example/solidconnection/news/repository/NewsRepository.java (1)

12-12: 정상적으로 동작합니다.

src/main/java/com/example/solidconnection/application/repository/ApplicationRepository.java (1)

44-44: 구현이 올바릅니다.

src/main/java/com/example/solidconnection/community/comment/repository/CommentRepository.java (1)

46-46: 정상적으로 동작합니다.

src/main/java/com/example/solidconnection/scheduler/UserRemovalScheduler.java (3)

60-67: 트랜잭션 경계 설정으로 인한 심각한 위험이 있습니다.

@Transactional 애노테이션이 scheduledUserRemoval 메서드에 적용되어 있어, 모든 사용자 삭제 작업이 하나의 거대한 트랜잭션으로 처리됩니다. 이는 다음과 같은 심각한 문제를 초래할 수 있습니다:

  1. 대량 데이터 처리 시 트랜잭션 타임아웃: 삭제 대상 사용자가 많을 경우 트랜잭션 지속 시간이 너무 길어져 타임아웃 발생 가능
  2. 부분 실패 시 전체 롤백: 100명 중 99번째 사용자 삭제 중 오류 발생 시 앞의 98명도 모두 롤백됨
  3. 테이블 락 장기 점유: 긴 트랜잭션으로 인해 다른 작업들이 블로킹될 수 있음
🔎 개별 사용자별 트랜잭션으로 개선하는 방법
  @Scheduled(cron = EVERY_MIDNIGHT)
- @Transactional
  public void scheduledUserRemoval() {
      LocalDate cutoffDate = LocalDate.now().minusDays(ACCOUNT_RECOVER_DURATION);
      List<SiteUser> usersToRemove = siteUserRepository.findUsersToBeRemoved(cutoffDate);
      
      usersToRemove.forEach(this::deleteUserAndRelatedData);
  }

+ @Transactional
  private void deleteUserAndRelatedData(SiteUser user) {
+     try {
          long siteUserId = user.getId();
          
          // ... 기존 삭제 로직 ...
          
          siteUserRepository.delete(user);
+     } catch (Exception e) {
+         log.error("Failed to delete user and related data for userId: {}", user.getId(), e);
+         // 개별 사용자 삭제 실패 시에도 다음 사용자 처리 계속
+     }
  }

이렇게 변경하면:

  • 각 사용자별로 독립적인 트랜잭션 실행
  • 한 사용자 삭제 실패가 다른 사용자에게 영향 없음
  • 트랜잭션 타임아웃 위험 최소화

94-94: S3 삭제 실패 시 DB 트랜잭션 롤백으로 인한 데이터 불일치 위험이 있습니다.

Line 94에서 S3 프로필 삭제를 트랜잭션 내부에서 수행하고 있습니다. 이는 다음과 같은 문제를 야기합니다:

  1. 네트워크 오류 시 전체 사용자 삭제 실패: S3 서비스가 일시적으로 불안정하면 DB 삭제도 롤백됨
  2. 데이터 불일치 가능성: S3는 트랜잭션에 참여하지 않으므로, 롤백 시에도 S3에서는 파일이 이미 삭제된 상태로 남을 수 있음
🔎 S3 삭제 오류 격리 처리 방법

방법 1: S3 삭제 실패를 격리하여 DB 삭제는 진행

+ try {
      s3Service.deleteExProfile(siteUserId);
+ } catch (Exception e) {
+     log.warn("Failed to delete S3 profile for userId: {}, continuing with user deletion", siteUserId, e);
+     // S3 삭제 실패해도 사용자 DB 삭제는 진행
+ }
  
  siteUserRepository.delete(user);

방법 2: S3 삭제를 트랜잭션 커밋 후에 실행 (권장)

- s3Service.deleteExProfile(siteUserId);
- 
  siteUserRepository.delete(user);
+ 
+ // 트랜잭션 커밋 후 S3 삭제 (TransactionSynchronizationManager 활용)
+ TransactionSynchronizationManager.registerSynchronization(
+     new TransactionSynchronization() {
+         @Override
+         public void afterCommit() {
+             try {
+                 s3Service.deleteExProfile(siteUserId);
+             } catch (Exception e) {
+                 log.error("Failed to delete S3 profile after user deletion for userId: {}", siteUserId, e);
+                 // 보상 트랜잭션 큐에 등록하거나 재시도 로직 구현
+             }
+         }
+     }
+ );

방법 2를 사용하면 DB 삭제가 안전하게 커밋된 후에 S3 삭제를 시도하므로, S3 실패가 DB 트랜잭션에 영향을 주지 않습니다.


69-97: ChatReadStatus 엔티티 삭제가 누락되어 고아 레코드가 남습니다.

Line 83에서 chatParticipantRepository.deleteAllBySiteUserId(siteUserId)로 ChatParticipant를 삭제하지만, ChatReadStatus 엔티티는 삭제하지 않고 있습니다.

ChatReadStatus는 chatParticipantId를 참조하고 있으며 cascade 설정이 없어 ChatParticipant 삭제 시 자동으로 제거되지 않습니다. 이로 인해 ChatReadStatus 레코드가 고아(orphan) 상태로 데이터베이스에 남게 됩니다.

🔎 ChatReadStatus 삭제 추가 방법

Line 83 앞에 ChatReadStatus 삭제 로직을 추가하세요:

+ // ChatParticipant 삭제 전에 먼저 ChatReadStatus를 삭제해야 함
+ List<ChatParticipant> participants = chatParticipantRepository.findAllBySiteUserId(siteUserId);
+ List<Long> participantIds = participants.stream()
+         .map(ChatParticipant::getId)
+         .toList();
+ if (!participantIds.isEmpty()) {
+     chatReadStatusRepository.deleteAllByChatParticipantIdIn(participantIds);
+ }
  chatParticipantRepository.deleteAllBySiteUserId(siteUserId);

또는 ChatReadStatusRepository에 deleteAllByChatParticipantSiteUserId 메서드가 있다면:

+ chatReadStatusRepository.deleteAllByChatParticipantSiteUserId(siteUserId);
  chatParticipantRepository.deleteAllBySiteUserId(siteUserId);

참고: ChatReadStatusRepository를 의존성으로 주입받아야 합니다.

src/main/java/com/example/solidconnection/mentor/repository/MentorRepository.java (1)

31-31: 필수 애노테이션 @Modifying이 누락되어 런타임 오류가 발생합니다.

Spring Data JPA의 파생 삭제 쿼리 메서드는 반드시 @Modifying 애노테이션이 필요합니다. 이 애노테이션 없이 실행하면 InvalidDataAccessApiUsageException이 발생합니다.

🔎 수정 제안
+    @Modifying
     void deleteAllBySiteUserId(long siteUserId);

파일 상단에 import 추가:

import org.springframework.data.jpa.repository.Modifying;

참고: 호출하는 측(UserRemovalScheduler)에서 이미 @Transactional 컨텍스트를 제공하고 있으므로, 이 메서드에는 @Transactional을 추가하지 않아도 됩니다.

📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d17592f and 07b7851.

📒 Files selected for processing (16)
  • src/main/java/com/example/solidconnection/application/repository/ApplicationRepository.java
  • src/main/java/com/example/solidconnection/chat/repository/ChatParticipantRepository.java
  • src/main/java/com/example/solidconnection/community/comment/repository/CommentRepository.java
  • src/main/java/com/example/solidconnection/community/post/repository/PostLikeRepository.java
  • src/main/java/com/example/solidconnection/community/post/repository/PostRepository.java
  • src/main/java/com/example/solidconnection/mentor/repository/MentorApplicationRepository.java
  • src/main/java/com/example/solidconnection/mentor/repository/MentorRepository.java
  • src/main/java/com/example/solidconnection/mentor/repository/MentoringRepository.java
  • src/main/java/com/example/solidconnection/news/repository/LikedNewsRepository.java
  • src/main/java/com/example/solidconnection/news/repository/NewsRepository.java
  • src/main/java/com/example/solidconnection/report/repository/ReportRepository.java
  • src/main/java/com/example/solidconnection/scheduler/UserRemovalScheduler.java
  • src/main/java/com/example/solidconnection/score/repository/GpaScoreRepository.java
  • src/main/java/com/example/solidconnection/score/repository/LanguageTestScoreRepository.java
  • src/main/java/com/example/solidconnection/siteuser/repository/UserBlockRepository.java
  • src/main/java/com/example/solidconnection/university/repository/LikedUnivApplyInfoRepository.java
🚧 Files skipped from review as they are similar to previous changes (5)
  • src/main/java/com/example/solidconnection/siteuser/repository/UserBlockRepository.java
  • src/main/java/com/example/solidconnection/report/repository/ReportRepository.java
  • src/main/java/com/example/solidconnection/community/post/repository/PostLikeRepository.java
  • src/main/java/com/example/solidconnection/university/repository/LikedUnivApplyInfoRepository.java
  • src/main/java/com/example/solidconnection/score/repository/LanguageTestScoreRepository.java
🔇 Additional comments (5)
src/main/java/com/example/solidconnection/mentor/repository/MentoringRepository.java (1)

43-43: 메서드 구현이 적절합니다.

deleteAllByMenteeId는 멘티가 탈퇴할 때 해당 멘티와 관련된 멘토링 관계를 정리하는 데 적합합니다. Spring Data JPA가 자동으로 트랜잭션 내에서 처리하므로 정상 동작합니다.

src/main/java/com/example/solidconnection/community/post/repository/PostRepository.java (1)

63-63: 구현이 정확합니다.

이 파일의 다른 메서드들(decreaseLikeCount, increaseLikeCount 등)은 @Query를 사용하므로 @Modifying이 필요하지만, derived delete 메서드인 deleteAllBySiteUserId는 애노테이션 없이도 정상 작동합니다.

src/main/java/com/example/solidconnection/mentor/repository/MentorApplicationRepository.java (1)

12-12: 애플리케이션 레벨 삭제 방식이 적절합니다.

과거 리뷰에서 DB 레벨 ON DELETE CASCADE 미적용을 지적했으나, PR 목적에 따르면 의도적으로 애플리케이션 레벨에서 삭제를 처리하기로 결정되었습니다. 이는 다음과 같은 이유로 타당합니다:

  1. 명시적 제어: 삭제 순서와 로직을 코드에서 명확하게 관리할 수 있습니다.
  2. 유연성: 향후 삭제 전 로깅, 검증 등 추가 로직 삽입이 용이합니다.
  3. 추적성: 애플리케이션 로그에서 삭제 과정을 추적할 수 있습니다.

UserRemovalScheduler가 올바른 순서로 연관 레코드를 삭제하는지는 위 LikedNewsRepository 리뷰의 검증 스크립트를 통해 확인해주세요.

src/main/java/com/example/solidconnection/news/repository/LikedNewsRepository.java (1)

13-13: 파생 삭제 메서드는 정상 동작합니다.

Spring Data JPA의 파생 삭제 메서드(예: deleteAllBySiteUserId)는 다음과 같이 작동합니다:

  1. 자동 트랜잭션 처리: SimpleJpaRepository 구현체가 자동으로 트랜잭션을 관리합니다.
  2. 애노테이션 불필요: @Modifying@Transactional@Query를 사용한 커스텀 메서드에만 필수입니다.

현재 구현은 정상적입니다. UserRemovalScheduler@Transactional이 모든 삭제 작업을 감싸고 있으며, 삭제 순서도 올바르게 의존성을 처리하고 있습니다.

성능 고려사항이 있습니다:

파생 삭제 메서드는 내부적으로 SELECT 후 개별 DELETE를 수행합니다(N+1 패턴). 삭제 대상이 많지 않다면 문제없으나, 대량 삭제 시 최적화를 고려할 수 있습니다.

src/main/java/com/example/solidconnection/scheduler/UserRemovalScheduler.java (1)

3-28: 의존성 추가 및 애플리케이션 레벨 삭제 구조를 확인했습니다.

변경 사항:

  1. 사용자 관련 데이터를 삭제하기 위해 15개 이상의 Repository 의존성 추가
  2. @Transactional 애노테이션 추가로 스케줄링 메서드를 트랜잭션 컨텍스트에서 실행

애플리케이션 레벨에서 연관 레코드를 명시적으로 삭제하는 방식으로 JPA cascade 제거 이후 발생한 Hard Delete 문제를 해결하는 접근은 타당합니다. 다만, 아래 리뷰 코멘트에서 지적하는 트랜잭션 경계 및 누락된 삭제 로직 문제를 반드시 확인해 주세요.

@whqtker whqtker force-pushed the fix/delete-siteuser-correctly branch from 07b7851 to 2f5c555 Compare December 29, 2025 11:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

버그 Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix: 사용자 Hard Delete가 안 되는 문제를 해결한다.

4 participants