Skip to content

Conversation

@minwoo1999
Copy link
Member

@minwoo1999 minwoo1999 commented Jun 20, 2025

🔗 관련 이슈

📘 작업 유형

  • ✨ Feature (기능 추가)
  • 🐞 Bugfix (버그 수정)
  • 🔧 Refactor (코드 리팩토링)
  • ⚙️ Chore (환경 설정)
  • 📝 Docs (문서 작성 및 수정)
  • ✅ Test (기능 테스트)
  • 🎨 style (코드 스타일 수정)

📙 작업 내역

  • 카카오/애플 소셜 로그인 기능 구현

  • AuthController, AuthService, AuthUseCase, AuthStrategy 등 역할별 모듈 분리

  • Kakao OAuth 인증 흐름 처리 및 닉네임 자동 생성

  • Apple OAuth 인증 처리 및 토큰 파싱 구현

  • 예외 처리 공통 핸들러(GlobalExceptionHandler) 정의

  • CommonException, BindException 대응 및 ErrorResponse 형식 반환

  • 각 모듈별 기본 설정 추가

  • application.yml, config, buildSrc 의존성 등 구성

  • gateway 인증/인가 필터 구현

  • infra 레이어의 Repository 구현 및 설정 분리

  • 도메인 모델 및 DTO 정의

  • AuthDto, TokenPair, User 등 소셜 로그인 관련 모델 정의

🎨 스크린샷 또는 시연 영상 (선택)

기능 미리보기 기능 미리보기
기능 설명 기능 설명

✅ PR 체크리스트

  • 커밋 메시지가 명확합니다
  • PR 제목이 컨벤션에 맞습니다
  • 관련 이슈 번호를 작성했습니다
  • 기능이 정상적으로 작동합니다
  • 불필요한 코드를 제거했습니다

발생했던 이슈 모음

💬 추가 설명 or 리뷰 포인트 (선택)

  • 이 PR은 기능 개발의 첫 단계로, 아키텍처 구조 정의에 초점을 맞췄습니다.
  • 도메인 영역은 정책, Entity, Interface만 담아 변화에 민감하지 않도록 구성했습니다.
  • apis 모듈은 usecase 중심으로 서비스 흐름을 작성했고,
  • 구현체(repository, 외부 API 연동)는 infra 모듈에 분리했습니다.
  • 모듈 간 순수한 참조 흐름을 유지하고자 했으며, 변경과 확장에 유연한 구조를 지향했습니다.

Summary by CodeRabbit

Summary by CodeRabbit

  • 신규 기능

    • 카카오 및 애플 소셜 로그인을 지원하는 인증 API와 테스트용 HTML 페이지 추가
    • JWT 기반 인증 및 리프레시 토큰 발급, 갱신, 폐기 기능 제공
    • 사용자 프로필 조회 API 제공
    • Swagger(OpenAPI) 문서 및 Swagger UI 연동
    • Redis 기반 토큰 저장소 및 관련 인프라 설정 추가
    • 글로벌 예외 처리 및 표준 에러 응답 구조 도입
    • 인증 전략 패턴 도입 및 소셜 로그인 인증 로직 분리
    • 닉네임 자동 생성 기능 추가
    • Gateway 모듈에 Spring Security 및 JWT 인증 필터 적용
  • 문서화

    • 카카오 소셜 로그인 테스트 및 설정 방법에 대한 README 추가
  • 환경설정

    • 환경 변수 기반 DB/Redis/JWT/Swagger 설정 지원
    • Redis, JPA 등 인프라 설정 및 의존성 추가
  • 스타일/구조 개선

    • 코드 및 패키지 구조 정비, KDoc 주석 추가

@coderabbitai
Copy link

coderabbitai bot commented Jun 20, 2025

📝 Walkthrough

"""

Walkthrough

이 변경사항은 카카오 및 애플 소셜 로그인 기능을 확장성 있게 구현하고, JWT 기반의 access/refresh 토큰 관리, 로그아웃, 사용자 프로필 조회 등 인증 전반의 기능을 도입합니다. 각 모듈별로 인증, 예외, 유저, 토큰, 인프라, 게이트웨이, 글로벌 유틸리티 등 관련 코드와 설정이 추가 및 구성되었습니다.

Changes

파일/경로 요약 변경 요약
apis/build.gradle.kts, infra/build.gradle.kts, gateway/build.gradle.kts, buildSrc/src/main/kotlin/Dependencies.kt 인증(JWT), Redis, Swagger, 로깅 등 신규 의존성 추가 및 프로젝트 의존성 확장
apis/src/main/resources/application.yml, gateway/src/main/resources/application.properties 환경 변수 기반 DB/Redis/JWT/Swagger 설정 및 서버 포트, Swagger UI 경로 등 추가 및 수정
apis/src/main/resources/static/kakao-login.html, apis/src/main/resources/static/README.md 카카오/애플 소셜 로그인 테스트용 HTML 및 설명 문서 추가
apis/src/main/kotlin/org/yapp/ApisApplication.kt, apis/src/main/kotlin/org/yapp/apis/config/InfraConfig.kt KDoc 주석 및 인프라 설정(REDIS 그룹 추가)
apis/src/main/kotlin/org/yapp/apis/config/RestTemplateConfig.kt, apis/src/main/kotlin/org/yapp/apis/config/SwaggerConfig.kt RestTemplate, Swagger(OpenAPI) Spring 설정 클래스 추가
apis/src/main/kotlin/org/yapp/apis/util/generateNickname.kt 닉네임 생성 유틸리티 추가
apis/src/main/kotlin/org/yapp/apis/util/AuthUtils.kt Authorization 헤더에서 userId 추출 유틸리티 추가
apis/src/main/kotlin/org/yapp/apis/auth/controller/AuthController.kt, apis/src/main/kotlin/org/yapp/apis/auth/controller/AuthControllerApi.kt 인증 API 컨트롤러 및 인터페이스(로그인, 토큰 갱신, 로그아웃, 프로필 조회) 신규 구현
apis/src/main/kotlin/org/yapp/apis/auth/dto/request/*, apis/src/main/kotlin/org/yapp/apis/auth/dto/response/* 인증 관련 요청/응답 DTO 신규 생성
apis/src/main/kotlin/org/yapp/apis/auth/service/*, apis/src/main/kotlin/org/yapp/apis/auth/strategy/* 인증 전략(카카오, 애플), 소셜 인증 서비스, 유저 서비스, 토큰 서비스 등 인증 비즈니스 로직 구현
apis/src/main/kotlin/org/yapp/apis/auth/exception/* 인증 관련 에러코드, 예외 클래스 추가
apis/src/main/kotlin/org/yapp/apis/auth/usecase/AuthUseCase.kt 인증 유스케이스(비즈니스 플로우) 서비스 추가
domain/src/main/kotlin/org/yapp/domain/auth/*, domain/src/main/kotlin/org/yapp/domain/user/*, domain/src/main/kotlin/org/yapp/domain/common/* ProviderType, User, UserEntity, UserRepository, TokenRepository, BaseTimeEntity 등 도메인 모델, 리포지토리, 베이스 엔티티 도입
domain/src/main/kotlin/org/yapp/domain/domainservice/UserDomainService.kt 유저 도메인 서비스(중복 체크 및 생성) 추가
infra/src/main/kotlin/org/yapp/InfraBaseConfigGroup.kt, infra/src/main/kotlin/org/yapp/jpa/*, infra/src/main/kotlin/org/yapp/jpa/repository/* REDIS 그룹 추가, JPA 엔티티/리포지토리 스캔 범위 명확화, JPA 기반 UserRepository 구현
infra/src/main/kotlin/org/yapp/redis/* RedisConfig, RedisTokenRepository 등 Redis 기반 토큰 저장소 구현
gateway/src/main/kotlin/org/yapp/gateway/config/SecurityConfig.kt, gateway/src/main/kotlin/org/yapp/gateway/filter/JwtAuthenticationFilter.kt 게이트웨이 Spring Security 설정, JWT 인증 필터 도입
gateway/src/main/kotlin/org/yapp/gateway/jwt/*, gateway/src/main/kotlin/org/yapp/gateway/jwt/exception/* 게이트웨이용 JWT 토큰 프로바이더, 서비스, 예외 및 에러코드 구현
global-utils/src/main/kotlin/org/yapp/global/exception/*, global-utils/src/main/kotlin/org/yapp/annotation/DomainService.kt 공통 에러코드/예외/에러 응답/글로벌 예외 핸들러, 도메인 서비스 어노테이션 추가

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Frontend (kakao-login.html)
    participant Apis (AuthController)
    participant SocialAuthService
    participant AuthStrategy(Kakao/Apple)
    participant UserService
    participant TokenService
    participant JwtTokenService
    participant Redis

    User->>Frontend (kakao-login.html): 카카오/애플 로그인 버튼 클릭
    Frontend->>카카오/애플: OAuth 인증 요청
    카카오/애플-->>Frontend: 인증 토큰 반환
    Frontend->>Apis: POST /api/v1/auth/signin (providerType, oauthToken)
    Apis->>SocialAuthService: resolve(credentials)
    SocialAuthService->>AuthStrategy: authenticate(credentials)
    AuthStrategy->>Apis: User 정보 반환
    Apis->>UserService: findOrCreateUser(User)
    UserService->>UserService: 중복 체크 및 생성
    UserService->>Apis: User 반환
    Apis->>JwtTokenService: access/refresh 토큰 생성
    Apis->>TokenService: refreshToken 저장
    TokenService->>Redis: refreshToken 저장
    Apis-->>Frontend: accessToken, refreshToken 반환
Loading
sequenceDiagram
    participant User
    participant Frontend
    participant Apis
    participant TokenService
    participant JwtTokenService
    participant Redis

    User->>Frontend: 토큰 만료 시
    Frontend->>Apis: POST /api/v1/auth/refresh (refreshToken)
    Apis->>TokenService: validateRefreshTokenOrThrow(userId, refreshToken)
    TokenService->>Redis: refreshToken 존재 여부 확인
    Apis->>JwtTokenService: access/refresh 토큰 재발급
    Apis->>TokenService: 새 refreshToken 저장
    TokenService->>Redis: refreshToken 갱신
    Apis-->>Frontend: 새 accessToken, refreshToken 반환
Loading
sequenceDiagram
    participant User
    participant Frontend
    participant Apis
    participant TokenService
    participant Redis

    User->>Frontend: 로그아웃 요청
    Frontend->>Apis: POST /api/v1/auth/signout (Authorization 헤더)
    Apis->>TokenService: delete(userId)
    TokenService->>Redis: refreshToken 삭제
    Apis-->>Frontend: 204 No Content
Loading

Assessment against linked issues

Objective Addressed Explanation
애플, 소셜(카카오) 로그인 구현 (#11)
accessToken, RefreshToken 관리 (#11)
로그아웃 기능 구현 (#11)
각 모듈별 고려하여 구현 (#11)

Assessment against linked issues: Out-of-scope changes

(해당 변경사항 내에서 요구사항과 무관하거나 범위를 벗어난 기능적 코드 변경은 발견되지 않았습니다.)
"""

✨ Finishing Touches
  • 📝 Generate Docstrings

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.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@minwoo1999 minwoo1999 force-pushed the BOOK-52-feature/#11 branch from 560f42a to 1600df7 Compare June 20, 2025 16:23
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 42

📜 Review details

Configuration used: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f7e871d and 1600df7.

