[feat]: 탈퇴 유저 30일 후 영구 삭제 스케줄러 구현#180
Conversation
매일 새벽 4시에 `deletedAt`이 30일 이상 경과한 탈퇴 사용자의 데이터를
FK 제약을 고려한 순서로 hard delete 처리하는 기능 추가
**변경 내용:**
- UserHardDeleteService: 사용자별 트랜잭션으로 고용주/근로자 케이스 구분
- 고용주: 사업장 → 계약 → 하위 모든 데이터(근무/수당/급여/결제) → 정정요청 → 공지 정리
- 근로자: 본인 계약만 정리, 다른 근로자 데이터 영향 없음
- FK 의존성 역순: CorrectionRequest → Payment → Salary → WorkRecord →
WeeklyAllowance → WorkerContract → Notice → Workplace → Employer/Worker → User
- UserHardDeleteScheduler: 매일 새벽 4시 실행
- 30일 경과 탈퇴 유저 조회 후 유저별 hard delete 호출
- 한 명 실패 시 try-catch로 격리, 다음 유저 계속 처리
- Repository 메서드 추가 (영구 삭제용 bulk delete)
- UserRepository: findAllByDeletedAtBefore
- WorkRecordRepository: deleteAllByContractIdIn
- WeeklyAllowanceRepository: deleteAllByContractIdIn
- SalaryRepository: findIdsByContractIdIn, deleteAllByContractIdIn
- PaymentRepository: deleteAllBySalaryIdIn
- CorrectionRequestRepository: deleteAllByRequesterId, deleteAllByContractIdIn
- NoticeRepository: deleteAllByAuthorId, deleteAllByWorkplaceIdIn
- WorkplaceRepository: deleteAllByEmployerId
- WorkerContractRepository: findIdsByWorkplaceIdIn, deleteAllByWorkplaceIdIn,
findIdsByWorkerId, deleteAllByWorkerId
- UserHardDeleteServiceTest: 고용주/근로자 케이스 및 FK 순서 검증
**선행 조건:**
이슈 #130 (detached 엔티티 버그) 완료 후 진행됨
**테스트:**
- ./gradlew test --tests UserHardDeleteServiceTest: 6개 시나리오 통과
- ./gradlew clean build: 전체 테스트 통과
Walkthrough사용자 hard-delete 기능을 구현하는 스케줄러와 서비스를 추가합니다. 스케줄러는 매일 04:00에 30일 이상 경과한 soft-deleted 사용자들을 hard-delete하고, 서비스는 사용자의 모든 관련 데이터를 외래 키 제약 순서대로 삭제합니다. 여러 도메인의 repository에 bulk delete 메서드를 추가하여 이를 지원합니다. Changes사용자 Hard-Delete 기능
Sequence DiagramsequenceDiagram
participant Scheduler as UserHardDeleteScheduler
participant UserRepo as UserRepository
participant HardDeleteSvc as UserHardDeleteService
participant EmployerRepo as EmployerRepository
participant WorkplaceRepo as WorkplaceRepository
participant ContractRepo as WorkerContractRepository
participant CorrectionRepo as CorrectionRequestRepository
participant SalaryRepo as SalaryRepository
participant PaymentRepo as PaymentRepository
participant WorkRecordRepo as WorkRecordRepository
participant AllowanceRepo as WeeklyAllowanceRepository
participant NoticeRepo as NoticeRepository
participant UserTokenRepo as UserTokenRepository
Scheduler->>UserRepo: findAllByDeletedAtBefore(30일 전)
UserRepo-->>Scheduler: 대상 사용자 목록
loop 각 사용자마다
Scheduler->>HardDeleteSvc: hardDeleteUser(userId)
HardDeleteSvc->>UserRepo: findById(userId)
UserRepo-->>HardDeleteSvc: User
alt UserType = EMPLOYER
HardDeleteSvc->>EmployerRepo: findByUserId(userId)
EmployerRepo-->>HardDeleteSvc: Employer
HardDeleteSvc->>WorkplaceRepo: findIdsByEmployerId()
WorkplaceRepo-->>HardDeleteSvc: workplace IDs
HardDeleteSvc->>ContractRepo: findIdsByWorkplaceIdIn()
ContractRepo-->>HardDeleteSvc: contract IDs
else UserType = WORKER
HardDeleteSvc->>ContractRepo: findIdsByWorkerId()
ContractRepo-->>HardDeleteSvc: contract IDs
end
HardDeleteSvc->>CorrectionRepo: deleteAllByRequesterId(userId)
HardDeleteSvc->>CorrectionRepo: deleteAllByContractIdIn(contractIds)
HardDeleteSvc->>SalaryRepo: findIdsByContractIdIn(contractIds)
SalaryRepo-->>HardDeleteSvc: salary IDs
HardDeleteSvc->>PaymentRepo: deleteAllBySalaryIdIn(salaryIds)
HardDeleteSvc->>SalaryRepo: deleteAllByContractIdIn(contractIds)
HardDeleteSvc->>WorkRecordRepo: deleteAllByContractIdIn(contractIds)
HardDeleteSvc->>AllowanceRepo: deleteAllByContractIdIn(contractIds)
alt UserType = EMPLOYER
HardDeleteSvc->>ContractRepo: deleteAllByWorkplaceIdIn(workplaceIds)
HardDeleteSvc->>NoticeRepo: deleteAllByWorkplaceIdIn(workplaceIds)
HardDeleteSvc->>WorkplaceRepo: deleteAllByEmployerId(employerId)
HardDeleteSvc->>EmployerRepo: delete(employer)
else UserType = WORKER
HardDeleteSvc->>ContractRepo: deleteAllByWorkerId(workerId)
HardDeleteSvc->>NoticeRepo: deleteAllByAuthorId(userId)
end
HardDeleteSvc->>NoticeRepo: deleteAllByAuthorId(userId)
HardDeleteSvc->>UserTokenRepo: 알림/FCM/설정/토큰 삭제
HardDeleteSvc->>UserRepo: delete(user)
Scheduler->>Scheduler: 성공/실패 카운트
end
Scheduler->>Scheduler: 완료 로그 (총/성공/실패)
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes 변경사항이 8개의 repository 인터페이스에 걸쳐 있고, 새로운 서비스와 스케줄러가 복잡한 deletion 순서 로직을 포함하며, 외래 키 제약을 존중하는지 검증이 필요하고, 227줄의 테스트 코드가 8개의 시나리오를 다루기 때문입니다. Possibly related issues
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
src/main/java/com/example/paycheck/domain/user/scheduler/UserHardDeleteScheduler.java (1)
36-49: ⚡ Quick win대상 조회를 엔티티 전체가 아니라 ID 단위로 줄이는 쪽을 권장합니다.
지금은 삭제 대상 전체를 엔티티로 로드한 뒤
id만 사용하고 있어서, 누적 데이터가 많아지면 메모리 사용량과 처리 시간이 불필요하게 커질 수 있습니다. 스케줄러는 ID 페이징 조회나 배치 처리로 쪼개는 편이 더 안전합니다.수정 방향 예시
- List<User> targets = userRepository.findAllByDeletedAtBefore(threshold); + List<Long> targetIds = userRepository.findIdsByDeletedAtBefore(threshold); - for (User user : targets) { + for (Long userId : targetIds) { try { - userHardDeleteService.hardDeleteUser(user.getId()); + userHardDeleteService.hardDeleteUser(userId);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/com/example/paycheck/domain/user/scheduler/UserHardDeleteScheduler.java` around lines 36 - 49, UserHardDeleteScheduler is loading full User entities via userRepository.findAllByDeletedAtBefore(threshold) but only uses user.getId(), causing memory/latency issues; change the repository call to fetch only IDs (e.g., add a method like findIdsByDeletedAtBefore(LocalDateTime, Pageable) or findAllIdsByDeletedAtBefore) and iterate in paged/batched loops (use PageRequest and loop until empty) instead of loading the entire list into targets, then call userHardDeleteService.hardDeleteUser(id) for each id; keep RETENTION_DAYS and the existing try/catch logic but operate on id batches to avoid retaining entity objects in memory.
🤖 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/com/example/paycheck/domain/user/service/UserHardDeleteService.java`:
- Around line 133-137: Update the Javadoc in UserHardDeleteService (the method
that deletes contract-related data) so the listed FK deletion order matches the
actual implementation: move CorrectionRequest to the front and list the sequence
as CorrectionRequest → WeeklyAllowance → WorkRecord → Salary → Payment (or the
exact order used by the delete methods in UserHardDeleteService). Reference the
method/class name (UserHardDeleteService) when editing the comment to keep it
accurate for future maintainers.
---
Nitpick comments:
In
`@src/main/java/com/example/paycheck/domain/user/scheduler/UserHardDeleteScheduler.java`:
- Around line 36-49: UserHardDeleteScheduler is loading full User entities via
userRepository.findAllByDeletedAtBefore(threshold) but only uses user.getId(),
causing memory/latency issues; change the repository call to fetch only IDs
(e.g., add a method like findIdsByDeletedAtBefore(LocalDateTime, Pageable) or
findAllIdsByDeletedAtBefore) and iterate in paged/batched loops (use PageRequest
and loop until empty) instead of loading the entire list into targets, then call
userHardDeleteService.hardDeleteUser(id) for each id; keep RETENTION_DAYS and
the existing try/catch logic but operate on id batches to avoid retaining entity
objects in memory.
🪄 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: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: b7c5ea99-d9b3-4f80-8520-255a75ca39d3
📒 Files selected for processing (12)
src/main/java/com/example/paycheck/domain/allowance/repository/WeeklyAllowanceRepository.javasrc/main/java/com/example/paycheck/domain/contract/repository/WorkerContractRepository.javasrc/main/java/com/example/paycheck/domain/correction/repository/CorrectionRequestRepository.javasrc/main/java/com/example/paycheck/domain/notice/repository/NoticeRepository.javasrc/main/java/com/example/paycheck/domain/payment/repository/PaymentRepository.javasrc/main/java/com/example/paycheck/domain/salary/repository/SalaryRepository.javasrc/main/java/com/example/paycheck/domain/user/repository/UserRepository.javasrc/main/java/com/example/paycheck/domain/user/scheduler/UserHardDeleteScheduler.javasrc/main/java/com/example/paycheck/domain/user/service/UserHardDeleteService.javasrc/main/java/com/example/paycheck/domain/workplace/repository/WorkplaceRepository.javasrc/main/java/com/example/paycheck/domain/workrecord/repository/WorkRecordRepository.javasrc/test/java/com/example/paycheck/domain/user/service/UserHardDeleteServiceTest.java
| /** | ||
| * 계약 산하 데이터를 FK 의존성 역순으로 삭제한다. | ||
| * Payment → Salary → WorkRecord → WeeklyAllowance → CorrectionRequest | ||
| * (CorrectionRequest는 WorkRecord/Contract를 참조하므로 가장 먼저 정리) | ||
| */ |
There was a problem hiding this comment.
주석의 삭제 순서 설명을 실제 코드와 맞춰 주세요.
현재 주석은 Payment → Salary → WorkRecord → WeeklyAllowance → CorrectionRequest로 적혀 있지만, 실제 구현은 CorrectionRequest를 가장 먼저 삭제합니다. 다음 유지보수자가 FK 순서를 오해하지 않도록 주석을 코드와 동일하게 정리하는 게 좋습니다.
수정 예시
/**
* 계약 산하 데이터를 FK 의존성 역순으로 삭제한다.
- * Payment → Salary → WorkRecord → WeeklyAllowance → CorrectionRequest
- * (CorrectionRequest는 WorkRecord/Contract를 참조하므로 가장 먼저 정리)
+ * CorrectionRequest → Payment → Salary → WorkRecord → WeeklyAllowance
*/📝 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.
| /** | |
| * 계약 산하 데이터를 FK 의존성 역순으로 삭제한다. | |
| * Payment → Salary → WorkRecord → WeeklyAllowance → CorrectionRequest | |
| * (CorrectionRequest는 WorkRecord/Contract를 참조하므로 가장 먼저 정리) | |
| */ | |
| /** | |
| * 계약 산하 데이터를 FK 의존성 역순으로 삭제한다. | |
| * CorrectionRequest → Payment → Salary → WorkRecord → WeeklyAllowance | |
| */ |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@src/main/java/com/example/paycheck/domain/user/service/UserHardDeleteService.java`
around lines 133 - 137, Update the Javadoc in UserHardDeleteService (the method
that deletes contract-related data) so the listed FK deletion order matches the
actual implementation: move CorrectionRequest to the front and list the sequence
as CorrectionRequest → WeeklyAllowance → WorkRecord → Salary → Payment (or the
exact order used by the delete methods in UserHardDeleteService). Reference the
method/class name (UserHardDeleteService) when editing the comment to keep it
accurate for future maintainers.
요약
Closes #131
구현 상세
UserHardDeleteService
UserHardDeleteScheduler
@Scheduled(cron = "0 0 4 * * *")— 매일 새벽 4시LocalDateTime.now().minusDays(30)Repository 메서드 추가
신규 bulk delete 메서드 (총 12개):
@Modifying(clearAutomatically = true)사용으로 영속성 컨텍스트 stale 방지테스트 전략
UserHardDeleteServiceTest (6개 시나리오)
테스트 실행 결과
선행 조건
이슈 #130 (detached 엔티티로 인한 deletedAt 미반영 버그) 완료
참고사항
🤖 Generated with Claude Code
Summary by CodeRabbit
릴리스 노트
새로운 기능
테스트