Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthrough직렬 번호 기반 인증을 OAuth 소셜 로그인으로 전환했습니다. Member 식별자와 DB 제약이 providerType/providerId 기반으로 바뀌고, Spring Security(JWT) 도입, Kakao/Google/Apple OAuth 어댑터, 관련 DTO·포트·서비스·컨트롤러·테스트가 추가/수정되었습니다. Changes
Sequence Diagram(s)sequenceDiagram
actor Client
participant AuthController
participant AuthService
participant OAuthPort
participant MemberService
participant MemberPort
participant TokenProvider
Client->>AuthController: POST /login (providerType, accessToken)
AuthController->>AuthService: socialLogin(providerType, accessToken)
AuthService->>OAuthPort: requestUserInfo(accessToken)
OAuthPort-->>AuthService: OAuthInfo(providerId, email, providerType)
AuthService->>MemberService: findOrCreateByOAuth(providerType, oAuthInfo)
MemberService->>MemberPort: findByProviderTypeAndProviderId(providerType, providerId)
alt existing member
MemberPort-->>MemberService: Member
else new member
MemberPort-->>MemberService: null
MemberService->>MemberPort: save(newMember)
MemberPort-->>MemberService: savedMember
end
MemberService-->>AuthService: MemberLoginResult(member, isNewMember)
AuthService->>TokenProvider: create(memberId)
TokenProvider-->>AuthService: JWT
AuthService-->>AuthController: AuthResult(memberId, accessToken, isNewMember)
AuthController-->>Client: 200/201 + OAuthLoginResponse
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related issues
Possibly related PRs
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
|
@coderabbitai full review |
✅ Actions performedFull review triggered. |
There was a problem hiding this comment.
Actionable comments posted: 13
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
linktrip-input-http/src/main/kotlin/com/linktrip/input/http/security/JwtTokenProvider.kt (1)
37-50:⚠️ Potential issue | 🟠 Major
IllegalArgumentException처리 추가로 예외 처리 명시화 필요현재 코드는 빈 JWT 토큰 입력 시 JJWT 파서가 던지는
IllegalArgumentException을 명시적으로 처리하지 않고 있습니다. 필터 레벨의 일반 예외 핸들러가 이를 처리하므로 현재 401 응답으로 반환되지만, 예외 처리를 명시화하는 것이 권장됩니다.🔧 제안 수정안
override fun validate(token: String): Boolean = try { Jwts.parser() .verifyWith(secretKey) .build() .parseSignedClaims(token) true + } catch (e: IllegalArgumentException) { + logger.warn { "비어 있거나 잘못된 JWT 토큰: ${e.message}" } + false } catch (e: ExpiredJwtException) { logger.warn { "만료된 JWT 토큰: ${e.message}" } false } catch (e: JwtException) { logger.warn { "유효하지 않은 JWT 토큰: ${e.message}" } false } override fun extractMemberId(token: String): String = try { Jwts.parser() .verifyWith(secretKey) .build() .parseSignedClaims(token) .payload .subject + } catch (e: IllegalArgumentException) { + logger.warn { "비어 있거나 잘못된 JWT 토큰으로 memberId 추출 시도: ${e.message}" } + throw LinktripException(ExceptionCode.TOKEN_INVALID) } catch (e: ExpiredJwtException) { logger.warn { "만료된 JWT 토큰으로 memberId 추출 시도: ${e.message}" } throw LinktripException(ExceptionCode.TOKEN_EXPIRED) } catch (e: JwtException) { logger.warn { "유효하지 않은 JWT 토큰으로 memberId 추출 시도: ${e.message}" } throw LinktripException(ExceptionCode.TOKEN_INVALID) }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@linktrip-input-http/src/main/kotlin/com/linktrip/input/http/security/JwtTokenProvider.kt` around lines 37 - 50, The validate(token: String) method in JwtTokenProvider currently catches ExpiredJwtException and JwtException but not IllegalArgumentException; add a catch block for IllegalArgumentException in validate to explicitly handle empty/invalid token input: log a warning via logger.warn including e.message (e.g., "잘못된 JWT 토큰: ${e.message}") and return false, placing this catch alongside the existing exception handlers in the validate function.
🧹 Nitpick comments (7)
linktrip-input-http/src/main/kotlin/com/linktrip/input/http/security/handler/JwtAuthenticationEntryPoint.kt (1)
13-37: LGTM - 공통 응답 작성 로직 추출을 고려해볼 수 있습니다.
AuthenticationEntryPoint구현이 올바릅니다.JwtAccessDeniedHandler와 응답 작성 로직이 거의 동일하므로, 향후 유지보수성을 위해 공통 유틸리티 함수로 추출하는 것도 고려해볼 수 있습니다.♻️ 공통 응답 작성 로직 추출 예시
// SecurityResponseHelper.kt object SecurityResponseHelper { fun writeErrorResponse( response: HttpServletResponse, objectMapper: ObjectMapper, exceptionCode: ExceptionCode, ) { response.status = exceptionCode.statusCode response.contentType = MediaType.APPLICATION_JSON_VALUE response.characterEncoding = Charsets.UTF_8.name() response.writer.write( objectMapper.writeValueAsString( ExceptionResponse( message = exceptionCode.defaultMessage, cause = null, timestamp = System.currentTimeMillis(), ), ), ) } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@linktrip-input-http/src/main/kotlin/com/linktrip/input/http/security/handler/JwtAuthenticationEntryPoint.kt` around lines 13 - 37, The JwtAuthenticationEntryPoint and JwtAccessDeniedHandler share identical response-writing logic; extract this into a shared helper (e.g., SecurityResponseHelper.writeErrorResponse) that accepts HttpServletResponse, ObjectMapper, and ExceptionCode, then replace the duplicated blocks in JwtAuthenticationEntryPoint.commence and JwtAccessDeniedHandler.handle with a call to that helper to centralize status, contentType, encoding, and ExceptionResponse serialization.linktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/entity/MemberEntity.kt (1)
17-26: 중복 인덱스가 존재합니다.
uk_member_providerunique constraint가(provider_type, provider_id)에 대해 암시적으로 인덱스를 생성하므로,idx_member_provider인덱스는 중복입니다. unique constraint만으로 조회 성능이 보장됩니다.♻️ 중복 인덱스 제거
indexes = [ - Index(name = "idx_member_provider", columnList = "provider_type, provider_id"), Index(name = "idx_member_email", columnList = "email"), ],🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@linktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/entity/MemberEntity.kt` around lines 17 - 26, Remove the redundant index idx_member_provider from the indexes list in MemberEntity (the unique constraint uk_member_provider on columns provider_type and provider_id already creates an index), i.e., delete the Index entry with name "idx_member_provider" (leave the UniqueConstraint "uk_member_provider" and other indexes like "idx_member_email" intact) and run schema migration tests to ensure no references depend on the removed index.linktrip-output-http/src/main/kotlin/com/linktrip/output/http/oauth/dto/AppleUserInfoResponse.kt (1)
3-14: 파일명과 클래스명이 일치하지 않습니다.파일명은
AppleUserInfoResponse.kt이지만 실제 클래스명은ApplePublicKeyResponse입니다. Kotlin 관례상 파일명과 public 클래스명을 일치시키는 것이 좋습니다.♻️ 수정 제안
파일명을
ApplePublicKeyResponse.kt로 변경하세요.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@linktrip-output-http/src/main/kotlin/com/linktrip/output/http/oauth/dto/AppleUserInfoResponse.kt` around lines 3 - 14, The file name does not match the public data class declared (file is AppleUserInfoResponse.kt but the class is ApplePublicKeyResponse); rename the file to ApplePublicKeyResponse.kt so Kotlin file/class convention is preserved and the public data class ApplePublicKeyResponse (and its nested AppleKey) are discoverable and consistent.linktrip-input-http/src/test/kotlin/com/linktrip/input/http/ratelimit/RateLimitInterceptorTest.kt (1)
37-39: 테스트 정리 로직을@AfterEach로 이동하는 것을 권장합니다.현재
clearAuthentication()이 assertion 이후에 호출되어, assertion 실패 시 security context가 정리되지 않아 다른 테스트에 영향을 줄 수 있습니다.♻️ 개선 제안
+import org.junit.jupiter.api.AfterEach + `@ExtendWith`(MockitoExtension::class) class RateLimitInterceptorTest { `@Mock` lateinit var rateLimitBucketStore: RateLimitBucketStore `@InjectMocks` lateinit var interceptor: RateLimitInterceptor private fun setAuthentication(memberId: String) { SecurityContextHolder.getContext().authentication = PostAuthorizationToken(memberId) } - private fun clearAuthentication() { - SecurityContextHolder.clearContext() - } + `@AfterEach` + fun tearDown() { + SecurityContextHolder.clearContext() + }그런 다음 각 테스트 메서드 끝의
clearAuthentication()호출을 제거하세요.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@linktrip-input-http/src/test/kotlin/com/linktrip/input/http/ratelimit/RateLimitInterceptorTest.kt` around lines 37 - 39, Move the manual cleanup into a JUnit `@AfterEach` method so the SecurityContextHolder is always cleared even if assertions fail: create an `@AfterEach` annotated function (e.g., fun clearAuthentication()) that calls SecurityContextHolder.clearContext() and remove all explicit clearAuthentication() calls at the end of individual tests; ensure the method name matches existing clearAuthentication to reuse the same logic.linktrip-input-http/src/main/kotlin/com/linktrip/input/http/security/PostAuthorizationToken.kt (1)
7-7: 향후 역할 기반 접근 제어(RBAC)가 필요할 경우 authorities 확장을 고려하세요.현재
emptyList()로 authorities를 설정했는데, 이는 현재 요구사항에는 충분하지만 추후 관리자/일반 사용자 구분 등이 필요하면 이 부분의 확장이 필요합니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@linktrip-input-http/src/main/kotlin/com/linktrip/input/http/security/PostAuthorizationToken.kt` at line 7, 현재 PostAuthorizationToken 클래스가 AbstractAuthenticationToken을 emptyList()로 초기화해 권한 정보를 고정하고 있으므로 향후 RBAC 확장이 어렵습니다; 수정안은 PostAuthorizationToken 생성자에 authorities: Collection<GrantedAuthority> (또는 기본값이 있는 오버로드 생성자)을 추가하고 해당 authorities를 super(authorities)로 전달하도록 변경하거나 내부에 GrantedAuthority 컬렉션 필드를 두고 getAuthorities()가 이를 반환하게 하여 관리자/사용자 등 역할 구분을 쉽게 확장할 수 있도록 하세요; 대상 심볼: PostAuthorizationToken 생성자, AbstractAuthenticationToken(super(...)), getAuthorities().linktrip-output-http/src/main/kotlin/com/linktrip/output/http/oauth/adapter/AppleOAuthAdapter.kt (1)
47-52: Apple 공개 키 캐싱을 고려하세요.현재 매 요청마다 Apple 서버에서 공개 키를 조회합니다. Apple의 키는 자주 변경되지 않으므로, 캐싱을 도입하면 성능을 크게 개선하고 외부 의존성 호출을 줄일 수 있습니다.
캐싱 로직이 포함된 구현을 생성해 드릴까요?
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@linktrip-output-http/src/main/kotlin/com/linktrip/output/http/oauth/adapter/AppleOAuthAdapter.kt` around lines 47 - 52, fetchApplePublicKeys is currently calling Apple on every invocation; add caching to AppleOAuthAdapter to avoid repeated network calls by storing the last ApplePublicKeyResponse along with its expiry (Apple keys rotate infrequently). Implement a simple thread-safe cache (e.g., volatile field + synchronized refresh or use a cache library like Caffeine) inside AppleOAuthAdapter: on fetchApplePublicKeys return cached response if present and not expired, otherwise call restClient.get().uri("/auth/keys").retrieve().body<ApplePublicKeyResponse>(), update the cache and expiry (configurable TTL), and fall back to the previous cached value on transient failures; ensure exceptions still map to LinktripException(ExceptionCode.OAUTH_PROVIDER_ERROR) when no valid cached value exists.linktrip-output-http/src/main/kotlin/com/linktrip/output/http/oauth/config/OAuthRestClientConfig.kt (1)
16-47: RestClient 생성 로직은 공통 팩토리로 추출하는 편이 유지보수에 유리합니다.현재 3개 빈이 동일한 패턴을 반복하고 있어, 타임아웃/에러정책 변경 시 누락 위험이 있습니다.
♻️ 제안 diff
class OAuthRestClientConfig { `@Bean`("kakaoOAuthRestClient") fun kakaoOAuthRestClient(): RestClient = - RestClient.builder() - .baseUrl(KAKAO_USER_INFO_URL) - .defaultStatusHandler({ it.isError }) { request, response -> - logger.error { "Kakao API 호출 실패: status=${response.statusCode}" } - throw LinktripException(ExceptionCode.OAUTH_PROVIDER_ERROR) - } - .requestFactory(clientHttpRequestFactory()) - .build() + buildOAuthRestClient(provider = "Kakao", baseUrl = KAKAO_USER_INFO_URL) `@Bean`("googleOAuthRestClient") fun googleOAuthRestClient(): RestClient = - RestClient.builder() - .baseUrl(GOOGLE_USER_INFO_URL) - .defaultStatusHandler({ it.isError }) { request, response -> - logger.error { "Google API 호출 실패: status=${response.statusCode}" } - throw LinktripException(ExceptionCode.OAUTH_PROVIDER_ERROR) - } - .requestFactory(clientHttpRequestFactory()) - .build() + buildOAuthRestClient(provider = "Google", baseUrl = GOOGLE_USER_INFO_URL) `@Bean`("appleOAuthRestClient") fun appleOAuthRestClient(): RestClient = + buildOAuthRestClient(provider = "Apple", baseUrl = APPLE_BASE_URL) + + private fun buildOAuthRestClient(provider: String, baseUrl: String): RestClient = RestClient.builder() - .baseUrl(APPLE_BASE_URL) - .defaultStatusHandler({ it.isError }) { request, response -> - logger.error { "Apple API 호출 실패: status=${response.statusCode}" } + .baseUrl(baseUrl) + .defaultStatusHandler({ it.isError }) { _, response -> + logger.error { "$provider API 호출 실패: status=${response.statusCode}" } throw LinktripException(ExceptionCode.OAUTH_PROVIDER_ERROR) } .requestFactory(clientHttpRequestFactory()) .build()🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@linktrip-output-http/src/main/kotlin/com/linktrip/output/http/oauth/config/OAuthRestClientConfig.kt` around lines 16 - 47, The three bean builders kakaoOAuthRestClient, googleOAuthRestClient and appleOAuthRestClient repeat identical RestClient construction (baseUrl, defaultStatusHandler, requestFactory), so extract a single private helper like buildOAuthRestClient(baseUrl: String): RestClient that uses clientHttpRequestFactory() and the shared defaultStatusHandler logic, then have each `@Bean` method simply call that helper with its respective URL constant (KAKAO_USER_INFO_URL, GOOGLE_USER_INFO_URL, APPLE_BASE_URL) to centralize timeouts/error policy changes and avoid duplication.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@linktrip-application/src/main/kotlin/com/linktrip/application/domain/member/AuthService.kt`:
- Line 26: 로그에 민감한 식별자(providerId)를 남기고 있으니 AuthService.kt의 해당 logger.info 호출에서
oAuthInfo.providerId를 제거하거나 마스킹하세요; 즉 AuthService 내 logger.info { "OAuth 사용자 정보
조회 완료: provider=$providerType" }로 바꾸거나 oAuthInfo.providerId를 부분 마스킹(예: 첫/끝 일부만
노출)하여 실제 식별자가 남지 않게 변경하고, 참조되는 심볼은 AuthService, logger.info, providerType,
oAuthInfo.providerId입니다.
In
`@linktrip-application/src/main/kotlin/com/linktrip/application/domain/member/MemberService.kt`:
- Around line 20-30: The current find → save flow in MemberService
(memberPort.findByProviderTypeAndProviderId + memberPort.save with
Member.create) can produce duplicate members under concurrent requests; add DB
unique constraint on (providerType, providerId) and wrap the save in error
recovery: attempt to save the new Member, and if the save throws a
duplicate-key/DataIntegrityViolation (or the persistence-specific duplicate
exception), catch it, re-query memberPort.findByProviderTypeAndProviderId and
return MemberLoginResult with the existing member (isNewMember=false); keep the
normal path returning the newly saved member as
MemberLoginResult(isNewMember=true). Ensure the recovery handles the same
identifying keys and uses the same symbols: memberPort.save,
memberPort.findByProviderTypeAndProviderId, Member.create, and
MemberLoginResult.
In
`@linktrip-application/src/main/kotlin/com/linktrip/application/port/output/persistence/MemberPort.kt`:
- Line 12: The MemberPort interface exposes an unused method findByEmail; remove
it or document its intended future use—either delete the findByEmail declaration
from MemberPort if not needed, or add a clear comment above findByEmail stating
it is reserved for future account/email-based lookups (e.g., account linking)
and why current flows (MemberService.findOrCreateByOAuth using
findByProviderTypeAndProviderId and save) do not call it; ensure references to
findByEmail are absent elsewhere and run tests/compilation after the change.
In
`@linktrip-input-http/src/main/kotlin/com/linktrip/input/http/config/WebMvcConfig.kt`:
- Around line 3-4: 현재 WebMvcConfig.kt의 import 순서가 알파벳 순서와 일치하지 않아 ktlint가 실패합니다;
파일에서 import com.linktrip.input.http.security.AuthenticatedMemberArgumentResolver
및 import com.linktrip.input.http.ratelimit.RateLimitInterceptor를 찾은 뒤
알파벳(패키지+클래스 기준) 순서로 정렬하여 RateLimitInterceptor(import
com.linktrip.input.http.ratelimit) 가 AuthenticatedMemberArgumentResolver(import
com.linktrip.input.http.security)보다 먼저 오도록 수정하십시오.
In
`@linktrip-input-http/src/main/kotlin/com/linktrip/input/http/controller/dto/request/AuthRequest.kt`:
- Around line 1-12: The filename currently (AuthRequest.kt) does not match the
Kotlin data class name OAuthLoginRequest and ktlint is failing; rename the file
from AuthRequest.kt to OAuthLoginRequest.kt so it matches the data class
OAuthLoginRequest, then update any imports/usages referencing the old filename
if necessary (search for AuthRequest and replace with OAuthLoginRequest) to
ensure compilation and linting pass.
In
`@linktrip-input-http/src/main/kotlin/com/linktrip/input/http/controller/dto/response/AuthResponse.kt`:
- Around line 1-7: The filename currently (AuthResponse.kt) does not match the
Kotlin data class name OAuthLoginResponse which fails ktlint; rename the source
file from AuthResponse.kt to OAuthLoginResponse.kt so the file name matches the
data class OAuthLoginResponse (ensure VCS commit includes only the rename and
run ktlint/build to verify).
In
`@linktrip-input-http/src/main/kotlin/com/linktrip/input/http/filter/MdcLoggingFilter.kt`:
- Around line 85-90: The LOGGING_SKIP list in the companion object currently
contains "/api/health/" which doesn't match the actual HealthCheckController
mappings; update LOGGING_SKIP (in MdcLoggingFilter's companion object) to
include the correct health endpoints (e.g. "/health", or the specific paths
"/health/api" and "/health/db" or a prefix pattern that matches all health
endpoints) so requests to HealthCheckController (mapped with
`@RequestMapping`("/health")) are skipped by the logging filter.
In
`@linktrip-input-http/src/main/kotlin/com/linktrip/input/http/security/AuthenticatedMemberArgumentResolver.kt`:
- Around line 25-26: The ktlint error is caused by breaking the expression
before the Elvis operator; update the formatting in
AuthenticatedMemberArgumentResolver so the Elvis operator is placed at the end
of the preceding line (or keep the entire assignment on one line): ensure the
expression "val authentication =
SecurityContextHolder.getContext().authentication ?: throw
LinktripException(ExceptionCode.AUTHENTICATION_FAILED)" does not start the
multiline with the operator and conforms to ktlint's rule.
In
`@linktrip-input-http/src/main/kotlin/com/linktrip/input/http/security/JwtSecurityFilter.kt`:
- Around line 29-44: The try/catch currently wraps filterChain.doFilter causing
non-auth errors to be handled as authentication failures; change
JwtSecurityFilter so only the authentication attempt (creation of
PreAuthorizationToken and call to authenticationManager.authenticate) is inside
the try block, then set SecurityContextHolder.getContext().authentication =
postAuthToken and call filterChain.doFilter(request, response) outside that try.
Keep the AuthenticationException catch to clear the context and call
authenticationEntryPoint.commence, but do not catch/generalize other exceptions
(let them propagate) so downstream errors aren't converted to 401.
In
`@linktrip-input-http/src/main/kotlin/com/linktrip/input/http/security/SecurityConfig.kt`:
- Line 29: SecurityConfig currently applies
securityMatcher(*SecurityPaths.PUBLIC_ENDPOINTS) too broadly; narrow the public
matcher to only truly public routes by removing dev/ops-sensitive paths from
SecurityPaths.PUBLIC_ENDPOINTS, create a separate SecurityPaths.DEV_PUBLIC (or
similar) for Swagger/Actuator/Test, and conditionally register that dev-only
matcher in SecurityConfig based on the active Spring profile (e.g., only add the
Swagger/Actuator/Test matcher when an "dev" or "local" profile is active) so
production never exposes those endpoints.
- Around line 26-54: The two SecurityFilterChain beans (publicFilterChain and
protectedFilterChain) need explicit ordering so the public securityMatcher
(SecurityPaths.PUBLIC_ENDPOINTS) is evaluated before the protected anyRequest()
chain; add `@Order` annotations (import org.springframework.core.annotation.Order)
to set publicFilterChain to a higher precedence (lower order value, e.g.
`@Order`(1)) and protectedFilterChain to a lower precedence (e.g. `@Order`(2)) so
the SecurityPaths.PUBLIC_ENDPOINTS matcher is applied before
JwtSecurityFilter/anyRequest() in protectedFilterChain.
In
`@linktrip-input-http/src/main/kotlin/com/linktrip/input/http/security/SecurityPaths.kt`:
- Around line 11-12: 현재 SecurityPaths에 들어있는 공개 경로 "/test/**"와 "/actuator/**"는
과도하므로 보안을 위해 축소해야 합니다: SecurityPaths 클래스(또는 관련 상수/배열)를 찾아서 "/actuator/**"를
"/actuator/health/**" 등 필요한 헬스 체크 엔드포인트로 제한하고 "/test/**"는 아예 제거하거나 로컬/테스트
프로파일에서만 허용되도록 조건부로 분리(예: 프로파일 기반 필터 또는 환경 검사)하세요; 또한 변경 후 접근 제어 로직(예:
WebSecurityConfigurerAdapter 설정)에서 해당 경로명이 올바르게 반영되는지 확인하십시오.
In
`@linktrip-output-http/src/main/kotlin/com/linktrip/output/http/oauth/adapter/AppleOAuthAdapter.kt`:
- Around line 72-76: The token parsing currently only verifies the signature
(Jwts.parser().verifyWith(publicKey).build().parseSignedClaims(idToken).payload)
but does not validate the issuer (iss) and audience (aud); update the
AppleOAuthAdapter token validation to extract claims after
parseSignedClaims(idToken) and explicitly check that claims.get("iss") equals
"https://appleid.apple.com" and that claims.get("aud") matches the expected app
identifier (the Bundle ID/Service ID provided to your adapter, e.g., an
expectedAudience or clientId field); if either check fails, throw a clear
authentication/invalid-token exception so tokens issued for other apps are
rejected.
---
Outside diff comments:
In
`@linktrip-input-http/src/main/kotlin/com/linktrip/input/http/security/JwtTokenProvider.kt`:
- Around line 37-50: The validate(token: String) method in JwtTokenProvider
currently catches ExpiredJwtException and JwtException but not
IllegalArgumentException; add a catch block for IllegalArgumentException in
validate to explicitly handle empty/invalid token input: log a warning via
logger.warn including e.message (e.g., "잘못된 JWT 토큰: ${e.message}") and return
false, placing this catch alongside the existing exception handlers in the
validate function.
---
Nitpick comments:
In
`@linktrip-input-http/src/main/kotlin/com/linktrip/input/http/security/handler/JwtAuthenticationEntryPoint.kt`:
- Around line 13-37: The JwtAuthenticationEntryPoint and JwtAccessDeniedHandler
share identical response-writing logic; extract this into a shared helper (e.g.,
SecurityResponseHelper.writeErrorResponse) that accepts HttpServletResponse,
ObjectMapper, and ExceptionCode, then replace the duplicated blocks in
JwtAuthenticationEntryPoint.commence and JwtAccessDeniedHandler.handle with a
call to that helper to centralize status, contentType, encoding, and
ExceptionResponse serialization.
In
`@linktrip-input-http/src/main/kotlin/com/linktrip/input/http/security/PostAuthorizationToken.kt`:
- Line 7: 현재 PostAuthorizationToken 클래스가 AbstractAuthenticationToken을
emptyList()로 초기화해 권한 정보를 고정하고 있으므로 향후 RBAC 확장이 어렵습니다; 수정안은
PostAuthorizationToken 생성자에 authorities: Collection<GrantedAuthority> (또는 기본값이
있는 오버로드 생성자)을 추가하고 해당 authorities를 super(authorities)로 전달하도록 변경하거나 내부에
GrantedAuthority 컬렉션 필드를 두고 getAuthorities()가 이를 반환하게 하여 관리자/사용자 등 역할 구분을 쉽게 확장할
수 있도록 하세요; 대상 심볼: PostAuthorizationToken 생성자,
AbstractAuthenticationToken(super(...)), getAuthorities().
In
`@linktrip-input-http/src/test/kotlin/com/linktrip/input/http/ratelimit/RateLimitInterceptorTest.kt`:
- Around line 37-39: Move the manual cleanup into a JUnit `@AfterEach` method so
the SecurityContextHolder is always cleared even if assertions fail: create an
`@AfterEach` annotated function (e.g., fun clearAuthentication()) that calls
SecurityContextHolder.clearContext() and remove all explicit
clearAuthentication() calls at the end of individual tests; ensure the method
name matches existing clearAuthentication to reuse the same logic.
In
`@linktrip-output-http/src/main/kotlin/com/linktrip/output/http/oauth/adapter/AppleOAuthAdapter.kt`:
- Around line 47-52: fetchApplePublicKeys is currently calling Apple on every
invocation; add caching to AppleOAuthAdapter to avoid repeated network calls by
storing the last ApplePublicKeyResponse along with its expiry (Apple keys rotate
infrequently). Implement a simple thread-safe cache (e.g., volatile field +
synchronized refresh or use a cache library like Caffeine) inside
AppleOAuthAdapter: on fetchApplePublicKeys return cached response if present and
not expired, otherwise call
restClient.get().uri("/auth/keys").retrieve().body<ApplePublicKeyResponse>(),
update the cache and expiry (configurable TTL), and fall back to the previous
cached value on transient failures; ensure exceptions still map to
LinktripException(ExceptionCode.OAUTH_PROVIDER_ERROR) when no valid cached value
exists.
In
`@linktrip-output-http/src/main/kotlin/com/linktrip/output/http/oauth/config/OAuthRestClientConfig.kt`:
- Around line 16-47: The three bean builders kakaoOAuthRestClient,
googleOAuthRestClient and appleOAuthRestClient repeat identical RestClient
construction (baseUrl, defaultStatusHandler, requestFactory), so extract a
single private helper like buildOAuthRestClient(baseUrl: String): RestClient
that uses clientHttpRequestFactory() and the shared defaultStatusHandler logic,
then have each `@Bean` method simply call that helper with its respective URL
constant (KAKAO_USER_INFO_URL, GOOGLE_USER_INFO_URL, APPLE_BASE_URL) to
centralize timeouts/error policy changes and avoid duplication.
In
`@linktrip-output-http/src/main/kotlin/com/linktrip/output/http/oauth/dto/AppleUserInfoResponse.kt`:
- Around line 3-14: The file name does not match the public data class declared
(file is AppleUserInfoResponse.kt but the class is ApplePublicKeyResponse);
rename the file to ApplePublicKeyResponse.kt so Kotlin file/class convention is
preserved and the public data class ApplePublicKeyResponse (and its nested
AppleKey) are discoverable and consistent.
In
`@linktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/entity/MemberEntity.kt`:
- Around line 17-26: Remove the redundant index idx_member_provider from the
indexes list in MemberEntity (the unique constraint uk_member_provider on
columns provider_type and provider_id already creates an index), i.e., delete
the Index entry with name "idx_member_provider" (leave the UniqueConstraint
"uk_member_provider" and other indexes like "idx_member_email" intact) and run
schema migration tests to ensure no references depend on the removed index.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 6dc6c163-f73c-4922-8745-fa00fcf5fe45
📒 Files selected for processing (42)
gradle/libs.versions.tomllinktrip-application/src/main/kotlin/com/linktrip/application/domain/member/AuthService.ktlinktrip-application/src/main/kotlin/com/linktrip/application/domain/member/Member.ktlinktrip-application/src/main/kotlin/com/linktrip/application/domain/member/MemberService.ktlinktrip-application/src/main/kotlin/com/linktrip/application/domain/member/ProviderType.ktlinktrip-application/src/main/kotlin/com/linktrip/application/port/input/AuthUseCase.ktlinktrip-application/src/main/kotlin/com/linktrip/application/port/output/auth/OAuthInfo.ktlinktrip-application/src/main/kotlin/com/linktrip/application/port/output/auth/OAuthPort.ktlinktrip-application/src/main/kotlin/com/linktrip/application/port/output/persistence/MemberPort.ktlinktrip-application/src/test/kotlin/com/linktrip/application/domain/member/AuthServiceTest.ktlinktrip-common/src/main/kotlin/com/linktrip/common/exception/ExceptionCode.ktlinktrip-input-http/src/main/kotlin/com/linktrip/input/http/config/FilterConfig.ktlinktrip-input-http/src/main/kotlin/com/linktrip/input/http/config/WebMvcConfig.ktlinktrip-input-http/src/main/kotlin/com/linktrip/input/http/controller/AuthController.ktlinktrip-input-http/src/main/kotlin/com/linktrip/input/http/controller/dto/request/AuthRequest.ktlinktrip-input-http/src/main/kotlin/com/linktrip/input/http/controller/dto/response/AuthResponse.ktlinktrip-input-http/src/main/kotlin/com/linktrip/input/http/filter/FilterPaths.ktlinktrip-input-http/src/main/kotlin/com/linktrip/input/http/filter/JwtAuthenticationFilter.ktlinktrip-input-http/src/main/kotlin/com/linktrip/input/http/filter/MdcLoggingFilter.ktlinktrip-input-http/src/main/kotlin/com/linktrip/input/http/ratelimit/RateLimitInterceptor.ktlinktrip-input-http/src/main/kotlin/com/linktrip/input/http/security/AuthenticatedMember.ktlinktrip-input-http/src/main/kotlin/com/linktrip/input/http/security/AuthenticatedMemberArgumentResolver.ktlinktrip-input-http/src/main/kotlin/com/linktrip/input/http/security/JwtAuthenticationProvider.ktlinktrip-input-http/src/main/kotlin/com/linktrip/input/http/security/JwtSecurityFilter.ktlinktrip-input-http/src/main/kotlin/com/linktrip/input/http/security/JwtTokenProvider.ktlinktrip-input-http/src/main/kotlin/com/linktrip/input/http/security/PostAuthorizationToken.ktlinktrip-input-http/src/main/kotlin/com/linktrip/input/http/security/PreAuthorizationToken.ktlinktrip-input-http/src/main/kotlin/com/linktrip/input/http/security/SecurityConfig.ktlinktrip-input-http/src/main/kotlin/com/linktrip/input/http/security/SecurityPaths.ktlinktrip-input-http/src/main/kotlin/com/linktrip/input/http/security/handler/JwtAccessDeniedHandler.ktlinktrip-input-http/src/main/kotlin/com/linktrip/input/http/security/handler/JwtAuthenticationEntryPoint.ktlinktrip-input-http/src/test/kotlin/com/linktrip/input/http/ratelimit/RateLimitInterceptorTest.ktlinktrip-output-http/src/main/kotlin/com/linktrip/output/http/oauth/adapter/AppleOAuthAdapter.ktlinktrip-output-http/src/main/kotlin/com/linktrip/output/http/oauth/adapter/GoogleOAuthAdapter.ktlinktrip-output-http/src/main/kotlin/com/linktrip/output/http/oauth/adapter/KakaoOAuthAdapter.ktlinktrip-output-http/src/main/kotlin/com/linktrip/output/http/oauth/config/OAuthRestClientConfig.ktlinktrip-output-http/src/main/kotlin/com/linktrip/output/http/oauth/dto/AppleUserInfoResponse.ktlinktrip-output-http/src/main/kotlin/com/linktrip/output/http/oauth/dto/GoogleUserInfoResponse.ktlinktrip-output-http/src/main/kotlin/com/linktrip/output/http/oauth/dto/KakaoUserInfoResponse.ktlinktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/adapter/MemberAdapter.ktlinktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/entity/MemberEntity.ktlinktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/repository/MemberJpaRepository.kt
💤 Files with no reviewable changes (3)
- linktrip-input-http/src/main/kotlin/com/linktrip/input/http/filter/FilterPaths.kt
- linktrip-input-http/src/main/kotlin/com/linktrip/input/http/config/FilterConfig.kt
- linktrip-input-http/src/main/kotlin/com/linktrip/input/http/filter/JwtAuthenticationFilter.kt
📜 Review details
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2026-03-18T01:07:53.575Z
Learnt from: toychip
Repo: Link-Trip/BackEnd PR: 24
File: linktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/adapter/YouTubeVideoPersistenceAdapter.kt:16-28
Timestamp: 2026-03-18T01:07:53.575Z
Learning: Apply the same deferred concurrent-write policy to all Kotlin persistence adapters in the MySQL adapter package: ensure writes are coordinated on a single server with ShedLock considered for the replication phase. For YouTubeVideoPersistenceAdapter.kt (and other adapters in this directory), verify that concurrent writes are serialized or properly guarded, document the policy in code comments, and ensure CI checks or deployment gating will catch any regression where multiple instances might attempt a conflicting write during replication.
Applied to files:
linktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/adapter/MemberAdapter.kt
🪛 GitHub Actions: CI - Pull request
linktrip-input-http/src/main/kotlin/com/linktrip/input/http/config/WebMvcConfig.kt
[error] 3-3: ktlintCheck (ktlintMainSourceSetCheck) failed: Imports must be ordered in lexicographic order without any empty lines in-between with "java", "javax", "kotlin" and aliases in the end.
linktrip-input-http/src/main/kotlin/com/linktrip/input/http/controller/dto/response/AuthResponse.kt
[error] 1-1: ktlintCheck (ktlintMainSourceSetCheck) failed: File 'AuthResponse.kt' contains a single class and should be named same after that class 'OAuthLoginResponse.kt' (cannot be auto-corrected).
linktrip-input-http/src/main/kotlin/com/linktrip/input/http/controller/dto/request/AuthRequest.kt
[error] 1-1: ktlintCheck (ktlintMainSourceSetCheck) failed: File 'AuthRequest.kt' contains a single class and should be named same after that class 'OAuthLoginRequest.kt' (cannot be auto-corrected).
linktrip-input-http/src/main/kotlin/com/linktrip/input/http/security/AuthenticatedMemberArgumentResolver.kt
[error] 25-25: ktlintCheck (ktlintMainSourceSetCheck) failed: A multiline expression should start on a new line (line 25, column 30).
🔇 Additional comments (28)
linktrip-input-http/src/main/kotlin/com/linktrip/input/http/filter/MdcLoggingFilter.kt (1)
28-28: 리팩토링 방향 적절함삭제된
FilterPaths클래스에서 로컬 companion object로LOGGING_SKIP상수를 이동한 것은 적절합니다. 이 필터에서만 사용되는 상수이므로 응집도가 높아졌습니다.linktrip-application/src/main/kotlin/com/linktrip/application/domain/member/ProviderType.kt (1)
3-7: Provider enum 정의가 현재 OAuth 범위와 일관됩니다.Line 3-7 구조가 단순하고 명확하며, 영속 계층의
EnumType.STRING저장 방식과도 잘 맞습니다.linktrip-input-http/src/main/kotlin/com/linktrip/input/http/security/JwtTokenProvider.kt (1)
25-35: 토큰 발급 로직은 간결하고 안전한 흐름입니다.Line 25-35에서
iat/exp와 서명 처리 순서가 명확합니다.linktrip-common/src/main/kotlin/com/linktrip/common/exception/ExceptionCode.kt (1)
11-11: OAuth 예외 코드 추가 방향이 적절합니다.Line 11과 Line 41이 입력 오류(400)와 외부 제공자 장애(502)를 분리해 운영 관점에서도 해석이 좋습니다.
Also applies to: 41-41
linktrip-application/src/main/kotlin/com/linktrip/application/port/output/auth/OAuthInfo.kt (1)
5-9: OAuth 사용자 식별 DTO 구조가 깔끔합니다.Line 5-9는 provider 식별자와 nullable 이메일을 분리해 provider별 응답 편차를 잘 수용합니다.
linktrip-application/src/main/kotlin/com/linktrip/application/port/output/auth/OAuthPort.kt (1)
5-9: 포트 계약이 목적에 맞게 최소화되어 있습니다.Line 5-9 인터페이스는 provider 라우팅과 사용자 정보 조회 책임을 분리해 확장하기 좋습니다.
gradle/libs.versions.toml (1)
32-32: 보안 리팩터링에 필요한 의존성 연결이 잘 반영되었습니다.Line 32, Line 92, Line 97 변경으로 input/output HTTP 모듈의 Security + JWT 요구사항이 일관되게 맞춰졌습니다.
Also applies to: 92-92, 97-97
linktrip-output-http/src/main/kotlin/com/linktrip/output/http/oauth/dto/KakaoUserInfoResponse.kt (1)
5-13: Kakao 응답 DTO 매핑이 어댑터 사용 방식과 일치합니다.Line 5-13의
id: Long및kakao_account매핑이KakaoOAuthAdapter의 변환 로직과 정확히 맞습니다.linktrip-input-http/src/main/kotlin/com/linktrip/input/http/security/handler/JwtAccessDeniedHandler.kt (1)
13-37: LGTM!Spring Security의
AccessDeniedHandler를 올바르게 구현하였습니다. JSON 응답 형식, Content-Type 설정, 문자 인코딩 처리가 적절합니다.linktrip-application/src/main/kotlin/com/linktrip/application/domain/member/Member.kt (1)
6-26: LGTM!OAuth 기반 인증을 위한 도메인 모델 변경이 적절합니다.
providerType과providerId조합으로 사용자를 고유하게 식별하는 구조가 잘 반영되었습니다.linktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/entity/MemberEntity.kt (1)
28-48: LGTM!Entity 구현이 적절합니다.
EnumType.STRING사용, nullable email 처리,toDomain()매핑이 도메인 모델과 일관성 있게 구현되었습니다.linktrip-input-http/src/main/kotlin/com/linktrip/input/http/security/AuthenticatedMember.kt (1)
1-5: LGTM!보안 관련 컴포넌트를
security패키지로 통합하는 리팩토링에 맞춰 패키지가 적절히 변경되었습니다.linktrip-output-http/src/main/kotlin/com/linktrip/output/http/oauth/dto/GoogleUserInfoResponse.kt (1)
3-6: LGTM!Google OAuth userinfo 응답을 위한 DTO가 적절하게 정의되었습니다.
linktrip-input-http/src/main/kotlin/com/linktrip/input/http/controller/dto/request/AuthRequest.kt (1)
10-11: OAuthLoginRequest.kt 파일명 수정 필요 (linktrip-input-http/src/main/kotlin/com/linktrip/input/http/controller/dto/request/OAuthLoginRequest.kt)
ProviderTypeenum이KAKAO,APPLE같은 대문자 상수를 사용하고 있으며, Jackson 커스텀 설정이 없는 상태입니다. 클라이언트가 소문자("kakao")로 전송하면 역직렬화가 실패합니다. 클라이언트 API 문서에서 대문자 사용을 명시하거나, 대소문자 무관하게 처리하려면 enum에@JsonCreator를 추가하는 것을 고려하세요.> Likely an incorrect or invalid review comment.linktrip-application/src/main/kotlin/com/linktrip/application/port/input/AuthUseCase.kt (1)
3-9: LGTM!OAuth 기반 소셜 로그인으로의 전환이 인터페이스에 잘 반영되었습니다.
ProviderType과accessToken을 매개변수로 받는 설계가 명확하고, 헥사고날 아키텍처의 입력 포트로서 적절합니다.linktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/repository/MemberJpaRepository.kt (1)
7-14: LGTM!OAuth 기반 멤버 조회를 위한 리포지토리 메서드가 Spring Data JPA 명명 규칙에 맞게 잘 정의되었습니다. AI 요약에 따르면
MemberEntity에providerType + providerId복합 인덱스와linktrip-application/src/main/kotlin/com/linktrip/application/port/output/persistence/MemberPort.kt (1)
7-12: LGTM!OAuth 기반 회원 조회를 위한 포트 인터페이스가 적절히 설계되었습니다.
linktrip-input-http/src/main/kotlin/com/linktrip/input/http/security/PreAuthorizationToken.kt (1)
5-15: LGTM!Spring Security의
AbstractAuthenticationToken을 올바르게 확장하여 인증 전 토큰을 구현했습니다. JWT를 credentials로 저장하고,isAuthenticated = false로 초기화하는 패턴이 적절합니다.linktrip-input-http/src/main/kotlin/com/linktrip/input/http/security/PostAuthorizationToken.kt (1)
5-15: LGTM!인증 완료 후 토큰 구현이 적절합니다.
memberId를 principal로 저장하여 다운스트림 컴포넌트에서 사용자를 식별할 수 있습니다.linktrip-input-http/src/main/kotlin/com/linktrip/input/http/ratelimit/RateLimitInterceptor.kt (1)
21-23: LGTM!
SecurityContextHolder에서memberId를 가져오도록 변경한 것은 새로운 JWT 보안 파이프라인과 잘 맞습니다. 인증되지 않은 요청에 대해return true로 처리하여 rate limit을 적용하지 않는 것은 공개 엔드포인트를 위한 적절한 설계입니다.linktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/adapter/MemberAdapter.kt (1)
14-21: LGTM!
MemberPort인터페이스를 올바르게 구현했습니다.findByProviderTypeAndProviderId와findByEmail메서드가 JPA 리포지토리에 적절히 위임하고toDomain()을 통해 매핑합니다. 학습된 내용에 따른 단일 서버 쓰기 정책도 유지되고 있습니다.linktrip-application/src/test/kotlin/com/linktrip/application/domain/member/AuthServiceTest.kt (3)
33-38: LGTM!테스트 설정이 프로덕션 코드의 생성자 시그니처와 정확히 일치합니다.
MemberService를 실제로 생성하고MemberPort만 모킹하는 전략은 OAuth 로그인 플로우의 통합 동작을 효과적으로 검증합니다.
40-65: LGTM!기존 회원 로그인 테스트가 적절합니다.
memberPort.save가 호출되지 않았음을 검증하고isNewMember가false임을 확인하여 기존 회원 플로우를 정확히 테스트합니다.
67-93: LGTM!신규 회원 등록 테스트가
memberPort.save호출과isNewMember == true검증을 통해 신규 가입 플로우를 정확히 커버합니다.linktrip-input-http/src/main/kotlin/com/linktrip/input/http/controller/AuthController.kt (1)
18-40: LGTM!OAuth 소셜 로그인으로의 전환이 깔끔하게 구현되었습니다.
isNewMember플래그에 따라201 Created/200 OK를 반환하는 것은 RESTful 설계 원칙에 부합합니다.@Valid어노테이션으로 입력 검증도 적절히 처리됩니다.linktrip-output-http/src/main/kotlin/com/linktrip/output/http/oauth/adapter/GoogleOAuthAdapter.kt (1)
23-39: LGTM!Google OAuth 어댑터 구현이 적절합니다.
RestClient주입, 에러 처리, 그리고OAuthInfo매핑이 다른 어댑터들과 일관성 있게 구현되어 있습니다.linktrip-output-http/src/main/kotlin/com/linktrip/output/http/oauth/adapter/KakaoOAuthAdapter.kt (1)
23-39: LGTM!Kakao OAuth 어댑터가 잘 구현되었습니다.
response.id.toString()으로 Long을 String으로 변환하여providerId의 일관된 타입을 유지하고,kakaoAccount?.email로 선택적 이메일을 안전하게 처리합니다.linktrip-input-http/src/main/kotlin/com/linktrip/input/http/security/JwtAuthenticationProvider.kt (1)
13-25: JWT 인증 흐름(검증 → memberId 추출 → 인증 토큰 반환)이 명확하고 일관적입니다.
linktrip-application/src/main/kotlin/com/linktrip/application/domain/member/AuthService.kt
Outdated
Show resolved
Hide resolved
linktrip-application/src/main/kotlin/com/linktrip/application/domain/member/MemberService.kt
Show resolved
Hide resolved
...p-application/src/main/kotlin/com/linktrip/application/port/output/persistence/MemberPort.kt
Outdated
Show resolved
Hide resolved
linktrip-input-http/src/main/kotlin/com/linktrip/input/http/config/WebMvcConfig.kt
Outdated
Show resolved
Hide resolved
...put-http/src/main/kotlin/com/linktrip/input/http/controller/dto/request/OAuthLoginRequest.kt
Show resolved
Hide resolved
linktrip-input-http/src/main/kotlin/com/linktrip/input/http/security/JwtSecurityFilter.kt
Outdated
Show resolved
Hide resolved
linktrip-input-http/src/main/kotlin/com/linktrip/input/http/security/SecurityConfig.kt
Show resolved
Hide resolved
linktrip-input-http/src/main/kotlin/com/linktrip/input/http/security/SecurityConfig.kt
Show resolved
Hide resolved
linktrip-input-http/src/main/kotlin/com/linktrip/input/http/security/SecurityPaths.kt
Show resolved
Hide resolved
...trip-output-http/src/main/kotlin/com/linktrip/output/http/oauth/adapter/AppleOAuthAdapter.kt
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (1)
linktrip-output-http/src/main/kotlin/com/linktrip/output/http/oauth/adapter/AppleOAuthAdapter.kt (1)
74-78:⚠️ Potential issue | 🟠 MajorApple 토큰의
iss/aud클레임 검증이 여전히 빠져 있습니다.Line 74~78은 서명만 확인하고 있어, 다른 클라이언트 대상으로 발급된 토큰 재사용을 차단하지 못합니다.
iss=https://appleid.apple.com와 기대aud(Service ID/Bundle ID) 검증을 추가해 주세요. (이 이슈는 이전 리뷰와 동일한 미해결 항목입니다.)🔒 제안 수정
+ `@org.springframework.beans.factory.annotation.Value`("\${oauth.apple.audience}") + private lateinit var expectedAudience: String @@ - return Jwts.parser() + val claims = Jwts.parser() .verifyWith(publicKey) .build() .parseSignedClaims(idToken) .payload + + if (claims.issuer != "https://appleid.apple.com") { + throw LinktripException(ExceptionCode.TOKEN_INVALID) + } + + val aud = claims["aud"] + val audienceMatched = + when (aud) { + is String -> aud == expectedAudience + is Collection<*> -> aud.any { it == expectedAudience } + else -> false + } + if (!audienceMatched) { + throw LinktripException(ExceptionCode.TOKEN_INVALID) + } + return claims🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@linktrip-output-http/src/main/kotlin/com/linktrip/output/http/oauth/adapter/AppleOAuthAdapter.kt` around lines 74 - 78, The current token parsing in AppleOAuthAdapter (using Jwts.parser().verifyWith(publicKey).parseSignedClaims(idToken)) only verifies signature; add explicit checks that the parsed payload's "iss" equals "https://appleid.apple.com" and that "aud" matches the expected client/service id (e.g., your configured Apple clientId/serviceId value), and reject/throw if either check fails; update the verification flow around parseSignedClaims(idToken) to retrieve the payload, compare payload.get("iss") and payload.get("aud") against the expected constants/config, and return only when both match.
🧹 Nitpick comments (1)
linktrip-output-http/src/main/kotlin/com/linktrip/output/http/oauth/adapter/AppleOAuthAdapter.kt (1)
47-52: Apple 공개키 호출은 짧은 TTL 캐시를 두는 것을 권장합니다.매 로그인마다
/auth/keys를 호출하면 지연/외부장애 전파 리스크가 커집니다. in-memory 캐시(예: 5~15분) + 실패 시 마지막 성공값 fallback을 두면 인증 가용성이 좋아집니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@linktrip-output-http/src/main/kotlin/com/linktrip/output/http/oauth/adapter/AppleOAuthAdapter.kt` around lines 47 - 52, The fetchApplePublicKeys function currently calls restClient.get().uri("/auth/keys") on every login; change it to use an in-memory TTL cache (e.g., 5–15 minutes) holding the last successful ApplePublicKeyResponse and a timestamp, returning the cached value while fresh; on cache miss, attempt the network call, update the cache on success, and on network failure return the last successful cached ApplePublicKeyResponse as a fallback and only throw LinktripException if there is no cached value. Modify fetchApplePublicKeys (and related private state: cached ApplePublicKeyResponse and cached timestamp) to be thread-safe (synchronized or atomic) so concurrent requests use the cache correctly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@linktrip-output-http/src/main/kotlin/com/linktrip/output/http/oauth/adapter/AppleOAuthAdapter.kt`:
- Around line 58-67: Wrap the Base64 decoding and Jackson parsing around
headerPart/headerJson/headerMap (where you call Base64.getUrlDecoder().decode
and objectMapper.readValue and extract kid) in a try/catch that converts any
thrown exception into throw LinktripException(ExceptionCode.TOKEN_INVALID); do
the same for the subsequent JWT parsing block (the code that validates/parses
the JWT after extracting kid) so any parsing/validation errors are consistently
rethrown as LinktripException(ExceptionCode.TOKEN_INVALID). Ensure you catch
generic Exception (or relevant parsing exceptions) and rethrow the
LinktripException at points interacting with idToken, headerPart, headerJson,
headerMap, and kid.
- Around line 35-43: claims.subject (assigned to providerId) can be null or
blank for invalid tokens; update the AppleOAuthAdapter code that builds
OAuthInfo to validate providerId (from claims.subject) and explicitly handle
null/blank by returning/throwing the TOKEN_INVALID error path instead of using
the value directly. Locate the block that reads val providerId = claims.subject
and before constructing OAuthInfo, add a check for providerId.isNullOrBlank()
and route to the existing TOKEN_INVALID handling (e.g., throw the same exception
or return the same error result used elsewhere), keeping the rest of OAuthInfo
construction (providerType = ProviderType.APPLE, email, etc.) only for valid
providerId.
---
Duplicate comments:
In
`@linktrip-output-http/src/main/kotlin/com/linktrip/output/http/oauth/adapter/AppleOAuthAdapter.kt`:
- Around line 74-78: The current token parsing in AppleOAuthAdapter (using
Jwts.parser().verifyWith(publicKey).parseSignedClaims(idToken)) only verifies
signature; add explicit checks that the parsed payload's "iss" equals
"https://appleid.apple.com" and that "aud" matches the expected client/service
id (e.g., your configured Apple clientId/serviceId value), and reject/throw if
either check fails; update the verification flow around
parseSignedClaims(idToken) to retrieve the payload, compare payload.get("iss")
and payload.get("aud") against the expected constants/config, and return only
when both match.
---
Nitpick comments:
In
`@linktrip-output-http/src/main/kotlin/com/linktrip/output/http/oauth/adapter/AppleOAuthAdapter.kt`:
- Around line 47-52: The fetchApplePublicKeys function currently calls
restClient.get().uri("/auth/keys") on every login; change it to use an in-memory
TTL cache (e.g., 5–15 minutes) holding the last successful
ApplePublicKeyResponse and a timestamp, returning the cached value while fresh;
on cache miss, attempt the network call, update the cache on success, and on
network failure return the last successful cached ApplePublicKeyResponse as a
fallback and only throw LinktripException if there is no cached value. Modify
fetchApplePublicKeys (and related private state: cached ApplePublicKeyResponse
and cached timestamp) to be thread-safe (synchronized or atomic) so concurrent
requests use the cache correctly.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 770e5454-a29a-4c07-b1d3-a17be53ed2d3
📒 Files selected for processing (6)
linktrip-input-http/src/main/kotlin/com/linktrip/input/http/config/WebMvcConfig.ktlinktrip-input-http/src/main/kotlin/com/linktrip/input/http/controller/dto/request/OAuthLoginRequest.ktlinktrip-input-http/src/main/kotlin/com/linktrip/input/http/controller/dto/response/OAuthLoginResponse.ktlinktrip-input-http/src/main/kotlin/com/linktrip/input/http/security/AuthenticatedMemberArgumentResolver.ktlinktrip-output-http/src/main/kotlin/com/linktrip/output/http/oauth/adapter/AppleOAuthAdapter.ktlinktrip-output-http/src/main/kotlin/com/linktrip/output/http/oauth/dto/ApplePublicKeyResponse.kt
✅ Files skipped from review due to trivial changes (3)
- linktrip-input-http/src/main/kotlin/com/linktrip/input/http/config/WebMvcConfig.kt
- linktrip-output-http/src/main/kotlin/com/linktrip/output/http/oauth/dto/ApplePublicKeyResponse.kt
- linktrip-input-http/src/main/kotlin/com/linktrip/input/http/controller/dto/request/OAuthLoginRequest.kt
🚧 Files skipped from review as they are similar to previous changes (1)
- linktrip-input-http/src/main/kotlin/com/linktrip/input/http/security/AuthenticatedMemberArgumentResolver.kt
📜 Review details
🔇 Additional comments (2)
linktrip-input-http/src/main/kotlin/com/linktrip/input/http/controller/dto/response/OAuthLoginResponse.kt (1)
3-7: LGTM!DTO 구조가
AuthUseCase.AuthResult인터페이스 계약과 정확히 일치하며,isNewMember플래그를 통해 컨트롤러에서 신규 회원(201 Created)과 기존 회원(200 OK) 응답을 구분할 수 있습니다. OAuth 로그인 플로우에 적합한 깔끔한 구현입니다.linktrip-output-http/src/main/kotlin/com/linktrip/output/http/oauth/adapter/AppleOAuthAdapter.kt (1)
81-87: RSA 공개키 생성 로직은 명확하고 구현 방향이 좋습니다.
n/e를RSAPublicKeySpec으로 변환하는 흐름이 직관적이고 유지보수성이 좋습니다.
...trip-output-http/src/main/kotlin/com/linktrip/output/http/oauth/adapter/AppleOAuthAdapter.kt
Outdated
Show resolved
Hide resolved
...trip-output-http/src/main/kotlin/com/linktrip/output/http/oauth/adapter/AppleOAuthAdapter.kt
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
linktrip-input-http/src/main/kotlin/com/linktrip/input/http/security/JwtTokenProvider.kt (1)
37-72: 선택적 개선: JWT 이중 파싱 고려
validate()와extractMemberId()가 각각 JWT를 파싱하여 유효한 토큰의 경우 이중 파싱이 발생합니다. 현재 구조에서 큰 문제는 아니지만, 향후 파싱 결과를 캐싱하거나 단일 메서드로 검증과 추출을 동시에 수행하는 방식을 고려할 수 있습니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@linktrip-input-http/src/main/kotlin/com/linktrip/input/http/security/JwtTokenProvider.kt` around lines 37 - 72, validate() and extractMemberId() each parse the JWT separately causing duplicate parsing; create a single private helper (e.g., parseSignedClaims(token): SignedJwtClaims or similar) that wraps Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token) and centralizes the try/catch for ExpiredJwtException, JwtException, and IllegalArgumentException; then have validate() call the helper and return true/false based on success, and have extractMemberId() call the same helper and read .payload.subject (rethrowing LinktripException where appropriate) so parsing logic is only implemented once (reference functions: validate, extractMemberId and Jwts.parser().verifyWith(secretKey).build().parseSignedClaims).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@linktrip-output-http/src/main/kotlin/com/linktrip/output/http/oauth/adapter/AppleOAuthAdapter.kt`:
- Around line 52-57: fetchApplePublicKeys currently only maps a null body to
LinktripException(ExceptionCode.OAUTH_PROVIDER_ERROR) but lets
RestClientException from restClient.get()/retrieve() bubble up; wrap the REST
call in a try/catch that catches RestClientException (and any transport-related
exceptions you use) and rethrow a LinktripException with
ExceptionCode.OAUTH_PROVIDER_ERROR, keeping the existing null-body check for
ApplePublicKeyResponse; update the method around fetchApplePublicKeys /
restClient.get() / retrieve() to ensure all external API failures are converted
to the same domain exception (LinktripException with
ExceptionCode.OAUTH_PROVIDER_ERROR).
---
Nitpick comments:
In
`@linktrip-input-http/src/main/kotlin/com/linktrip/input/http/security/JwtTokenProvider.kt`:
- Around line 37-72: validate() and extractMemberId() each parse the JWT
separately causing duplicate parsing; create a single private helper (e.g.,
parseSignedClaims(token): SignedJwtClaims or similar) that wraps
Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token) and
centralizes the try/catch for ExpiredJwtException, JwtException, and
IllegalArgumentException; then have validate() call the helper and return
true/false based on success, and have extractMemberId() call the same helper and
read .payload.subject (rethrowing LinktripException where appropriate) so
parsing logic is only implemented once (reference functions: validate,
extractMemberId and
Jwts.parser().verifyWith(secretKey).build().parseSignedClaims).
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: bdf3639a-1f78-4a3f-84cd-e96efe2a38b7
📒 Files selected for processing (12)
linktrip-application/src/main/kotlin/com/linktrip/application/domain/member/AuthService.ktlinktrip-application/src/main/kotlin/com/linktrip/application/port/output/persistence/MemberPort.ktlinktrip-bootstrap/src/main/resources/application.ymllinktrip-input-http/src/main/kotlin/com/linktrip/input/http/security/JwtAuthenticationProvider.ktlinktrip-input-http/src/main/kotlin/com/linktrip/input/http/security/JwtSecurityFilter.ktlinktrip-input-http/src/main/kotlin/com/linktrip/input/http/security/JwtTokenProvider.ktlinktrip-input-http/src/main/kotlin/com/linktrip/input/http/security/SecurityConfig.ktlinktrip-input-http/src/main/kotlin/com/linktrip/input/http/security/SecurityPaths.ktlinktrip-output-http/src/main/kotlin/com/linktrip/output/http/oauth/adapter/AppleOAuthAdapter.ktlinktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/adapter/MemberAdapter.ktlinktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/entity/MemberEntity.ktlinktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/repository/MemberJpaRepository.kt
✅ Files skipped from review due to trivial changes (2)
- linktrip-bootstrap/src/main/resources/application.yml
- linktrip-input-http/src/main/kotlin/com/linktrip/input/http/security/SecurityPaths.kt
🚧 Files skipped from review as they are similar to previous changes (5)
- linktrip-application/src/main/kotlin/com/linktrip/application/port/output/persistence/MemberPort.kt
- linktrip-input-http/src/main/kotlin/com/linktrip/input/http/security/JwtSecurityFilter.kt
- linktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/adapter/MemberAdapter.kt
- linktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/entity/MemberEntity.kt
- linktrip-application/src/main/kotlin/com/linktrip/application/domain/member/AuthService.kt
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: 자동 검증 (ktlint + test)
🧰 Additional context used
🪛 detekt (1.23.8)
linktrip-output-http/src/main/kotlin/com/linktrip/output/http/oauth/adapter/AppleOAuthAdapter.kt
[warning] 88-88: The caught exception is swallowed. The original exception could be lost.
(detekt.exceptions.SwallowedException)
🔇 Additional comments (9)
linktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/repository/MemberJpaRepository.kt (2)
3-3: 승인:ProviderTypeimport 반영이 적절합니다.회원 식별 모델 전환 방향과 일치하는 변경입니다.
8-11: 승인: 저장소 조회 시그니처가 신규 식별 체계와 정확히 맞습니다.
findByProviderTypeAndProviderId(...)도입으로 provider 기반 회원 조회 의도가 명확해졌습니다.linktrip-input-http/src/main/kotlin/com/linktrip/input/http/security/SecurityConfig.kt (3)
31-31: PUBLIC_ENDPOINTS 범위 검증 필요 (기존 리뷰 참조)
SecurityPaths.PUBLIC_ENDPOINTS에 포함된 경로가 운영 환경에서 적절한지 확인이 필요합니다. Swagger, Actuator, 테스트 전용 경로가 포함되어 있다면 프로파일별로 분리하는 것이 권장됩니다.
27-37:@Order적용 확인 - 이전 리뷰 이슈 해결됨
publicFilterChain에@Order(1),protectedFilterChain에@Order(2)가 적용되어 공개 엔드포인트 매처가 먼저 평가됩니다. 이전 리뷰에서 지적된 필터 체인 순서 문제가 해결되었습니다.
39-59: 보호된 필터 체인 설정 LGTM적절한 구성입니다:
exceptionHandling에authenticationEntryPoint와accessDeniedHandler가 올바르게 연결되었습니다.JwtSecurityFilter가UsernamePasswordAuthenticationFilter앞에 추가되어 JWT 인증이 먼저 수행됩니다.- Stateless 세션 관리, CSRF 비활성화 등 JWT 기반 API에 적합한 설정입니다.
linktrip-input-http/src/main/kotlin/com/linktrip/input/http/security/JwtTokenProvider.kt (2)
50-53: IllegalArgumentException 처리 추가 LGTMjjwt 라이브러리가 빈 문자열이나 null 토큰에 대해
IllegalArgumentException을 던지므로, 이를 명시적으로 처리하는 것은 적절합니다. 상위 레이어(JwtAuthenticationProvider)에서BadCredentialsException으로 래핑되어JwtSecurityFilter에서 올바르게 처리됩니다.
69-72: 예외 처리 일관성 확인
extractMemberId()에서IllegalArgumentException처리가 기존JwtException처리와 일관되게 구현되었습니다.LinktripException(TOKEN_INVALID)는JwtAuthenticationProvider의 generic exception handler에서BadCredentialsException으로 래핑되어 적절히 처리됩니다.linktrip-input-http/src/main/kotlin/com/linktrip/input/http/security/JwtAuthenticationProvider.kt (1)
9-32: Spring Security AuthenticationProvider 구현 LGTM구현이 적절합니다:
supports()가PreAuthorizationToken만 처리하도록 제한하고,PreAuthorizationToken.getCredentials()가String을 반환하므로 Line 14의 캐스트는 타입 안전합니다.- 예외 처리가 방어적으로 구현되어
BadCredentialsException은 그대로 전파하고, 다른 예외는 래핑합니다.JwtSecurityFilter가AuthenticationException을 적절히 처리하여 전체 인증 흐름이 올바르게 동작합니다.linktrip-output-http/src/main/kotlin/com/linktrip/output/http/oauth/adapter/AppleOAuthAdapter.kt (1)
37-40:providerId(sub)null/blank 방어가 깔끔합니다.Line 37-40에서
claims.subject를 검증하고TOKEN_INVALID로 실패 처리한 점은 인증 안정성 측면에서 좋습니다.
...trip-output-http/src/main/kotlin/com/linktrip/output/http/oauth/adapter/AppleOAuthAdapter.kt
Outdated
Show resolved
Hide resolved
a6a2d43 to
4091f77
Compare
There was a problem hiding this comment.
Actionable comments posted: 4
♻️ Duplicate comments (1)
linktrip-output-http/src/main/kotlin/com/linktrip/output/http/oauth/adapter/AppleOAuthAdapter.kt (1)
52-57:⚠️ Potential issue | 🟠 MajorApple 공개키 조회 시 예외 처리가 누락되었습니다.
restClient.get()호출 중 발생하는RestClientException이 도메인 예외로 변환되지 않아 500 에러로 노출될 수 있습니다.🛡️ 제안 수정
- private fun fetchApplePublicKeys(): ApplePublicKeyResponse = - restClient.get() - .uri("/auth/keys") - .retrieve() - .body<ApplePublicKeyResponse>() - ?: throw LinktripException(ExceptionCode.OAUTH_PROVIDER_ERROR) + private fun fetchApplePublicKeys(): ApplePublicKeyResponse = + try { + restClient.get() + .uri("/auth/keys") + .retrieve() + .body<ApplePublicKeyResponse>() + ?: throw LinktripException(ExceptionCode.OAUTH_PROVIDER_ERROR) + } catch (e: LinktripException) { + throw e + } catch (e: Exception) { + logger.warn(e) { "Apple 공개키 조회 실패" } + throw LinktripException(ExceptionCode.OAUTH_PROVIDER_ERROR) + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@linktrip-output-http/src/main/kotlin/com/linktrip/output/http/oauth/adapter/AppleOAuthAdapter.kt` around lines 52 - 57, The fetchApplePublicKeys method in AppleOAuthAdapter calls restClient.get() but doesn't catch RestClientException; wrap the REST call in a try-catch inside fetchApplePublicKeys, catch RestClientException (and optionally other transport exceptions), and rethrow a LinktripException with ExceptionCode.OAUTH_PROVIDER_ERROR (include the caught exception as the cause or message) so transport errors are translated to your domain error; keep the original body<ApplePublicKeyResponse>() null check and throw as before if response is null.
🧹 Nitpick comments (3)
linktrip-input-http/src/main/kotlin/com/linktrip/input/http/security/JwtAuthenticationProvider.kt (1)
13-27:credentials캐스팅 시 안전한 처리를 권장합니다.Line 14에서
authentication.credentials as String캐스팅이 실패할 경우ClassCastException이 발생할 수 있습니다.supports()메서드로 타입이 제한되어 있지만, 방어적 코딩을 권장합니다.♻️ 제안 수정
override fun authenticate(authentication: Authentication): Authentication { - val jwt = authentication.credentials as String + val jwt = authentication.credentials as? String + ?: throw BadCredentialsException("JWT 토큰이 제공되지 않았습니다.") try {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@linktrip-input-http/src/main/kotlin/com/linktrip/input/http/security/JwtAuthenticationProvider.kt` around lines 13 - 27, The authenticate method in JwtAuthenticationProvider unsafely casts authentication.credentials to String which can throw ClassCastException; update JwtAuthenticationProvider.authenticate to defensively handle credentials by checking type (or using a safe cast) and throwing a BadCredentialsException if credentials is null or not a String before calling tokenProvider.validate/extractMemberId, then continue to return PostAuthorizationToken(memberId) as before and retain the existing exception handling for other errors.linktrip-input-http/src/main/kotlin/com/linktrip/input/http/security/JwtTokenProvider.kt (1)
23-23:toByteArray()호출 시 명시적 charset 지정을 권장합니다.
toByteArray()는 플랫폼 기본 charset을 사용합니다. 서로 다른 환경(로컬/서버)에서 charset이 다를 경우 키가 달라져 토큰 검증이 실패할 수 있습니다.♻️ 제안 diff
- private val secretKey: SecretKey = Keys.hmacShaKeyFor(secretKeyString.toByteArray()) + private val secretKey: SecretKey = Keys.hmacShaKeyFor(secretKeyString.toByteArray(Charsets.UTF_8))🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@linktrip-input-http/src/main/kotlin/com/linktrip/input/http/security/JwtTokenProvider.kt` at line 23, The secretKey initialization in JwtTokenProvider currently calls secretKeyString.toByteArray() which uses the platform default charset; change this to use an explicit charset (e.g., StandardCharsets.UTF_8) when converting secretKeyString to bytes so the key is consistent across environments—update the expression that constructs secretKey (the line assigning private val secretKey: SecretKey) to use the explicit charset.linktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/adapter/MemberAdapter.kt (1)
23-30: 동시쓰기 가정(단일 서버/추후 분산락)을 코드에 짧게 명시해 두는 것을 권장합니다.현재 구현은 중복 저장을 DB 제약 + 재조회로 흡수하고 있어 실용적입니다. 다만 운영 가정이 코드에 드러나지 않아, 다중 인스턴스 전환 시 회귀 위험이 있습니다. 간단한 주석(TODO)로 ShedLock 도입 시점을 남겨두면 좋겠습니다.
주석 추가 예시 diff
override fun save(member: Member): Member = try { memberJpaRepository.save(MemberEntity.from(member)).toDomain() } catch (ex: DuplicateKeyException) { + // NOTE: 현재는 단일 서버 운영 가정에서 DB 유니크 제약 + 재조회로 동시 저장을 완화. + // 멀티 인스턴스/복제 환경 전환 시 ShedLock 등 분산 락 도입 필요. logger.warn { "동시 요청으로 인한 중복 회원 저장 시도 감지: provider=${member.providerType}" } memberJpaRepository.findByProviderTypeAndProviderId(member.providerType, member.providerId) ?.toDomain() ?: throw ex } catch (ex: DataIntegrityViolationException) {Based on learnings: In
linktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/adapter/YouTubeVideoPersistenceAdapter.kt, concurrent-write safety is currently deferred under a single-server assumption, and ShedLock is planned when replication is introduced.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@linktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/adapter/MemberAdapter.kt` around lines 23 - 30, Add a short TODO comment in MemberAdapter.save (the try/catch that handles DataIntegrityViolationException and re-queries via memberJpaRepository.findByProviderTypeAndProviderId) stating the current single-server concurrency assumption and that a distributed lock (e.g., ShedLock) should be introduced when moving to multi-instance/distributed deployments; reference the DataIntegrityViolationException handling and the requery fallback so future maintainers know why this approach exists and where to plug in the distributed-lock solution.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@linktrip-application/src/main/kotlin/com/linktrip/application/domain/member/AuthService.kt`:
- Around line 38-40: Replace the non-existent ExceptionCode value in
AuthService#getOAuthPort: change the thrown LinktripException to use
ExceptionCode.BAD_REQUEST_OAUTH_PROVIDER instead of
ExceptionCode.UNSUPPORTED_OAUTH_PROVIDER so the code compiles and unsupported
OAuth providers map to the correct enum constant.
In
`@linktrip-output-http/src/main/kotlin/com/linktrip/output/http/oauth/adapter/GoogleOAuthAdapter.kt`:
- Line 32: Remove the Personally Identifiable Information from the debug log in
GoogleOAuthAdapter: the logger.debug call that includes response.id should be
changed to avoid logging the raw user identifier (response.id). Edit the
logger.debug in GoogleOAuthAdapter to either log a generic success message
(e.g., "Google user info retrieval succeeded") or log non-identifying metadata
(no full IDs or use a truncated/hashed value if absolutely required), and ensure
any reference to response.id is removed from logs.
- Around line 23-39: Wrap the external call in
GoogleOAuthAdapter.requestUserInfo (the restClient.get() call) in a try-catch
and convert client/network errors into the domain LinktripException with
ExceptionCode.OAUTH_PROVIDER_ERROR (same behavior as AppleOAuthAdapter);
specifically catch RestClientException / WebClientResponseException (or a broad
Exception if those types are not available) around the
restClient.get().retrieve().body<GoogleUserInfoResponse>() call and rethrow
LinktripException(ExceptionCode.OAUTH_PROVIDER_ERROR) so network/HTTP errors
don’t bubble as raw 500s.
In
`@linktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/adapter/MemberAdapter.kt`:
- Around line 26-29: The catch in MemberAdapter currently swallows all
DataIntegrityViolationException and uses `!!` on
memberJpaRepository.findByProviderTypeAndProviderId, which can mask
non-duplicate integrity errors and cause NPEs; update the save flow in
MemberAdapter to detect a duplicate-key violation only (inspect the exception
cause e.g. ConstraintViolationException or SQLState/error code and/or constraint
name "uk_member_provider"), return the existing member via
memberJpaRepository.findByProviderTypeAndProviderId(member.providerType,
member.providerId) if present, and for non-duplicate integrity violations
rethrow the original exception; also handle the null case from
findByProviderTypeAndProviderId safely (throw a clear IllegalStateException or
Domain-specific exception instead of using `!!`).
---
Duplicate comments:
In
`@linktrip-output-http/src/main/kotlin/com/linktrip/output/http/oauth/adapter/AppleOAuthAdapter.kt`:
- Around line 52-57: The fetchApplePublicKeys method in AppleOAuthAdapter calls
restClient.get() but doesn't catch RestClientException; wrap the REST call in a
try-catch inside fetchApplePublicKeys, catch RestClientException (and optionally
other transport exceptions), and rethrow a LinktripException with
ExceptionCode.OAUTH_PROVIDER_ERROR (include the caught exception as the cause or
message) so transport errors are translated to your domain error; keep the
original body<ApplePublicKeyResponse>() null check and throw as before if
response is null.
---
Nitpick comments:
In
`@linktrip-input-http/src/main/kotlin/com/linktrip/input/http/security/JwtAuthenticationProvider.kt`:
- Around line 13-27: The authenticate method in JwtAuthenticationProvider
unsafely casts authentication.credentials to String which can throw
ClassCastException; update JwtAuthenticationProvider.authenticate to defensively
handle credentials by checking type (or using a safe cast) and throwing a
BadCredentialsException if credentials is null or not a String before calling
tokenProvider.validate/extractMemberId, then continue to return
PostAuthorizationToken(memberId) as before and retain the existing exception
handling for other errors.
In
`@linktrip-input-http/src/main/kotlin/com/linktrip/input/http/security/JwtTokenProvider.kt`:
- Line 23: The secretKey initialization in JwtTokenProvider currently calls
secretKeyString.toByteArray() which uses the platform default charset; change
this to use an explicit charset (e.g., StandardCharsets.UTF_8) when converting
secretKeyString to bytes so the key is consistent across environments—update the
expression that constructs secretKey (the line assigning private val secretKey:
SecretKey) to use the explicit charset.
In
`@linktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/adapter/MemberAdapter.kt`:
- Around line 23-30: Add a short TODO comment in MemberAdapter.save (the
try/catch that handles DataIntegrityViolationException and re-queries via
memberJpaRepository.findByProviderTypeAndProviderId) stating the current
single-server concurrency assumption and that a distributed lock (e.g.,
ShedLock) should be introduced when moving to multi-instance/distributed
deployments; reference the DataIntegrityViolationException handling and the
requery fallback so future maintainers know why this approach exists and where
to plug in the distributed-lock solution.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 7bf7fdfc-25fa-4db2-a4b3-33a96e098042
📒 Files selected for processing (43)
gradle/libs.versions.tomllinktrip-application/src/main/kotlin/com/linktrip/application/domain/member/AuthService.ktlinktrip-application/src/main/kotlin/com/linktrip/application/domain/member/Member.ktlinktrip-application/src/main/kotlin/com/linktrip/application/domain/member/MemberService.ktlinktrip-application/src/main/kotlin/com/linktrip/application/domain/member/ProviderType.ktlinktrip-application/src/main/kotlin/com/linktrip/application/port/input/AuthUseCase.ktlinktrip-application/src/main/kotlin/com/linktrip/application/port/output/auth/OAuthInfo.ktlinktrip-application/src/main/kotlin/com/linktrip/application/port/output/auth/OAuthPort.ktlinktrip-application/src/main/kotlin/com/linktrip/application/port/output/persistence/MemberPort.ktlinktrip-application/src/test/kotlin/com/linktrip/application/domain/member/AuthServiceTest.ktlinktrip-bootstrap/src/main/resources/application.ymllinktrip-common/src/main/kotlin/com/linktrip/common/exception/ExceptionCode.ktlinktrip-input-http/src/main/kotlin/com/linktrip/input/http/config/FilterConfig.ktlinktrip-input-http/src/main/kotlin/com/linktrip/input/http/config/WebMvcConfig.ktlinktrip-input-http/src/main/kotlin/com/linktrip/input/http/controller/AuthController.ktlinktrip-input-http/src/main/kotlin/com/linktrip/input/http/controller/dto/request/AuthRequest.ktlinktrip-input-http/src/main/kotlin/com/linktrip/input/http/controller/dto/request/OAuthLoginRequest.ktlinktrip-input-http/src/main/kotlin/com/linktrip/input/http/controller/dto/response/OAuthLoginResponse.ktlinktrip-input-http/src/main/kotlin/com/linktrip/input/http/filter/FilterPaths.ktlinktrip-input-http/src/main/kotlin/com/linktrip/input/http/filter/MdcLoggingFilter.ktlinktrip-input-http/src/main/kotlin/com/linktrip/input/http/ratelimit/RateLimitInterceptor.ktlinktrip-input-http/src/main/kotlin/com/linktrip/input/http/security/AuthenticatedMember.ktlinktrip-input-http/src/main/kotlin/com/linktrip/input/http/security/AuthenticatedMemberArgumentResolver.ktlinktrip-input-http/src/main/kotlin/com/linktrip/input/http/security/JwtAuthenticationProvider.ktlinktrip-input-http/src/main/kotlin/com/linktrip/input/http/security/JwtSecurityFilter.ktlinktrip-input-http/src/main/kotlin/com/linktrip/input/http/security/JwtTokenProvider.ktlinktrip-input-http/src/main/kotlin/com/linktrip/input/http/security/PostAuthorizationToken.ktlinktrip-input-http/src/main/kotlin/com/linktrip/input/http/security/PreAuthorizationToken.ktlinktrip-input-http/src/main/kotlin/com/linktrip/input/http/security/SecurityConfig.ktlinktrip-input-http/src/main/kotlin/com/linktrip/input/http/security/SecurityPaths.ktlinktrip-input-http/src/main/kotlin/com/linktrip/input/http/security/handler/JwtAccessDeniedHandler.ktlinktrip-input-http/src/main/kotlin/com/linktrip/input/http/security/handler/JwtAuthenticationEntryPoint.ktlinktrip-input-http/src/test/kotlin/com/linktrip/input/http/ratelimit/RateLimitInterceptorTest.ktlinktrip-output-http/src/main/kotlin/com/linktrip/output/http/oauth/adapter/AppleOAuthAdapter.ktlinktrip-output-http/src/main/kotlin/com/linktrip/output/http/oauth/adapter/GoogleOAuthAdapter.ktlinktrip-output-http/src/main/kotlin/com/linktrip/output/http/oauth/adapter/KakaoOAuthAdapter.ktlinktrip-output-http/src/main/kotlin/com/linktrip/output/http/oauth/config/OAuthRestClientConfig.ktlinktrip-output-http/src/main/kotlin/com/linktrip/output/http/oauth/dto/ApplePublicKeyResponse.ktlinktrip-output-http/src/main/kotlin/com/linktrip/output/http/oauth/dto/GoogleUserInfoResponse.ktlinktrip-output-http/src/main/kotlin/com/linktrip/output/http/oauth/dto/KakaoUserInfoResponse.ktlinktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/adapter/MemberAdapter.ktlinktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/entity/MemberEntity.ktlinktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/repository/MemberJpaRepository.kt
💤 Files with no reviewable changes (3)
- linktrip-input-http/src/main/kotlin/com/linktrip/input/http/filter/FilterPaths.kt
- linktrip-input-http/src/main/kotlin/com/linktrip/input/http/controller/dto/request/AuthRequest.kt
- linktrip-input-http/src/main/kotlin/com/linktrip/input/http/config/FilterConfig.kt
✅ Files skipped from review due to trivial changes (11)
- linktrip-bootstrap/src/main/resources/application.yml
- linktrip-application/src/main/kotlin/com/linktrip/application/domain/member/ProviderType.kt
- linktrip-application/src/main/kotlin/com/linktrip/application/port/output/auth/OAuthPort.kt
- linktrip-application/src/main/kotlin/com/linktrip/application/port/output/auth/OAuthInfo.kt
- linktrip-input-http/src/test/kotlin/com/linktrip/input/http/ratelimit/RateLimitInterceptorTest.kt
- linktrip-input-http/src/main/kotlin/com/linktrip/input/http/security/SecurityPaths.kt
- linktrip-output-http/src/main/kotlin/com/linktrip/output/http/oauth/dto/GoogleUserInfoResponse.kt
- linktrip-input-http/src/main/kotlin/com/linktrip/input/http/security/handler/JwtAccessDeniedHandler.kt
- linktrip-input-http/src/main/kotlin/com/linktrip/input/http/security/PostAuthorizationToken.kt
- linktrip-output-http/src/main/kotlin/com/linktrip/output/http/oauth/dto/ApplePublicKeyResponse.kt
- linktrip-output-http/src/main/kotlin/com/linktrip/output/http/oauth/dto/KakaoUserInfoResponse.kt
🚧 Files skipped from review as they are similar to previous changes (16)
- linktrip-input-http/src/main/kotlin/com/linktrip/input/http/filter/MdcLoggingFilter.kt
- linktrip-input-http/src/main/kotlin/com/linktrip/input/http/security/AuthenticatedMember.kt
- linktrip-input-http/src/main/kotlin/com/linktrip/input/http/security/PreAuthorizationToken.kt
- linktrip-application/src/main/kotlin/com/linktrip/application/port/input/AuthUseCase.kt
- linktrip-input-http/src/main/kotlin/com/linktrip/input/http/controller/dto/response/OAuthLoginResponse.kt
- linktrip-common/src/main/kotlin/com/linktrip/common/exception/ExceptionCode.kt
- linktrip-input-http/src/main/kotlin/com/linktrip/input/http/config/WebMvcConfig.kt
- linktrip-input-http/src/main/kotlin/com/linktrip/input/http/controller/dto/request/OAuthLoginRequest.kt
- linktrip-application/src/main/kotlin/com/linktrip/application/domain/member/Member.kt
- linktrip-output-http/src/main/kotlin/com/linktrip/output/http/oauth/adapter/KakaoOAuthAdapter.kt
- linktrip-output-http/src/main/kotlin/com/linktrip/output/http/oauth/config/OAuthRestClientConfig.kt
- linktrip-application/src/main/kotlin/com/linktrip/application/domain/member/MemberService.kt
- linktrip-input-http/src/main/kotlin/com/linktrip/input/http/ratelimit/RateLimitInterceptor.kt
- linktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/entity/MemberEntity.kt
- linktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/repository/MemberJpaRepository.kt
- linktrip-application/src/test/kotlin/com/linktrip/application/domain/member/AuthServiceTest.kt
📜 Review details
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2026-03-18T01:07:53.575Z
Learnt from: toychip
Repo: Link-Trip/BackEnd PR: 24
File: linktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/adapter/YouTubeVideoPersistenceAdapter.kt:16-28
Timestamp: 2026-03-18T01:07:53.575Z
Learning: Apply the same deferred concurrent-write policy to all Kotlin persistence adapters in the MySQL adapter package: ensure writes are coordinated on a single server with ShedLock considered for the replication phase. For YouTubeVideoPersistenceAdapter.kt (and other adapters in this directory), verify that concurrent writes are serialized or properly guarded, document the policy in code comments, and ensure CI checks or deployment gating will catch any regression where multiple instances might attempt a conflicting write during replication.
Applied to files:
linktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/adapter/MemberAdapter.kt
📚 Learning: 2026-03-18T01:08:05.661Z
Learnt from: toychip
Repo: Link-Trip/BackEnd PR: 24
File: linktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/adapter/YouTubeVideoPersistenceAdapter.kt:16-28
Timestamp: 2026-03-18T01:08:05.661Z
Learning: In `linktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/adapter/YouTubeVideoPersistenceAdapter.kt`, the concurrent-write safety (duplicate videoId in batch, race condition on uk_youtube_video_video_id) is intentionally deferred. The maintainer (toychip) confirmed the system is currently single-server, so this is not a concern yet. When replication is introduced in the future, ShedLock will be used for distributed locking to address this. At that point, in-batch videoId deduplication should also be applied.
Applied to files:
linktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/adapter/MemberAdapter.kt
🪛 detekt (1.23.8)
linktrip-output-http/src/main/kotlin/com/linktrip/output/http/oauth/adapter/AppleOAuthAdapter.kt
[warning] 88-88: The caught exception is swallowed. The original exception could be lost.
(detekt.exceptions.SwallowedException)
🪛 GitHub Actions: CI - Pull request
gradle/libs.versions.toml
[warning] 1-1: Kapt warning: Language version 2.0+ is not supported; falling back to 1.9.
[warning] 1-1: Deprecated Gradle features were used, making the build incompatible with Gradle 9.0.
linktrip-application/src/main/kotlin/com/linktrip/application/domain/member/AuthService.kt
[error] 40-40: Kotlin compilation failed: Unresolved reference 'UNSUPPORTED_OAUTH_PROVIDER' at AuthService.kt:40:54.
🔇 Additional comments (15)
gradle/libs.versions.toml (1)
32-32: LGTM!의존성 추가가 적절합니다.
spring-boot-starter-security와 JWT 라이브러리들이 올바른 번들에 포함되었습니다.Also applies to: 92-92, 97-97
linktrip-input-http/src/main/kotlin/com/linktrip/input/http/security/handler/JwtAuthenticationEntryPoint.kt (1)
17-36: LGTM!
AuthenticationEntryPoint구현이 적절합니다. JSON 응답 형식과 문자 인코딩이 올바르게 설정되어 있습니다.linktrip-application/src/main/kotlin/com/linktrip/application/domain/member/AuthService.kt (1)
20-36: OAuth 로그인 로직이 적절하게 구현되었습니다.OAuth 포트 선택, 사용자 정보 조회, 회원 조회/생성, 토큰 발급의 흐름이 명확합니다. 외부 API 호출이 트랜잭션 밖에서 수행되는 설계가 좋습니다.
linktrip-input-http/src/main/kotlin/com/linktrip/input/http/controller/AuthController.kt (1)
18-40: LGTM!OAuth 로그인 엔드포인트가 적절하게 구현되었습니다. 신규 회원 시 201 Created, 기존 회원 시 200 OK 응답을 반환하는 RESTful 패턴이 좋습니다.
linktrip-input-http/src/main/kotlin/com/linktrip/input/http/security/AuthenticatedMemberArgumentResolver.kt (1)
14-31: LGTM!
HandlerMethodArgumentResolver구현이 적절합니다.SecurityContextHolder에서 인증 정보를 안전하게 추출하고 있습니다.linktrip-input-http/src/main/kotlin/com/linktrip/input/http/security/JwtAuthenticationProvider.kt (1)
30-31: LGTM!
supports()메서드가PreAuthorizationToken타입만 처리하도록 올바르게 제한하고 있습니다.linktrip-output-http/src/main/kotlin/com/linktrip/output/http/oauth/adapter/AppleOAuthAdapter.kt (1)
86-90: 예외 원인을 보존하되,LinktripException의 실제 생성자 시그니처에 맞춰 수정하세요.
LinktripException은cause파라미터를 지원하지 않습니다. 대신 원본 예외를 손실하지 않으려면 다음과 같이 처리할 수 있습니다:
- 첫 번째 생성자를 사용하여 예외 정보를 상세 메시지에 포함
- 예외를 로깅한 후 새 예외 발생
LinktripException에cause파라미터 지원 추가 (선택사항)♻️ 제안 수정
} catch (e: LinktripException) { throw e } catch (e: Exception) { + logger.debug(e) { "Apple ID 토큰 검증 실패" } - throw LinktripException(ExceptionCode.TOKEN_INVALID) + throw LinktripException(ExceptionCode.TOKEN_INVALID, "Token validation failed: ${e.message}") }> Likely an incorrect or invalid review comment.linktrip-input-http/src/main/kotlin/com/linktrip/input/http/security/SecurityConfig.kt (3)
31-31: 운영 환경에서 PUBLIC_ENDPOINTS 범위 검토가 필요합니다.이전 리뷰에서 언급된 것처럼,
SecurityPaths.PUBLIC_ENDPOINTS에 Swagger/Actuator/Test 경로가 포함되어 있다면 프로파일별로 분리하는 것이 안전합니다.
27-37:@Order애노테이션이 올바르게 적용되었습니다.이전 리뷰에서 지적된 다중
SecurityFilterChain우선순위 문제가 해결되었습니다.publicFilterChain이@Order(1)로,protectedFilterChain이@Order(2)로 설정되어 공개 엔드포인트가 먼저 평가됩니다.
39-59: 보호된 필터 체인 설정이 적절합니다.
- Stateless 세션 관리, CSRF 비활성화가 JWT 기반 API에 적합합니다.
JwtSecurityFilter가UsernamePasswordAuthenticationFilter앞에 올바르게 배치되었습니다.- 예외 처리 핸들러(
authenticationEntryPoint,accessDeniedHandler)가 적절히 구성되었습니다.linktrip-input-http/src/main/kotlin/com/linktrip/input/http/security/JwtSecurityFilter.kt (1)
12-54: LGTM! 인증 예외 처리 구조가 올바르게 구현되었습니다.이전 리뷰에서 지적된 문제가 해결되었습니다:
- 인증 시도만
try블록 내에서 수행되고,filterChain.doFilter는 외부에서 호출됩니다.AuthenticationException만 처리하고 다른 예외는 정상적으로 전파됩니다.- 토큰이 없는 경우 인증 없이 필터 체인을 계속 진행합니다.
linktrip-input-http/src/main/kotlin/com/linktrip/input/http/security/JwtTokenProvider.kt (2)
55-72: 예외 처리 및 로깅 구현이 적절합니다.만료된 토큰과 유효하지 않은 토큰을 구분하여 다른 예외 코드(
TOKEN_EXPIRED,TOKEN_INVALID)로 처리하고, 디버깅을 위한 경고 로그도 적절히 남기고 있습니다.
29-42: jjwt 버전 호환성이 확인되었습니다.프로젝트에서 사용 중인 jjwt 버전은 0.12.6으로, 코드에서 사용하는
subject(),expiration(),verifyWith(),parseSignedClaims()등의 메서드들이 모두 이 버전에서 지원하는 올바른 fluent API입니다. 필요한 모든 의존성(jjwt-api, jjwt-impl, jjwt-jackson)도 proper하게 구성되어 있어 추가 작업이 필요 없습니다.linktrip-application/src/main/kotlin/com/linktrip/application/port/output/persistence/MemberPort.kt (1)
7-10: OAuth 식별자 기반 포트 시그니처가 도메인 변경과 잘 정렬되었습니다.
providerType + providerId조회 계약이 명확하고 현재 PR 목적과 일관됩니다.linktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/adapter/MemberAdapter.kt (1)
18-21: ProviderType/ProviderId 조회 위임 구현은 깔끔합니다.리포지토리 조회 후 도메인 매핑 흐름이 단순하고 의도에 맞습니다.
linktrip-application/src/main/kotlin/com/linktrip/application/domain/member/AuthService.kt
Outdated
Show resolved
Hide resolved
| override fun requestUserInfo(accessToken: String): OAuthInfo { | ||
| val response = | ||
| restClient.get() | ||
| .uri("/oauth2/v1/userinfo") | ||
| .header("Authorization", "Bearer $accessToken") | ||
| .retrieve() | ||
| .body<GoogleUserInfoResponse>() | ||
| ?: throw LinktripException(ExceptionCode.OAUTH_PROVIDER_ERROR) | ||
|
|
||
| logger.debug { "Google 사용자 정보 조회 성공: id=${response.id}" } | ||
|
|
||
| return OAuthInfo( | ||
| providerType = ProviderType.GOOGLE, | ||
| providerId = response.id, | ||
| email = response.email, | ||
| ) | ||
| } |
There was a problem hiding this comment.
외부 API 호출 시 예외 처리가 누락되었습니다.
restClient.get() 호출 중 네트워크 오류나 RestClientException이 발생하면 전역 핸들러로 전파되어 500 에러로 처리됩니다. AppleOAuthAdapter와 동일하게 도메인 예외로 변환해야 합니다.
🛡️ 제안 수정
override fun requestUserInfo(accessToken: String): OAuthInfo {
- val response =
- restClient.get()
- .uri("/oauth2/v1/userinfo")
- .header("Authorization", "Bearer $accessToken")
- .retrieve()
- .body<GoogleUserInfoResponse>()
- ?: throw LinktripException(ExceptionCode.OAUTH_PROVIDER_ERROR)
+ val response =
+ try {
+ restClient.get()
+ .uri("/oauth2/v1/userinfo")
+ .header("Authorization", "Bearer $accessToken")
+ .retrieve()
+ .body<GoogleUserInfoResponse>()
+ ?: throw LinktripException(ExceptionCode.OAUTH_PROVIDER_ERROR)
+ } catch (e: LinktripException) {
+ throw e
+ } catch (e: Exception) {
+ logger.warn(e) { "Google 사용자 정보 조회 실패" }
+ throw LinktripException(ExceptionCode.OAUTH_PROVIDER_ERROR)
+ }
- logger.debug { "Google 사용자 정보 조회 성공: id=${response.id}" }
+ logger.debug { "Google 사용자 정보 조회 성공" }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@linktrip-output-http/src/main/kotlin/com/linktrip/output/http/oauth/adapter/GoogleOAuthAdapter.kt`
around lines 23 - 39, Wrap the external call in
GoogleOAuthAdapter.requestUserInfo (the restClient.get() call) in a try-catch
and convert client/network errors into the domain LinktripException with
ExceptionCode.OAUTH_PROVIDER_ERROR (same behavior as AppleOAuthAdapter);
specifically catch RestClientException / WebClientResponseException (or a broad
Exception if those types are not available) around the
restClient.get().retrieve().body<GoogleUserInfoResponse>() call and rethrow
LinktripException(ExceptionCode.OAUTH_PROVIDER_ERROR) so network/HTTP errors
don’t bubble as raw 500s.
| .body<GoogleUserInfoResponse>() | ||
| ?: throw LinktripException(ExceptionCode.OAUTH_PROVIDER_ERROR) | ||
|
|
||
| logger.debug { "Google 사용자 정보 조회 성공: id=${response.id}" } |
There was a problem hiding this comment.
디버그 로그에서 사용자 식별자를 제거하세요.
response.id는 사용자 식별 정보로, 로그 최소수집 원칙에 어긋납니다.
🔧 제안 수정
- logger.debug { "Google 사용자 정보 조회 성공: id=${response.id}" }
+ logger.debug { "Google 사용자 정보 조회 성공" }📝 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.
| logger.debug { "Google 사용자 정보 조회 성공: id=${response.id}" } | |
| logger.debug { "Google 사용자 정보 조회 성공" } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@linktrip-output-http/src/main/kotlin/com/linktrip/output/http/oauth/adapter/GoogleOAuthAdapter.kt`
at line 32, Remove the Personally Identifiable Information from the debug log in
GoogleOAuthAdapter: the logger.debug call that includes response.id should be
changed to avoid logging the raw user identifier (response.id). Edit the
logger.debug in GoogleOAuthAdapter to either log a generic success message
(e.g., "Google user info retrieval succeeded") or log non-identifying metadata
(no full IDs or use a truncated/hashed value if absolutely required), and ensure
any reference to response.id is removed from logs.
| } catch (_: DataIntegrityViolationException) { | ||
| logger.warn { "동시 요청으로 인한 중복 회원 저장 시도 감지: provider=${member.providerType}" } | ||
| memberJpaRepository.findByProviderTypeAndProviderId(member.providerType, member.providerId)!! | ||
| .toDomain() |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# 목적: Member 관련 제약조건과 예외 처리 범위가 "중복키 복구" 의도와 일치하는지 정적 점검
# 기대결과:
# 1) MemberEntity/DDL에 (provider_type, provider_id) 유니크 제약이 존재
# 2) 어댑터에서 중복키 예외와 일반 무결성 예외를 구분 처리하도록 개선 가능한 근거 확보
echo "=== MemberAdapter ==="
fd -i "MemberAdapter.kt" --exec nl -ba {} \;
echo "=== MemberEntity ==="
fd -i "MemberEntity.kt" --exec nl -ba {} \;
echo "=== Kotlin: 무결성/중복키 예외 사용처 ==="
rg -n --type kotlin -C2 'DataIntegrityViolationException|DuplicateKeyException'
echo "=== SQL/마이그레이션: member 유니크 제약 탐색 ==="
rg -n -C2 --iglob '*sql' --iglob '*migration*' --iglob '*ddl*' 'member|provider_type|provider_id|unique|constraint'Repository: Link-Trip/BackEnd
Length of output: 133
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== Finding MemberAdapter.kt ==="
fd -type f "MemberAdapter.kt" 2>/dev/null | head -20
echo "=== Finding MemberEntity.kt ==="
fd -type f "MemberEntity.kt" 2>/dev/null | head -20
echo "=== Finding migration/DDL files ==="
fd -type f -e "sql" -e "ddl" 2>/dev/null | grep -i member | head -20Repository: Link-Trip/BackEnd
Length of output: 93
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Search for MemberAdapter.kt content
echo "=== MemberAdapter Content ==="
find . -name "MemberAdapter.kt" -type f -exec cat -n {} \;
echo ""
echo "=== MemberEntity Content ==="
find . -name "MemberEntity.kt" -type f -exec cat -n {} \;Repository: Link-Trip/BackEnd
Length of output: 3191
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Search for DataIntegrityViolationException and DuplicateKeyException usage
echo "=== Exception handling patterns in Kotlin files ==="
rg -n -A3 -B3 "DataIntegrityViolationException|DuplicateKeyException" --type kotlin
echo ""
echo "=== Unique constraints in SQL/migration files ==="
rg -n -i -A2 -B2 "unique.*constraint|constraint.*unique|uk_.*member|provider_type" --type sql | head -50Repository: Link-Trip/BackEnd
Length of output: 2689
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== YouTubeVideoPersistenceAdapter (from learnings) ==="
find . -name "YouTubeVideoPersistenceAdapter.kt" -type f -exec cat -n {} \;
echo ""
echo "=== Search for DuplicateKeyException usage ==="
rg -n "DuplicateKeyException" --type kotlin
echo ""
echo "=== All exception handling in adapter classes ==="
rg -n "catch.*Exception" --type kotlin | grep -i adapterRepository: Link-Trip/BackEnd
Length of output: 3423
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Check Spring dependencies to see if DuplicateKeyException is available
echo "=== Spring dependencies in build files ==="
find . -name "build.gradle*" -o -name "pom.xml" -o -name "build.gradle.kts" | xargs cat 2>/dev/null | grep -i spring -A2 -B2 | head -60Repository: Link-Trip/BackEnd
Length of output: 1132
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Search for Spring's DAO exception hierarchy documentation in code
echo "=== Checking Spring Data/DAO exception imports ==="
rg -n "import org.springframework.dao" --type kotlin | head -20
echo ""
echo "=== All catch blocks in adapters ==="
rg -n "catch" --type kotlin linktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/adapter/
echo ""
echo "=== Check if there are any database migration files ==="
find . -type f \( -name "*.sql" -o -name "*migration*" \) | head -20Repository: Link-Trip/BackEnd
Length of output: 717
🌐 Web query:
Spring Framework DuplicateKeyException org.springframework.dao availability
💡 Result:
Yes, Spring Framework provides org.springframework.dao.DuplicateKeyException. It is a subclass of DataIntegrityViolationException, thrown when an insert or update violates a primary key or unique constraint. This class is available in the spring-tx module and documented in official Spring API docs up to version 7.0.6 (current as of 2026). Source code confirms its presence in the main branch. Note that in some cases (e.g., certain JDBC drivers like MySQL), a more general DataIntegrityViolationException may be thrown instead, but DuplicateKeyException exists and is part of the DAO exception hierarchy.
Citations:
- 1: https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/dao/DuplicateKeyException.html
- 2: https://github.com/spring-projects/spring-framework/blob/main/spring-tx/src/main/java/org/springframework/dao/DuplicateKeyException.java
- 3: https://github.com/spring-projects/spring-framework/blob/master/spring-tx/src/main/java/org/springframework/dao/DuplicateKeyException.java
- 4: https://docs.spring.io/spring-framework/docs/5.3.39/javadoc-api/org/springframework/dao/DataIntegrityViolationException.html
- 5: https://docs.enterprise.spring.io/spring-framework/docs/5.3.42/javadoc-api/org/springframework/dao/DuplicateKeyException.html
DataIntegrityViolationException 전체 캐치는 실제 무결성 오류를 숨길 수 있습니다.
Line 26에서 예외 범위를 넓게 잡고, Line 28에서 !!를 사용해 fallback이 비면 NPE로 바뀔 수 있습니다. MemberEntity에 정의된 uk_member_provider 유니크 제약(provider_type, provider_id)은 중복 저장 시나리오를 유발하지만, 현재 코드는 중복키뿐만 아니라 다른 모든 무결성 위반을 같은 방식으로 처리하므로 실제 오류를 은폐할 위험이 있습니다. 중복키 충돌만 복구하고, 그 외 무결성 예외는 원본 예외를 유지해 재던지도록 분리하며, null 케이스도 안전하게 처리하는 편이 필요합니다.
수정 제안 diff
import mu.KotlinLogging
+import org.springframework.dao.DuplicateKeyException
import org.springframework.dao.DataIntegrityViolationException
import org.springframework.stereotype.Component
@@
override fun save(member: Member): Member =
try {
memberJpaRepository.save(MemberEntity.from(member)).toDomain()
- } catch (_: DataIntegrityViolationException) {
+ } catch (ex: DuplicateKeyException) {
logger.warn { "동시 요청으로 인한 중복 회원 저장 시도 감지: provider=${member.providerType}" }
- memberJpaRepository.findByProviderTypeAndProviderId(member.providerType, member.providerId)!!
- .toDomain()
+ memberJpaRepository.findByProviderTypeAndProviderId(member.providerType, member.providerId)
+ ?.toDomain()
+ ?: throw ex
+ } catch (ex: DataIntegrityViolationException) {
+ throw ex
}
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@linktrip-output-persistence/mysql/src/main/kotlin/com/linktrip/output/persistence/mysql/adapter/MemberAdapter.kt`
around lines 26 - 29, The catch in MemberAdapter currently swallows all
DataIntegrityViolationException and uses `!!` on
memberJpaRepository.findByProviderTypeAndProviderId, which can mask
non-duplicate integrity errors and cause NPEs; update the save flow in
MemberAdapter to detect a duplicate-key violation only (inspect the exception
cause e.g. ConstraintViolationException or SQLState/error code and/or constraint
name "uk_member_provider"), return the existing member via
memberJpaRepository.findByProviderTypeAndProviderId(member.providerType,
member.providerId) if present, and for non-duplicate integrity violations
rethrow the original exception; also handle the null case from
findByProviderTypeAndProviderId safely (throw a clear IllegalStateException or
Domain-specific exception instead of using `!!`).
599bf85 to
a37f999
Compare
관련 이슈
변경 내용
SecurityFilterChain기반 인증 구조로 전환Member도메인 모델 OAuth 필드로 변경 (serialNumber→providerType,providerId,email)AuthenticationEntryPoint,AccessDeniedHandler를 통한 인증/인가 에러 처리 통합@Transactional(AuthService→MemberService분리)JwtAuthenticationFilter,FilterPaths)체크리스트
Summary by CodeRabbit
New Features
Changes