📒 Files selected for processing (50)
  • apis/build.gradle.kts (1 hunks)
  • apis/src/main/kotlin/org/yapp/ApisApplication.kt (1 hunks)
  • apis/src/main/kotlin/org/yapp/apis/auth/controller/AuthController.kt (1 hunks)
  • apis/src/main/kotlin/org/yapp/apis/auth/controller/AuthControllerApi.kt (1 hunks)
  • apis/src/main/kotlin/org/yapp/apis/auth/dto/AuthDto.kt (1 hunks)
  • apis/src/main/kotlin/org/yapp/apis/auth/dto/UserProfileResponse.kt (1 hunks)
  • apis/src/main/kotlin/org/yapp/apis/auth/exception/AuthErrorCode.kt (1 hunks)
  • apis/src/main/kotlin/org/yapp/apis/auth/exception/AuthException.kt (1 hunks)
  • apis/src/main/kotlin/org/yapp/apis/auth/service/AuthCredentials.kt (1 hunks)
  • apis/src/main/kotlin/org/yapp/apis/auth/service/AuthService.kt (1 hunks)
  • apis/src/main/kotlin/org/yapp/apis/auth/service/AuthServiceImpl.kt (1 hunks)
  • apis/src/main/kotlin/org/yapp/apis/auth/strategy/AppleAuthStrategy.kt (1 hunks)
  • apis/src/main/kotlin/org/yapp/apis/auth/strategy/AuthStrategy.kt (1 hunks)
  • apis/src/main/kotlin/org/yapp/apis/auth/strategy/KakaoAuthStrategy.kt (1 hunks)
  • apis/src/main/kotlin/org/yapp/apis/auth/usecase/AuthUseCase.kt (1 hunks)
  • apis/src/main/kotlin/org/yapp/apis/auth/usecase/AuthUseCaseImpl.kt (1 hunks)
  • apis/src/main/kotlin/org/yapp/apis/config/InfraConfig.kt (1 hunks)
  • apis/src/main/kotlin/org/yapp/apis/config/RestTemplateConfig.kt (1 hunks)
  • apis/src/main/kotlin/org/yapp/apis/config/SwaggerConfig.kt (1 hunks)
  • apis/src/main/kotlin/org/yapp/apis/util/generateNickname.kt (1 hunks)
  • apis/src/main/resources/application.yml (2 hunks)
  • apis/src/main/resources/static/README.md (1 hunks)
  • apis/src/main/resources/static/kakao-login.html (1 hunks)
  • buildSrc/src/main/kotlin/Dependencies.kt (1 hunks)
  • domain/src/main/kotlin/org/yapp/domain/auth/ProviderType.kt (1 hunks)
  • domain/src/main/kotlin/org/yapp/domain/auth/TokenRepository.kt (1 hunks)
  • domain/src/main/kotlin/org/yapp/domain/user/User.kt (1 hunks)
  • domain/src/main/kotlin/org/yapp/domain/user/UserEntity.kt (1 hunks)
  • domain/src/main/kotlin/org/yapp/domain/user/UserRepository.kt (1 hunks)
  • gateway/build.gradle.kts (1 hunks)
  • gateway/src/main/kotlin/org/yapp/gateway/config/SecurityConfig.kt (1 hunks)
  • gateway/src/main/kotlin/org/yapp/gateway/filter/JwtAuthenticationFilter.kt (1 hunks)
  • gateway/src/main/kotlin/org/yapp/gateway/jwt/JwtTokenProvider.kt (1 hunks)
  • gateway/src/main/kotlin/org/yapp/gateway/jwt/JwtTokenService.kt (1 hunks)
  • gateway/src/main/kotlin/org/yapp/gateway/jwt/JwtTokenServiceImpl.kt (1 hunks)
  • gateway/src/main/kotlin/org/yapp/gateway/jwt/exception/JwtErrorCode.kt (1 hunks)
  • gateway/src/main/kotlin/org/yapp/gateway/jwt/exception/JwtException.kt (1 hunks)
  • gateway/src/main/resources/application.properties (0 hunks)
  • global-utils/src/main/kotlin/org/yapp/global/exception/BaseErrorCode.kt (1 hunks)
  • global-utils/src/main/kotlin/org/yapp/global/exception/CommonErrorCode.kt (1 hunks)
  • global-utils/src/main/kotlin/org/yapp/global/exception/CommonException.kt (1 hunks)
  • global-utils/src/main/kotlin/org/yapp/global/exception/ErrorResponse.kt (1 hunks)
  • global-utils/src/main/kotlin/org/yapp/global/exception/GlobalExceptionHandler.kt (1 hunks)
  • infra/build.gradle.kts (1 hunks)
  • infra/src/main/kotlin/org/yapp/InfraBaseConfigGroup.kt (1 hunks)
  • infra/src/main/kotlin/org/yapp/jpa/JpaConfig.kt (1 hunks)
  • infra/src/main/kotlin/org/yapp/jpa/repository/JpaUserRepository.kt (1 hunks)
  • infra/src/main/kotlin/org/yapp/jpa/repository/UserRepositoryImpl.kt (1 hunks)
  • infra/src/main/kotlin/org/yapp/redis/RedisConfig.kt (1 hunks)
  • infra/src/main/kotlin/org/yapp/redis/RedisTokenRepository.kt (1 hunks)
💤 Files with no reviewable changes (1)
  • gateway/src/main/resources/application.properties
🧰 Additional context used
🪛 markdownlint-cli2 (0.17.2)
apis/src/main/resources/static/README.md

45-45: Files should end with a single newline character
null

(MD047, single-trailing-newline)

🪛 YAMLlint (1.37.1)
apis/src/main/resources/application.yml

[error] 41-41: too many blank lines

(2 > 0) (empty-lines)

🪛 detekt (1.23.8)
infra/src/main/kotlin/org/yapp/redis/RedisConfig.kt

[warning] 21-22: Usages of lateinit should be avoided.

(detekt.potential-bugs.LateinitUsage)

apis/src/main/kotlin/org/yapp/apis/auth/strategy/AppleAuthStrategy.kt

[warning] 57-57: The caught exception is too generic. Prefer catching specific exceptions to the case that is currently handled.

(detekt.exceptions.TooGenericExceptionCaught)


[warning] 57-57: The caught exception is swallowed. The original exception could be lost.

(detekt.exceptions.SwallowedException)

apis/src/main/kotlin/org/yapp/apis/auth/service/AuthServiceImpl.kt

[warning] 58-58: The caught exception is swallowed. The original exception could be lost.

(detekt.exceptions.SwallowedException)


[warning] 93-93: The caught exception is swallowed. The original exception could be lost.

(detekt.exceptions.SwallowedException)

gateway/src/main/kotlin/org/yapp/gateway/jwt/JwtTokenProvider.kt

[warning] 77-77: The caught exception is swallowed. The original exception could be lost.

(detekt.exceptions.SwallowedException)


[warning] 79-79: The caught exception is swallowed. The original exception could be lost.

(detekt.exceptions.SwallowedException)


[warning] 81-81: The caught exception is swallowed. The original exception could be lost.

(detekt.exceptions.SwallowedException)


[warning] 83-83: The caught exception is swallowed. The original exception could be lost.

(detekt.exceptions.SwallowedException)


[warning] 85-85: The caught exception is swallowed. The original exception could be lost.

(detekt.exceptions.SwallowedException)


[warning] 103-103: The caught exception is swallowed. The original exception could be lost.

(detekt.exceptions.SwallowedException)


[warning] 105-105: The caught exception is swallowed. The original exception could be lost.

(detekt.exceptions.SwallowedException)


[warning] 107-107: The caught exception is swallowed. The original exception could be lost.

(detekt.exceptions.SwallowedException)


[warning] 109-109: The caught exception is swallowed. The original exception could be lost.

(detekt.exceptions.SwallowedException)


[warning] 111-111: The caught exception is swallowed. The original exception could be lost.

(detekt.exceptions.SwallowedException)


[warning] 132-132: The caught exception is swallowed. The original exception could be lost.

(detekt.exceptions.SwallowedException)


[warning] 134-134: The caught exception is swallowed. The original exception could be lost.

(detekt.exceptions.SwallowedException)


[warning] 136-136: The caught exception is swallowed. The original exception could be lost.

(detekt.exceptions.SwallowedException)


[warning] 138-138: The caught exception is swallowed. The original exception could be lost.

(detekt.exceptions.SwallowedException)


[warning] 140-140: The caught exception is swallowed. The original exception could be lost.

(detekt.exceptions.SwallowedException)


[warning] 166-166: The caught exception is swallowed. The original exception could be lost.

(detekt.exceptions.SwallowedException)


[warning] 168-168: The caught exception is swallowed. The original exception could be lost.

(detekt.exceptions.SwallowedException)


[warning] 170-170: The caught exception is swallowed. The original exception could be lost.

(detekt.exceptions.SwallowedException)


[warning] 172-172: The caught exception is swallowed. The original exception could be lost.

(detekt.exceptions.SwallowedException)


[warning] 174-174: The caught exception is swallowed. The original exception could be lost.

(detekt.exceptions.SwallowedException)


[warning] 176-176: The caught exception is swallowed. The original exception could be lost.

(detekt.exceptions.SwallowedException)


[warning] 212-212: The caught exception is swallowed. The original exception could be lost.

(detekt.exceptions.SwallowedException)


[warning] 214-214: The caught exception is swallowed. The original exception could be lost.

(detekt.exceptions.SwallowedException)


[warning] 216-216: The caught exception is swallowed. The original exception could be lost.

(detekt.exceptions.SwallowedException)


[warning] 218-218: The caught exception is swallowed. The original exception could be lost.

(detekt.exceptions.SwallowedException)


[warning] 220-220: The caught exception is swallowed. The original exception could be lost.

(detekt.exceptions.SwallowedException)

🔇 Additional comments (54)
infra/build.gradle.kts (2)

8-8: 적절한 Redis 의존성 추가

리프레시 토큰 저장소 구현을 위한 Redis 의존성 추가가 적절합니다.


10-10: Kotlin Reflect 의존성 추가 승인

Spring Boot에서 Kotlin 클래스의 리플렉션 처리를 위한 적절한 의존성 추가입니다.

apis/src/main/kotlin/org/yapp/apis/util/generateNickname.kt (1)

21-25: User 엔티티 정의 및 DB 마이그레이션 파일에서 users 테이블·nickname 컬럼 제약을 확인하기 위해 아래 스크립트를 실행해주세요:

#!/bin/bash
# 1. JPA Entity (@Entity / @Table) 정의 확인
rg "@Entity" -n apis/src/main/kotlin
rg "@Table" -n apis/src/main/kotlin

# 2. Repository 확장 확인하여 Entity 클래스 위치 추적
rg "extends .*Repository" -n apis/src/main/kotlin

# 3. Flyway 마이그레이션 파일(V*.sql) 검색
find apis/src/main/resources -type f | grep -En "V[0-9]+__.*\.sql"

# 4. Liquibase changelog(yml/xml)에서 users 테이블 생성 및 컬럼 제약 확인
find apis/src/main/resources -type f \( -name "*.yml" -o -name "*.xml" \) | xargs rg -n "createTable.*users" -A3
global-utils/src/main/kotlin/org/yapp/global/exception/BaseErrorCode.kt (1)

9-31: 잘 설계된 에러 코드 인터페이스

에러 처리의 일관성을 보장하는 잘 설계된 인터페이스입니다. HTTP 상태, 에러 코드, 메시지를 표준화하여 전역 예외 처리에 효과적입니다.

domain/src/main/kotlin/org/yapp/domain/auth/TokenRepository.kt (3)

14-14: 토큰 존재 여부 확인 메서드의 보안성 강화

existsRefreshToken 메서드는 토큰 값까지 비교하여 보안성을 높인 좋은 설계입니다. 단순히 userId만 확인하는 것보다 안전합니다.


6-15: 도메인 레이어의 적절한 추상화

도메인 레이어에 위치한 인터페이스로 의존성 역전 원칙을 잘 따르고 있으며, 리프레시 토큰 관리에 필요한 핵심 기능들을 적절히 추상화했습니다.


8-8: ```shell
#!/bin/bash

saveRefreshToken 구현부 확인

rg -n -C3 "saveRefreshToken" infra/src/main/kotlin/org/yapp/redis/RedisTokenRepository.kt


</details>
<details>
<summary>apis/src/main/kotlin/org/yapp/ApisApplication.kt (2)</summary>

`6-8`: **문서화 개선이 좋습니다.**

KDoc 스타일의 주석 추가가 코드 이해도를 높여줍니다. 다만 프로젝트 전체의 문서화 언어 일관성을 확인해보시기 바랍니다.

---

`12-14`: **메인 함수 문서화 적절합니다.**

메인 함수의 역할을 명확히 설명하는 주석이 추가되었습니다.

</details>
<details>
<summary>gateway/build.gradle.kts (1)</summary>

`8-11`: **의존성 버전 검증 추가 요청**

`buildSrc/src/main/kotlin/Dependencies.kt` 파일에 정의된 JWT 및 Spring Security 라이브러리의 버전을 직접 확인하고, 최신 보안 패치가 적용되었는지 점검해 주세요.

- 검토 대상:  
  - buildSrc/src/main/kotlin/Dependencies.kt

다음 스크립트를 실행하여 각 의존성의 버전 정보를 출력해 확인해 주시기 바랍니다:


```shell
#!/bin/bash
echo "JWT 및 Spring Security 의존성 버전 확인:"
grep -nE "JWT|SECURITY" buildSrc/src/main/kotlin/Dependencies.kt
domain/src/main/kotlin/org/yapp/domain/auth/ProviderType.kt (1)

3-8: Provider 타입 열거형이 잘 설계되었습니다.

소셜 로그인 제공자를 타입 안전하게 구분할 수 있는 열거형이 적절히 구현되었습니다. 향후 다른 소셜 로그인 제공자(Google, Facebook 등) 추가 시 확장 가능한 구조입니다.

apis/src/main/kotlin/org/yapp/apis/config/InfraConfig.kt (1)

8-8: Redis 설정 추가가 적절합니다.

인증 시스템에서 리프레시 토큰 저장을 위한 Redis 설정이 올바르게 추가되었습니다. 기존 JPA, ASYNC와 함께 일관된 패턴으로 구성되어 있습니다.

infra/src/main/kotlin/org/yapp/InfraBaseConfigGroup.kt (2)

5-5: RedisConfig import 추가가 적절합니다.

REDIS 설정 그룹 추가를 위한 필요한 import입니다.


10-12: 열거형 확장과 포매팅 개선이 좋습니다.

REDIS 설정 그룹이 기존 패턴과 일관되게 추가되었고, trailing comma 사용으로 향후 확장 시 diff가 깔끔해집니다.

apis/build.gradle.kts (3)

8-8: GATEWAY 의존성 추가 승인

게이트웨이 모듈 의존성 추가가 적절합니다. 인증 기능을 위해 필요한 의존성으로 보입니다.


16-19: JWT 및 Swagger 의존성 추가 승인

JWT 인증과 API 문서화를 위한 의존성들이 적절히 추가되었습니다. 소셜 로그인 구현에 필요한 핵심 라이브러리들입니다.


25-28: BootJar 설정 승인

메인 클래스가 명시적으로 설정되어 있어 애플리케이션 실행이 명확합니다.

apis/src/main/kotlin/org/yapp/apis/auth/dto/UserProfileResponse.kt (1)

3-8: 사용자 프로필 응답 DTO 구현 승인

