fix: 유저 삭제 시 소프트 딜리트 방식으로 전환 - #329
Conversation
📝 WalkthroughWalkthrough사용자 엔티티에 소프트 삭제 기능을 도입하여 물리적 삭제 대신 논리적 삭제를 지원합니다. Changes
Sequence Diagram(s)sequenceDiagram
actor User as 사용자
participant OAuthLoginHelper as OAuthLoginHelper
participant UserRepository as UserRepository
participant UserModel as User 모델
participant Database as 데이터베이스
User->>OAuthLoginHelper: OAuth 로그인 시도
OAuthLoginHelper->>UserRepository: findByEmailAndProvider() 호출<br/>(deletedAt IS NULL 필터)
UserRepository->>Database: 활성 사용자 조회
alt 활성 사용자 존재
Database-->>UserRepository: 사용자 반환
UserRepository-->>OAuthLoginHelper: 활성 사용자
OAuthLoginHelper-->>User: 로그인 완료
else 활성 사용자 없음 (탈퇴 상태)
Database-->>UserRepository: 결과 없음
UserRepository-->>OAuthLoginHelper: Optional.empty()
alt Apple 제공자 + providerId 존재
OAuthLoginHelper->>UserRepository: findFirstByProviderIdAndProviderAndDeletedAtIsNotNull()
else 이메일 기반 복구
OAuthLoginHelper->>UserRepository: findFirstByEmailAndProviderAndDeletedAtIsNotNull()
end
UserRepository->>Database: 탈퇴한 사용자 조회 (최신순)
Database-->>UserRepository: 탈퇴한 사용자
UserRepository-->>OAuthLoginHelper: 탈퇴한 사용자 반환
OAuthLoginHelper->>OAuthLoginHelper: canRestore(now, 7일) 검증<br/>(7일 내 탈퇴 확인)
alt 복구 가능
OAuthLoginHelper->>UserModel: restore() 호출
UserModel->>UserModel: deletedAt = null 설정
OAuthLoginHelper->>UserRepository: save(user) 호출
UserRepository->>Database: 사용자 업데이트 (active_flag = 1)
Database-->>UserRepository: 저장 완료
UserRepository-->>OAuthLoginHelper: 복구된 사용자
OAuthLoginHelper-->>User: 로그인 완료
else 복구 기간 초과
OAuthLoginHelper-->>User: 새 계정 생성 필요
end
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested labels
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
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/gg/agit/konect/domain/user/service/UserActivityService.java (1)
18-35: 🧹 Nitpick | 🔵 Trivial[LEVEL: low]
updateLastLoginAt과updateLastActivityAt의 예외 처리 방식 불일치문제:
updateLastLoginAt은 사용자가 없으면 예외를 던지지만,updateLastActivityAt은 조용히 무시함.
영향: 탈퇴 직후 로그인 시간 갱신 호출 시 예외 발생 가능성 존재.
제안: 의도된 동작이라면 주석으로 명시, 아니라면 동일한 방식으로 통일 검토 필요.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/gg/agit/konect/domain/user/service/UserActivityService.java` around lines 18 - 35, The two methods updateLastLoginAt and updateLastActivityAt currently handle missing users differently: updateLastLoginAt calls userRepository.getById(userId) which will throw if the user is absent, while updateLastActivityAt uses userRepository.findById(...).ifPresent(...) and silently skips; make the behavior consistent by either (A) changing updateLastLoginAt to use userRepository.findById(userId).ifPresent(user -> user.updateLastLoginAt(...)) to silently ignore missing users like updateLastActivityAt, or (B) if throwing is intended, update updateLastActivityAt to call userRepository.getById(userId).updateLastActivityAt(...) and add a clarifying comment on the intended semantics; update the code path accordingly and keep the null check for userId.
🤖 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/gg/agit/konect/domain/user/model/User.java`:
- Around line 182-187: The current User.getName() hides withdrawn users' real
names by returning WITHDRAWN_USER_NAME when deletedAt != null; add a new
accessor (e.g., getActualName() or getRawName()) in the User class that always
returns the underlying name field regardless of deletedAt so admin/logging code
can retrieve the real name when needed; update any admin/logging call sites to
use the new method where appropriate and keep getName() behavior for
public-facing usage.
- Around line 31-46: The JPA entity User's uniqueConstraints declares a
constraint named "uq_users_phone_number" that must match the migration; update
the name to "uq_users_phone_number_active" in the `@UniqueConstraint` inside the
uniqueConstraints array on the User class so the annotation (uniqueConstraints /
`@UniqueConstraint`) uses the migration-consistent identifier
"uq_users_phone_number_active".
In `@src/main/java/gg/agit/konect/domain/user/repository/UserRepository.java`:
- Line 66: The repository method findFirstByRoleOrderByIdAsc in UserRepository
lacks a soft-delete filter and can return users with deletedAt set; change this
derived query to an explicit `@Query` that adds "AND u.deletedAt IS NULL" (or the
equivalent column) so it only returns non-deleted users, mirroring other
repository methods; update the method signature (still named
findFirstByRoleOrderByIdAsc) to use the `@Query` annotation and ensure callers
like UserService.sendWelcomeMessage will only receive active users.
In `@src/main/java/gg/agit/konect/global/auth/oauth/OAuthLoginHelper.java`:
- Around line 33-56: Add a transactional boundary to the findUserByProvider
method so the sequence of reads and potential re-create/save operations (calls
to restoreWithdrawnByProviderId, restoreWithdrawnByEmail and subsequent
userRepository.save) run in a single DB transaction; annotate the
findUserByProvider method with `@Transactional` (e.g., Spring's
org.springframework.transaction.annotation.Transactional with default
REQUIRED/read-write) and ensure the import is added so concurrent requests
cannot cause race conditions or unique-constraint violations during
restore/save.
In
`@src/main/resources/db/migration/V40__add_user_soft_delete_and_active_unique_constraints.sql`:
- Around line 10-18: The migration drops four unique indexes
(uq_users_phone_number, uq_users_email_provider,
uq_users_university_id_student_number, uq_users_provider_provider_id) and then
adds new UNIQUE constraints with _active suffix; prepare a rollback and safer
deployment plan by either (1) providing a rollback SQL that re-creates the
original indexes with their original definitions and drops the newly added
constraints (uq_users_phone_number_active, uq_users_email_provider_active,
uq_users_university_id_student_number_active,
uq_users_provider_provider_id_active), or (2) restructure the migration to
create the new UNIQUE constraints first (uq_users_..._active) and only drop the
old indexes after verification, and ensure the migration runs inside a
transaction or is tested in staging before production to guarantee
recoverability.
---
Outside diff comments:
In `@src/main/java/gg/agit/konect/domain/user/service/UserActivityService.java`:
- Around line 18-35: The two methods updateLastLoginAt and updateLastActivityAt
currently handle missing users differently: updateLastLoginAt calls
userRepository.getById(userId) which will throw if the user is absent, while
updateLastActivityAt uses userRepository.findById(...).ifPresent(...) and
silently skips; make the behavior consistent by either (A) changing
updateLastLoginAt to use userRepository.findById(userId).ifPresent(user ->
user.updateLastLoginAt(...)) to silently ignore missing users like
updateLastActivityAt, or (B) if throwing is intended, update
updateLastActivityAt to call
userRepository.getById(userId).updateLastActivityAt(...) and add a clarifying
comment on the intended semantics; update the code path accordingly and keep the
null check for userId.
ℹ️ Review info
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (6)
src/main/java/gg/agit/konect/domain/user/model/User.javasrc/main/java/gg/agit/konect/domain/user/repository/UserRepository.javasrc/main/java/gg/agit/konect/domain/user/service/UserActivityService.javasrc/main/java/gg/agit/konect/domain/user/service/UserService.javasrc/main/java/gg/agit/konect/global/auth/oauth/OAuthLoginHelper.javasrc/main/resources/db/migration/V40__add_user_soft_delete_and_active_unique_constraints.sql
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Agent
🧰 Additional context used
📓 Path-based instructions (3)
src/main/resources/db/migration/**/*.sql
⚙️ CodeRabbit configuration file
src/main/resources/db/migration/**/*.sql: Flyway 마이그레이션 리뷰 규칙:
- 버전 파일명 규칙(V{number}__{description}.sql) 위반 여부를 우선 확인한다.
- 이미 배포된 마이그레이션 수정/재번호 부여 위험이 있으면 반드시 차단 코멘트를 남긴다.
- 파괴적 변경(drop, rename 등)은 롤백 가능성과 운영 영향 관점에서 검토한다.
Files:
src/main/resources/db/migration/V40__add_user_soft_delete_and_active_unique_constraints.sql
**/*
⚙️ CodeRabbit configuration file
**/*: 공통 리뷰 톤 가이드:
- 모든 코멘트는 첫 줄에
[LEVEL: ...]태그를 포함한다.- 과장된 표현 없이 사실 기반으로 작성한다.
- 한 코멘트에는 하나의 이슈만 다룬다.
- 코드 예시가 필요하면 최소 수정 예시를 제시한다.
- 가독성/단순화/확장성 이슈를 발견하면 우선순위를 높여 코멘트한다.
Files:
src/main/resources/db/migration/V40__add_user_soft_delete_and_active_unique_constraints.sqlsrc/main/java/gg/agit/konect/global/auth/oauth/OAuthLoginHelper.javasrc/main/java/gg/agit/konect/domain/user/service/UserActivityService.javasrc/main/java/gg/agit/konect/domain/user/service/UserService.javasrc/main/java/gg/agit/konect/domain/user/repository/UserRepository.javasrc/main/java/gg/agit/konect/domain/user/model/User.java
src/main/java/**/*.java
⚙️ CodeRabbit configuration file
src/main/java/**/*.java: 아래 원칙으로 리뷰 코멘트를 작성한다.
- 코멘트는 반드시 한국어로 작성한다.
- 반드시 수정이 필요한 항목만 코멘트로 남기고, 단순 취향 차이는 지적하지 않는다.
- 각 코멘트 첫 줄에 심각도를
[LEVEL: high|medium|low]형식으로 반드시 표기한다.- 심각도 기준: high=운영 장애 가능, medium=품질 저하, low=개선 권고.
- 각 코멘트는 "문제 -> 영향 -> 제안" 순서로 3문장 이내로 간결하게 작성한다.
- 가능하면 재현 조건 및 실패 시나리오도 포함한다.
- 제안은 현재 코드베이스(Spring Boot + JPA + Flyway) 패턴과 일치해야 한다.
- 보안, 트랜잭션 경계, 예외 처리, N+1, 성능 회귀 가능성을 우선 점검한다.
- 가독성: 변수/메서드 이름이 의도를 바로 드러내는지, 중첩과 메서드 길이가 과도하지 않은지 점검한다.
- 단순화: 불필요한 추상화, 중복 로직, 과한 방어 코드가 있으면 더 단순한 대안을 제시한다.
- 확장성: 새 요구사항 추가 시 변경 범위가 최소화되는 구조인지(하드코딩 분기/값 여부 포함) 점검한다.
Files:
src/main/java/gg/agit/konect/global/auth/oauth/OAuthLoginHelper.javasrc/main/java/gg/agit/konect/domain/user/service/UserActivityService.javasrc/main/java/gg/agit/konect/domain/user/service/UserService.javasrc/main/java/gg/agit/konect/domain/user/repository/UserRepository.javasrc/main/java/gg/agit/konect/domain/user/model/User.java
🔇 Additional comments (2)
src/main/java/gg/agit/konect/domain/user/service/UserService.java (1)
213-229: 소프트 삭제 전환 구현이 적절함하드 삭제에서 소프트 삭제로의 전환이 올바르게 구현됨. Apple 토큰 해지 → withdraw → save → 이벤트 발행 순서가 적절하며, 트랜잭션 범위 내에서 일관성 있게 처리됨.
src/main/java/gg/agit/konect/domain/user/repository/UserRepository.java (1)
18-146: 소프트 삭제 필터링이 일관되게 적용됨대부분의 조회 메서드에
deletedAt IS NULL조건이 추가되어 소프트 삭제된 사용자가 일반 조회에서 제외됨. 복구용 메서드(findFirst...DeletedAtIsNotNull...)도 적절히 분리되어 있음.
There was a problem hiding this comment.
Pull request overview
이 PR은 사용자 삭제를 하드 삭제에서 소프트 삭제 방식으로 전환하는 중요한 변경 사항을 포함합니다. 사용자 탈퇴 시 데이터를 완전히 제거하는 대신, deleted_at 타임스탬프를 설정하여 논리적으로 삭제 처리하며, OAuth 로그인 시 7일 이내 탈퇴한 계정을 자동으로 복구하는 기능을 추가했습니다.
Changes:
- 데이터베이스 마이그레이션을 통해
deleted_at컬럼과active_flag생성 컬럼 추가, 유니크 제약 조건에active_flag포함 User엔티티에 소프트 삭제 관련 필드 및 메서드(withdraw,restore,canRestore) 추가, 탈퇴 사용자 이름 마스킹 로직 구현UserRepository의 모든 조회 메서드에deleted_at IS NULL필터 추가UserService.deleteUser를 하드 삭제에서 소프트 삭제로 변경OAuthLoginHelper에 7일 유예 기간 내 탈퇴 계정 자동 복구 로직 추가UserActivityService.updateLastActivityAt을getById에서findById+ifPresent패턴으로 변경
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| V40__add_user_soft_delete_and_active_unique_constraints.sql | 소프트 삭제를 위한 deleted_at 컬럼과 active_flag 생성 컬럼 추가, 유니크 제약 조건에 active_flag 포함하여 재생성 |
| User.java | deleted_at, active_flag 필드 추가, getName() 오버라이드로 탈퇴 사용자 마스킹, withdraw/restore/canRestore 메서드 구현, 유니크 제약 조건에 active_flag 추가 |
| UserRepository.java | 모든 조회 메서드에 deleted_at IS NULL 필터 추가, 탈퇴 사용자 조회 메서드 추가, deleteByUserId 메서드 제거 |
| UserService.java | deleteUser 메서드를 하드 삭제에서 소프트 삭제(withdraw + save)로 변경 |
| UserActivityService.java | updateLastActivityAt을 안전한 Optional 패턴(findById + ifPresent)으로 변경 |
| OAuthLoginHelper.java | 7일 유예 기간 내 탈퇴 계정 자동 복구 로직 추가 (restoreWithdrawnByProviderId, restoreWithdrawnByEmail) |
🔍 개요
🚀 주요 변경 내용
회원탈퇴를 하드 삭제에서 소프트 삭제로 전환했습니다.
OAuth 로그인 시 탈퇴 계정을 7일 유예기간 내 자동 복구하도록 변경했습니다.
active_flag컬럼은deleted_at컬럼에 따라 1(존재) 또는 NULL(삭제)가 결정됩니다.탈퇴한 사용자의 경우 이름 조회 시
탈퇴한 사용자로 표시됩니다.💬 참고 사항
✅ Checklist (완료 조건)