fix: 탈퇴 유저 재가입 허용 및 삭제/스케줄러 정합성 개선#277
Conversation
## 재가입 막힘 수정 - V30이 uq_provider_provider_id_deleted를 추가했지만 V4의 옛 제약 uq_provider_provider_id가 DROP되지 않아 두 제약이 공존했다. 탈퇴 유저 재로그인 시 익명화 UPDATE보다 신규 유저 INSERT가 먼저 flush되면서 옛 제약에 걸려 재가입이 500으로 실패했다. - V44로 옛 제약을 제거해 정책(재가입 = 신규 계정)대로 동작하게 한다. ## 하드 삭제 시 achievement 고아 데이터 정리 - 탈퇴 90일 경과 유저 하드 삭제 시 user_badges/user_missions/user_levels/ weekly_retro_streaks/daily_access_streaks가 남아 고아가 되었다(FK CASCADE 없음). - AchievementDeletionPort/Adapter를 추가해 유저 스코프 achievement 5종을 userId로 함께 삭제한다. ## 스케줄러 트랜잭션 격리 개선 - Cleanup/Mission/Notification 스케줄러가 배치 전체를 단일 트랜잭션으로 처리해, 한 건 실패 시 배치 전체가 롤백되고 나머지도 처리되지 않았다. - 건별 @transactional 헬퍼 빈(CleanupExecutor/MissionFailureProcessor/ ReminderNotificationSender)으로 분리하고, 스케줄러는 건별 runCatching으로 격리하여 한 건 실패가 나머지 처리를 막지 않도록 한다. ## 타임존 일원화 - NotificationScheduler의 인라인 ZoneId.of("Asia/Seoul")를 ServiceTime.ZONE로 통일한다. cron은 앱 규칙(UTC 발화 + ServiceTime 변환)에 따라 그대로 둔다. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8a251afa95
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| @@ -0,0 +1 @@ | |||
| ALTER TABLE users DROP INDEX uq_provider_provider_id; | |||
There was a problem hiding this comment.
Preserve uniqueness for active social accounts
Dropping this index leaves only uq_provider_provider_id_deleted(provider, provider_id, deleted_at), but on this MySQL-backed schema a UNIQUE index permits multiple rows when deleted_at is NULL, so active users are no longer protected from duplicate (provider, provider_id) rows. In the concurrent-login path in AuthService.resolveUser/createNewUser, two transactions can both observe no active user and insert the same social account; later findByProviderAndProviderId expects a single row and login can fail or bind to ambiguous data. Please replace this with an active-only uniqueness mechanism rather than relying on the nullable deleted_at column.
Useful? React with 👍 / 👎.
배경
탈퇴 후 같은 소셜 계정으로 재가입 시 로그인이 실패(앱에서 일반 오류 문구 노출)하는 문제를 조사하던 중, 재가입 로직 외에 탈퇴 데이터 삭제/스케줄러 쪽 정합성 이슈를 함께 발견해 정리했습니다.
1. 재가입 막힘 수정
V30에서uq_provider_provider_id_deleted(provider, provider_id, deleted_at)를 추가했지만,V4의 옛 제약uq_provider_provider_id(provider, provider_id)가 DROP되지 않아 두 제약이 공존했습니다.resolveUser가 옛 행을 익명화(providerId=null)하고 신규 유저를 생성하는데, Hibernate가 INSERT를 익명화 UPDATE보다 먼저 flush하면서 옛 제약에 걸려 500(DataIntegrityViolation) 으로 재가입이 실패했습니다.V44로 옛 제약을 제거해, 정책(재가입 = 신규 계정)대로 정상 동작하도록 했습니다.WITHDRAWN_USER예외는 정의만 되어 있고 throw되는 곳이 없어 이번 건과 무관합니다.2. 하드 삭제 시 achievement 고아 데이터 정리
project/notification/retrospect만 삭제하고user_badges/user_missions/user_levels/weekly_retro_streaks/daily_access_streaks는 남아 고아 데이터가 되었습니다(FK CASCADE 없음, 과거V42에서 수동 정리 이력).AchievementDeletionPort/AchievementDeletionAdapter를 추가하고 리포지토리 5종에deleteAllByUserId를 더해, 유저 스코프 achievement 데이터를 함께 삭제합니다.3. 스케줄러 트랜잭션 격리 개선
Cleanup/Mission/Notification스케줄러가 배치 전체를 단일 트랜잭션으로 처리해, 한 건 실패 시 배치 전체가 롤백되고 나머지도 처리되지 않는 구조였습니다.@Transactional헬퍼 빈(CleanupExecutor/MissionFailureProcessor/ReminderNotificationSender)으로 분리하고, 스케줄러는 건별runCatching으로 격리 → 한 건 실패가 나머지 처리를 막지 않도록 했습니다. 각 스케줄러에 격리 검증 테스트를 추가했습니다.4. 타임존 일원화
NotificationScheduler의 인라인ZoneId.of("Asia/Seoul")를ServiceTime.ZONE으로 통일했습니다.ServiceTime로 KST 변환)에 따라 그대로 두었습니다(발화 시각 변경 없음).검증
compileKotlin/compileTestKotlin통과spotlessCheck(ktlint) 통과UserRepositoryTest(JPA 컨텍스트 부팅 → 파생 쿼리 파싱 검증) 통과🤖 Generated with Claude Code