깔끔한 데이터 클래스 구현입니다. 불변 속성(val) 사용과 명확한 필드 명명이 좋습니다.

infra/src/main/kotlin/org/yapp/jpa/JpaConfig.kt (1)

9-10: 패키지 스캔 범위 제한 승인

JPA 엔티티와 리포지토리 스캔 범위를 특정 패키지로 제한한 것이 좋습니다. 이는 애플리케이션 시작 시간을 개선하고 의도하지 않은 컴포넌트 스캔을 방지합니다.

gateway/src/main/kotlin/org/yapp/gateway/jwt/exception/JwtException.kt (1)

5-25: JWT 예외 클래스 구현 승인

JWT 관련 예외를 위한 잘 구현된 커스텀 예외 클래스입니다. CommonException을 적절히 확장하고, 두 개의 생성자로 유연성을 제공하며, 명확한 문서화가 되어 있습니다.

apis/src/main/kotlin/org/yapp/apis/auth/strategy/AuthStrategy.kt (1)

7-16: 인증 전략 인터페이스 설계 승인

Strategy 패턴을 적절히 구현한 인증 전략 인터페이스입니다. 다양한 소셜 로그인 제공자를 지원할 수 있는 깔끔한 추상화를 제공하며, 향후 새로운 인증 제공자 추가 시 확장성이 좋습니다.

global-utils/src/main/kotlin/org/yapp/global/exception/CommonException.kt (1)

9-31: 잘 설계된 예외 클래스입니다

ResponseStatusException을 상속하여 Spring Web과의 통합이 우수하고, BaseErrorCode를 통한 에러 관리 방식이 일관성 있게 구현되었습니다. 생성자 오버로딩을 통해 유연성도 제공합니다.

buildSrc/src/main/kotlin/Dependencies.kt (2)

25-25: Swagger 라이브러리 버전 검증 권장

Springdoc OpenAPI 라이브러리의 최신 버전 확인을 권장합니다.

What is the latest version of org.springdoc:springdoc-openapi-starter-webmvc-ui and any compatibility issues with Spring Boot 3.x?

19-21: JWT 라이브러리 버전 검증 필요

JWT 라이브러리 버전(0.11.5)이 최신인지 확인이 필요합니다. 보안 취약점이나 새로운 기능을 위해 최신 버전 사용을 권장합니다.

What is the latest version of io.jsonwebtoken:jjwt-api library and are there any security advisories for version 0.11.5?
apis/src/main/kotlin/org/yapp/apis/config/SwaggerConfig.kt (1)

23-47: 잘 구성된 Swagger 설정입니다

JWT Bearer 인증이 포함된 OpenAPI 설정이 적절하게 구현되었습니다. 보안 스키마와 메타데이터 설정이 깔끔하게 정리되어 있습니다.

domain/src/main/kotlin/org/yapp/domain/user/User.kt (1)

18-27: 잘 설계된 도메인 모델입니다

소셜 로그인에 필요한 모든 속성이 적절히 포함되어 있고, 불변 데이터 클래스로 구현되어 도메인 모델의 무결성이 보장됩니다.

gateway/src/main/kotlin/org/yapp/gateway/jwt/JwtTokenService.kt (1)

1-49: JWT 토큰 서비스 인터페이스가 잘 설계되어 있습니다.

인터페이스가 깔끔하게 설계되어 있고 모든 필수 JWT 작업을 포함하고 있습니다. 메서드 문서화도 적절하며 예외 처리에 대한 명세도 명확합니다.

domain/src/main/kotlin/org/yapp/domain/user/UserRepository.kt (2)

8-17: 도메인 저장소 인터페이스 설계가 우수합니다.

도메인 주도 설계 원칙을 잘 따르고 있으며, 메서드명이 직관적이고 도메인 타입을 적절히 사용하고 있습니다.


16-16: findById 메서드의 반환 타입을 확인해주세요.

findById 메서드가 nullable하지 않은 User를 반환합니다. 사용자가 존재하지 않을 경우 예외를 던지는 것이 의도된 동작인지 확인이 필요합니다.

다음 스크립트로 findById 사용 패턴을 확인해보세요:

#!/bin/bash
# Description: Check how findById is used across the codebase
# Expected: Verify if exception handling is properly implemented for non-existent users

rg -A 5 -B 2 "findById" --type kotlin
infra/src/main/kotlin/org/yapp/jpa/repository/JpaUserRepository.kt (1)

1-17: JPA 저장소 인터페이스가 표준을 잘 따르고 있습니다.

Spring Data JPA 컨벤션을 올바르게 따르고 있으며, 커스텀 쿼리 메서드들이 적절히 명명되어 있습니다. @Repository 어노테이션도 올바르게 적용되어 있습니다.

infra/src/main/kotlin/org/yapp/redis/RedisConfig.kt (1)

21-22: lateinit 사용이 적절합니다.

정적 분석 도구에서 lateinit 사용에 대한 경고가 있지만, Spring의 @value 어노테이션을 통한 의존성 주입에서는 lateinit 사용이 일반적이고 적절한 패턴입니다.

infra/src/main/kotlin/org/yapp/redis/RedisTokenRepository.kt (2)

16-18: 상수 정의가 잘 되어 있습니다.

companion object를 사용하여 프리픽스 상수를 정의하는 것이 좋은 패턴입니다.


35-38: 성능을 개선할 수 있습니다.

현재 구현은 토큰을 가져온 후 비교하지만, Redis의 원자적 연산을 활용하면 더 효율적입니다.

다음과 같이 개선할 수 있습니다:

 override fun existsRefreshToken(userId: Long, refreshToken: String): Boolean {
-    val storedToken = getRefreshToken(userId)
-    return storedToken != null && storedToken == refreshToken
+    val key = getRefreshTokenKey(userId)
+    val storedToken = redisTemplate.opsForValue().get(key)
+    return storedToken == refreshToken
 }

또는 더 명확하게:

 override fun existsRefreshToken(userId: Long, refreshToken: String): Boolean {
     val key = getRefreshTokenKey(userId)
-    val storedToken = redisTemplate.opsForValue().get(key)
-    return storedToken != null && storedToken == refreshToken
+    return redisTemplate.hasKey(key) && redisTemplate.opsForValue().get(key) == refreshToken
 }

Likely an incorrect or invalid review comment.

apis/src/main/kotlin/org/yapp/apis/auth/service/AuthCredentials.kt (1)

6-8: 인터페이스 설계가 깔끔합니다.

AuthCredentials 인터페이스를 통한 다형성 구현이 잘 되어 있습니다.

gateway/src/main/kotlin/org/yapp/gateway/jwt/JwtTokenServiceImpl.kt (2)

7-16: 서비스 계층 구조가 잘 설계되었습니다.

JwtTokenProvider를 래핑하는 서비스 계층으로 관심사 분리가 잘 되어 있고, 의존성 주입도 적절합니다.


18-66: 메서드 문서화가 훌륭합니다.

모든 메서드에 대한 KDoc 주석이 잘 작성되어 있고, 위임 패턴을 통한 구현이 깔끔합니다.

gateway/src/main/kotlin/org/yapp/gateway/jwt/exception/JwtErrorCode.kt (1)

23-25: BaseErrorCode 인터페이스 구현이 정확합니다.

오버라이드 메서드들이 올바르게 구현되어 있습니다.

gateway/src/main/kotlin/org/yapp/gateway/filter/JwtAuthenticationFilter.kt (1)

46-52: 토큰 추출 로직 검증 완료

Authorization 헤더에서 JWT 토큰을 안전하게 추출하는 로직이 잘 구현되어 있습니다. Bearer prefix 검증과 StringUtils 사용으로 안전한 문자열 처리가 이루어지고 있습니다.

apis/src/main/kotlin/org/yapp/apis/auth/usecase/AuthUseCase.kt (1)

10-23: 잘 설계된 Use Case 인터페이스

Clean Architecture 원칙을 잘 따르고 있으며, 인증 관련 모든 핵심 기능이 적절히 추상화되어 있습니다. 메서드 시그니처가 명확하고 일관성 있게 정의되어 있습니다.

apis/src/main/kotlin/org/yapp/apis/auth/exception/AuthErrorCode.kt (1)

27-30: BaseErrorCode 인터페이스 구현 완료

BaseErrorCode 인터페이스의 모든 메서드가 올바르게 구현되어 있습니다.

apis/src/main/resources/static/README.md (1)

1-45: 상세하고 유용한 테스트 가이드

카카오 로그인 테스트를 위한 종합적인 가이드가 잘 작성되어 있습니다. 설정부터 테스트, 문제 해결까지 모든 단계가 명확하게 설명되어 있어 개발자들이 쉽게 따라할 수 있을 것 같습니다.

global-utils/src/main/kotlin/org/yapp/global/exception/CommonErrorCode.kt (1)

18-21: BaseErrorCode 인터페이스 구현 완료

BaseErrorCode 인터페이스의 필수 메서드들이 올바르게 구현되어 있습니다.

global-utils/src/main/kotlin/org/yapp/global/exception/ErrorResponse.kt (1)

6-72: 잘 구현된 ErrorResponse 클래스입니다.

표준화된 에러 응답을 위한 데이터 클래스가 깔끔하게 구현되어 있고, 빌더 패턴을 통해 유연한 객체 생성이 가능합니다. 문서화도 잘 되어 있어 유지보수성이 좋습니다.

domain/src/main/kotlin/org/yapp/domain/user/UserEntity.kt (1)

39-50: 도메인 변환 메서드가 잘 구현되어 있습니다.

JPA 엔티티와 도메인 모델 간의 변환이 깔끔하게 구현되어 있어 레이어 간 분리가 잘 이루어져 있습니다.

gateway/src/main/kotlin/org/yapp/gateway/config/SecurityConfig.kt (1)

28-44: JWT 기반 보안 설정이 적절하게 구성되어 있습니다.

Stateless 세션 정책과 JWT 인증 필터가 올바르게 설정되어 있고, 인증이 필요없는 엔드포인트들이 적절하게 허용되어 있습니다. 소셜 로그인 구현에 적합한 보안 구성입니다.

infra/src/main/kotlin/org/yapp/jpa/repository/UserRepositoryImpl.kt (1)

17-29: 리포지토리 구현이 잘 되어 있습니다.

도메인 인터페이스를 JPA로 구현하는 어댑터 패턴이 깔끔하게 적용되어 있고, 엔티티와 도메인 모델 간의 변환도 적절히 처리되어 있습니다.

apis/src/main/kotlin/org/yapp/apis/auth/strategy/AppleAuthStrategy.kt (1)

21-35: 인증 로직이 잘 구현되어 있습니다.

Apple 자격 증명 타입 검증과 사용자 객체 생성 로직이 적절하게 구현되어 있습니다. 닉네임 자동 생성과 프로필 이미지 처리도 Apple의 특성에 맞게 잘 반영되어 있습니다.

apis/src/main/kotlin/org/yapp/apis/auth/strategy/KakaoAuthStrategy.kt (1)

37-37: Content-Type 헤더 설정 확인 필요

GET 요청에 Content-Type: application/x-www-form-urlencoded 헤더를 설정하고 있습니다. 일반적으로 GET 요청에는 request body가 없으므로 이 헤더가 필요하지 않을 수 있습니다.

Kakao API v2/user/me endpoint에서 GET 요청 시 Content-Type 헤더가 필요한가요?
apis/src/main/resources/static/kakao-login.html (1)

143-147: 애플 로그인 클라이언트 ID와 리다이렉트 URI를 확인하세요.

애플 로그인 설정에서 clientId가 빈 문자열이고, redirectURI가 하드코딩되어 있습니다. 실제 테스트를 위해서는 적절한 값들로 설정해야 합니다.

apis/src/main/kotlin/org/yapp/apis/auth/controller/AuthController.kt (1)

13-22: LGTM! 깔끔한 구현입니다.

의존성 주입과 레이어 분리가 잘 되어 있고, 각 메서드의 책임이 명확합니다.

apis/src/main/kotlin/org/yapp/apis/auth/controller/AuthControllerApi.kt (1)

17-43: OpenAPI 문서화가 잘 되어 있습니다.

각 엔드포인트에 대한 설명과 응답 코드가 명확하게 정의되어 있어 API 사용자가 이해하기 쉽습니다.

gateway/src/main/kotlin/org/yapp/gateway/jwt/JwtTokenProvider.kt (3)

70-89: validateToken 메서드의 예외 처리는 의도적입니다.

Static analysis에서 SwallowedException 경고가 나오지만, 이는 boolean을 반환하는 검증 메서드의 의도적인 설계입니다. 별도의 validateTokenAndThrow 메서드가 예외를 발생시키는 역할을 담당하고 있습니다.


32-43: JWT 토큰 생성 로직이 잘 구현되어 있습니다.

액세스 토큰과 리프레시 토큰을 구분하는 type 클레임과 적절한 만료 시간 설정이 좋습니다. HMAC SHA-512 알고리즘 사용도 보안상 적절합니다.


188-196: Authentication 객체 생성이 적절합니다.

Spring Security와의 통합을 위한 Authentication 객체 생성이 올바르게 구현되어 있습니다. 토큰 검증 후 사용자 권한을 설정하는 방식이 적절합니다.

Comment on lines +4 to +5
main:
allow-bean-definition-overriding: true
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

Bean 정의 오버라이딩 설정에 대한 검토 필요

allow-bean-definition-overriding: true 설정은 잠재적인 설계 문제를 나타낼 수 있습니다. 이 설정이 필요한 구체적인 이유를 문서화하고, 가능하다면 Bean 이름 충돌을 해결하여 이 설정을 제거하는 것을 고려해보세요.

