-
Notifications
You must be signed in to change notification settings - Fork 1
feat: 애플 회원탈퇴 구현 완료 #76
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
Walkthrough이 변경은 회원 프로필 조회 및 약관 동의 엔드포인트를 기존 AuthController에서 UserController로 이동시키고, 애플 회원 탈퇴 기능을 새롭게 추가합니다. 또한 소셜 로그인 전략 구조를 리팩터링하며, 회원 탈퇴를 위한 Apple/Kakao 전략 및 관련 DTO, 서비스, 도메인 계층을 확장합니다. Changes
Sequence Diagram(s)애플 회원 탈퇴 주요 흐름sequenceDiagram
participant Client
participant AuthController
participant AuthUseCase
participant UserAccountService
participant UserDomainService
participant WithdrawStrategyResolver
participant AppleWithdrawStrategy
participant AppleApiManager
participant AppleApi
participant AuthTokenService
Client->>AuthController: DELETE /withdraw (WithdrawRequest)
AuthController->>AuthUseCase: withdraw(userId, withdrawRequest)
AuthUseCase->>UserAccountService: findWithdrawUserById(userId)
UserAccountService->>UserDomainService: findWithdrawUserById(userId)
UserDomainService-->>UserAccountService: WithdrawTargetUserVO
AuthUseCase->>WithdrawStrategyResolver: resolve(providerType)
WithdrawStrategyResolver-->>AuthUseCase: AppleWithdrawStrategy
AuthUseCase->>AppleWithdrawStrategy: withdraw(WithdrawStrategyRequest)
AppleWithdrawStrategy->>AppleApiManager: revokeToken(appleRefreshToken)
AppleApiManager->>AppleApi: revokeAppleToken(clientId, clientSecret, refreshToken, "refresh_token")
AppleApi-->>AppleApiManager: Result<Unit>
AppleApiManager-->>AppleWithdrawStrategy: (성공/실패)
AppleWithdrawStrategy-->>AuthUseCase: (성공/실패)
AuthUseCase->>AuthTokenService: deleteRefreshTokenForSignOutOrWithdraw
AuthUseCase->>UserAccountService: withdrawUser(userId)
UserAccountService->>UserDomainService: deleteUser(userId)
AuthUseCase-->>AuthController: (204 No Content)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~40 minutes Assessment against linked issues
Assessment against linked issues: Out-of-scope changes
Possibly related PRs
Suggested labels
Suggested reviewers
Note ⚡️ Unit Test Generation is now available in beta!Learn more here, or try it out under "Finishing Touches" below. ✨ 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 comments)
Other keywords and placeholders
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: 8
🔭 Outside diff range comments (4)
apis/src/main/kotlin/org/yapp/apis/user/dto/request/FindOrCreateUserRequest.kt (1)
38-46: Nullable 필드 +!!사용은 NPE 위험
providerType과providerId를 nullable 로 선언한 뒤!!로 강제 추출하고 있습니다. DTO 유효성 검증이 누락되면 런타임 NPE 가 발생할 수 있으니, 두 필드를 non-null 타입으로 선언하거나requireNotNull로 방어 로직을 추가하는 편이 안전합니다.- val providerType: ProviderType? = null, + val providerType: ProviderType, ... - val providerId: String? = null + val providerId: Stringapis/src/main/kotlin/org/yapp/apis/user/dto/request/TermsAgreementRequest.kt (1)
7-13: Nullable Boolean 대신 Non-Null 권장
termsAgreed: Boolean?로 두고!!로 꺼내고 있습니다. 불필요한 nullable 처리보다 non-null 으로 선언하면 NPE 위험과 불필요한 캐스팅을 제거할 수 있습니다.- val termsAgreed: Boolean? = null + val termsAgreed: Booleanapis/src/main/kotlin/org/yapp/apis/user/dto/request/FindUserIdentityRequest.kt (2)
5-5: 레거시 패키지 import로 컴파일 실패 가능
UserIdResponse가 아직org.yapp.apis.auth.dto.response에 남아있는지 확인이 필요합니다. 이번 PR 에서 대부분의 DTO 가user.dto.response로 이동했으므로, 해당 import 가 빌드 오류를 유발할 수 있습니다.-import org.yapp.apis.auth.dto.response.UserIdResponse +import org.yapp.apis.user.dto.response.UserIdResponse
12-20: Nullable UUID +!!사용 주의
위 DTO 두 건과 마찬가지로userId를 nullable 로 두고!!로 추출하고 있습니다. Non-Null 로 선언해 NPE 위험을 제거하는 방안을 검토해주세요.- val userId: UUID? = null + val userId: UUID
📜 Review details
Configuration used: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (43)
apis/src/main/kotlin/org/yapp/apis/auth/controller/AuthController.kt(2 hunks)apis/src/main/kotlin/org/yapp/apis/auth/controller/AuthControllerApi.kt(2 hunks)apis/src/main/kotlin/org/yapp/apis/auth/dto/request/GenerateTokenPairRequest.kt(1 hunks)apis/src/main/kotlin/org/yapp/apis/auth/dto/request/SaveAppleRefreshTokenRequest.kt(1 hunks)apis/src/main/kotlin/org/yapp/apis/auth/dto/request/SocialLoginRequest.kt(3 hunks)apis/src/main/kotlin/org/yapp/apis/auth/dto/request/WithdrawRequest.kt(1 hunks)apis/src/main/kotlin/org/yapp/apis/auth/dto/request/WithdrawStrategyRequest.kt(1 hunks)apis/src/main/kotlin/org/yapp/apis/auth/exception/AuthErrorCode.kt(1 hunks)apis/src/main/kotlin/org/yapp/apis/auth/manager/AppleApiManager.kt(1 hunks)apis/src/main/kotlin/org/yapp/apis/auth/service/AuthTokenService.kt(1 hunks)apis/src/main/kotlin/org/yapp/apis/auth/strategy/AuthStrategy.kt(0 hunks)apis/src/main/kotlin/org/yapp/apis/auth/strategy/signin/AppleSignInStrategy.kt(2 hunks)apis/src/main/kotlin/org/yapp/apis/auth/strategy/signin/KakaoSignInStrategy.kt(3 hunks)apis/src/main/kotlin/org/yapp/apis/auth/strategy/signin/SignInCredentials.kt(1 hunks)apis/src/main/kotlin/org/yapp/apis/auth/strategy/signin/SignInStrategy.kt(1 hunks)apis/src/main/kotlin/org/yapp/apis/auth/strategy/signin/SignInStrategyResolver.kt(1 hunks)apis/src/main/kotlin/org/yapp/apis/auth/strategy/withdraw/AppleWithdrawStrategy.kt(1 hunks)apis/src/main/kotlin/org/yapp/apis/auth/strategy/withdraw/KakaoWithdrawStrategy.kt(1 hunks)apis/src/main/kotlin/org/yapp/apis/auth/strategy/withdraw/WithdrawStrategy.kt(1 hunks)apis/src/main/kotlin/org/yapp/apis/auth/strategy/withdraw/WithdrawStrategyResolver.kt(1 hunks)apis/src/main/kotlin/org/yapp/apis/auth/usecase/AuthUseCase.kt(2 hunks)apis/src/main/kotlin/org/yapp/apis/book/dto/request/UserBooksByIsbnsRequest.kt(1 hunks)apis/src/main/kotlin/org/yapp/apis/book/service/UserBookService.kt(1 hunks)apis/src/main/kotlin/org/yapp/apis/book/usecase/BookUseCase.kt(6 hunks)apis/src/main/kotlin/org/yapp/apis/readingrecord/usecase/ReadingRecordUseCase.kt(5 hunks)apis/src/main/kotlin/org/yapp/apis/seed/usecase/SeedUseCase.kt(1 hunks)apis/src/main/kotlin/org/yapp/apis/user/controller/UserController.kt(1 hunks)apis/src/main/kotlin/org/yapp/apis/user/controller/UserControllerApi.kt(1 hunks)apis/src/main/kotlin/org/yapp/apis/user/dto/request/FindOrCreateUserRequest.kt(1 hunks)apis/src/main/kotlin/org/yapp/apis/user/dto/request/FindUserIdentityRequest.kt(1 hunks)apis/src/main/kotlin/org/yapp/apis/user/dto/request/TermsAgreementRequest.kt(1 hunks)apis/src/main/kotlin/org/yapp/apis/user/dto/response/CreateUserResponse.kt(1 hunks)apis/src/main/kotlin/org/yapp/apis/user/dto/response/UserAuthInfoResponse.kt(1 hunks)apis/src/main/kotlin/org/yapp/apis/user/dto/response/UserProfileResponse.kt(1 hunks)apis/src/main/kotlin/org/yapp/apis/user/dto/response/WithdrawTargetUserResponse.kt(1 hunks)apis/src/main/kotlin/org/yapp/apis/user/service/UserAccountService.kt(2 hunks)apis/src/main/kotlin/org/yapp/apis/user/service/UserService.kt(1 hunks)apis/src/main/kotlin/org/yapp/apis/user/usecase/UserUseCase.kt(1 hunks)domain/src/main/kotlin/org/yapp/domain/user/UserDomainService.kt(3 hunks)domain/src/main/kotlin/org/yapp/domain/user/UserRepository.kt(1 hunks)domain/src/main/kotlin/org/yapp/domain/user/vo/WithdrawTargetUserVO.kt(1 hunks)infra/src/main/kotlin/org/yapp/infra/external/oauth/apple/AppleApi.kt(1 hunks)infra/src/main/kotlin/org/yapp/infra/user/repository/impl/UserRepositoryImpl.kt(1 hunks)
💤 Files with no reviewable changes (1)
- apis/src/main/kotlin/org/yapp/apis/auth/strategy/AuthStrategy.kt
⏰ 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 (54)
apis/src/main/kotlin/org/yapp/apis/user/dto/request/FindOrCreateUserRequest.kt (2)
48-54: 기본 이메일 생성 로직의 의도 확인
"${validProviderId()}@${validProviderType().name.lowercase()}.local"형태로 이메일을 생성하고 있습니다.
providerId가 UUID 등 민감 정보일 경우 노출 위험이 있습니다.lowercase()변환은 Enum 값이 한국어로 변경될 때 예측 불가한 문자열을 만들 수 있습니다.의도적인 설계인지 다시 한 번 확인해 주세요.
1-1: 레거시 import 검토 완료 — 더 이상auth.dto.request.FindOrCreateUserRequest참조 없음
rg --line-number 'org\.yapp\.apis\.auth\.dto\.request\.FindOrCreateUserRequest'실행 결과, 해당 패키지의 레거시 import는 모두 제거된 상태임을 확인했습니다.apis/src/main/kotlin/org/yapp/apis/user/dto/request/TermsAgreementRequest.kt (1)
1-1: 패키지 경로 변경 후 사용처 확인
TermsAgreementRequest를 호출하는 컨트롤러/서비스의 import 도 모두user.dto.request로 교체되었는지 확인 부탁드립니다.apis/src/main/kotlin/org/yapp/apis/user/dto/response/CreateUserResponse.kt (1)
1-1: 패키지 이동 확인 완료
패키지 변경 외 로직 수정이 없으며 기존 매핑 메서드도 영향이 없습니다.apis/src/main/kotlin/org/yapp/apis/book/service/UserBookService.kt (1)
8-9: import 경로 교체 확인
UserBooksByIsbnsRequest경로가auth.dto.request→book.dto.request로 변경되었습니다. 나머지 레이어(Controller/UseCase) 의 import 도 동일하게 교체되었는지 점검 바랍니다.apis/src/main/kotlin/org/yapp/apis/user/dto/response/UserAuthInfoResponse.kt (1)
1-1: 패키지 구조 개선이 적절합니다.사용자 관련 DTO를
auth패키지에서user패키지로 이동한 것은 관심사 분리 측면에서 올바른 리팩터링입니다. 인증 로직과 사용자 관리 로직을 명확히 구분하는 좋은 변경사항입니다.apis/src/main/kotlin/org/yapp/apis/user/dto/response/UserProfileResponse.kt (1)
1-1: 패키지 이동이 아키텍처 관점에서 적절합니다.사용자 프로필 응답 DTO를
user패키지로 이동한 것은 도메인 경계를 명확히 하는 좋은 결정입니다.apis/src/main/kotlin/org/yapp/apis/auth/dto/request/GenerateTokenPairRequest.kt (1)
5-6: 임포트 업데이트가 리팩터링과 일관성 있게 적용되었습니다.DTO 패키지 이동에 따른 임포트 경로 변경이 올바르게 반영되었습니다.
apis/src/main/kotlin/org/yapp/apis/auth/dto/request/SaveAppleRefreshTokenRequest.kt (1)
6-7: 전략 패턴 구조화와 DTO 패키지 분리가 잘 반영되었습니다.
CreateUserResponse의 user 패키지 이동과AppleAuthCredentials의 signin 하위 패키지 구성이 전략 패턴 구현을 더욱 체계적으로 만들어 줍니다.domain/src/main/kotlin/org/yapp/domain/user/UserRepository.kt (1)
19-19: 회원 탈퇴 기능 구현을 위한 필수 메서드 추가가 적절합니다.
deleteById메서드는 애플 회원 탈퇴 기능 구현에 필요한 핵심 메서드입니다. UUID 매개변수 사용과 Unit 반환 타입이 기존 repository 패턴과 일관성 있게 설계되었습니다.apis/src/main/kotlin/org/yapp/apis/book/dto/request/UserBooksByIsbnsRequest.kt (1)
1-1: 패키지 재구성이 도메인 관점에서 적절합니다.
UserBooksByIsbnsRequest를auth패키지에서book패키지로 이동한 것은 도메인 주도 설계 관점에서 올바른 결정입니다. 이 DTO는 사용자의 도서 정보를 다루므로 book 도메인에 속하는 것이 더 적합합니다.apis/src/main/kotlin/org/yapp/apis/seed/usecase/SeedUseCase.kt (1)
6-6: 서비스 리팩터링이 아키텍처 개선과 일치합니다.
UserAuthService에서UserService로의 변경이 적절합니다. 사용자 검증 로직이UserService로 이동된 것은 도메인 분리 관점에서 올바른 결정이며, 다른 UseCase들과 일관성을 유지합니다.Also applies to: 13-13, 17-17
infra/src/main/kotlin/org/yapp/infra/external/oauth/apple/AppleApi.kt (1)
39-39: 메소드 명명이 더 명확해졌습니다.
revokeToken에서revokeAppleToken으로 변경한 것은 Apple 특화 토큰 해지 기능임을 명확히 하여 가독성을 향상시킵니다. 회원 탈퇴 기능 구현과 잘 연계됩니다.infra/src/main/kotlin/org/yapp/infra/user/repository/impl/UserRepositoryImpl.kt (1)
45-47: deleteById 메소드의 예외 처리 및 Cascade 설정 검증이 필요합니다.다음 사항을 검토해 주세요:
- jpaUserRepository.deleteById(userId) 호출 시
• 존재하지 않는 ID 입력 시 어떤 예외(예: EmptyResultDataAccessException)가 발생하는지 확인
• 예외 발생 시 비즈니스 레이어에서 어떻게 처리할지 결정- UserEntity의 연관 관계에 대해
• CascadeType.REMOVE 또는 orphanRemoval 설정 여부 확인
• 연관 엔터티 삭제가 의도대로 이루어지는지 검증apis/src/main/kotlin/org/yapp/apis/readingrecord/usecase/ReadingRecordUseCase.kt (1)
10-10: 서비스 리팩터링이 일관성 있게 적용되었습니다.
UserAuthService에서UserService로의 변경이 모든 메소드에서 일관성 있게 적용되었습니다. 사용자 검증 로직의 도메인 분리가 잘 이루어져 있으며, 다른 UseCase들과 동일한 패턴을 따르고 있습니다.Also applies to: 19-19, 28-28, 42-42, 56-56
apis/src/main/kotlin/org/yapp/apis/auth/manager/AppleApiManager.kt (1)
39-56: 구현이 올바르게 되었습니다Apple 토큰 취소 기능이 적절히 구현되어 있고, 에러 처리와 로깅도 잘 되어 있습니다. 기존 fetchAppleOauthTokens 메서드와 일관성 있는 패턴을 따르고 있어 좋습니다.
apis/src/main/kotlin/org/yapp/apis/book/usecase/BookUseCase.kt (2)
27-27: 서비스 의존성 변경이 올바르게 적용되었습니다UserAuthService에서 UserService로의 리팩터링이 일관성 있게 적용되었고, 모든 validateUserExists 호출이 적절히 업데이트되었습니다.
7-7: import 경로 수정 확인UserBooksByIsbnsRequest의 import 경로가 올바른 패키지로 수정되었습니다.
apis/src/main/kotlin/org/yapp/apis/auth/strategy/signin/SignInStrategy.kt (1)
6-11: 전략 패턴이 올바르게 구현되었습니다SignInStrategy 인터페이스가 깔끔하게 정의되어 있고, authenticate 메서드가 명확한 반환 타입을 가지고 있어 좋습니다. WithdrawStrategy와 일관성 있는 구조를 가지고 있습니다.
apis/src/main/kotlin/org/yapp/apis/auth/strategy/signin/SignInCredentials.kt (1)
1-21: 리팩터링이 깔끔하게 완료되었습니다AuthCredentials에서 SignInCredentials로의 리네이밍과 패키지 재구성이 일관성 있게 적용되었습니다. sealed class를 사용한 구조도 적절하고, 각 구현체의 getProviderType() 메서드도 올바르게 구현되어 있습니다.
apis/src/main/kotlin/org/yapp/apis/auth/strategy/withdraw/WithdrawStrategyResolver.kt (1)
6-14: 전략 패턴 구현이 잘 되어 있습니다!WithdrawStrategyResolver 클래스가 깔끔하게 구현되었습니다. 의존성 주입을 통해 전략 목록을 받아오고, 적절한 예외 처리와 함께 provider type에 따른 전략을 올바르게 선택합니다.
apis/src/main/kotlin/org/yapp/apis/auth/dto/request/SocialLoginRequest.kt (2)
7-9: 리팩터링이 적절하게 반영되었습니다.인증 전략을 로그인과 탈퇴로 분리하면서 import 경로와 클래스명이 올바르게 변경되었습니다.
AuthCredentials에서SignInCredentials로의 이름 변경이 의미적으로 더 명확합니다.
44-61: 메서드 시그니처 변경이 적절합니다.
toCredentials메서드의 반환 타입이SignInCredentials로 변경되어 리팩터링과 일관성을 유지합니다. 예외 처리 로직도 그대로 유지되어 있습니다.apis/src/main/kotlin/org/yapp/apis/auth/exception/AuthErrorCode.kt (1)
23-24: 새로운 에러 코드가 적절하게 추가되었습니다.탈퇴 기능을 위한 두 가지 에러 코드가 잘 정의되었습니다:
PROVIDER_TYPE_MISMATCH: 탈퇴 요청 시 프로바이더 타입 불일치 검증APPLE_REFRESH_TOKEN_MISSING: 애플 탈퇴 시 리프레시 토큰 누락에러 코드 순서와 메시지 형식이 기존 패턴과 일관성을 유지합니다.
domain/src/main/kotlin/org/yapp/domain/user/vo/WithdrawTargetUserVO.kt (1)
6-22: 도메인 값 객체가 잘 설계되었습니다.WithdrawTargetUserVO가 도메인 모델링 원칙에 따라 잘 구현되었습니다:
- private constructor로 제어된 인스턴스 생성
- companion object의 factory 메서드로 User 엔티티에서 안전한 변환
- 탈퇴에 필요한 데이터만 포함하여 관심사 분리
appleRefreshToken의 nullable 처리가 Apple 사용자에게만 해당되는 로직을 잘 반영불변 객체로 설계되어 안전하고 명확한 데이터 전달이 가능합니다.
domain/src/main/kotlin/org/yapp/domain/user/UserDomainService.kt (1)
27-30: LGTM! 일관된 패턴으로 구현되었습니다.기존 find 메서드들과 동일한 패턴으로 구현되어 있고, 적절한 예외 처리가 되어 있습니다.
apis/src/main/kotlin/org/yapp/apis/auth/strategy/withdraw/AppleWithdrawStrategy.kt (2)
16-21: Apple 회원탈퇴 전략이 올바르게 구현되었습니다.Apple 리프레시 토큰 검증과 예외 처리가 적절하게 구현되어 있고,
appleApiManager.revokeToken호출을 통해 실제 토큰 취소 작업을 수행하고 있습니다.
1-14: 전략 패턴과 패키지 구조가 잘 설계되었습니다.회원탈퇴 전략을 별도 패키지로 분리하고, Spring의 의존성 주입을 적절히 활용한 깔끔한 구조입니다.
apis/src/main/kotlin/org/yapp/apis/user/usecase/UserUseCase.kt (1)
10-23: 사용자 관련 로직이 잘 분리되어 구현되었습니다.
@Transactional(readOnly = true)를 기본으로 설정하고, 쓰기 작업에서만@Transactional을 오버라이드하는 것이 효율적입니다. 각 메서드가UserService에 명확하게 위임하여 책임이 잘 분리되어 있습니다.apis/src/main/kotlin/org/yapp/apis/auth/strategy/signin/SignInStrategyResolver.kt (1)
8-17: 로그인 전략 리졸버가 명확하게 분리되어 구현되었습니다.제네릭한
AuthStrategy에서 구체적인SignInStrategy로 변경하여 책임이 더 명확해졌습니다. 지원하지 않는 제공자 타입에 대한 예외 처리도 적절합니다.apis/src/main/kotlin/org/yapp/apis/user/dto/response/WithdrawTargetUserResponse.kt (1)
8-36: 잘 구현된 DTO 클래스입니다.회원 탈퇴를 위한 사용자 정보를 담는 응답 DTO가 올바르게 구현되었습니다. private 생성자와 companion object의 팩토리 메서드 패턴, Swagger 문서화, 그리고 도메인 VO에서 DTO로의 변환 로직이 적절합니다.
apis/src/main/kotlin/org/yapp/apis/auth/dto/request/WithdrawStrategyRequest.kt (1)
8-45: 전략 패턴을 위한 요청 DTO가 잘 구현되었습니다.회원 탈퇴 처리를 위한 내부 요청 DTO가 적절하게 구현되었습니다. WithdrawTargetUserResponse에서의 변환 로직이 정확하고, Swagger 문서화도 예시와 함께 잘 작성되었습니다.
apis/src/main/kotlin/org/yapp/apis/auth/strategy/signin/AppleSignInStrategy.kt (3)
1-1: 패키지 구조 개선이 적절합니다.인증 전략을 signin과 withdraw로 분리하여 더 구체적인 패키지 구조로 개선된 점이 좋습니다.
13-15: 인터페이스 변경이 적절합니다.AuthStrategy에서 SignInStrategy로 인터페이스를 변경하여 책임을 더 명확히 구분한 점이 좋습니다.
20-25: 메서드 시그니처 업데이트가 정확합니다.SignInCredentials로 파라미터 타입을 변경하여 새로운 인터페이스와 일관성을 유지했습니다. 핵심 인증 로직은 그대로 유지되어 안정성이 보장됩니다.
apis/src/main/kotlin/org/yapp/apis/auth/controller/AuthController.kt (2)
9-9: 새로운 DTO 임포트가 적절합니다.회원 탈퇴 기능을 위한 WithdrawRequest 임포트가 추가되었습니다.
38-45: 회원 탈퇴 엔드포인트가 올바르게 구현되었습니다.DELETE HTTP 메서드 사용, @Valid를 통한 요청 검증, AuthenticationPrincipal을 통한 사용자 식별, 그리고 204 No Content 응답이 모두 적절합니다. REST 규약을 잘 따르고 있습니다.
apis/src/main/kotlin/org/yapp/apis/auth/strategy/signin/KakaoSignInStrategy.kt (3)
1-1: 패키지 구조 개선이 일관성 있게 적용되었습니다.AppleSignInStrategy와 동일하게 signin 패키지로 이동하여 일관된 구조를 유지했습니다.
14-16: 인터페이스 변경이 일관성 있게 적용되었습니다.SignInStrategy 인터페이스 구현으로 Apple 전략과 일관된 구조를 유지했습니다.
22-34: 메서드 시그니처 업데이트가 정확합니다.SignInCredentials 타입으로 변경하여 새로운 인터페이스와 일관성을 유지했습니다. 기존 Kakao 인증 로직과 예외 처리가 그대로 유지되어 안정성이 보장됩니다.
apis/src/main/kotlin/org/yapp/apis/auth/service/AuthTokenService.kt (3)
40-42: 메소드명 변경이 적절함
deleteTokenForReissue에서deleteRefreshTokenForReissue로 변경하여 메소드의 목적이 더 명확해졌습니다.
44-46: 회원탈퇴 기능을 위한 메소드명 확장이 적절함
deleteTokenForSignOut에서deleteRefreshTokenForSignOutOrWithdraw로 변경하여 로그아웃과 회원탈퇴 모두에서 사용할 수 있도록 확장한 것이 적절합니다. 다만@Valid어노테이션이 유지된 것은 좋습니다.
35-38: @Valid 어노테이션 제거 검증 결과AuthTokenService.validateAndGetUserIdFromRefreshToken에서 @Valid를 제거했지만,
- 컨트롤러(AuthController / AuthControllerApi)의
@RequestBody @Valid TokenRefreshRequest- RefreshTokenService 클래스(@validated) 내
getUserIdByToken(@Valid TokenRefreshRequest)두 단계에서 모두 TokenRefreshRequest에 대한 Bean Validation이 수행되므로, AuthTokenService 레벨에서 추가적인 @Valid는 불필요합니다.
apis/src/main/kotlin/org/yapp/apis/user/service/UserAccountService.kt (2)
1-19: 서비스 패키지 분리가 적절함
UserAuthService에서UserAccountService로 이름을 변경하고auth.service에서user.service패키지로 이동한 것이 관심사 분리 측면에서 적절합니다.
40-47: 회원탈퇴 로직 구현 검증 필요UserAccountService에서 호출하는 UserDomainService의
findWithdrawUserById및deleteUser메소드 구현을 확인할 수 없습니다. 해당 메소드들이 다음 요구사항을 충족하는지 직접 검증 부탁드립니다:
- 권한 검증: 본인 인증 또는 관리자 권한 확인 로직 적용 여부
- 트랜잭션 처리:
@Transactional등 일관된 트랜잭션 경계 설정확인 대상 위치 예시:
- apis/src/main/kotlin/org/yapp/apis/user/service/UserDomainService.kt
apis/src/main/kotlin/org/yapp/apis/auth/controller/AuthControllerApi.kt (2)
12-17: 필요한 import 추가가 적절함회원탈퇴 기능을 위한
DeleteMapping과WithdrawRequestimport가 적절하게 추가되었습니다.
91-115: 회원탈퇴 API 문서화가 포괄적임회원탈퇴 엔드포인트의 OpenAPI 문서화가 잘 작성되어 있습니다. 특히 Apple/Kakao 서버 연결 해제 실패에 대한 500 에러 응답을 명시한 것이 좋습니다.
apis/src/main/kotlin/org/yapp/apis/user/controller/UserController.kt (1)
1-33: 사용자 관련 엔드포인트 분리가 잘 구현됨인증 관련 컨트롤러에서 사용자 관련 기능을 별도 컨트롤러로 분리한 것이 적절합니다. Spring Boot 규칙을 잘 따르고 있으며, 유효성 검사와 인증 처리도 올바르게 구현되었습니다.
apis/src/main/kotlin/org/yapp/apis/user/controller/UserControllerApi.kt (1)
1-87: 사용자 API 인터페이스가 잘 설계됨사용자 관련 API를 별도 인터페이스로 분리하고 포괄적인 OpenAPI 문서화를 제공한 것이 매우 좋습니다. 에러 응답에 대한 문서화도 적절하게 작성되었습니다.
apis/src/main/kotlin/org/yapp/apis/auth/usecase/AuthUseCase.kt (5)
23-33: 좋은 구조 개선입니다!의존성이 더 명확하게 분리되어 있고, 각 서비스의 책임이 명확해졌습니다. SignIn과 Withdraw 전략을 분리한 것은 좋은 설계 결정입니다.
34-46: 잘 구현된 로그인 플로우입니다!전략 패턴을 활용한 로그인 구현이 깔끔합니다. Apple 리프레시 토큰 처리를 별도 메소드로 분리한 것도 좋은 결정입니다.
48-58: 토큰 재발급 로직이 명확해졌습니다!메소드명이 더 명시적으로 변경되어 가독성이 향상되었습니다.
70-75: 적절한 보안 검증입니다!요청된 provider type과 실제 사용자의 provider type을 비교하는 것은 중요한 보안 조치입니다. 명확한 에러 메시지도 좋습니다.
85-93: Apple 토큰 처리가 잘 캡슐화되었습니다!타입 체크를 통한 조건부 처리가 깔끔하고, Apple 특화 로직이 잘 분리되어 있습니다.
apis/src/main/kotlin/org/yapp/apis/auth/strategy/withdraw/KakaoWithdrawStrategy.kt
Show resolved
Hide resolved
apis/src/main/kotlin/org/yapp/apis/auth/strategy/withdraw/WithdrawStrategy.kt
Show resolved
Hide resolved
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: 1
📜 Review details
Configuration used: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (2)
apis/src/main/kotlin/org/yapp/apis/auth/service/RefreshTokenService.kt(2 hunks)domain/src/main/kotlin/org/yapp/domain/token/RefreshTokenDomainService.kt(1 hunks)
🔇 Additional comments (2)
domain/src/main/kotlin/org/yapp/domain/token/RefreshTokenDomainService.kt (1)
12-14: 클래스 이름 변경 확인 완료
TokenDomainRedisService→RefreshTokenDomainService로의 리네이밍이 일관성 있게 반영되었습니다. DI·패키지 구조 모두 문제없어 보입니다.apis/src/main/kotlin/org/yapp/apis/auth/service/RefreshTokenService.kt (1)
10-17: 의존성 교체에 따른 빈 주입 검증 필요
RefreshTokenDomainService로 교체되면서 스프링 컨텍스트에 해당 빈이 정상적으로 등록돼 있어야 합니다.
실행 전@DomainService애노테이션이 컴포넌트 스캔 범위에 포함되는지 다시 한번 확인해주세요.
- AuthUseCase.withdraw()에서 @transactional 제거하여 외부 API 호출을 트랜잭션 밖으로 분리 - UserWithdrawalService.processWithdrawal()에 @transactional 추가하여 DB 작업을 별도 트랜잭션으로 격리 - UserSignInService 추가로 로그인 시 사용자 생성 로직 분리 - 외부 API 호출 실패 시 DB 롤백 문제 해결 - 단방향 의존성 auth → user 유지 변경 사항: - AuthUseCase.withdraw(): 트랜잭션 제거, 외부 API 호출 후 UserWithdrawalService 호출 - UserWithdrawalService: 토큰 삭제와 사용자 삭제를 하나의 트랜잭션에서 처리 - UserSignInService: 로그인 시 사용자 생성 로직 분리
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)
apis/src/main/kotlin/org/yapp/apis/auth/usecase/AuthUseCase.kt (1)
66-80: 외부 API와 DB 작업 간 일관성 문제 개선 필요현재 구현은 외부 API 호출이 트랜잭션 밖에서 실행되어 이전 리뷰의 우려사항을 부분적으로 해결했습니다. 하지만 여전히 다음과 같은 경쟁 조건이 존재합니다:
- Line 77: 외부 API 호출 성공 (Apple/Kakao 토큰 폐기)
- Line 79: DB 작업 실패 (시스템 오류 등)
- 결과: 외부 서비스에서는 탈퇴되었지만 내부 시스템에는 사용자가 남아있는 불일치 상태
보상 트랜잭션 패턴 또는 Saga 패턴 적용을 고려하세요:
+ @Transactional fun withdraw(userId: UUID, withdrawRequest: WithdrawRequest) { val withdrawTargetUserResponse = userAccountService.findWithdrawUserById(userId) if (withdrawTargetUserResponse.providerType != withdrawRequest.validProviderType()) { throw AuthException( AuthErrorCode.PROVIDER_TYPE_MISMATCH, "The provider type in the request does not match the user's actual provider type." ) } + // 1. 먼저 내부 시스템에서 사용자를 "탈퇴 진행 중" 상태로 변경 + userWithdrawalService.markUserAsWithdrawing(userId) + + try { + // 2. 외부 API 호출 (트랜잭션 커밋 후 실행되도록 이벤트 발행 고려) val strategy = withdrawStrategyResolver.resolve(withdrawRequest.validProviderType()) strategy.withdraw(WithdrawStrategyRequest.from(withdrawTargetUserResponse)) - - userWithdrawalService.processWithdrawal(userId) + + // 3. 성공 시 완전 탈퇴 처리 + userWithdrawalService.completeWithdrawal(userId) + } catch (e: Exception) { + // 4. 실패 시 보상 처리 (상태 복구 또는 재시도 스케줄링) + userWithdrawalService.rollbackWithdrawal(userId) + throw e + } }또는 이벤트 기반 비동기 처리를 통해 최종 일관성을 보장하는 방법도 고려해보세요.
📜 Review details
Configuration used: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (3)
apis/src/main/kotlin/org/yapp/apis/auth/service/UserSignInService.kt(1 hunks)apis/src/main/kotlin/org/yapp/apis/auth/service/UserWithdrawalService.kt(1 hunks)apis/src/main/kotlin/org/yapp/apis/auth/usecase/AuthUseCase.kt(2 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
apis/src/main/kotlin/org/yapp/apis/auth/service/UserWithdrawalService.kt
Show resolved
Hide resolved
- signIn 로직에서 외부 API 호출과 DB 트랜잭션의 경계를 분리했습니다. - AuthUseCase.signIn()은 트랜잭션 없이 외부 API 호출 및 흐름 제어만 담당하도록 수정했습니다. - 실제 DB 작업은 UserSignInService.processSignIn()이라는 별도의 트랜잭션 메서드로 격리하여 처리합니다. - 외부 API 지연이 DB 커넥션 풀에 영향을 주던 잠재적인 성능 문제를 해결하고, 로그인 프로세스의 안정성을 확보했습니다.
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: 1
♻️ Duplicate comments (2)
apis/src/main/kotlin/org/yapp/apis/auth/service/UserSignInService.kt (1)
14-38: 불필요한 추상화 레이어와 책임 분리 문제
UserSignInService가 Apple 특화 로직을 포함하고 있어 단일 책임 원칙을 위반합니다. Apple refresh token 처리는AppleSignInStrategy내부에서 처리되어야 합니다.다음과 같이 리팩토링을 제안합니다:
- Apple refresh token 저장 로직을
AppleSignInStrategy.authenticate()메서드로 이동UserSignInService제거 후AuthUseCase에서 직접UserAccountService사용// AppleSignInStrategy.kt override fun authenticate(credentials: SignInCredentials): UserCreateInfoResponse { // ... 기존 인증 로직 ... + val createUserResponse = userAccountService.findOrCreateUser(request) + if (credentials is AppleAuthCredentials) { + val saveTokenRequest = SaveAppleRefreshTokenRequest.of(createUserResponse, credentials) + appleAuthService.saveAppleRefreshTokenIfMissing(saveTokenRequest) + } return userCreateInfoResponse }apis/src/main/kotlin/org/yapp/apis/auth/usecase/AuthUseCase.kt (1)
73-76: 외부 API 호출 순서 및 보상 로직 추가 필요Apple 전략에서
appleApiManager.revokeToken()호출 후 곧바로 DB 철회 처리를 수행하고 있어, 외부 호출 성공·DB 실패 시 상태 불일치가 발생할 수 있습니다.
Kakao 전략은 현재 외부 호출이 주석 처리되어 있지만, 추후 활성화 시 동일 이슈가 발생할 수 있습니다.
- 대상 클래스:
apis/src/main/kotlin/org/yapp/apis/auth/usecase/AuthUseCase.kt- 대상 메서드:
withdraw(Lines 73–76)제안 예시:
val strategy = withdrawStrategyResolver.resolve(withdrawRequest.validProviderType()) - strategy.withdraw(WithdrawStrategyRequest.from(withdrawTargetUserResponse)) - userWithdrawalService.processWithdrawal(userId) + try { + strategy.withdraw(WithdrawStrategyRequest.from(withdrawTargetUserResponse)) + userWithdrawalService.processWithdrawal(userId) + } catch (e: Exception) { + // • 외부 API 호출 성공 후 DB 실패 시 보상 로직 또는 알림 처리 필요 + logger.error("Withdrawal inconsistency for user $userId", e) + throw e + }또는 Saga 패턴, 보상 트랜잭션 등 장기적 안정성을 위한 아키텍처 검토를 권장합니다.
📜 Review details
Configuration used: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (2)
apis/src/main/kotlin/org/yapp/apis/auth/service/UserSignInService.kt(1 hunks)apis/src/main/kotlin/org/yapp/apis/auth/usecase/AuthUseCase.kt(2 hunks)
| fun withdraw(userId: UUID, withdrawRequest: WithdrawRequest) { | ||
| val withdrawTargetUserResponse = userAccountService.findWithdrawUserById(userId) | ||
|
|
||
| @Transactional | ||
| fun updateTermsAgreement(userId: UUID, termsAgreed: Boolean): UserProfileResponse { | ||
| return userAuthService.updateTermsAgreement(userId, termsAgreed) | ||
| if (withdrawTargetUserResponse.providerType != withdrawRequest.validProviderType()) { | ||
| throw AuthException( | ||
| AuthErrorCode.PROVIDER_TYPE_MISMATCH, | ||
| "The provider type in the request does not match the user's actual provider type." | ||
| ) | ||
| } | ||
|
|
||
| val strategy = withdrawStrategyResolver.resolve(withdrawRequest.validProviderType()) | ||
| strategy.withdraw(WithdrawStrategyRequest.from(withdrawTargetUserResponse)) | ||
|
|
||
| userWithdrawalService.processWithdrawal(userId) | ||
| } |
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.
트랜잭션 어노테이션 누락
withdraw 메서드가 @Transactional 어노테이션 없이 구현되어 클래스 레벨의 @Transactional(readOnly = true)를 상속받고 있습니다. 회원 탈퇴는 쓰기 작업이므로 읽기 전용 트랜잭션으로 실행되면 안 됩니다.
+ @Transactional
fun withdraw(userId: UUID, withdrawRequest: WithdrawRequest) {📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| fun withdraw(userId: UUID, withdrawRequest: WithdrawRequest) { | |
| val withdrawTargetUserResponse = userAccountService.findWithdrawUserById(userId) | |
| @Transactional | |
| fun updateTermsAgreement(userId: UUID, termsAgreed: Boolean): UserProfileResponse { | |
| return userAuthService.updateTermsAgreement(userId, termsAgreed) | |
| if (withdrawTargetUserResponse.providerType != withdrawRequest.validProviderType()) { | |
| throw AuthException( | |
| AuthErrorCode.PROVIDER_TYPE_MISMATCH, | |
| "The provider type in the request does not match the user's actual provider type." | |
| ) | |
| } | |
| val strategy = withdrawStrategyResolver.resolve(withdrawRequest.validProviderType()) | |
| strategy.withdraw(WithdrawStrategyRequest.from(withdrawTargetUserResponse)) | |
| userWithdrawalService.processWithdrawal(userId) | |
| } | |
| @Transactional | |
| fun withdraw(userId: UUID, withdrawRequest: WithdrawRequest) { | |
| val withdrawTargetUserResponse = userAccountService.findWithdrawUserById(userId) | |
| if (withdrawTargetUserResponse.providerType != withdrawRequest.validProviderType()) { | |
| throw AuthException( | |
| AuthErrorCode.PROVIDER_TYPE_MISMATCH, | |
| "The provider type in the request does not match the user's actual provider type." | |
| ) | |
| } | |
| val strategy = withdrawStrategyResolver.resolve(withdrawRequest.validProviderType()) | |
| strategy.withdraw(WithdrawStrategyRequest.from(withdrawTargetUserResponse)) | |
| userWithdrawalService.processWithdrawal(userId) | |
| } |
🤖 Prompt for AI Agents
In apis/src/main/kotlin/org/yapp/apis/auth/usecase/AuthUseCase.kt around lines
63 to 77, the withdraw method lacks a @Transactional annotation and inherits the
class-level @Transactional(readOnly = true), which is inappropriate for a write
operation like user withdrawal. Add the @Transactional annotation directly on
the withdraw method without readOnly=true to ensure it runs within a proper
write transaction.
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.
@coderabbitai
아래 이유로 뺐어
이전 문제
- 문제: 외부 API 호출이 트랜잭션 내에서 실행되어, DB 작업 실패 시 외부 API 호출은 롤백할 수 없음
수정된 단계별 트랜잭션 흐름
핵심 원리
- 외부 API 호출을 트랜잭션 밖으로 분리
- DB 작업을 별도 트랜잭션으로 격리
- 실패 지점별로 적절한 롤백 처리
Step 1: 조회 및 검증 (트랜잭션 없음)
// AuthUseCase.withdraw() – @Transactional 없음
val withdrawTargetUserResponse = userAccountService.findWithdrawUserResponse()
// readOnly = true로 실행됨 (클래스 레벨 설정)Step 2: 외부 API 호출 (트랜잭션 없음)
// AuthUseCase.withdraw() – @Transactional 없음
strategy.withdraw(...) // Apple/Kakao API 호출
// 외부 API 호출이 실패해도 DB에 영향 없음Step 3: DB 작업 (별도 트랜잭션)
// UserWithdrawalService.processWithdrawal() – @Transactional
@Transactional
fun processWithdrawal(userId: UUID) {
// DB 작업들만 수행
refreshTokenService.deleteRefreshTokenByToken(...)
userAccountService.withdrawUser(userId)
}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.
@hoonyworld 완전히 맞는 접근입니다! 외부 API 호출과 DB 트랜잭션을 분리한 설계가 정확합니다.
당신의 트랜잭션 설계는 분산 시스템에서의 보상 트랜잭션(Compensating Transaction) 패턴을 올바르게 적용한 것입니다:
장점:
- 원자성 보장: 외부 API 호출 실패 시 DB 변경사항이 없음
- 일관성 유지: DB 작업 실패 시에도 이미 성공한 토큰 취소는 회원탈퇴 목적에 부합
- 격리된 책임: 각 단계별로 명확한 트랜잭션 경계 설정
트랜잭션 흐름 분석:
- Step 1: 읽기 전용으로 사용자 조회 ✅
- Step 2: 외부 API 호출 (롤백 불가능한 작업) ✅
- Step 3: DB 변경사항만 별도 트랜잭션으로 격리 ✅
이런 패턴은 특히 결제, 외부 인증, 알림 등 되돌릴 수 없는 외부 시스템과의 통합에서 중요합니다. 훌륭한 아키텍처 설계 결정입니다!
(\ /)
( ._.)
o_(")(")
Thoughtful design! 🧠
✏️ Learnings added
Learnt from: hoonyworld
PR: YAPP-Github/Reed-Server#76
File: apis/src/main/kotlin/org/yapp/apis/auth/usecase/AuthUseCase.kt:63-77
Timestamp: 2025-08-06T07:54:21.880Z
Learning: 회원 탈퇴와 같이 외부 API 호출(토큰 취소)이 포함된 작업에서는 외부 API 호출을 트랜잭션 밖으로 분리하고, DB 작업만 별도 트랜잭션으로 처리하는 것이 올바른 패턴이다. 이는 외부 API 호출이 롤백될 수 없기 때문이며, 보상 트랜잭션 패턴을 적용한 것이다.
Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.
로그인 프로세스의 트랜잭션 경계 분리 및 안정성 강화문제점 (Problem)기존 이로 인해 외부 API의 응답을 기다리는 동안 DB 커넥션을 불필요하게 오래 점유하게 되어, 시스템 부하 시 성능 저하 및 장애를 유발할 수 있는 잠재적 위험이 존재했습니다. 또한, 외부 호출 실패 후 트랜잭션이 롤백되면서 사용자에게 혼란을 주는 등 예외 처리 흐름이 명확하지 않았습니다. 해결 방안 (Solution)'외부 호출 선행, DB 작업 후행' 원칙에 따라 로그인 전체 구조를 리팩토링하여 역할과 책임을 명확히 분리하고 트랜잭션 경계를 재설정했습니다.
기대 효과 (Expected Effects)
인증/탈퇴 플로우의 트랜잭션 경계 재설정으로 안정성 강화문제점 (Problem)기존
해결 방안 (Solution)'외부 호출 선행, DB 작업 후행' 원칙을 1. 로그인 (signIn) 프로세스 개선
2. 회원탈퇴 (withdraw) 프로세스 개선
기대 효과 (Expected Effects)
|
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)
apis/src/main/kotlin/org/yapp/apis/auth/service/UserSignInService.kt (1)
10-27: 불필요한 추상화 레이어 제거 필요
UserSignInService는 단순히UserAccountService로 요청을 전달하는 역할만 수행하고 있어 불필요한 추상화 레이어입니다. 비즈니스 로직, 검증, 에러 처리 등이 없으므로AuthUseCase에서 직접UserAccountService를 사용하는 것이 더 간단하고 유지보수하기 쉽습니다.
📜 Review details
Configuration used: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (4)
apis/src/main/kotlin/org/yapp/apis/auth/service/AppleAuthService.kt(1 hunks)apis/src/main/kotlin/org/yapp/apis/auth/service/UserSignInService.kt(1 hunks)apis/src/main/kotlin/org/yapp/apis/auth/usecase/AuthUseCase.kt(2 hunks)apis/src/main/kotlin/org/yapp/apis/user/service/UserAccountService.kt(2 hunks)
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: hoonyworld
PR: YAPP-Github/Reed-Server#76
File: apis/src/main/kotlin/org/yapp/apis/auth/usecase/AuthUseCase.kt:63-77
Timestamp: 2025-08-06T07:54:21.880Z
Learning: 회원 탈퇴와 같이 외부 API 호출(토큰 취소)이 포함된 작업에서는 외부 API 호출을 트랜잭션 밖으로 분리하고, DB 작업만 별도 트랜잭션으로 처리하는 것이 올바른 패턴이다. 이는 외부 API 호출이 롤백될 수 없기 때문이며, 보상 트랜잭션 패턴을 적용한 것이다.
📚 Learning: 회원 탈퇴와 같이 외부 api 호출(토큰 취소)이 포함된 작업에서는 외부 api 호출을 트랜잭션 밖으로 분리하고, db 작업만 별도 트랜잭션으로 처리하는 것이 올바른 패턴이다. 이...
Learnt from: hoonyworld
PR: YAPP-Github/Reed-Server#76
File: apis/src/main/kotlin/org/yapp/apis/auth/usecase/AuthUseCase.kt:63-77
Timestamp: 2025-08-06T07:54:21.880Z
Learning: 회원 탈퇴와 같이 외부 API 호출(토큰 취소)이 포함된 작업에서는 외부 API 호출을 트랜잭션 밖으로 분리하고, DB 작업만 별도 트랜잭션으로 처리하는 것이 올바른 패턴이다. 이는 외부 API 호출이 롤백될 수 없기 때문이며, 보상 트랜잭션 패턴을 적용한 것이다.
Applied to files:
apis/src/main/kotlin/org/yapp/apis/auth/usecase/AuthUseCase.kt
⏰ 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)
apis/src/main/kotlin/org/yapp/apis/user/service/UserAccountService.kt (1)
17-69: 잘 구조화된 서비스 리팩토링
UserAccountService로의 리네이밍과 책임 분리가 적절하게 이루어졌습니다. 계정 생성, 탈퇴, Apple 리프레시 토큰 관리에 집중하여 단일 책임 원칙을 잘 따르고 있습니다.apis/src/main/kotlin/org/yapp/apis/auth/usecase/AuthUseCase.kt (1)
66-80: 트랜잭션 경계가 올바르게 구현됨회원 탈퇴 로직이 보상 트랜잭션 패턴을 올바르게 적용하여 구현되었습니다:
- 외부 API 호출 (Line 77)이 트랜잭션 밖에서 실행
- DB 작업 (Line 79)만 별도 트랜잭션으로 처리
- 실패 지점별로 적절한 격리 보장
이는 외부 API 호출이 롤백될 수 없는 특성을 고려한 적절한 설계입니다.
| fun fetchAppleOauthTokens(authorizationCode: String): AppleTokenResponse { | ||
| val tokenResponse = appleApiManager.fetchAppleOauthTokens(authorizationCode) | ||
|
|
||
| val refreshToken = tokenResponse.refreshToken | ||
| ?: throw AuthException(AuthErrorCode.MISSING_APPLE_REFRESH_TOKEN) | ||
| tokenResponse.refreshToken | ||
| ?: throw AuthException(AuthErrorCode.MISSING_APPLE_REFRESH_TOKEN) | ||
|
|
||
| userDomainService.updateAppleRefreshToken(request.validUserId(), refreshToken) | ||
| } | ||
| return tokenResponse | ||
| } |
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)
불필요한 null 체크 로직 간소화 가능
refreshToken의 존재 여부만 확인하고 전체 tokenResponse를 반환하는 현재 로직은 다음과 같이 간소화할 수 있습니다:
fun fetchAppleOauthTokens(authorizationCode: String): AppleTokenResponse {
- val tokenResponse = appleApiManager.fetchAppleOauthTokens(authorizationCode)
-
- tokenResponse.refreshToken
- ?: throw AuthException(AuthErrorCode.MISSING_APPLE_REFRESH_TOKEN)
-
- return tokenResponse
+ return appleApiManager.fetchAppleOauthTokens(authorizationCode).also {
+ it.refreshToken ?: throw AuthException(AuthErrorCode.MISSING_APPLE_REFRESH_TOKEN)
+ }
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| fun fetchAppleOauthTokens(authorizationCode: String): AppleTokenResponse { | |
| val tokenResponse = appleApiManager.fetchAppleOauthTokens(authorizationCode) | |
| val refreshToken = tokenResponse.refreshToken | |
| ?: throw AuthException(AuthErrorCode.MISSING_APPLE_REFRESH_TOKEN) | |
| tokenResponse.refreshToken | |
| ?: throw AuthException(AuthErrorCode.MISSING_APPLE_REFRESH_TOKEN) | |
| userDomainService.updateAppleRefreshToken(request.validUserId(), refreshToken) | |
| } | |
| return tokenResponse | |
| } | |
| fun fetchAppleOauthTokens(authorizationCode: String): AppleTokenResponse { | |
| return appleApiManager.fetchAppleOauthTokens(authorizationCode).also { | |
| it.refreshToken ?: throw AuthException(AuthErrorCode.MISSING_APPLE_REFRESH_TOKEN) | |
| } | |
| } |
🤖 Prompt for AI Agents
In apis/src/main/kotlin/org/yapp/apis/auth/service/AppleAuthService.kt around
lines 15 to 22, the current code checks if tokenResponse.refreshToken is null
and throws an exception if so, then returns tokenResponse. Simplify this by
using a single expression that returns tokenResponse only if refreshToken is not
null, otherwise throws the exception, removing the explicit null check and
making the code more concise.
| fun signIn(socialLoginRequest: SocialLoginRequest): TokenPairResponse { | ||
| val credentials = SocialLoginRequest.toCredentials(socialLoginRequest) | ||
| val strategy = authStrategyResolver.resolve(credentials) | ||
| val strategy = signInStrategyResolver.resolve(credentials) | ||
| val userCreateInfoResponse = strategy.authenticate(credentials) | ||
|
|
||
| val createUserResponse = userAuthService.findOrCreateUser(FindOrCreateUserRequest.from(userCreateInfoResponse)) | ||
| val appleRefreshToken = fetchAppleRefreshTokenIfNeeded(credentials) | ||
|
|
||
| if (credentials is AppleAuthCredentials) { | ||
| appleAuthService.saveAppleRefreshTokenIfMissing( | ||
| SaveAppleRefreshTokenRequest.of( | ||
| createUserResponse, | ||
| credentials | ||
| ) | ||
| ) | ||
| } | ||
| val createUserResponse = userSignInService.processSignIn( | ||
| FindOrCreateUserRequest.from(userCreateInfoResponse), | ||
| appleRefreshToken | ||
| ) | ||
|
|
||
| return authTokenService.generateTokenPair(GenerateTokenPairRequest.from(createUserResponse)) | ||
| } |
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.
signIn 메서드의 트랜잭션 설정 검토 필요
signIn 메서드는 클래스 레벨의 @Transactional(readOnly = true)를 상속받지만, 내부에서 호출되는 userSignInService.processSignIn은 쓰기 작업을 수행합니다. 읽기 전용 트랜잭션 내에서 쓰기 트랜잭션이 중첩되는 것은 예상치 못한 동작을 일으킬 수 있습니다.
+@Transactional
fun signIn(socialLoginRequest: SocialLoginRequest): TokenPairResponse {📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| fun signIn(socialLoginRequest: SocialLoginRequest): TokenPairResponse { | |
| val credentials = SocialLoginRequest.toCredentials(socialLoginRequest) | |
| val strategy = authStrategyResolver.resolve(credentials) | |
| val strategy = signInStrategyResolver.resolve(credentials) | |
| val userCreateInfoResponse = strategy.authenticate(credentials) | |
| val createUserResponse = userAuthService.findOrCreateUser(FindOrCreateUserRequest.from(userCreateInfoResponse)) | |
| val appleRefreshToken = fetchAppleRefreshTokenIfNeeded(credentials) | |
| if (credentials is AppleAuthCredentials) { | |
| appleAuthService.saveAppleRefreshTokenIfMissing( | |
| SaveAppleRefreshTokenRequest.of( | |
| createUserResponse, | |
| credentials | |
| ) | |
| ) | |
| } | |
| val createUserResponse = userSignInService.processSignIn( | |
| FindOrCreateUserRequest.from(userCreateInfoResponse), | |
| appleRefreshToken | |
| ) | |
| return authTokenService.generateTokenPair(GenerateTokenPairRequest.from(createUserResponse)) | |
| } | |
| @Transactional | |
| fun signIn(socialLoginRequest: SocialLoginRequest): TokenPairResponse { | |
| val credentials = SocialLoginRequest.toCredentials(socialLoginRequest) | |
| val strategy = signInStrategyResolver.resolve(credentials) | |
| val userCreateInfoResponse = strategy.authenticate(credentials) | |
| val appleRefreshToken = fetchAppleRefreshTokenIfNeeded(credentials) | |
| val createUserResponse = userSignInService.processSignIn( | |
| FindOrCreateUserRequest.from(userCreateInfoResponse), | |
| appleRefreshToken | |
| ) | |
| return authTokenService.generateTokenPair(GenerateTokenPairRequest.from(createUserResponse)) | |
| } |
🤖 Prompt for AI Agents
In apis/src/main/kotlin/org/yapp/apis/auth/usecase/AuthUseCase.kt around lines
33 to 46, the signIn method inherits a class-level @Transactional(readOnly =
true) annotation but calls userSignInService.processSignIn which performs write
operations. To fix this, override the transactional setting on the signIn method
by annotating it with @Transactional without readOnly=true, ensuring it allows
write transactions and prevents issues from nested read-only transactions.
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.
문제점 (Problem)
기존 signIn 로직은 단일 트랜잭션(@transactional) 내에서 외부 API 호출(Kakao 사용자 정보 조회, Apple 토큰 발급 등)과 DB 쓰기 작업(사용자 생성/수정)을 함께 처리했습니다.
이로 인해 외부 API의 응답을 기다리는 동안 DB 커넥션을 불필요하게 오래 점유하게 되어, 시스템 부하 시 성능 저하 및 장애를 유발할 수 있는 잠재적 위험이 존재했습니다. 또한, 외부 호출 실패 후 트랜잭션이 롤백되면서 사용자에게 혼란을 주는 등 예외 처리 흐름이 명확하지 않았습니다.
해결 방안 (Solution)
'외부 호출 선행, DB 작업 후행' 원칙에 따라 로그인 전체 구조를 리팩토링하여 역할과 책임을 명확히 분리하고 트랜잭션 경계를 재설정했습니다.
AuthUseCase (Orchestrator)
signIn 메서드의 @transactional을 제거하고, 전체 흐름을 지휘하는 오케스트레이터 역할만 수행하도록 변경했습니다.
외부 소셜 인증 및 필요한 경우 Apple Refresh Token 조회까지의 모든 외부 통신을 먼저 완료한 후, 그 결과를 모아 하위 서비스 계층에 전달합니다.
UserSignInService (Transactional Executor)
processSignIn 메서드에 @transactional을 적용하여, 전달받은 데이터로 '사용자 생성/조회'와 'Apple Refresh Token 업데이트'를 하나의 원자적 DB 트랜잭션으로 처리하는 실행자 역할을 맡습니다.
이 서비스는 이제 외부 API 호출 없이 순수하게 DB 작업에만 집중합니다.
AppleAuthService 및 UserAccountService 역할 명확화
AppleAuthService: DB 저장 로직을 제거하고, 순수하게 Apple 서버로부터 토큰을 받아오는 외부 통신 책임만 갖도록 역할을 축소했습니다.
UserAccountService: updateAppleRefreshToken 메서드를 추가하여, Apple Refresh Token을 DB에 업데이트하는 책임을 부여받음으로써 사용자 계정 정보 관리 전문가로서의 역할을 강화했습니다.
기대 효과 (Expected Effects)
DB 커넥션 점유 시간을 최소화하여 로그인 API의 응답 속도 및 처리량 향상
외부 서비스의 지연이 시스템 전체의 안정성에 미치는 영향을 최소화
각 서비스의 역할과 책임이 명확해져 코드의 가독성 및 유지보수성 증대
이러한 이유로 썼어
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.
@hoonyworld 완벽한 설명입니다! 제가 이전에 제안한 @Transactional 추가는 잘못된 분석에 기반한 것이었습니다.
당신의 아키텍처 결정이 올바릅니다:
- AuthUseCase (오케스트레이터): 외부 API 호출을 먼저 처리하고 트랜잭션 없이 전체 흐름을 조율
- UserSignInService (실행자): 순수 DB 작업만을
@Transactional로 보호
이는 검색된 학습 내용과도 일치하는 패턴으로, 외부 API 호출을 트랜잭션 밖으로 분리하여 DB 커넥션 점유 시간을 최소화하고 시스템 안정성을 높이는 보상 트랜잭션 패턴입니다.
상세한 설명 감사합니다. 이런 아키텍처 결정 배경을 공유해 주셔서 향후 리뷰에 많은 도움이 됩니다!
(◕‿◕)♡ 아키텍처 설계에 대한 깊이 있는 통찰을 공유해 주셔서 정말 배움이 많았습니다!
✏️ Learnings added
Learnt from: hoonyworld
PR: YAPP-Github/Reed-Server#76
File: apis/src/main/kotlin/org/yapp/apis/auth/usecase/AuthUseCase.kt:33-46
Timestamp: 2025-08-06T08:18:36.218Z
Learning: 로그인 작업에서는 외부 API 호출(소셜 인증, Apple 토큰 발급)과 DB 작업을 분리하여, AuthUseCase는 트랜잭션 없이 외부 호출을 먼저 처리하는 오케스트레이터 역할을 하고, UserSignInService는 Transactional로 DB 작업만 처리하는 실행자 역할을 한다. 이는 DB 커넥션 점유 시간 최소화와 시스템 안정성 향상을 위한 패턴이다.
Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.
🧠 Learnings used
Learnt from: hoonyworld
PR: YAPP-Github/Reed-Server#76
File: apis/src/main/kotlin/org/yapp/apis/auth/usecase/AuthUseCase.kt:63-77
Timestamp: 2025-08-06T07:54:21.880Z
Learning: 회원 탈퇴와 같이 외부 API 호출(토큰 취소)이 포함된 작업에서는 외부 API 호출을 트랜잭션 밖으로 분리하고, DB 작업만 별도 트랜잭션으로 처리하는 것이 올바른 패턴이다. 이는 외부 API 호출이 롤백될 수 없기 때문이며, 보상 트랜잭션 패턴을 적용한 것이다.
|


🔗 관련 이슈
📘 작업 유형
📙 작업 내역
주요 기능 추가
회원 탈퇴 기능 구현
코드 리팩토링
도메인 계층 분리 및 재배치
🧪 테스트 내역
🎨 스크린샷 또는 시연 영상 (선택)
✅ PR 체크리스트
-[x] 기능이 정상적으로 작동합니다
💬 추가 설명 or 리뷰 포인트 (선택)
Summary by CodeRabbit
신규 기능
기능 개선
기타