Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package org.yapp.infra.user.repository

import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.data.jpa.repository.Query
import org.springframework.data.repository.query.Param
import org.yapp.domain.user.ProviderType
import org.yapp.infra.user.entity.UserEntity
import java.util.*
Expand All @@ -15,6 +16,12 @@ interface JpaUserRepository : JpaRepository<UserEntity, UUID> {

fun existsByEmail(email: String): Boolean

@Query("SELECT u FROM UserEntity u WHERE u.providerType = :providerType AND u.providerId = :providerId")
fun findByProviderTypeAndProviderIdIncludingDeleted(providerType: ProviderType, providerId: String): UserEntity?
@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?
Copy link

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.

Copy link

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 파라미터를 StringProviderType으로 수정
• infra/src/main/kotlin/org/yapp/infra/user/repository/impl/UserRepositoryImpl.kt
– 호출부에서 providerType.nameproviderType 으로 변경

--- 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.

}
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ class UserRepositoryImpl(
providerType: ProviderType,
providerId: String
): User? {
return jpaUserRepository.findByProviderTypeAndProviderIdIncludingDeleted(providerType, providerId)?.toDomain()
return jpaUserRepository.findByProviderTypeAndProviderIdIncludingDeleted(providerType.name, providerId)?.toDomain()
Copy link

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.

}

override fun deleteById(userId: UUID) {
Expand Down