🤖 Prompt for AI Agents
In apis/src/main/resources/application.yml at lines 4 to 5, the setting
allow-bean-definition-overriding: true may mask underlying design issues by
allowing bean name conflicts. Review why this override is necessary, document
the specific reasons for it, and if possible, resolve bean name collisions in
the codebase to remove this setting and improve configuration clarity.

Comment on lines 75 to 84
override fun getUserProfile(userId: Long): UserProfileResponse {
val user = userRepository.findById(userId)

return UserProfileResponse(
id = user.id!!,
email = user.email,
nickname = user.nickname,
provider = user.providerType.name
)
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

NullPointerException 위험

userRepository.findById가 null을 반환할 수 있는데 null 체크가 없습니다. 또한 user.id!!는 안전하지 않습니다.

override fun getUserProfile(userId: Long): UserProfileResponse {
-    val user = userRepository.findById(userId)
+    val user = userRepository.findById(userId)
+        ?: throw AuthException(AuthErrorCode.USER_NOT_FOUND, "User not found with id: $userId")

    return UserProfileResponse(
-        id = user.id!!,
+        id = user.id ?: throw IllegalStateException("User id should not be null"),
        email = user.email,
        nickname = user.nickname,
        provider = user.providerType.name
    )
}
📝 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.

Suggested change
override fun getUserProfile(userId: Long): UserProfileResponse {
val user = userRepository.findById(userId)
return UserProfileResponse(
id = user.id!!,
email = user.email,
nickname = user.nickname,
provider = user.providerType.name
)
}
override fun getUserProfile(userId: Long): UserProfileResponse {
val user = userRepository.findById(userId)
?: throw AuthException(AuthErrorCode.USER_NOT_FOUND, "User not found with id: $userId")
return UserProfileResponse(
id = user.id ?: throw IllegalStateException("User id should not be null"),
email = user.email,
nickname = user.nickname,
provider = user.providerType.name
)
}
🤖 Prompt for AI Agents
In apis/src/main/kotlin/org/yapp/apis/auth/service/AuthServiceImpl.kt around
lines 75 to 84, the code calls userRepository.findById without checking if the
result is null, and uses user.id!! which can cause a NullPointerException.
Modify the code to safely handle the case when the user is not found by checking
if the user is null and throwing an appropriate exception or returning a default
response. Also, avoid using the !! operator by safely accessing user.id after
confirming user is not null.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Review continued from previous batch...

Comment on lines 85 to 86
@PostMapping("/signout")
fun signOut(@RequestBody userId: Long): ResponseEntity<Void>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

signOut API 설계를 보안 관점에서 개선하세요.

현재 API에서 userId를 요청 바디로 받고 있는데, 이는 다른 사용자의 계정을 로그아웃시킬 수 있는 보안 위험이 있습니다. JWT 토큰에서 사용자 정보를 추출하는 방식으로 변경해야 합니다.

 @PostMapping("/signout")
-fun signOut(@RequestBody userId: Long): ResponseEntity<Void>
+fun signOut(@RequestHeader("Authorization") authorization: String): ResponseEntity<Void>
🤖 Prompt for AI Agents
In apis/src/main/kotlin/org/yapp/apis/auth/controller/AuthControllerApi.kt at
lines 85-86, the signOut API currently accepts userId in the request body, which
poses a security risk by allowing logout of other users. Modify the method to
extract the user identity from the JWT token in the request context instead of
accepting userId as input. Remove the userId parameter and implement logic to
retrieve the authenticated user's ID from the token for logout processing.

Copy link
Member

@move-hoon move-hoon left a comment

Choose a reason for hiding this comment

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

고생하셨습니다 민우님!!
시간 관계상 AuthServiceImpl 전까지 리뷰 남기고, 내일 마저 남기겠습니다!

지금까지 본 코드에 대해서 개인적으로 총 두가지의 의견이 있습니다.

제 의견은 정답이 아니기 때문에 편하게 피드백 주셔도 좋을 것 같아요 😁

1. Usecase는 인터페이스와 Impl 클래스를 분리할 필요가 없을 것 같다.

  • 파사드의 역할을 해주고 있고, 인터페이스 1-depth 추가를 했지만, controller와 같이 swagger 명세의 역할도 아니기 때문에 오히려 흐름을 파악하는데 어려움을 느꼈습니다.

2. AuthServiceImpl의 일부분은 도메인 서비스로 가져가도 좋을 것 같다.

  • 저는 보면서 아래와 같은 기준으로, 기존 사용자 존재 시 로그인 처리 결정 정책(외부 의존성 없이 판단 가능한 업무 규칙), 이메일 중복 금지 규칙(도메인 정책, 기술과 무관)은 도메인 서비스로 가져가도 될 것 같다고 생각이 들었습니다.

  • 물론 민우님 말씀처럼 도메인 모듈에 최대한 변하지 않는 로직을 넣는 방향성도 좋은 방향성이라고 생각합니다. 하지만, 결국 각 역할에 맞는 서비스 구현체를 두는 것이 더 명확한 책임 분리와 확장성 측면에서 유리하다고 생각합니다.

  • 그리고 제 생각에는 도메인 규칙이 변하면(ex. 30% sale -> 20% sale) apis 모듈을 수정하는것이 아닌 도메인 모듈을 수정하는게 맞는 판단이라고 생각됩니다.

  • 또한, 이 방식도 트랜잭션 스크립트 패턴을 이용하는 것이 아닌 도메인 모델 패턴을 이용한다면,간단한 비즈니스 변경이라면 서비스 레이어 수정까지 갈 필요 없이 엔티티나 값 객체의 메서드나 데이터만 수정하면 되기 때문에 더욱 역할에 맞으리라 생각합니다. (참고링크)

**순수 비즈니스 로직 (Domain Service)**
- 외부 의존성 없이 도메인 객체만으로 처리 가능
- 업무 규칙과 정책 (비즈니스 요구사항)
- 기술과 무관한 도메인 지식
- 테스트 시 Mock 없이 검증 가능

**애플리케이션 비즈니스 로직 (Application Service)**
- 외부 시스템(DB, API, 파일 등) 의존
- 기술적 관심사 (프레임워크, 인프라)
- 흐름 제어와 조정 역할
- 예외 변환, 트랜잭션 관리

/**
* DTOs for authentication requests and responses.
*/
object AuthDto {
Copy link
Member

Choose a reason for hiding this comment

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

kotlin에서 제공해주는 object를 사용하셨군요!
class 방식이 아닌, object로 data class dto를 그룹핑하신 이유가 궁금합니다!!

Copy link
Member Author

Choose a reason for hiding this comment

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

object AuthDto는 SocialLoginRequest, TokenRefreshRequest, AuthResponse처럼 인증 도메인에서만 사용하는 DTO들을 하나의 범주로 묶기 위해 사용했습니다.
이렇게 하면 DTO가 많아져도 클래스명이 중복되지 않아 구조적인 가독성과 유지보수성으로 좋지 않을까 싶었고요!
또 object는 인스턴스를 만들 필요 없이 정적 접근이 가능해 접근성도 좋다고 판단했습니다.

Copy link
Member

Choose a reason for hiding this comment

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

저는 개인적으로 request와 response DTO를 패키지를 통해 분리하는 것을 선호합니다!!
AuthDto 내부에 auth 관련한 DTO를 모아두는 것은 정말 좋은 아이디어라고 생각합니다. 다만, request와 response가 함께 있어서 역할 구분이 다소 모호하고, 현재 DTO가 2-3개밖에 없는 상황에서는 Object 래핑이 불필요한 복잡성을 추가하는 것 같다는 생각이 듭니다!

그래서 개인적으로는 관리할 dto가 많아진다면 그때 Object를 도입해봐도 좋을 것 같다고 생각이 들었습니다.(ex. Auth Request DTO 개수 5개 -> Auth Request Object로 래핑)

이 부분에 대해서 민우님의 생각이 궁금합니다 😊

Copy link
Member Author

Choose a reason for hiding this comment

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

좋은 생각 같아요. 첫 비즈니스 로직이 들어간 코드인 만큼 이런 부분 많이 맞춰가면 좋을 것 같아요! 그러면 request 하고 response 폴더 구분에서 dto를 별도로 관리하는 방향으로 가보죠!


fun signOut(userId: Long)

fun getUserById(userId: Long): User?
Copy link
Member

Choose a reason for hiding this comment

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

해당 메서드는 사용하지 않아서 삭제해도 될 것 같습니다!

Copy link
Member Author

Choose a reason for hiding this comment

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

GOOD 반영했습니다

/**
* Interface for authentication services.
*/
interface AuthService {
Copy link
Member

Choose a reason for hiding this comment

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

지금 UseCase와 Service가 거의 동일한 상황입니다.

UseCase는 비즈니스 플로우, Service는 단일 책임으로 가야 의미가 있을 것으로 생각됩니다.
따라서 AuthService를 좀 더 세분화 하면 좋을 것 같습니다. (ex. TokenService, SocialAuthService, AuthUserManagementService 등)

Copy link
Member Author

Choose a reason for hiding this comment

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

의견 주셔서 감사합니다.
TokenService와 관련된 기능은 현재 API Gateway에서 정의되어 있어, 토큰 관리 책임은 Gateway 측에서 처리하는 구조가 더 자연스러울 것이라고 판단하고 있습니다.

또한 socialAuth 관련 로직은 현재 전략 패턴을 통해 AuthStrategy로 유동적으로 동작하도록 구성되어 있어,
단순한 책임 분리보다 전략 구현체 간 확장성과 유연성이 우선시되는 구조입니다.

그렇기에 서비스 분리를 진행하더라도 실제로 역할 간 결합도가 높아지지 않도록 신중히 고민해볼 필요가 있을 것 같습니다.
다만 말씀해주신 방향처럼 UseCase는 명확하게 흐름(플로우)만 담당하고,
Service는 해당 로직을 구성하는 작은 단위 역할로 분리하는 방향은 충분히 검토해볼 가치가 있다고 생각합니다.

Copy link
Member Author

Choose a reason for hiding this comment

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

아 말씀하신부분이 jwtTokenservice가 아니라 tokenservice부분을 말씀하신거군요~ 좋은 생각 같아요 로직 구현하면서 갑자기 생각나서 이부분도 반영하면 좋을 것 같아요 좋은 피드백 감사합니다 :)

Comment on lines 20 to 21
private val userRepository: UserRepository,
private val tokenRepository: TokenRepository,
Copy link
Member

Choose a reason for hiding this comment

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

저희가 룰을 정했을 때 서비스 <-> 서비스 의존으로 가자고 했었는데요!

따라서 해당 부분은 서비스를 의존하도록 바꾸는 방식은 어떨까요?

Copy link
Member Author

Choose a reason for hiding this comment

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

동의합니다 해당 부분 반영하겠습니다~

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 15

♻️ Duplicate comments (5)
apis/src/main/kotlin/org/yapp/apis/auth/service/AuthCredentials.kt (1)

5-19: Sealed class 구조가 타입 안전성을 잘 보장합니다.

이전 리뷰 댓글에서 제안된 sealed class 구조가 적절히 구현되었습니다. 각 프로바이더별로 명확한 타입 분리가 되어 있고, when 표현식에서 exhaustive checking이 가능합니다.

apis/src/main/kotlin/org/yapp/apis/auth/exception/AuthErrorCode.kt (1)

15-31: 에러 코드 구조화가 잘 되어 있습니다

HTTP 상태 코드별로 에러를 그룹화한 점이 좋습니다. 이전 리뷰 피드백이 잘 반영되었네요.

apis/src/main/kotlin/org/yapp/apis/auth/controller/AuthControllerApi.kt (1)

87-88: 보안 이슈가 잘 해결되었습니다

signOut API에서 Authorization 헤더를 사용하도록 변경한 것이 보안상 올바른 접근입니다.

apis/src/main/kotlin/org/yapp/apis/auth/controller/AuthController.kt (1)

31-35: 토큰 추출 로직 개선이 잘 반영되었습니다

AuthUtils를 사용하여 토큰 파싱 로직을 공통화한 점과 Authorization 헤더에서 사용자 ID를 추출하는 보안적 접근이 훌륭합니다.

global-utils/src/main/kotlin/org/yapp/global/exception/GlobalExceptionHandler.kt (1)

78-82: 보안 위험: 민감한 정보 노출 방지 필요

ex.localizedMessage를 클라이언트에 노출하는 것은 내부 시스템 정보를 유출할 수 있는 보안 위험이 있습니다.

     val error = ErrorResponse.builder()
         .status(commonErrorCode.getHttpStatus().value())
-        .message("Malformed JSON request: ${ex.localizedMessage}")
+        .message("Malformed JSON request")
         .code(commonErrorCode.getCode())
         .build()
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 96534ab and f2db88e.

📒 Files selected for processing (19)
  • apis/src/main/kotlin/org/yapp/apis/auth/controller/AuthController.kt (1 hunks)
  • apis/src/main/kotlin/org/yapp/apis/auth/controller/AuthControllerApi.kt (1 hunks)
  • apis/src/main/kotlin/org/yapp/apis/auth/dto/request/SocialLoginRequest.kt (1 hunks)
  • apis/src/main/kotlin/org/yapp/apis/auth/dto/request/TokenRefreshRequest.kt (1 hunks)
  • apis/src/main/kotlin/org/yapp/apis/auth/dto/response/AuthResponse.kt (1 hunks)
  • apis/src/main/kotlin/org/yapp/apis/auth/dto/response/TokenPairResponse.kt (1 hunks)
  • apis/src/main/kotlin/org/yapp/apis/auth/dto/response/UserProfileResponse.kt (1 hunks)
  • apis/src/main/kotlin/org/yapp/apis/auth/exception/AuthErrorCode.kt (1 hunks)
  • apis/src/main/kotlin/org/yapp/apis/auth/exception/AuthException.kt (1 hunks)
  • apis/src/main/kotlin/org/yapp/apis/auth/service/AuthCredentials.kt (1 hunks)
  • apis/src/main/kotlin/org/yapp/apis/auth/service/SocialAuthService.kt (1 hunks)
  • apis/src/main/kotlin/org/yapp/apis/auth/service/TokenService.kt (1 hunks)
  • apis/src/main/kotlin/org/yapp/apis/auth/service/UserService.kt (1 hunks)
  • apis/src/main/kotlin/org/yapp/apis/auth/usecase/AuthUseCase.kt (1 hunks)
  • domain/src/main/kotlin/org/yapp/domain/domainservice/UserDomainService.kt (1 hunks)
  • gateway/src/main/kotlin/org/yapp/gateway/jwt/exception/JwtException.kt (1 hunks)
  • global-utils/src/main/kotlin/org/yapp/annotation/DomainService.kt (1 hunks)
  • global-utils/src/main/kotlin/org/yapp/global/exception/CommonException.kt (1 hunks)
  • global-utils/src/main/kotlin/org/yapp/global/exception/GlobalExceptionHandler.kt (1 hunks)
🧰 Additional context used
🪛 detekt (1.23.8)
global-utils/src/main/kotlin/org/yapp/global/exception/CommonException.kt

[warning] 1-10: The file does not contain a package declaration.

(detekt.potential-bugs.MissingPackageDeclaration)

🔇 Additional comments (12)
global-utils/src/main/kotlin/org/yapp/annotation/DomainService.kt (1)

1-11: 도메인 서비스 어노테이션이 적절하게 정의되었습니다.

깔끔한 구조로 도메인 계층의 서비스를 명확히 구분할 수 있게 해주는 좋은 접근입니다. @component 메타 어노테이션으로 Spring 컨테이너에서 자동 관리되도록 설정한 것도 적절합니다.

apis/src/main/kotlin/org/yapp/apis/auth/dto/response/AuthResponse.kt (1)

11-11: TokenPairResponse 임포트가 누락되었습니다.

fromTokenPair 메서드에서 사용하는 TokenPairResponse 타입의 임포트 선언이 필요합니다.

파일 상단에 다음 임포트를 추가하세요:

 package org.yapp.apis.auth.dto.response
+
+import org.yapp.apis.auth.dto.response.TokenPairResponse

Likely an incorrect or invalid review comment.

apis/src/main/kotlin/org/yapp/apis/auth/service/SocialAuthService.kt (1)

9-20: 전략 패턴을 활용한 깔끔한 설계입니다.

여러 소셜 로그인 제공자를 지원하기 위한 전략 패턴 구현이 잘 되어 있고, 에러 처리도 적절합니다. 확장성이 우수한 설계입니다.

apis/src/main/kotlin/org/yapp/apis/auth/dto/request/SocialLoginRequest.kt (2)

15-22: 유효성 검사 로직이 잘 구현되어 있습니다.

init 블록에서 필수 필드들에 대한 적절한 검증을 수행하고 있습니다. 빈 문자열 체크는 소셜 로그인에서 중요한 보안 요소입니다.


24-38: toCredentials() 메서드의 타입 안전성이 우수합니다.

runCatching을 사용한 예외 처리와 when 표현식을 통한 타입 안전한 크리덴셜 생성이 잘 구현되어 있습니다. 지원하지 않는 프로바이더 타입에 대한 적절한 예외 처리도 포함되어 있습니다.

apis/src/main/kotlin/org/yapp/apis/auth/service/TokenService.kt (1)

13-26: 토큰 서비스 메서드들이 적절히 구현되었습니다.

각 메서드는 단일 책임 원칙을 잘 따르고 있으며, validateRefreshTokenOrThrow 메서드에서 적절한 예외 처리를 하고 있습니다. 토큰 관리에 필요한 핵심 기능들이 잘 캡슐화되어 있습니다.

apis/src/main/kotlin/org/yapp/apis/auth/service/UserService.kt (2)

16-19: 사용자 조회 로직이 적절히 구현되었습니다.

엘비스 연산자를 사용한 null 체크와 적절한 예외 처리가 구현되어 있습니다. 에러 메시지에 사용자 ID를 포함하여 디버깅에 도움이 됩니다.


21-25: 도메인 서비스와의 통합이 잘 설계되었습니다.

Result 패턴을 사용하여 함수형 에러 핸들링을 구현하고 있으며, getOrElse를 통해 적절한 예외 변환을 수행하고 있습니다. 이메일 중복 처리도 명확하게 되어 있습니다.

apis/src/main/kotlin/org/yapp/apis/auth/usecase/AuthUseCase.kt (2)

20-26: 인증 플로우가 적절히 구현되었습니다.

전략 패턴을 사용한 소셜 인증과 사용자 생성/조회 로직이 잘 구성되어 있습니다. 각 단계별로 명확한 책임 분리가 되어 있습니다.


52-59: 토큰 생성 로직이 잘 구현되었습니다.

액세스 토큰과 리프레시 토큰 생성, 만료 시간 설정, 저장소 저장까지의 전체 플로우가 적절히 구현되어 있습니다.

apis/src/main/kotlin/org/yapp/apis/auth/controller/AuthControllerApi.kt (1)

108-111: API 설계가 일관성 있게 잘 구현되었습니다

getUserProfile 엔드포인트가 /me 경로를 사용하여 RESTful 설계 원칙을 잘 따르고 있고, Authorization 헤더에서 사용자 정보를 추출하는 방식이 보안상 적절합니다.

apis/src/main/kotlin/org/yapp/apis/auth/controller/AuthController.kt (1)

16-18: 의존성 주입과 책임 분리가 잘 구현되었습니다

생성자 주입을 통한 의존성 관리와 비즈니스 로직을 AuthUseCase로 위임하는 구조가 클린 아키텍처 원칙을 잘 따르고 있습니다.

Comment on lines 11 to 24
fun findOrCreate(user: User): Result<User> {
val existingByProvider = userRepository.findByProviderTypeAndProviderId(user.providerType, user.providerId)
if (existingByProvider != null) {
return Result.success(existingByProvider)
}

val existingByEmail = userRepository.findByEmail(user.email)
return if (existingByEmail == null) {
val saved = userRepository.save(user)
Result.success(saved)
} else {
Result.failure(IllegalStateException("Email already in use"))
}
}
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

도메인 로직은 적절하나 예외 처리를 개선해보세요.

findOrCreate 메서드의 로직은 올바르지만, 다음 개선사항을 고려해보세요:

  1. IllegalStateException보다는 도메인 특화 예외 클래스를 사용하는 것이 더 명확합니다.
  2. 메서드에 대한 KDoc 문서화를 추가하면 가독성이 향상됩니다.

다음과 같은 개선을 제안합니다:

+/**
+ * 제공업체 정보로 사용자를 찾거나 새 사용자를 생성합니다.
+ * 이메일이 이미 존재하는 경우 실패를 반환합니다.
+ */
 fun findOrCreate(user: User): Result<User> {
     val existingByProvider = userRepository.findByProviderTypeAndProviderId(user.providerType, user.providerId)
     if (existingByProvider != null) {
         return Result.success(existingByProvider)
     }

     val existingByEmail = userRepository.findByEmail(user.email)
     return if (existingByEmail == null) {
         val saved = userRepository.save(user)
         Result.success(saved)
     } else {
-        Result.failure(IllegalStateException("Email already in use"))
+        Result.failure(EmailAlreadyInUseException("Email already in use: ${user.email}"))
     }
 }
📝 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.

Suggested change
fun findOrCreate(user: User): Result<User> {
val existingByProvider = userRepository.findByProviderTypeAndProviderId(user.providerType, user.providerId)
if (existingByProvider != null) {
return Result.success(existingByProvider)
}
val existingByEmail = userRepository.findByEmail(user.email)
return if (existingByEmail == null) {
val saved = userRepository.save(user)
Result.success(saved)
} else {
Result.failure(IllegalStateException("Email already in use"))
}
}
/**
* 제공업체 정보로 사용자를 찾거나 새 사용자를 생성합니다.
* 이메일이 이미 존재하는 경우 실패를 반환합니다.
*/
fun findOrCreate(user: User): Result<User> {
val existingByProvider = userRepository.findByProviderTypeAndProviderId(user.providerType, user.providerId)
if (existingByProvider != null) {
return Result.success(existingByProvider)
}
val existingByEmail = userRepository.findByEmail(user.email)
return if (existingByEmail == null) {
val saved = userRepository.save(user)
Result.success(saved)
} else {
Result.failure(EmailAlreadyInUseException("Email already in use: ${user.email}"))
}
}
🤖 Prompt for AI Agents
In domain/src/main/kotlin/org/yapp/domain/domainservice/UserDomainService.kt
around lines 11 to 24, improve exception handling by replacing the generic
IllegalStateException with a custom domain-specific exception class that clearly
indicates the email conflict. Additionally, add KDoc comments above the
findOrCreate method to describe its purpose, parameters, and return type to
enhance code readability and maintainability.

Comment on lines 3 to 5
data class TokenRefreshRequest(
val refreshToken: String
) No newline at end of file
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

입력 검증과 보안 고려사항을 추가해보세요.

DTO 구조는 적절하지만 다음 개선사항을 고려해보세요:

+import jakarta.validation.constraints.NotBlank
+
 data class TokenRefreshRequest(
+    @field:NotBlank(message = "Refresh token cannot be blank")
     val refreshToken: String
-)
+) {
+    override fun toString(): String {
+        return "TokenRefreshRequest(refreshToken=***)"
+    }
+}
  1. @NotBlank 검증 어노테이션으로 빈 토큰을 방지합니다.
  2. toString() 오버라이드로 로그에 토큰이 노출되지 않도록 보호합니다.
📝 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.

Suggested change
data class TokenRefreshRequest(
val refreshToken: String
)
import jakarta.validation.constraints.NotBlank
data class TokenRefreshRequest(
@field:NotBlank(message = "Refresh token cannot be blank")
val refreshToken: String
) {
override fun toString(): String {
return "TokenRefreshRequest(refreshToken=***)"
}
}
🤖 Prompt for AI Agents
In apis/src/main/kotlin/org/yapp/apis/auth/dto/request/TokenRefreshRequest.kt
lines 3 to 5, add the @NotBlank annotation to the refreshToken property to
prevent empty token inputs. Also, override the toString() method to avoid
exposing the token value in logs by returning a masked or generic string instead
of the actual token.

Comment on lines 3 to 6
data class TokenPairResponse(
val accessToken: String,
val refreshToken: String
) No newline at end of file
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

토큰 보안을 위한 추가 고려사항이 있습니다.

토큰 정보를 담는 응답 DTO로서 적절하지만, 보안을 위한 개선을 고려해보세요:

 data class TokenPairResponse(
     val accessToken: String,
     val refreshToken: String
-)
+) {
+    override fun toString(): String {
+        return "TokenPairResponse(accessToken=***, refreshToken=***)"
+    }
+}

toString() 메서드를 오버라이드하여 로그나 디버깅 시 토큰 값이 노출되지 않도록 보호하는 것을 권장합니다.

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

Suggested change
data class TokenPairResponse(
val accessToken: String,
val refreshToken: String
)
data class TokenPairResponse(
val accessToken: String,
val refreshToken: String
) {
override fun toString(): String {
return "TokenPairResponse(accessToken=***, refreshToken=***)"
}
}
🤖 Prompt for AI Agents
In apis/src/main/kotlin/org/yapp/apis/auth/dto/response/TokenPairResponse.kt
between lines 3 and 6, the data class TokenPairResponse currently exposes token
values in its default toString() method, which can lead to sensitive token
information being logged or exposed during debugging. Override the toString()
method in this class to mask or omit the accessToken and refreshToken values,
ensuring that token details are not revealed in logs or debug outputs.

Comment on lines 3 to 8
data class UserProfileResponse(
val id: Long,
val email: String,
val nickname: String,
val provider: String
) No newline at end of file
Copy link

@coderabbitai coderabbitai bot Jun 22, 2025

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

사용자 프로필 DTO에 타입 안전성과 문서화를 고려해보세요.

기본 구조는 적절하지만 다음 개선사항을 고려해보세요:

+/**
+ * 사용자 프로필 정보를 담는 응답 DTO
+ * @property id 사용자 고유 식별자
+ * @property email 사용자 이메일
+ * @property nickname 사용자 닉네임
+ * @property provider 소셜 로그인 제공업체
+ */
 data class UserProfileResponse(
     val id: Long,
     val email: String,
     val nickname: String,
-    val provider: String
+    val provider: String // 추후 ProviderType enum 사용 고려
 )
  1. KDoc 문서화를 추가하여 각 필드의 의미를 명확히 합니다.
  2. provider 필드는 추후 도메인의 ProviderType enum을 사용하면 타입 안전성이 향상됩니다.
  3. 이메일 정보가 포함되어 있으므로 사용 시 개인정보 보호 정책을 준수해야 합니다.
