Skip to content

fix: 탈퇴 유저 재가입 허용 및 삭제/스케줄러 정합성 개선#277

Merged
Develop-KIM merged 1 commit into
developfrom
fix/withdrawn-user-rejoin-and-scheduler-isolation
Jul 1, 2026
Merged

fix: 탈퇴 유저 재가입 허용 및 삭제/스케줄러 정합성 개선#277
Develop-KIM merged 1 commit into
developfrom
fix/withdrawn-user-rejoin-and-scheduler-isolation

Conversation

@Develop-KIM

Copy link
Copy Markdown
Member

배경

탈퇴 후 같은 소셜 계정으로 재가입 시 로그인이 실패(앱에서 일반 오류 문구 노출)하는 문제를 조사하던 중, 재가입 로직 외에 탈퇴 데이터 삭제/스케줄러 쪽 정합성 이슈를 함께 발견해 정리했습니다.

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 고아 데이터 정리

  • 탈퇴 90일 경과 유저 하드 삭제 시 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 으로 통일했습니다.
  • cron은 앱 규칙(JVM UTC 발화 + ServiceTime로 KST 변환)에 따라 그대로 두었습니다(발화 시각 변경 없음).

검증

  • compileKotlin/compileTestKotlin 통과
  • spotlessCheck(ktlint) 통과
  • 스케줄러 단위 테스트 17개 통과(격리 테스트 포함)
  • UserRepositoryTest(JPA 컨텍스트 부팅 → 파생 쿼리 파싱 검증) 통과

🤖 Generated with Claude Code

## 재가입 막힘 수정
- 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>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge 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 👍 / 👎.

@Develop-KIM Develop-KIM merged commit 888fd57 into develop Jul 1, 2026
3 checks passed
@Develop-KIM Develop-KIM deleted the fix/withdrawn-user-rejoin-and-scheduler-isolation branch July 1, 2026 07:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant