-
Notifications
You must be signed in to change notification settings - Fork 1
fix: 탈퇴한 유저 재가입 시 기존 정보 복구되지 않는 문제 해결 #100
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
WalkthroughJPQL 기반 조회 메서드를 네이티브 쿼리로 교체하고, 파라미터에 @param을 명시하여 provider_type/provider_id 기준으로 삭제 여부와 무관하게 users 테이블에서 단일 사용자 행을 조회하도록 변경했습니다. Changes
Sequence Diagram(s)sequenceDiagram
participant Service
participant JpaUserRepository
participant Database
Service->>JpaUserRepository: findByProviderTypeAndProviderIdIncludingDeleted(providerType, providerId)
JpaUserRepository->>Database: Native SQL: SELECT u.* FROM users WHERE provider_type = :providerType.name AND provider_id = :providerId
Database-->>JpaUserRepository: User row (including soft-deleted)
JpaUserRepository-->>Service: UserEntity?
Estimated code review effort🎯 2 (Simple) | ⏱️ ~10 minutes Assessment against linked issues
Tip 🔌 Remote MCP (Model Context Protocol) integration is now available!Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats. ✨ Finishing Touches
🧪 Generate unit tests
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. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
Status, Documentation and Community
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 4
📜 Review details
Configuration used: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (2)
infra/src/main/kotlin/org/yapp/infra/user/repository/JpaUserRepository.kt(2 hunks)infra/src/main/kotlin/org/yapp/infra/user/repository/impl/UserRepositoryImpl.kt(1 hunks)
⏰ 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: build-validation
🔇 Additional comments (2)
| providerId: String | ||
| ): User? { | ||
| return jpaUserRepository.findByProviderTypeAndProviderIdIncludingDeleted(providerType, providerId)?.toDomain() | ||
| return jpaUserRepository.findByProviderTypeAndProviderIdIncludingDeleted(providerType.name, providerId)?.toDomain() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
DB 표현 누수 방지를 위해 .name 전달을 없애고 Enum 자체를 전달하세요
Repository 메서드 파라미터를 String으로 변경하면서 infra 계층에서 Enum → String 변환이 필요해졌습니다. 앞선 코멘트처럼 Repository 시그니처를 ProviderType으로 되돌리면, 여기서는 그대로 Enum을 넘기면 됩니다. 타입 안정성과 일관성을 모두 개선합니다.
적용 diff(Repository 시그니처를 ProviderType으로 되돌렸다는 전제):
- return jpaUserRepository.findByProviderTypeAndProviderIdIncludingDeleted(providerType.name, providerId)?.toDomain()
+ return jpaUserRepository.findByProviderTypeAndProviderIdIncludingDeleted(providerType, providerId)?.toDomain()시그니처 변경 후 다른 사용처가 없는지 확인:
🏁 Script executed:
#!/bin/bash
# 메서드 정의와 호출 모두 최신 시그니처(ProviderType, String)로 정렬되었는지 확인
rg -n -C2 'findByProviderTypeAndProviderIdIncludingDeleted\s*\('Length of output: 2790
infra 계층: Repository 메서드에 Enum 직접 전달하도록 수정 필요
아래 두 군데를 함께 변경해주시면 타입 안정성과 일관성을 확보할 수 있습니다.
- infra/src/main/kotlin/org/yapp/infra/user/repository/JpaUserRepository.kt
•findByProviderTypeAndProviderIdIncludingDeleted파라미터 타입 String → ProviderType으로 변경 - infra/src/main/kotlin/org/yapp/infra/user/repository/impl/UserRepositoryImpl.kt
• 호출부에서.name제거하고 Enum 자체를 전달
적용 예시:
--- a/infra/src/main/kotlin/org/yapp/infra/user/repository/JpaUserRepository.kt
+++ b/infra/src/main/kotlin/org/yapp/infra/user/repository/JpaUserRepository.kt
@@ -21,7 +21,7 @@ interface JpaUserRepository : JpaRepository<UserEntity, Long> {
nativeQuery = true
)
fun findByProviderTypeAndProviderIdIncludingDeleted(
- @Param("providerType") providerType: String,
+ @Param("providerType") providerType: ProviderType,
@Param("providerId") providerId: String
): UserEntity?--- a/infra/src/main/kotlin/org/yapp/infra/user/repository/impl/UserRepositoryImpl.kt
+++ b/infra/src/main/kotlin/org/yapp/infra/user/repository/impl/UserRepositoryImpl.kt
@@ -42,7 +42,7 @@ class UserRepositoryImpl(
jpaUserRepository.findByProviderTypeAndProviderIdIncludingDeleted(providerType.name, providerId)
?.toDomain()
- return jpaUserRepository.findByProviderTypeAndProviderIdIncludingDeleted(providerType.name, providerId)?.toDomain()
+ return jpaUserRepository.findByProviderTypeAndProviderIdIncludingDeleted(providerType, providerId)?.toDomain()🤖 Prompt for AI Agents
In
infra/src/main/kotlin/org/yapp/infra/user/repository/impl/UserRepositoryImpl.kt
around line 42, the call passes providerType.name (String) to the JPA method;
change the JPA method signature in
infra/src/main/kotlin/org/yapp/infra/user/repository/JpaUserRepository.kt to
accept ProviderType (enum) instead of String, and update the call here to pass
the ProviderType enum directly (remove .name) so the repository uses the enum
type for type safety and consistency.
infra/src/main/kotlin/org/yapp/infra/user/repository/JpaUserRepository.kt
Show resolved
Hide resolved
| @Query( | ||
| value = "SELECT * FROM users u WHERE u.provider_type = :providerType AND u.provider_id = :providerId", | ||
| nativeQuery = true | ||
| ) | ||
| fun findByProviderTypeAndProviderIdIncludingDeleted( | ||
| @Param("providerType") providerType: String, | ||
| @Param("providerId") providerId: String | ||
| ): UserEntity? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
중복 레코드 존재 시 단일 결과 요구로 인한 런타임 에러 위험 (NonUniqueResultException/IncorrectResultSizeDataAccessException)
해당 버그로 이미 “soft-deleted + 새로 가입된 활성”의 중복이 존재할 수 있습니다. 현재 메서드는 단일 엔티티를 반환하도록 되어 있어, 두 건 이상이 반환되면 런타임 예외가 발생할 수 있습니다. 최소한 결정적 정렬과 1건 제한을 추가해 주세요.
- 옵션 A(활성 우선): 운영 플로우에서 활성 계정을 우선시하려면 deleted_at IS NULL이 먼저 오도록 정렬
- 옵션 B(복구 우선): 복구 로직에서 soft-deleted를 우선시하려면 deleted_at IS NOT NULL이 먼저 오도록 정렬
- 어떤 정책이 맞는지 확인 후 반영 필요
- DB 벤더 중립적으로는 FETCH FIRST 1 ROW ONLY 사용을 권장합니다.
예시 diff:
- @Query(
- value = "SELECT * FROM users u WHERE u.provider_type = :providerType AND u.provider_id = :providerId",
- nativeQuery = true
- )
+ @Query(
+ value = """
+ SELECT u.*
+ FROM users u
+ WHERE u.provider_type = :providerType
+ AND u.provider_id = :providerId
+ ORDER BY CASE WHEN u.deleted_at IS NULL THEN 0 ELSE 1 END, u.updated_at DESC
+ FETCH FIRST 1 ROW ONLY
+ """,
+ nativeQuery = true
+ )검증 스크립트(메서드 사용처 파악 및 정책 결정에 참고):
정책이 확정되지 않았다면, 반환 타입을 List로 바꾸고 상위 계층에서 해석(복구/병합/경고)하는 방안을 고려할 수도 있습니다.
🏁 Script executed:
#!/bin/bash
# 메서드 사용처 확인
rg -n -C3 '\bfindByProviderTypeAndProviderIdIncludingDeleted\s*\('Length of output: 3644
중복 레코드로 인한 NonUniqueResultException 방지: ORDER BY + LIMIT 추가 필수
수정 대상:
- infra/src/main/kotlin/org/yapp/infra/user/repository/JpaUserRepository.kt:
findByProviderTypeAndProviderIdIncludingDeleted메서드
설명:
soft-deleted된 레코드와 동일한 providerType/providerId로 활성 계정이 재가입된 경우, 현재 쿼리는 단일 결과를 기대하므로 2건 이상 반환 시 NonUniqueResultException 또는 IncorrectResultSizeDataAccessException이 발생할 수 있습니다. 이를 방지하기 위해 결정적 정렬 및 1건 제한(FETCH FIRST 1 ROW ONLY)을 추가해 주세요.
예시 diff:
@@ infra/src/main/kotlin/org/yapp/infra/user/repository/JpaUserRepository.kt
- @Query(
- value = "SELECT * FROM users u WHERE u.provider_type = :providerType AND u.provider_id = :providerId",
- nativeQuery = true
- )
+ @Query(
+ value = """
+ SELECT u.*
+ FROM users u
+ WHERE u.provider_type = :providerType
+ AND u.provider_id = :providerId
+ ORDER BY CASE WHEN u.deleted_at IS NULL THEN 0 ELSE 1 END, u.updated_at DESC
+ FETCH FIRST 1 ROW ONLY
+ """,
+ nativeQuery = true
+ )
fun findByProviderTypeAndProviderIdIncludingDeleted(
@Param("providerType") providerType: String,
@Param("providerId") providerId: String
): UserEntity?정책 결정:
- 옵션 A (활성 우선):
deleted_at IS NULL레코드를 먼저 정렬 - 옵션 B (복구 우선):
deleted_at IS NOT NULL레코드를 먼저 정렬
위 정책 중 적합한 방안을 선택해 반영하거나, 정책이 확정되지 않았다면 반환 타입을 List<UserEntity>로 변경 후 호출부에서 처리하도록 고려해 주세요.
검증 스크립트:
rg -n -C3 '\bfindByProviderTypeAndProviderIdIncludingDeleted\s*\('🤖 Prompt for AI Agents
In infra/src/main/kotlin/org/yapp/infra/user/repository/JpaUserRepository.kt
around lines 19-26, the native query for
findByProviderTypeAndProviderIdIncludingDeleted can return multiple rows
(soft-deleted + re-registered active) causing NonUniqueResultException; modify
the query to add a deterministic ORDER BY that prioritizes active records and
limit to a single row (e.g., ORDER BY (deleted_at IS NULL) DESC or equivalent so
NULL deleted_at comes first) with FETCH FIRST 1 ROW ONLY, keeping the method
return type UserEntity?; alternatively if policy undecided, change the return
type to List<UserEntity> and handle deduplication in callers.
| fun findByProviderTypeAndProviderIdIncludingDeleted( | ||
| @Param("providerType") providerType: String, | ||
| @Param("providerId") providerId: String | ||
| ): UserEntity? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
타입 안정성을 위해 파라미터 타입을 ProviderType으로 되돌리는 것을 권장
Repository 메서드에서 ProviderType을 String으로 바꾸면 DB 표현(열 값)이 인프라 경계를 넘어 서비스/도메인 계층까지 전파됩니다. JPA는 Enum 매핑(@Enumerated)을 통해 네이티브 쿼리 파라미터도 적절히 바인딩할 수 있습니다. 가능하다면 ProviderType을 그대로 유지하세요.
아래와 같이 메서드 시그니처를 되돌리고, 구현부에서도 .name 전달을 제거하는 것을 제안합니다.
먼저 UserEntity의 providerType 필드가 @Enumerated(EnumType.STRING)으로 매핑되어 있는지 확인해 주세요. 확인 스크립트:
적용 diff (본 파일 내 변경):
- fun findByProviderTypeAndProviderIdIncludingDeleted(
- @Param("providerType") providerType: String,
- @Param("providerId") providerId: String
- ): UserEntity?
+ fun findByProviderTypeAndProviderIdIncludingDeleted(
+ @Param("providerType") providerType: ProviderType,
+ @Param("providerId") providerId: String
+ ): UserEntity?그리고 호출부(UserRepositoryImpl.kt)의 해당 라인도 같이 수정해야 합니다(해당 파일 코멘트 참고).
🏁 Script executed:
#!/bin/bash
# UserEntity 선언과 providerType 매핑을 확인
rg -n -C3 'class\s+UserEntity\b'
rg -n -C3 'providerType\b'
rg -n -C3 '@Enumerated|@Column|EnumType'Length of output: 52761
ProviderType Enum 사용으로 파라미터 타입 복원 필요
UserEntity.providerType가 @Enumerated(EnumType.STRING)으로 매핑되어 있으므로, 네이티브 쿼리 메서드 시그니처를 ProviderType으로 변경하고, 구현부에서 .name 호출을 제거해야 합니다.
• infra/src/main/kotlin/org/yapp/infra/user/repository/JpaUserRepository.kt
– findByProviderTypeAndProviderIdIncludingDeleted 파라미터를 String → ProviderType으로 수정
• infra/src/main/kotlin/org/yapp/infra/user/repository/impl/UserRepositoryImpl.kt
– 호출부에서 providerType.name → providerType 으로 변경
--- a/infra/src/main/kotlin/org/yapp/infra/user/repository/JpaUserRepository.kt
+++ b/infra/src/main/kotlin/org/yapp/infra/user/repository/JpaUserRepository.kt
@@ -23,7 +23,7 @@ interface JpaUserRepository : JpaRepository<UserEntity, UUID> {
@Query(
value = "SELECT * FROM users u WHERE u.provider_type = :providerType AND u.provider_id = :providerId",
nativeQuery = true
)
- fun findByProviderTypeAndProviderIdIncludingDeleted(
- @Param("providerType") providerType: String,
+ fun findByProviderTypeAndProviderIdIncludingDeleted(
+ @Param("providerType") providerType: ProviderType,
@Param("providerId") providerId: String
): UserEntity?--- a/infra/src/main/kotlin/org/yapp/infra/user/repository/impl/UserRepositoryImpl.kt
+++ b/infra/src/main/kotlin/org/yapp/infra/user/repository/impl/UserRepositoryImpl.kt
@@ -39,7 +39,7 @@ class UserRepositoryImpl(
override fun findByProviderTypeAndProviderIdIncludingDeleted(
providerType: ProviderType,
providerId: String
): User? {
- return jpaUserRepository.findByProviderTypeAndProviderIdIncludingDeleted(providerType.name, providerId)
+ return jpaUserRepository.findByProviderTypeAndProviderIdIncludingDeleted(providerType, providerId)
?.toDomain()
}🤖 Prompt for AI Agents
In infra/src/main/kotlin/org/yapp/infra/user/repository/JpaUserRepository.kt
around lines 23-26, change the method signature parameter types from String to
the ProviderType enum (e.g., @Param("providerType") providerType: ProviderType)
so the repository uses the enum mapped by @Enumerated(EnumType.STRING); then in
infra/src/main/kotlin/org/yapp/infra/user/repository/impl/UserRepositoryImpl.kt
update the call site to pass the ProviderType value directly (remove .name) and
add any necessary import for ProviderType.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
♻️ Duplicate comments (1)
infra/src/main/kotlin/org/yapp/infra/user/repository/JpaUserRepository.kt (1)
19-26: 여러 행 반환 시 IncorrectResultSizeDataAccessException 위험 — 결정적 정렬 + 1건 제한 추가 필요현재 메서드는 단일 엔티티(UserEntity?) 반환을 기대하지만, 이미 존재할 수 있는 “soft-deleted + 재가입된 활성” 중복 상황에서 2건 이상이 조회되면 런타임 예외(NonUniqueResultException/IncorrectResultSizeDataAccessException)가 발생할 수 있습니다. 결정적 정렬과 1건 제한을 추가해 주세요. 정렬 정책(활성 우선 vs 복구 우선)은 비즈니스 정책에 맞춰 선택이 필요합니다.
예시(활성 우선: deleted_at IS NULL 먼저, 동률 시 id DESC):
- @Query( - value = "SELECT u.* FROM users u WHERE u.provider_type = :#{#providerType.name()} AND u.provider_id = :providerId", - nativeQuery = true - ) + @Query( + value = """ + SELECT u.* + FROM users u + WHERE u.provider_type = :#{#providerType.name()} + AND u.provider_id = :providerId + ORDER BY CASE WHEN u.deleted_at IS NULL THEN 0 ELSE 1 END, u.id DESC + FETCH FIRST 1 ROW ONLY + """, + nativeQuery = true + )MySQL 등에서 FETCH FIRST 1 ROW ONLY가 지원되지 않는 경우:
- FETCH FIRST 1 ROW ONLY + LIMIT 1정책이 미정이라면 반환 타입을 List로 바꾸고 상위 레이어에서 해석/병합하는 방안도 고려해 주세요.
📜 Review details
Configuration used: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (1)
infra/src/main/kotlin/org/yapp/infra/user/repository/JpaUserRepository.kt(2 hunks)
🔇 Additional comments (2)
infra/src/main/kotlin/org/yapp/infra/user/repository/JpaUserRepository.kt (2)
5-5: 네이티브 쿼리에서의 파라미터 바인딩 안정화 — @param 추가 적절네이티브 쿼리와 SpEL을 혼용하는 경우 이름 기반 바인딩을 명시하는 것이 안전합니다. 현재 변경은 적절합니다.
19-26: DB 벤더: MySQL8Dialect 사용 중이므로 LIMIT 문법 변경 불필요infra 모듈의 application-persistence.yml을 확인한 결과
- driver-class-name: com.mysql.cj.jdbc.Driver
- spring.jpa.database-platform: org.hibernate.dialect.MySQL8Dialect
로 설정되어 있어, MySQL 기준의
LIMIT문법만 사용하시면 됩니다.
현재 네이티브 쿼리에는LIMIT절이 포함되어 있지 않으므로 별도의 벤더별 처리 없이 유지하셔도 무방합니다.
| @Query( | ||
| value = "SELECT u.* FROM users u WHERE u.provider_type = :#{#providerType.name()} AND u.provider_id = :providerId", | ||
| nativeQuery = true | ||
| ) | ||
| fun findByProviderTypeAndProviderIdIncludingDeleted( | ||
| @Param("providerType") providerType: ProviderType, | ||
| @Param("providerId") providerId: String | ||
| ): UserEntity? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick (assertive)
리그레션 방지 테스트 추가 제안
경계 케이스 중심의 리포지토리/통합 테스트를 추가해 주세요.
- soft-deleted와 active가 공존할 때: 정책에 맞는 1건만 반환되는지
- soft-deleted만 존재할 때: 정상 조회되는지
- 존재하지 않을 때: null 반환되는지
- Enum 바인딩(SpEL .name())이 실제 저장 값과 일치하는지
필요하시면 테스트 스켈레톤 초안 제공 가능합니다.
🤖 Prompt for AI Agents
In infra/src/main/kotlin/org/yapp/infra/user/repository/JpaUserRepository.kt
around lines 19 to 26, add repository/integration tests to prevent regressions:
create tests that (1) insert both an active and a soft-deleted UserEntity with
the same providerType/providerId and assert that the query returns only the
policy-allowed one, (2) insert only a soft-deleted entity and assert it is
returned, (3) assert null is returned when none exist, and (4) verify the enum
binding used in the query (SpEL .name()) matches the stored DB value by
asserting persisted provider_type equals ProviderType.name() for sample entries;
implement cleanup/transactional isolation for each test.
🧹 Nitpick (assertive)
중복 계정 방지용 데이터 모델 가드레일 권장(인덱스/제약)
애플리케이션 레벨 방어(ORDER BY + 1건 제한)와 별개로, 활성 레코드의 중복 생성을 DDL로 차단하는 것을 권장합니다.
- Postgres 권장: 부분 유니크 인덱스
- CREATE UNIQUE INDEX CONCURRENTLY ux_users_provider_active ON users(provider_type, provider_id) WHERE deleted_at IS NULL;
- MySQL 권장: 생성 칼럼(또는 가상 칼럼) 기반 유니크 인덱스
- ALTER TABLE users ADD COLUMN is_active TINYINT(1) GENERATED ALWAYS AS (CASE WHEN deleted_at IS NULL THEN 1 ELSE 0 END) STORED;
- CREATE UNIQUE INDEX ux_users_provider_active ON users(provider_type, provider_id, is_active) WHERE is_active = 1;
- MySQL은 부분 인덱스를 지원하지 않으므로 WHERE 절은 생략하고 3컬럼 유니크로 대체하는 패턴도 흔합니다.
이런 제약을 두면 동일 provider에 대해 활성 상태 중복이 구조적으로 불가능해져, 본 PR과 같은 문제의 재발을 방지할 수 있습니다.
🤖 Prompt for AI Agents
infra/src/main/kotlin/org/yapp/infra/user/repository/JpaUserRepository.kt around
lines 19-26: add a DB-level uniqueness constraint to prevent duplicate active
provider accounts; for Postgres create a partial unique index on (provider_type,
provider_id) where deleted_at IS NULL, and for MySQL add a stored/generated
is_active column (true when deleted_at IS NULL) and a unique index on
(provider_type, provider_id, is_active) or equivalent 3-column unique index,
apply these changes via a new migration, and add/adjust integration tests to
verify attempts to insert duplicate active provider records fail at the database
level.
|
minwoo1999
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
복구 시에는 native query로 복구 하는 방향으로 컨벤션 맞추는 거 좋습니다~
고생하셨습니다.



🔗 관련 이슈
📘 작업 유형
📙 작업 내역
🧪 테스트 내역
🎨 스크린샷 또는 시연 영상 (선택)
✅ PR 체크리스트
💬 추가 설명 or 리뷰 포인트 (선택)
Summary by CodeRabbit