🤖 Prompt for AI Agents
In apis/src/main/kotlin/org/yapp/apis/auth/dto/response/UserProfileResponse.kt
around lines 3 to 8, enhance the UserProfileResponse data class by adding KDoc
comments to each field to clarify their meanings. Replace the String type of the
provider field with a domain-specific enum type like ProviderType to improve
type safety. Also, ensure that usage of the email field complies with privacy
policies due to its sensitive nature.

Copy link
Member

Choose a reason for hiding this comment

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

@minwoo1999 저도 여기서 String 타입이 아닌, ProviderType enum으로 받는게 타입 안정성 측면에서 좋아보이는데 어떻게 생각하시나요?

Copy link

Choose a reason for hiding this comment

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

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

Comment on lines 40 to 46
return UserProfileResponse(
id = user.id!!,
email = user.email,
nickname = user.nickname,
provider = user.providerType.name
)
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

사용자 프로필 조회에서 null 안전성을 개선하세요.

user.id!! 강제 언래핑이 여기서도 사용되고 있습니다. 일관된 null 처리 전략이 필요합니다.

-        return UserProfileResponse(
-            id = user.id!!,
-            email = user.email,
-            nickname = user.nickname,
-            provider = user.providerType.name
-        )
+        val userId = user.id ?: throw AuthException(AuthErrorCode.USER_NOT_FOUND, "User ID is null")
+        return UserProfileResponse(
+            id = userId,
+            email = user.email,
+            nickname = user.nickname,
+            provider = user.providerType.name
+        )
🤖 Prompt for AI Agents
In apis/src/main/kotlin/org/yapp/apis/auth/usecase/AuthUseCase.kt around lines
40 to 46, the code uses forced unwrapping with user.id!! which risks a
NullPointerException. Replace the forced unwrap with a safe null handling
approach, such as using a safe call with an elvis operator to provide a default
value or throwing a controlled exception if id is null, ensuring consistent and
safe null handling throughout the user profile retrieval.

Copy link
Member

@move-hoon move-hoon left a comment

Choose a reason for hiding this comment

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

틈틈 리뷰 하나만 남기고 갑니다 ㅎㅎ

Comment on lines 15 to 22
init {
require(providerType.isNotBlank()) {
"Provider type must not be blank"
}
require(oauthToken.isNotBlank()) {
"Access token must not be blank"
}
}
Copy link
Member

Choose a reason for hiding this comment

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

틈틈 리뷰 하나만 남기고 갑니다 ㅎㅎ

@field:NotBlank(message = "Provider type is required")
val providerType: String,
    
@field:NotBlank(message = "OAuth token is required")
val oauthToken: String

@PostMapping("/login")
fun socialLogin(
    @Valid @RequestBody request: SocialLoginRequest  
): AuthResponse {

이렇게 validation 어노테이션도 코틀린에서 쓸수 있다고 하네요!

Comment on lines 6 to 9
init {
require(refreshToken.isNotBlank()) {
"Refresh token must not be blank"
}
Copy link
Member

Choose a reason for hiding this comment

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

여기도 어노테이션으로 바꾸면 더 깔끔해 질 것 같아요!

Copy link
Member

@move-hoon move-hoon left a comment

Choose a reason for hiding this comment

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

https://spoqa.github.io/2022/08/16/kotlin-jpa-entity.html
https://easywritten.com/post/is-it-okay-to-use-a-val-field-in-a-kotlin-jpa-entity/

엔티티 설계 관련해서 위 블로그를 2개를 보고 가장 공감되었던 부분이라 공유를 드립니다.
간단히 요약하자면 다음과 같습니다!

1. 엔티티에서 val는 private final property + public getter, var은 private property + public getter/setter 이다.

2. JPA와 Hibernate 문서는 엔티티가 지켜야 하는 조건 3가지를 제시하며, 때문에 val를 직접적으로 엔티티에 사용하는건 지양해야 할 수도 있다.

1. 클래스를 final로 선언하면 안된다.
2. 메소드(함수)를 final로 선언하면 안된다.
3. 필드(변수, 인스턴스 변수, 영속성 인스턴스 변수)를 final로 선언하면 안된다.

이에 대한 제 의견은 다음과 같습니다.

  • 1번은 kotlin plugIn allOpen으로 인해 entity class를 open 클래스로 만들어주기에 hibernate 프록시 객체가 상속하기 때문에 해결이 가능하다.
  • 2번도 allOpen으로 해결이 가능하다.
  • 3번의 경우 val로 할 경우 private final이 되버리므로 안된다.
    • 그렇다고 var로 할경우 public setter를 열어버리기 된다.
    • 따라서 var + protected set + 명시적 업데이트 방식을 선택하는 방식이 현재로서는 최선의 선택지라고 생각된다.

3. equal and hashcode를 id 기반으로 구현해주면 좋다

val user1 = User(id = 1L, ...)
val user2 = User(id = 1L, ...)

val set = mutableSetOf(user1)
set.contains(user2)  // 재정의 안 하면 false
  • id가 같으면 같은 사용자로 인식해야 하나, 기본 클래스는 equals()가 참조 동일성 (==) 으로 동작하므로 id는 같아도 객체가 다르면 equals()가 false로 나오게 됩니다. 따라서 id를 기준으로 equals and hashcode를 재정의 해줘야 합니다. (JAVA에서도 마찬가지)
  • data class를 엔티티로 사용는 사례도 있는데, 모든 프로퍼티에 대해서 재정의 해주기 때문에 모든 값이 같아야 같은 사용자라고 인식하는 문제가 있어서 지양해야 한다고 하네요!

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

♻️ Duplicate comments (15)
apis/src/main/kotlin/org/yapp/apis/auth/dto/request/SocialLoginRequest.kt (1)

10-15: 과거 리뷰 코멘트의 검증 어노테이션 개선 제안이 유효합니다.

이전 리뷰에서 언급된 검증 어노테이션 사용법이 올바릅니다. 현재 구현도 적절하지만, 필요시 다음과 같이 개선할 수 있습니다.

apis/src/main/kotlin/org/yapp/apis/auth/dto/response/UserProfileResponse.kt (1)

5-10: 타입 안전성과 문서화 개선을 위한 제안사항들이 아직 반영되지 않았습니다.

이전 리뷰에서 제안된 개선사항들을 고려해보세요:

  1. KDoc 문서화 추가로 각 필드의 의미를 명확히 하기
  2. provider 필드에 도메인의 ProviderType enum 사용으로 타입 안전성 향상
  3. 이메일 정보 포함으로 인한 개인정보 보호 정책 준수 필요성
apis/src/main/kotlin/org/yapp/apis/auth/dto/request/TokenRefreshRequest.kt (1)

5-8: 보안을 위한 toString() 오버라이드가 누락되었습니다.

검증 어노테이션은 잘 적용되었지만, 이전 리뷰에서 제안된 보안 개선사항이 아직 반영되지 않았습니다. 로그에 토큰이 노출되지 않도록 toString() 메서드를 오버라이드해야 합니다.

 data class TokenRefreshRequest(
     @field:NotBlank(message = "Refresh token is required")
     val refreshToken: String
-)
+) {
+    override fun toString(): String {
+        return "TokenRefreshRequest(refreshToken=***)"
+    }
+}
apis/src/main/kotlin/org/yapp/apis/util/AuthUtils.kt (1)

6-12: 입력 검증 및 에러 핸들링이 여전히 부족합니다.

이전 리뷰에서 지적된 보안 취약점이 해결되지 않았습니다:

  1. Authorization 헤더 형식 검증 없음
  2. removePrefix는 "Bearer "로 시작하지 않아도 원본 문자열을 반환하여 잘못된 토큰이 전달될 수 있음
  3. getUserId 함수 호출 시 예외 처리 없음
 fun extractUserIdFromAuthHeader(
     authorizationHeader: String,
     getUserId: (String) -> UUID
 ): UUID {
-    val token = authorizationHeader.removePrefix("Bearer ").trim()
+    if (!authorizationHeader.startsWith("Bearer ")) {
+        throw IllegalArgumentException("Invalid authorization header format")
+    }
+    val token = authorizationHeader.substring(7).trim()
+    if (token.isBlank()) {
+        throw IllegalArgumentException("Token cannot be empty")
+    }
     return getUserId(token)
 }
apis/src/main/resources/application.yml (3)

4-5: Bean 정의 오버라이딩 설정에 대한 재검토가 필요합니다.

이전 리뷰에서 지적된 allow-bean-definition-overriding: true 설정이 여전히 존재합니다. 이 설정이 필요한 구체적인 이유를 문서화하고, 가능하다면 Bean 이름 충돌을 해결하여 이 설정을 제거하는 것을 고려해보세요.


26-26: JWT 비밀키 보안 취약점이 여전히 존재합니다.

이전 리뷰에서 지적된 중요한 보안 문제가 해결되지 않았습니다. 하드코딩된 기본 JWT 비밀키는 프로덕션 환경에서 심각한 보안 위험을 초래합니다.

-  secret-key: ${JWT_SECRET_KEY:9a4f2c8d3b7a1e6f5d4c2b8a7f3e9d1c6b4a8f2e7d3c5b9a1f4e8d2c7b3a6f5e9}
+  secret-key: ${JWT_SECRET_KEY}

기본값을 제거하여 환경변수를 통해서만 비밀키를 설정하도록 해야 합니다.


42-42: 파일 끝의 불필요한 빈 줄을 제거해주세요.

정적 분석 도구에서 지적한 대로 파일 끝의 불필요한 빈 줄을 제거해야 합니다.

-
domain/src/main/kotlin/org/yapp/domain/user/User.kt (1)

26-27: 타임스탬프 기본값 설정을 도메인 레이어에서 제거해야 합니다.

도메인 모델에서 LocalDateTime.now()를 기본값으로 사용하는 것은 테스트 가능성을 저해하고 도메인 모델의 순수성을 해칩니다.

apis/src/main/kotlin/org/yapp/apis/auth/service/TokenService.kt (1)

1-1: 패키지 선언이 파일 경로와 일치하지 않습니다.

파일 경로는 apis/src/main/kotlin/org/yapp/apis/auth/service/TokenService.kt인데 패키지 선언은 org.yapp.apis.token.service로 되어 있습니다.

-package org.yapp.apis.token.service
+package org.yapp.apis.auth.service
apis/src/main/kotlin/org/yapp/apis/auth/usecase/AuthUseCase.kt (2)

25-26: null 안전성을 개선하세요.

user.id!! 강제 언래핑은 잠재적인 런타임 예외를 발생시킬 수 있습니다. 도메인 모델에서 ID가 항상 존재함을 보장하거나 안전한 처리가 필요합니다.

-        val user = userService.findOrCreateUser(userInfo)
-        return generateTokenPair(user.id!!)
+        val user = userService.findOrCreateUser(userInfo)
+        val userId = user.id ?: throw AuthException(AuthErrorCode.USER_NOT_FOUND, "User ID is null")
+        return generateTokenPair(userId)

41-46: 사용자 프로필 조회에서 null 안전성을 개선하세요.

user.id!! 강제 언래핑이 여기서도 사용되고 있습니다. 일관된 null 처리 전략이 필요합니다.

-        return UserProfileResponse(
-            id = user.id!!,
-            email = user.email,
-            nickname = user.nickname,
-            provider = user.providerType.name
-        )
+        val userId = user.id ?: throw AuthException(AuthErrorCode.USER_NOT_FOUND, "User ID is null")
+        return UserProfileResponse(
+            id = userId,
+            email = user.email,
+            nickname = user.nickname,
+            provider = user.providerType.name
+        )
domain/src/main/kotlin/org/yapp/domain/user/UserEntity.kt (1)

32-32: updatedAt 필드 자동 업데이트 기능 추가 필요

현재 updatedAt 필드가 생성 시점에만 설정되고 수정 시에는 자동으로 업데이트되지 않습니다. JPA의 @PreUpdate 어노테이션을 사용하여 엔티티 수정 시 자동으로 업데이트되도록 개선이 필요합니다.

gateway/src/main/kotlin/org/yapp/gateway/jwt/JwtTokenProvider.kt (3)

25-25: Secret key를 Base64로 인코딩하여 관리하는 것을 고려하세요

현재 평문 문자열로 secret key를 관리하고 있습니다. Base64 인코딩된 키를 사용하면 더 안전하고 표준적입니다.


51-58: validateToken 메서드가 구체적인 오류 정보를 숨깁니다

현재 구현은 모든 JWT 예외를 false로 반환하여 구체적인 실패 이유를 알 수 없습니다. 디버깅을 위해 로깅을 추가하는 것을 고려하세요.


73-77: 타입 캐스팅 시 더 안전한 방법 사용

현재 as? 연산자를 사용하고 있지만, 예상치 못한 타입이 저장된 경우를 더 명확히 처리할 수 있습니다.

📜 Review details

Configuration used: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c44cae5 and 6c4fca5.

📒 Files selected for processing (27)
  • apis/src/main/kotlin/org/yapp/ApisApplication.kt (1 hunks)
  • apis/src/main/kotlin/org/yapp/apis/auth/controller/AuthControllerApi.kt (1 hunks)
  • apis/src/main/kotlin/org/yapp/apis/auth/dto/request/SocialLoginRequest.kt (1 hunks)
  • apis/src/main/kotlin/org/yapp/apis/auth/dto/request/TokenRefreshRequest.kt (1 hunks)
  • apis/src/main/kotlin/org/yapp/apis/auth/dto/response/UserProfileResponse.kt (1 hunks)
  • apis/src/main/kotlin/org/yapp/apis/auth/service/TokenService.kt (1 hunks)
  • apis/src/main/kotlin/org/yapp/apis/auth/service/UserService.kt (1 hunks)
  • apis/src/main/kotlin/org/yapp/apis/auth/usecase/AuthUseCase.kt (1 hunks)
  • apis/src/main/kotlin/org/yapp/apis/util/AuthUtils.kt (1 hunks)
  • apis/src/main/resources/application.yml (1 hunks)
  • buildSrc/src/main/kotlin/Dependencies.kt (1 hunks)
  • domain/src/main/kotlin/org/yapp/domain/auth/TokenRepository.kt (1 hunks)
  • domain/src/main/kotlin/org/yapp/domain/domainservice/UserDomainService.kt (1 hunks)
  • domain/src/main/kotlin/org/yapp/domain/domainservice/UserDomainServiceImpl.kt (1 hunks)
  • domain/src/main/kotlin/org/yapp/domain/redisservice/TokenDomainRedisService.kt (1 hunks)
  • domain/src/main/kotlin/org/yapp/domain/redisservice/TokenDomainRedisServiceImpl.kt (1 hunks)
  • domain/src/main/kotlin/org/yapp/domain/user/User.kt (1 hunks)
  • domain/src/main/kotlin/org/yapp/domain/user/UserEntity.kt (1 hunks)
  • domain/src/main/kotlin/org/yapp/domain/user/UserRepository.kt (1 hunks)
  • gateway/src/main/kotlin/org/yapp/gateway/jwt/JwtTokenProvider.kt (1 hunks)
  • gateway/src/main/kotlin/org/yapp/gateway/jwt/JwtTokenService.kt (1 hunks)
  • gateway/src/main/kotlin/org/yapp/gateway/jwt/JwtTokenServiceImpl.kt (1 hunks)
  • global-utils/build.gradle.kts (1 hunks)
  • global-utils/src/main/kotlin/org/yapp/global/util/UuidCreator.kt (1 hunks)
  • infra/src/main/kotlin/org/yapp/jpa/repository/JpaUserRepository.kt (1 hunks)
  • infra/src/main/kotlin/org/yapp/jpa/repository/UserRepositoryImpl.kt (1 hunks)
  • infra/src/main/kotlin/org/yapp/redis/RedisTokenRepository.kt (1 hunks)
🧰 Additional context used
🪛 YAMLlint (1.37.1)
apis/src/main/resources/application.yml

[error] 42-42: too many blank lines

(2 > 0) (empty-lines)

🪛 detekt (1.23.8)
gateway/src/main/kotlin/org/yapp/gateway/jwt/JwtTokenProvider.kt

[warning] 55-55: The caught exception is swallowed. The original exception could be lost.

(detekt.exceptions.SwallowedException)


[warning] 100-100: The caught exception is swallowed. The original exception could be lost.

(detekt.exceptions.SwallowedException)


[warning] 102-102: The caught exception is swallowed. The original exception could be lost.

(detekt.exceptions.SwallowedException)


[warning] 104-104: The caught exception is swallowed. The original exception could be lost.

(detekt.exceptions.SwallowedException)


[warning] 106-106: The caught exception is swallowed. The original exception could be lost.

(detekt.exceptions.SwallowedException)


[warning] 108-108: The caught exception is swallowed. The original exception could be lost.

(detekt.exceptions.SwallowedException)

🔇 Additional comments (18)
global-utils/build.gradle.kts (1)

5-5: UUID 생성기 의존성 추가가 적절합니다.

새로운 UuidCreator 유틸리티 클래스를 지원하기 위한 의존성 추가가 올바르게 구현되었습니다.

buildSrc/src/main/kotlin/Dependencies.kt (3)

10-11: 새로운 의존성 추가가 적절합니다.

Redis와 Kotlin reflection 의존성 추가가 소셜 로그인 구현에 필요한 기능들을 잘 지원합니다.


24-41: 새로운 의존성 객체들이 잘 구성되었습니다.

Swagger, Logging, Uuid 객체 추가가 프로젝트 구조와 잘 맞으며, 각각의 용도가 명확합니다.


18-22: JWT 라이브러리 버전을 확인해주세요.

JWT 라이브러리 버전(0.11.5)이 최신인지 확인이 필요합니다. 보안 취약점이나 최신 기능 지원을 위해 최신 버전 사용을 권장합니다.

What is the latest version of jjwt (Java JWT) library and are there any security advisories for version 0.11.5?
apis/src/main/kotlin/org/yapp/ApisApplication.kt (1)

6-8: 문서화 개선이 좋습니다.

KDoc 주석 추가로 메인 애플리케이션 클래스의 목적이 명확해졌습니다.

domain/src/main/kotlin/org/yapp/domain/redisservice/TokenDomainRedisServiceImpl.kt (1)

8-24: 깔끔한 구현입니다.

도메인 서비스가 TokenRepository에 올바르게 위임하고 있으며, @domainservice 어노테이션 사용도 적절합니다. 단순하고 명확한 구조로 잘 구현되었습니다.

gateway/src/main/kotlin/org/yapp/gateway/jwt/JwtTokenService.kt (1)

6-50: 잘 설계된 JWT 토큰 서비스 인터페이스입니다.

필요한 모든 JWT 작업이 포함되어 있고, 문서화가 잘 되어 있으며, 메서드 시그니처도 적절합니다. 인터페이스 분리 원칙을 잘 준수하고 있습니다.

infra/src/main/kotlin/org/yapp/jpa/repository/JpaUserRepository.kt (1)

1-18: LGTM! JPA 리포지토리 인터페이스가 올바르게 구현되었습니다.

Spring Data JPA의 표준 패턴을 따르고 있으며, 소셜 로그인에 필요한 쿼리 메서드들이 적절히 정의되어 있습니다.

domain/src/main/kotlin/org/yapp/domain/user/UserRepository.kt (1)

1-18: LGTM! 도메인 리포지토리 인터페이스가 적절히 정의되었습니다.

클린 아키텍처의 도메인 레이어 추상화가 잘 구현되어 있으며, 필요한 사용자 관련 오퍼레이션들이 포함되어 있습니다.

apis/src/main/kotlin/org/yapp/apis/auth/service/UserService.kt (1)

10-25: LGTM! 서비스 로직과 예외 처리가 적절합니다.

도메인 서비스를 활용한 비즈니스 로직 위임과 명확한 예외 처리가 잘 구현되어 있습니다.

gateway/src/main/kotlin/org/yapp/gateway/jwt/JwtTokenServiceImpl.kt (1)

1-68: LGTM! JWT 토큰 서비스 구현이 매우 깔끔합니다.

위임 패턴을 통한 구현이 적절하고, KDoc 문서화도 상세하게 잘 되어 있습니다. Spring의 의존성 주입도 올바르게 활용되고 있습니다.

infra/src/main/kotlin/org/yapp/redis/RedisTokenRepository.kt (1)

12-44: Redis 기반 토큰 저장소 구현이 잘 되어 있습니다.

구현의 장점:

  • 적절한 키 prefix 사용으로 네임스페이스 관리
  • TTL을 활용한 자동 만료 처리
  • existsRefreshToken에서 실제 토큰 값 검증으로 보안성 강화
  • 간결하고 명확한 메서드 구현
domain/src/main/kotlin/org/yapp/domain/domainservice/UserDomainServiceImpl.kt (1)

33-46: findOrCreate 로직이 잘 구현되어 있습니다.

비즈니스 로직의 구현이 적절합니다:

  • Provider 정보로 먼저 기존 사용자 확인
  • Email 중복 검사를 통한 계정 충돌 방지
  • Result 타입을 활용한 안전한 에러 처리
infra/src/main/kotlin/org/yapp/jpa/repository/UserRepositoryImpl.kt (1)

32-36: 안전한 엔티티 조회 구현입니다.

orElseThrow를 사용하여 Optional 안전성 문제가 잘 해결되었습니다. 명확한 에러 메시지와 함께 예외 처리가 적절합니다.

apis/src/main/kotlin/org/yapp/apis/auth/controller/AuthControllerApi.kt (1)

20-113: 잘 설계된 인증 API 인터페이스입니다.

API 설계의 장점:

  • 포괄적인 OpenAPI 문서화로 명확한 API 스펙 제공
  • Authorization header를 통한 안전한 인증 정보 전달
  • 적절한 HTTP 상태 코드 정의
  • 명확한 에러 응답 스키마

보안 측면에서도 이전에 지적된 문제들이 잘 해결되었습니다.

domain/src/main/kotlin/org/yapp/domain/user/UserEntity.kt (1)

62-64: ```shell
#!/bin/bash
rg -n "fromDomain" -A10 domain/src/main/kotlin/org/yapp/domain/user/UserEntity.kt


</details>
<details>
<summary>gateway/src/main/kotlin/org/yapp/gateway/jwt/JwtTokenProvider.kt (2)</summary>

`90-112`: **JWT 파싱 로직 중앙화 잘 구현되었습니다**

공통 JWT 파싱 로직을 `parseToken` 메서드로 추출하여 코드 중복을 효과적으로 제거했습니다. 각 예외 타입에 대한 적절한 에러 코드 매핑도 잘 되어 있습니다.

---

`68-70`: **잘못된 예외 타입 처리**

`UUID.fromString()`은 `IllegalArgumentException`을 던지는데, `NumberFormatException`을 캐치하고 있습니다. 올바른 예외 타입을 처리해야 합니다.


```diff
-        } catch (e: NumberFormatException) {
+        } catch (e: IllegalArgumentException) {
             throw JwtException(JwtErrorCode.INVALID_JWT_TOKEN, "Invalid user ID in token")
         }

Likely an incorrect or invalid review comment.

Comment on lines 17 to 26
fun toCredentials(): AuthCredentials {
return when (providerType.uppercase()) {
"KAKAO" -> KakaoAuthCredentials(oauthToken)
"APPLE" -> AppleAuthCredentials(oauthToken)
else -> throw AuthException(
AuthErrorCode.UNSUPPORTED_PROVIDER_TYPE,
"Unsupported provider type: $providerType"
)
}
}
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

보안과 예외 처리 개선을 고려해보세요.

현재 구현은 기능적으로 올바르지만 다음 사항들을 검토해보세요:

  1. 예외 메시지에서 실제 제공업체 타입을 노출하는 것이 정보 유출 위험이 있을 수 있습니다
  2. 대소문자를 무시하는 처리가 의도된 것인지 확인이 필요합니다
 fun toCredentials(): AuthCredentials {
     return when (providerType.uppercase()) {
         "KAKAO" -> KakaoAuthCredentials(oauthToken)
         "APPLE" -> AppleAuthCredentials(oauthToken)
         else -> throw AuthException(
             AuthErrorCode.UNSUPPORTED_PROVIDER_TYPE,
-            "Unsupported provider type: $providerType"
+            "지원하지 않는 인증 제공업체입니다"
         )
     }
 }
📝 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.

Suggested change
fun toCredentials(): AuthCredentials {
return when (providerType.uppercase()) {
"KAKAO" -> KakaoAuthCredentials(oauthToken)
"APPLE" -> AppleAuthCredentials(oauthToken)
else -> throw AuthException(
AuthErrorCode.UNSUPPORTED_PROVIDER_TYPE,
"Unsupported provider type: $providerType"
)
}
}
fun toCredentials(): AuthCredentials {
return when (providerType.uppercase()) {
"KAKAO" -> KakaoAuthCredentials(oauthToken)
"APPLE" -> AppleAuthCredentials(oauthToken)
else -> throw AuthException(
AuthErrorCode.UNSUPPORTED_PROVIDER_TYPE,
"지원하지 않는 인증 제공업체입니다"
)
}
}
🤖 Prompt for AI Agents
In apis/src/main/kotlin/org/yapp/apis/auth/dto/request/SocialLoginRequest.kt
around lines 17 to 26, avoid exposing the actual providerType value in the
exception message to prevent potential information leakage. Instead, use a
generic error message without revealing the input. Also, confirm if the
uppercase conversion for case-insensitive matching is intended; if not, adjust
the logic to handle providerType case sensitivity explicitly.

Copy link
Member

@move-hoon move-hoon left a comment

Choose a reason for hiding this comment

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

너무 고생많으셨습니다!!
사실 지금 바로 머지해도 좋은 로직이라고 생각됩니다 😊 (aprv 하겠습니다 ㅎㅎ)

다만, 마지막으로 저희가 체크했으면 좋을 부분이 있는데요! 바로 에러 핸들링입니다.
이와 관련해 좋은 블로그 2개를 찾았어서 이를 공유드립니다!

해당 블로그 보고 편하게 의견주시면 감사할 것 같습니다!

https://medium.com/@galcyurio/kotlin%EC%97%90%EC%84%9C%EC%9D%98-%EC%98%88%EC%99%B8-%EC%B2%98%EB%A6%AC-%EB%B0%A9%EB%B2%95-48a5cd94a4e6
https://toss.tech/article/21010

댓글 창이 없어서 여기에 코멘트 남기겠습니다!

말씀하신 예외 처리 철학에는 공감하지만, 현실적인 개발에서는 논리 오류와 비논리 오류를 구분하는 책임이 결국 개발자에게 있다고 생각합니다.
예를 들어, PositiveNumber(-1)처럼 음수를 양수로 표현하려는 것은 명백한 논리 오류로 예외를 던지는 것이 맞지만, 사용자의 로그인 실패처럼 외부 입력에 따른 오류는 일반적인 비논리 오류로 처리해야겠죠.
이처럼 도메인마다 그 기준이 달라질 수 있기 때문에 상황에 따라 유연하게 판단할 필요가 있다고 생각했어요!

또한 try-catch는 남용은 피해야 하지만, 외부 라이브러리나 예기치 못한 예외를 잡아주는 최후의 안전망으로 여전히 중요하다고 봅니다.
예를 들어 JSON 파싱이나 네트워크 요청 실패는 우리가 통제할 수 없는 영역이기 때문에, 적절한 try-catch 없이는 앱이 비정상 종료될 수 있습니다.
그래서 저는 제어 가능한 영역은 require나 sealed class로 처리하고, 예측이 어려운 외부 예외는 try-catch로 보완하는 전략이 더 현실적이고 안정적이라고 생각합니다 :)

Comment on lines 3 to 8
data class UserProfileResponse(
val id: Long,
val email: String,
val nickname: String,
val provider: String
) No newline at end of file
Copy link
Member

Choose a reason for hiding this comment

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

@minwoo1999 저도 여기서 String 타입이 아닌, ProviderType enum으로 받는게 타입 안정성 측면에서 좋아보이는데 어떻게 생각하시나요?

Comment on lines 41 to 45
return UserProfileResponse(
id = user.id!!,
email = user.email,
nickname = user.nickname,
provider = user.providerType.name
Copy link
Member

Choose a reason for hiding this comment

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

저는 개인적으로 정적 팩토리 메서드에 User VO를 넘기고, DTO 내부에서 필요한 필드를 추출하는 방식을 선호합니다.

그 이유는 다음과 같습니다.

  • 서비스 로직에서 VO의 getter가 직접 노출되는 것을 방지할 수 있어 응집도를 높이고 캡슐화를 지킬 수 있습니다.
  • UserProfileResponse처럼 여러 군데에서 사용되는 DTO의 필드가 변경되었을 때, 각 호출 지점마다 getter를 일일이 수정해야 하는 번거로움이 발생할 수 있습니다.
    하지만 정적 팩토리 메서드를 활용하면, 필드 변경 시 해당 메서드 내부만 수정하면 되어 유지보수가 훨씬 수월합니다.

다만, 이러한 방식은 DTO가 VO를 직접 의존하게 된다는 점에서 계층 간 의존성이 생길 수는 있습니다.

이러한 트레이드오프에 대해 혹시 다른 의견이나 조언이 있다면 편하게 말씀 부탁드립니다 😊

@move-hoon
Copy link
Member

LGTM!!

Copy link

@coderabbitai coderabbitai bot left a 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 (2)
apis/src/main/kotlin/org/yapp/apis/auth/dto/response/TokenPairResponse.kt (1)

3-6: 토큰 보안을 위해 toString() 메서드 오버라이드가 필요합니다.

데이터 클래스의 기본 toString() 메서드는 토큰 값을 그대로 노출하여 로그나 디버깅 시 보안 위험을 초래할 수 있습니다.

 data class TokenPairResponse(
     val accessToken: String,
     val refreshToken: String
-) {
+) {
+    override fun toString(): String {
+        return "TokenPairResponse(accessToken=***, refreshToken=***)"
+    }
apis/src/main/kotlin/org/yapp/apis/auth/dto/response/UserProfileResponse.kt (1)

6-11: KDoc 문서화를 추가하여 API 사용성을 개선하세요.

데이터 클래스의 각 필드에 대한 명확한 문서화가 필요합니다. 특히 email 필드는 개인정보가 포함되어 있어 사용 시 주의사항을 명시해야 합니다.

+/**
+ * 사용자 프로필 응답 DTO
+ * @property id 사용자 고유 식별자
+ * @property email 사용자 이메일 (개인정보 처리 시 주의)
+ * @property nickname 사용자 닉네임
+ * @property provider 소셜 로그인 제공업체
+ */
 data class UserProfileResponse(
     val id: UUID,
     val email: String,
     val nickname: String,
     val provider: ProviderType
 )
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6789a4a and 6d0e3a8.

📒 Files selected for processing (2)
  • apis/src/main/kotlin/org/yapp/apis/auth/dto/response/TokenPairResponse.kt (1 hunks)
  • apis/src/main/kotlin/org/yapp/apis/auth/dto/response/UserProfileResponse.kt (1 hunks)

Copy link

@coderabbitai coderabbitai bot left a 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

📥 Commits

Reviewing files that changed from the base of the PR and between 6d0e3a8 and c10eb1d.

📒 Files selected for processing (1)
  • apis/src/main/kotlin/org/yapp/apis/auth/usecase/AuthUseCase.kt (1 hunks)
🔇 Additional comments (1)
apis/src/main/kotlin/org/yapp/apis/auth/usecase/AuthUseCase.kt (1)

14-62: 전반적인 구조가 잘 설계되었습니다

AuthUseCase 클래스가 clean architecture 원칙을 잘 따르고 있습니다:

  • 단일 책임 원칙을 준수하는 명확한 메서드 분리
  • 적절한 의존성 주입과 서비스 계층 분리
  • 전략 패턴을 통한 소셜 인증 처리
  • 토큰 생명주기 관리의 체계적인 구현

Copy link

@coderabbitai coderabbitai bot left a 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

♻️ Duplicate comments (5)
apis/src/main/kotlin/org/yapp/apis/auth/dto/response/TokenPairResponse.kt (3)

3-6: 토큰 보안을 위한 toString() 오버라이드가 필요합니다.

민감한 토큰 정보가 로그나 디버깅 시 노출될 수 있습니다.

보안을 위해 toString() 메서드를 오버라이드하세요:

 data class TokenPairResponse private constructor(
     val accessToken: String,
     val refreshToken: String
 ) {
+    override fun toString(): String {
+        return "TokenPairResponse(accessToken=***, refreshToken=***)"
+    }
+    
     companion object {

7-14: 팩토리 메서드에 검증 로직을 추가하세요.

현재 of 메서드는 단순한 생성자 래퍼입니다. 토큰 검증 로직을 추가하여 팩토리 메서드의 가치를 높이세요.

 companion object {
     fun of(accessToken: String, refreshToken: String): TokenPairResponse {
+        require(accessToken.isNotBlank()) { "Access token cannot be blank" }
+        require(refreshToken.isNotBlank()) { "Refresh token cannot be blank" }
         return TokenPairResponse(
             accessToken = accessToken,
             refreshToken = refreshToken
         )
     }
 }

1-15: AuthResponse와 일관성을 위해 팩토리 메서드명을 통일하세요.

hoonyworld의 제안처럼 중앙 집중식 객체 생성 관리를 위해 AuthResponse와 동일한 네이밍 패턴을 사용하는 것을 고려해보세요.

AuthResponse는 fromTokenPair를 사용하므로, 일관성을 위해 다음과 같이 수정할 수 있습니다:

 companion object {
-    fun of(accessToken: String, refreshToken: String): TokenPairResponse {
+    fun create(accessToken: String, refreshToken: String): TokenPairResponse {
         require(accessToken.isNotBlank()) { "Access token cannot be blank" }
         require(refreshToken.isNotBlank()) { "Refresh token cannot be blank" }
         return TokenPairResponse(
             accessToken = accessToken,
             refreshToken = refreshToken
         )
     }
 }
apis/src/main/kotlin/org/yapp/apis/auth/dto/request/TokenRefreshRequest.kt (1)

5-8: 보안 개선사항이 누락되었습니다.

검증 어노테이션은 잘 구현되었지만, 이전 리뷰에서 제안된 toString() 오버라이드가 구현되지 않았습니다. 토큰이 로그에 노출될 위험이 있습니다.

다음과 같이 보안을 강화해주세요:

data class TokenRefreshRequest private constructor(
    @field:NotBlank(message = "Refresh token is required")
    val refreshToken: String
-)
+) {
+    override fun toString(): String {
+        return "TokenRefreshRequest(refreshToken=***)"
+    }
+}
apis/src/main/kotlin/org/yapp/apis/auth/dto/request/SocialLoginRequest.kt (1)

20-29: 예외 처리 및 보안 개선이 필요합니다.

현재 구현에서 몇 가지 문제점이 있습니다:

  1. ProviderType.valueOf()IllegalArgumentException을 발생시킬 수 있지만 이를 처리하지 않고 있습니다
  2. 예외 메시지에서 실제 제공업체 타입을 노출하고 있어 정보 유출 위험이 있습니다 (과거 리뷰에서도 지적된 사항)
  3. ProviderType이 enum이라면 모든 값을 다루고 있을 때 else 절이 불필요할 수 있습니다
 fun toCredentials(request: SocialLoginRequest): AuthCredentials {
-    return when (ProviderType.valueOf(request.providerType.uppercase())) {
-        ProviderType.KAKAO -> KakaoAuthCredentials(request.oauthToken)
-        ProviderType.APPLE -> AppleAuthCredentials(request.oauthToken)
-        else -> throw AuthException(
-            AuthErrorCode.UNSUPPORTED_PROVIDER_TYPE,
-            "Unsupported provider type: ${request.providerType}"
-        )
-    }
+    return try {
+        when (ProviderType.valueOf(request.providerType.uppercase())) {
+            ProviderType.KAKAO -> KakaoAuthCredentials(request.oauthToken)
+            ProviderType.APPLE -> AppleAuthCredentials(request.oauthToken)
+        }
+    } catch (e: IllegalArgumentException) {
+        throw AuthException(
+            AuthErrorCode.UNSUPPORTED_PROVIDER_TYPE,
+            "지원하지 않는 인증 제공업체입니다"
+        )
+    }
 }
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c10eb1d and b17e98d.

📒 Files selected for processing (6)
  • apis/src/main/kotlin/org/yapp/apis/auth/dto/request/SocialLoginRequest.kt (1 hunks)
  • apis/src/main/kotlin/org/yapp/apis/auth/dto/request/TokenRefreshRequest.kt (1 hunks)
  • apis/src/main/kotlin/org/yapp/apis/auth/dto/response/AuthResponse.kt (1 hunks)
  • apis/src/main/kotlin/org/yapp/apis/auth/dto/response/TokenPairResponse.kt (1 hunks)
  • build.gradle.kts (1 hunks)
  • domain/src/main/kotlin/org/yapp/domain/user/UserEntity.kt (1 hunks)
🔇 Additional comments (5)
domain/src/main/kotlin/org/yapp/domain/user/UserEntity.kt (3)

42-51: toDomain()/fromDomain() 변환 로직 승인
도메인 모델과 엔티티 간 변환 메서드가 명확하게 작성되어 있으며, 필요한 모든 필드를 일관성 있게 매핑하고 있습니다.


67-73: equals() 및 hashCode() 오버라이드 적절
식별자(id) 기반 비교 전략이 잘 적용되어 있으며, 동등성 판단과 해시코드 생성이 일관적입니다.


12-32: 빌드 스크립트에서 kotlin-jpa 플러그인이 실제로 적용되었는지 확인합니다.

#!/bin/bash
# build.gradle.kts 파일 내에서 kotlin-jpa 플러그인 적용 여부 검색
rg -n 'kotlin\(.*jpa' --glob '*.gradle.kts'
rg -n 'org.jetbrains.kotlin.plugin.jpa' --glob '*.gradle.kts'
rg -n 'Plugins\.JPA' --glob '*.gradle.kts'
apis/src/main/kotlin/org/yapp/apis/auth/dto/request/TokenRefreshRequest.kt (1)

5-5: 클래스 내부에 팩토리 메서드나 companion object가 정의되어 있는지 확인하기 위해 아래 스크립트를 실행해주세요.

#!/bin/bash
# TokenRefreshRequest 클래스 전체 구현 확인
head -n 200 apis/src/main/kotlin/org/yapp/apis/auth/dto/request/TokenRefreshRequest.kt
build.gradle.kts (1)

50-53: 해당 플래그가 Kotlin 1.9.25에서 지원되는지 확인하기 위해, 도입 버전을 웹에서 조회합니다.

[web_search]

Which Kotlin version introduced "-Xconsistent-data-class-copy-visibility" compiler flag?

Comment on lines +15 to +18
@Id
@JdbcTypeCode(Types.VARCHAR)
@Column(length = 36, updatable = false, nullable = false)
val id: UUID = UuidGenerator.create(),
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

UUID 매핑 전략 개선 제안
현재 @JdbcTypeCode(Types.VARCHAR)로 UUID를 문자열로 저장하고 있는데, Hibernate 표준 uuid-char 타입(@Type(type = "uuid-char"))을 사용하거나, 필드 접근 방식을 명시(@Access(AccessType.FIELD))하여 매핑 안정성을 높이는 것을 고려해 보세요.

🤖 Prompt for AI Agents
In domain/src/main/kotlin/org/yapp/domain/user/UserEntity.kt around lines 15 to
18, the UUID field is currently mapped using @JdbcTypeCode(Types.VARCHAR), which
stores the UUID as a string. To improve mapping stability and follow Hibernate
standards, replace @JdbcTypeCode with @Type(type = "uuid-char") or add
@Access(AccessType.FIELD) to explicitly specify field access. This change
ensures consistent and reliable UUID persistence.

@minwoo1999 minwoo1999 merged commit cc8c085 into develop Jun 24, 2025
2 checks passed
@coderabbitai coderabbitai bot mentioned this pull request Jun 24, 2025
15 tasks
@move-hoon move-hoon deleted the BOOK-52-feature/#11 branch August 12, 2025 06:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BOOK-52/feat] 애플로그인,카카오로그인 구현

3 participants