[WTH-157] global 도메인 마이그레이션#13
Conversation
|
No actionable comments were generated in the recent review. 🎉 📝 WalkthroughWalkthroughJava 기반 인증·예외·OAuth 구현을 Kotlin으로 마이그레이션하고 JWT 토큰 관리(포트/어댑터), Spring Security 설정, Swagger 설정 및 관련 테스트를 대규모로 재구성/교체했습니다. 다수의 Java 파일이 삭제되고 대응하는 Kotlin 파일이 추가되었습니다. Changes
Sequence Diagram(s)sequenceDiagram
participant Client
participant Filter as JwtAuthenticationProcessingFilter
participant Extractor as JwtTokenExtractor
participant Provider as JwtTokenProvider
participant SecContext as SecurityContext
participant UseCase as JwtManageUseCase
participant Store as RefreshTokenStorePort
Note over Client,UseCase: 토큰 발급
Client->>UseCase: create(userId, email, role)
UseCase->>Provider: createAccessToken(id,email,role)
Provider-->>UseCase: accessToken
UseCase->>Provider: createRefreshToken(id)
Provider-->>UseCase: refreshToken
UseCase->>Store: save(userId, refreshToken, role, email)
UseCase-->>Client: JwtDto(accessToken, refreshToken)
Note over Client,Filter: 요청 인증 흐름
Client->>Filter: doFilterInternal(request)
Filter->>Extractor: extractAccessToken(request)
Extractor-->>Filter: accessToken
Filter->>Provider: validate(accessToken)
Provider-->>Filter: valid
Filter->>Provider: parseClaims(accessToken)
Provider-->>Filter: Claims(id,email,role)
Filter->>SecContext: setAuthentication(AuthenticatedUser(id,email,role))
SecContext-->>Filter: auth set
Note over Client,UseCase: 리프레시(재발급)
Client->>UseCase: reIssueToken(oldRefreshToken)
UseCase->>Extractor: extractId(oldRefreshToken)
Extractor-->>UseCase: userId
UseCase->>Store: validateRefreshToken(userId, oldRefreshToken)
Store-->>UseCase: validated
UseCase->>Store: getRole(userId)
Store-->>UseCase: role
UseCase->>Store: getEmail(userId)
Store-->>UseCase: email
UseCase->>Provider: createAccessToken(id,email,role)
UseCase->>Provider: createRefreshToken(id)
UseCase->>Store: save(userId,newRefreshToken,role,email)
UseCase-->>Client: JwtDto(newAccessToken,newRefreshToken)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60분 Possibly related PRs
Suggested reviewers
Poem
🚥 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)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (13)
src/main/kotlin/com/weeth/global/common/controller/ExceptionDocController.kt (1)
59-63: TODO(SAS 예외) 릴리스 전 정리 필요Line 59의 TODO가 제품 요구사항이라면 릴리스 전에 이슈로 트래킹하거나 이번 PR에서 마무리하는 편이 안전합니다.
원하시면 SAS 예외 코드 추가 범위를 정리해 구현안/이슈 템플릿을 만들어 드릴까요?
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/kotlin/com/weeth/global/common/controller/ExceptionDocController.kt` around lines 59 - 63, The TODO comment about adding SAS-related exceptions must be resolved before release: either create a tracked issue and reference it from the TODO or implement the SAS error codes now by adding the appropriate error enum/class and annotating the controller endpoint; update ExceptionDocController.authErrorCodes to include the SAS error code class (or replace the TODO with an issue link), and ensure the new SAS error enum is included in `@ApiErrorCodeExample` alongside JwtErrorCode so the /auth Operation exposes those codes.src/main/kotlin/com/weeth/global/auth/kakao/KakaoAuthService.kt (1)
29-37: Kakao API 호출 실패 시 에러 처리 고려
RestClient.retrieve()는 4xx/5xx 응답 시 예외를 발생시키지만, Kakao API 고유의 에러 응답을 적절히 처리하지 않습니다. 인증 실패, 토큰 만료 등의 케이스에서 더 명확한 예외 변환을 위해.onStatus()핸들러 추가를 고려해 보세요.♻️ 에러 처리 추가 예시
return requireNotNull( restClient .post() .uri(kakaoProperties.tokenUri) - .body(body) .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .body(body) .retrieve() + .onStatus({ it.isError }) { _, response -> + throw IllegalStateException("Kakao token request failed: ${response.statusCode}") + } .body<KakaoTokenResponse>(), )🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/kotlin/com/weeth/global/auth/kakao/KakaoAuthService.kt` around lines 29 - 37, The RestClient call in KakaoAuthService (the chain starting with restClient.post().uri(kakaoProperties.tokenUri) and returning body<KakaoTokenResponse>()) doesn't handle Kakao-specific error payloads; add a .onStatus(...) handler on the ResponseSpec returned by .retrieve() to map 4xx/5xx responses into a meaningful exception (e.g., KakaoAuthException) by reading the error body (into a DTO or String) and throwing that exception, so authentication failures / token errors from Kakao are translated into clear, actionable exceptions rather than generic WebClient errors.src/main/java/com/weeth/domain/user/application/usecase/UserUseCaseImpl.java (2)
178-186: JwtDto 재생성 불필요 — 직접 반환으로 단순화Line 185에서 새로운
JwtDto를 생성하는 것은 불필요합니다.JwtDto는accessToken과refreshToken2개 필드만 포함하므로,reIssueToken()반환값을 그대로 반환하면 됩니다.권장 변경
- JwtDto token = jwtManageUseCase.reIssueToken(requestToken); - log.info("RefreshToken 발급 완료: {}", token); - return new JwtDto(token.getAccessToken(), token.getRefreshToken()); + JwtDto token = jwtManageUseCase.reIssueToken(requestToken); + log.info("RefreshToken 발급 완료: {}", token); + return token;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/com/weeth/domain/user/application/usecase/UserUseCaseImpl.java` around lines 178 - 186, The refresh method creates a redundant new JwtDto from jwtManageUseCase.reIssueToken(requestToken); instead of returning the existing object; modify the refresh(String refreshToken) implementation so it strips the BEARER, calls jwtManageUseCase.reIssueToken(requestToken) and returns that JwtDto directly (remove the unnecessary new JwtDto(...) construction and return the token from reIssueToken as-is), keeping the log.info("RefreshToken 발급 완료: {}", token) and using the same variable names (refresh, requestToken, token) to locate the change.
298-313: 신규 헬퍼 메서드는 Kotlin으로 작성하거나 추후 UseCase 마이그레이션 시 함께 이관 권장isDevEnvironment() 헬퍼가 2026-02-20에 새로 추가된 코드이며, 가이드라인상 신규 코드는 Kotlin으로 작성해야 합니다. 현재 UserUseCaseImpl은 Java로만 존재하므로, 다음 중 하나를 고려해주세요:
- 헬퍼만 별도의 Kotlin 유틸리티로 작성, 또는
- UseCase 전체 마이그레이션 시 함께 이관
참고로 Kotlin에서는
environment.activeProfiles.any { it in setOf("dev", "local") }같은 더 간결한 표현이 가능합니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/com/weeth/domain/user/application/usecase/UserUseCaseImpl.java` around lines 298 - 313, The new Java helper method isDevEnvironment in UserUseCaseImpl was added but new code should be written in Kotlin; either move this helper into a small Kotlin utility (e.g., create a Kotlin function isDevEnvironment(environment: Environment): Boolean that checks activeProfiles for "dev" or "local") and call that from UserUseCaseImpl, or postpone and plan a full UseCase migration to Kotlin and remove the Java helper; update references to use the Kotlin utility (or mark for migration) and ensure behavior matches the current check for "dev" or "local".src/test/kotlin/com/weeth/global/auth/jwt/filter/JwtAuthenticationProcessingFilterTest.kt (1)
47-54: Kotest matchers를 더 관용적으로 사용할 수 있습니다.null 체크와 타입 체크에 Kotest의 내장 matcher를 사용하면 더 읽기 쉬운 테스트가 됩니다.
♻️ 더 관용적인 Kotest assertions 사용 제안
- (authentication == null) shouldBe false - (authentication.principal is AuthenticatedUser) shouldBe true + authentication.shouldNotBeNull() + authentication.principal.shouldBeInstanceOf<AuthenticatedUser>()
shouldNotBeNull()및shouldBeInstanceOf<T>()를 사용하려면 import 추가 필요:import io.kotest.matchers.nulls.shouldNotBeNull import io.kotest.matchers.types.shouldBeInstanceOf🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/test/kotlin/com/weeth/global/auth/jwt/filter/JwtAuthenticationProcessingFilterTest.kt` around lines 47 - 54, Replace the raw null/type checks in the test that inspects SecurityContextHolder.getContext().authentication by using Kotest matchers: call SecurityContextHolder.getContext().authentication.shouldNotBeNull(), then use shouldBeInstanceOf<AuthenticatedUser>() on that authentication.principal (or on the variable you cast) and assign the result to principal; keep subsequent assertions (principal.id, principal.email, principal.role.name, and authentication.authorities check) unchanged, and add the imports io.kotest.matchers.nulls.shouldNotBeNull and io.kotest.matchers.types.shouldBeInstanceOf to the test file.src/main/kotlin/com/weeth/global/config/RedisConfig.kt (1)
33-39: RedisConnectionFactory가 중복 생성될 수 있습니다.
redisTemplate()내에서redisConnectionFactory()를 직접 호출하면 새로운LettuceConnectionFactory인스턴스가 생성됩니다. Spring이 빈을 프록시하지만, 명시적으로 의존성 주입을 사용하는 것이 더 명확합니다.♻️ 의존성 주입 방식으로 변경 제안
`@Bean` - fun redisTemplate(): RedisTemplate<String, String> = + fun redisTemplate(redisConnectionFactory: RedisConnectionFactory): RedisTemplate<String, String> = RedisTemplate<String, String>().apply { keySerializer = StringRedisSerializer() valueSerializer = StringRedisSerializer() - connectionFactory = redisConnectionFactory() + connectionFactory = redisConnectionFactory }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/kotlin/com/weeth/global/config/RedisConfig.kt` around lines 33 - 39, redisTemplate() currently calls redisConnectionFactory() directly which can create a duplicate LettuceConnectionFactory; change redisTemplate to accept a RedisConnectionFactory (or LettuceConnectionFactory) parameter and use that injected instance for connectionFactory instead of calling redisConnectionFactory() so Spring provides the single shared bean; update the method signature and remove the direct factory call, keeping keySerializer/valueSerializer assignments on RedisTemplate<String,String>.src/test/kotlin/com/weeth/global/auth/jwt/application/service/JwtTokenExtractorTest.kt (2)
43-52: extractRefreshToken 성공 케이스 테스트 추가를 고려하세요.현재 예외 케이스만 테스트하고 있습니다. 정상적으로 refresh token을 추출하는 성공 케이스도 추가하면 좋습니다.
♻️ 성공 케이스 테스트 예시
it("헤더에서 refresh token을 추출한다") { val request = mockk<HttpServletRequest>() every { request.getHeader("Refresh") } returns "refresh-token" val token = jwtTokenExtractor.extractRefreshToken(request) token shouldBe "refresh-token" }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/test/kotlin/com/weeth/global/auth/jwt/application/service/JwtTokenExtractorTest.kt` around lines 43 - 52, Add a success-case unit test in JwtTokenExtractorTest for the extractRefreshToken method: create a mock HttpServletRequest, stub getHeader("Refresh") to return a sample token (e.g., "refresh-token"), call jwtTokenExtractor.extractRefreshToken(request), and assert the returned value equals that sample token; place this alongside the existing exception test in the describe("extractRefreshToken") block using the same test style.
32-41: extractAccessToken의 엣지 케이스 테스트 추가를 고려하세요.Bearer prefix가 없거나 헤더가 null인 경우에 대한 테스트가 있으면 커버리지가 향상됩니다.
♻️ 추가 테스트 케이스 예시
it("헤더가 없으면 null을 반환한다") { val request = mockk<HttpServletRequest>() every { request.getHeader("Auth") } returns null val token = jwtTokenExtractor.extractAccessToken(request) token shouldBe null }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/test/kotlin/com/weeth/global/auth/jwt/application/service/JwtTokenExtractorTest.kt` around lines 32 - 41, Add edge-case unit tests to JwtTokenExtractorTest for JwtTokenExtractor.extractAccessToken: add one test where HttpServletRequest.getHeader("Auth") returns null and assert the method returns null, and another where the header exists but does not start with "Bearer " (e.g., returns "access-token" or "Bearer"), asserting null (or expected behavior) to cover missing-prefix behavior; use mockk to stub HttpServletRequest and keep test names consistent with existing style.src/main/kotlin/com/weeth/global/auth/jwt/filter/JwtAuthenticationProcessingFilter.kt (1)
22-35: 예상치 못한 RuntimeException은 삼키지 않는 편이 안전합니다.현재는 모든 RuntimeException을 info 로그 후 통과시켜 내부 오류가 숨겨질 수 있습니다. 토큰 관련 예외만 처리하고 나머지는 전파(또는 공통 예외 처리기로 위임)하는 쪽을 권장합니다.
♻️ 예외 처리 범위 축소 제안
+import com.weeth.global.auth.jwt.application.exception.InvalidTokenException @@ } catch (e: TokenNotFoundException) { log.debug("Token not found: {}", e.message) - } catch (e: RuntimeException) { - log.info("error token: {}", e.message) + } catch (e: InvalidTokenException) { + log.info("invalid token: {}", e.message) }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/kotlin/com/weeth/global/auth/jwt/filter/JwtAuthenticationProcessingFilter.kt` around lines 22 - 35, The current doFilterInternal in JwtAuthenticationProcessingFilter swallows all RuntimeException; change it to only catch token-related exceptions (e.g., TokenNotFoundException and whatever specific token validation exception your jwtTokenProvider.validate can throw), log those, and continue the filter chain, but allow any other unexpected exceptions to propagate (or be handled by the global exception handler) instead of catching RuntimeException. Specifically update doFilterInternal to catch TokenNotFoundException (and the token-validation-specific exception type), call saveAuthentication on success, ensure filterChain.doFilter(request, response) is always invoked for normal flow, and remove or rethrow the broad catch (RuntimeException) so non-token errors surface.src/main/kotlin/com/weeth/global/auth/apple/AppleAuthService.kt (4)
82-85: JWT 파트 수 검증을 강화하세요.JWT는
header.payload.signature형식으로 정확히 3개의 파트로 구성됩니다. 현재< 2검사는 header와 payload 접근에는 충분하지만, 서명이 없는 불완전한 토큰도 통과시킬 수 있습니다.♻️ 제안된 수정
val tokenParts = idToken.split(".") - if (tokenParts.size < 2) { + if (tokenParts.size != 3) { throw AppleAuthenticationException() }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/kotlin/com/weeth/global/auth/apple/AppleAuthService.kt` around lines 82 - 85, The current idToken split only checks for tokenParts.size < 2 and can allow malformed JWTs without a signature; update the validation in AppleAuthService.kt where idToken is split into tokenParts to require exactly 3 parts (header, payload, signature) and throw AppleAuthenticationException when tokenParts.size != 3 (include a clear message or error code if needed) so incomplete or signature-less tokens are rejected; keep references to the existing tokenParts variable and AppleAuthenticationException class when implementing the change.
248-253: 예외 처리 시 원인 예외를 유지하는 것을 권장합니다.
parseJson에서 모든 예외를 잡아 새AppleAuthenticationException으로 던지면 디버깅에 유용한 스택 트레이스가 손실됩니다. 예외 체인을 유지하면 로깅 시 근본 원인 파악에 도움이 됩니다.♻️ 예외 체인 유지 제안
AppleAuthenticationException이 원인 예외를 받을 수 있도록 수정되어 있다면:private fun parseJson(json: String): ObjectNode = try { objectMapper.readTree(json) as? ObjectNode ?: throw AppleAuthenticationException() } catch (e: Exception) { - throw AppleAuthenticationException() + throw AppleAuthenticationException(e) }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/kotlin/com/weeth/global/auth/apple/AppleAuthService.kt` around lines 248 - 253, The parseJson function currently catches all exceptions and rethrows a new AppleAuthenticationException, losing the original cause; update parseJson to preserve the exception chain by passing the caught exception (e) as the cause when constructing AppleAuthenticationException (so the original stack trace from objectMapper.readTree(json) is retained) and remove swallowing behavior so callers can inspect/log the root cause.
227-246: 동시성 경쟁 조건(Race Condition) 가능성이 있습니다.
@Volatile은 가시성만 보장하고 원자성은 보장하지 않습니다. 캐시가 만료되거나 null일 때 여러 스레드가 동시에 Apple 서버로 요청을 보낼 수 있습니다. 현재 트래픽 수준에서는 큰 문제가 아닐 수 있지만, 고려해 볼 만한 사항입니다.♻️ 동기화를 사용한 제안
+ private val cacheLock = Any() + private fun getApplePublicKeys(): ApplePublicKeys { val now = Instant.now(clock) cached?.let { if (now.isBefore(it.expiresAt)) { return it.keys } } - val fetched = - requireNotNull( - restClient - .get() - .uri(appleProperties.keysUri) - .retrieve() - .body<ApplePublicKeys>(), - ) - - cached = CachedKeys(fetched, now.plus(publicKeysTtl)) - return fetched + synchronized(cacheLock) { + // Double-check after acquiring lock + cached?.let { + if (now.isBefore(it.expiresAt)) { + return it.keys + } + } + + val fetched = + restClient + .get() + .uri(appleProperties.keysUri) + .retrieve() + .body<ApplePublicKeys>() + ?: throw AppleAuthenticationException() + + cached = CachedKeys(fetched, now.plus(publicKeysTtl)) + return fetched + } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/kotlin/com/weeth/global/auth/apple/AppleAuthService.kt` around lines 227 - 246, getApplePublicKeys currently reads and updates the shared cached field without synchronization, allowing multiple threads to fetch keys concurrently; fix by introducing a lock (e.g., a private val lock = Any() or Kotlin Mutex) in the AppleAuthService and apply double-checked locking inside getApplePublicKeys: first check cached/staleness, if stale/null enter synchronized(lock) (or acquire mutex) then re-check cached, and only then call restClient to fetch and update cached with CachedKeys(now.plus(publicKeysTtl)); ensure all reads/writes to cached happen under the same synchronization to guarantee atomicity.
69-77:requireNotNull대신 도메인 예외 사용을 권장합니다.
requireNotNull은 null일 경우IllegalArgumentException을 발생시키는데, 이는 다른 메서드에서 사용하는AppleAuthenticationException과 일관성이 맞지 않습니다.♻️ 제안된 수정
- return requireNotNull( - restClient - .post() - .uri(appleProperties.tokenUri) - .body(body) - .contentType(MediaType.APPLICATION_FORM_URLENCODED) - .retrieve() - .body<AppleTokenResponse>(), - ) + return restClient + .post() + .uri(appleProperties.tokenUri) + .body(body) + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .retrieve() + .body<AppleTokenResponse>() + ?: throw AppleAuthenticationException()🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/kotlin/com/weeth/global/auth/apple/AppleAuthService.kt` around lines 69 - 77, Replace the use of requireNotNull around the WebClient call in AppleAuthService (the chain ending with .body<AppleTokenResponse>()) with an explicit null check that throws the domain exception AppleAuthenticationException instead of IllegalArgumentException; for example, assign the result to a variable and if it's null throw new AppleAuthenticationException with a clear message (including context like "failed to fetch Apple token") so callers remain consistent with other Apple auth failures.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/main/kotlin/com/weeth/global/auth/authentication/ErrorMessage.kt`:
- Around line 3-9: The ErrorMessage enum must implement ErrorCodeInterface and
replace HTTP status numeric codes with project error codes in the 2xxx range (or
reuse the existing JwtErrorCode constants) to follow the project's error-code
pattern; update the enum declaration ErrorMessage to implement
ErrorCodeInterface, change members (e.g., UNAUTHORIZED ->
ANONYMOUS_AUTHENTICATION with code 2903 and FORBIDDEN -> a suitable 2xxx code or
map to an existing JwtErrorCode entry), and ensure the enum exposes the
interface-required properties/methods so it conforms to the same shape as
JwtErrorCode.
In `@src/main/kotlin/com/weeth/global/auth/kakao/dto/KakaoAccount.kt`:
- Around line 10-11: The KakaoAccount DTO's email property must be nullable
because Kakao may omit email when user doesn't consent; change the declaration
in KakaoAccount from "val email: String" to "val email: String?" while keeping
the `@field`:JsonProperty("email") annotation, and review any usages of
KakaoAccount.email (and related flags isEmailValid/isEmailVerified) to handle
null safely (e.g., null checks or safe-call) so the code no longer assumes email
is always present.
In `@src/main/kotlin/com/weeth/global/common/converter/JsonConverter.kt`:
- Around line 7-10: Remove the unnecessary `@Converter` annotation from the
abstract class JsonConverter<T>; locate the class declaration for
JsonConverter<T> and delete the `@Converter` above it so only concrete converters
(e.g., BoardConfigConverter) remain annotated and the entity field (config)
continues to use `@Convert`(converter = BoardConfigConverter::class) as before.
In `@src/main/kotlin/com/weeth/global/common/exception/CommonExceptionHandler.kt`:
- Around line 74-91: The handler function handle(Exception) currently uses
ex.message directly in the client response; change it to return a generic
message (e.g., INPUT_FORMAT_ERROR_MESSAGE for format errors or a fixed generic
string like "서버에서 오류가 발생했습니다.") when building CommonResponse.createFailure,
while still logging the detailed ex.message and stacktrace via log.warn (use the
existing LOG_FORMAT and the first log.warn that logs the exception). Keep the
statusCode determination logic (using ErrorResponse) and update only the
response body creation in handle and the logging lines so detailed internals
remain in logs but the response to clients contains a non-sensitive, generalized
message.
---
Nitpick comments:
In
`@src/main/java/com/weeth/domain/user/application/usecase/UserUseCaseImpl.java`:
- Around line 178-186: The refresh method creates a redundant new JwtDto from
jwtManageUseCase.reIssueToken(requestToken); instead of returning the existing
object; modify the refresh(String refreshToken) implementation so it strips the
BEARER, calls jwtManageUseCase.reIssueToken(requestToken) and returns that
JwtDto directly (remove the unnecessary new JwtDto(...) construction and return
the token from reIssueToken as-is), keeping the log.info("RefreshToken 발급 완료:
{}", token) and using the same variable names (refresh, requestToken, token) to
locate the change.
- Around line 298-313: The new Java helper method isDevEnvironment in
UserUseCaseImpl was added but new code should be written in Kotlin; either move
this helper into a small Kotlin utility (e.g., create a Kotlin function
isDevEnvironment(environment: Environment): Boolean that checks activeProfiles
for "dev" or "local") and call that from UserUseCaseImpl, or postpone and plan a
full UseCase migration to Kotlin and remove the Java helper; update references
to use the Kotlin utility (or mark for migration) and ensure behavior matches
the current check for "dev" or "local".
In `@src/main/kotlin/com/weeth/global/auth/apple/AppleAuthService.kt`:
- Around line 82-85: The current idToken split only checks for tokenParts.size <
2 and can allow malformed JWTs without a signature; update the validation in
AppleAuthService.kt where idToken is split into tokenParts to require exactly 3
parts (header, payload, signature) and throw AppleAuthenticationException when
tokenParts.size != 3 (include a clear message or error code if needed) so
incomplete or signature-less tokens are rejected; keep references to the
existing tokenParts variable and AppleAuthenticationException class when
implementing the change.
- Around line 248-253: The parseJson function currently catches all exceptions
and rethrows a new AppleAuthenticationException, losing the original cause;
update parseJson to preserve the exception chain by passing the caught exception
(e) as the cause when constructing AppleAuthenticationException (so the original
stack trace from objectMapper.readTree(json) is retained) and remove swallowing
behavior so callers can inspect/log the root cause.
- Around line 227-246: getApplePublicKeys currently reads and updates the shared
cached field without synchronization, allowing multiple threads to fetch keys
concurrently; fix by introducing a lock (e.g., a private val lock = Any() or
Kotlin Mutex) in the AppleAuthService and apply double-checked locking inside
getApplePublicKeys: first check cached/staleness, if stale/null enter
synchronized(lock) (or acquire mutex) then re-check cached, and only then call
restClient to fetch and update cached with CachedKeys(now.plus(publicKeysTtl));
ensure all reads/writes to cached happen under the same synchronization to
guarantee atomicity.
- Around line 69-77: Replace the use of requireNotNull around the WebClient call
in AppleAuthService (the chain ending with .body<AppleTokenResponse>()) with an
explicit null check that throws the domain exception
AppleAuthenticationException instead of IllegalArgumentException; for example,
assign the result to a variable and if it's null throw new
AppleAuthenticationException with a clear message (including context like
"failed to fetch Apple token") so callers remain consistent with other Apple
auth failures.
In
`@src/main/kotlin/com/weeth/global/auth/jwt/filter/JwtAuthenticationProcessingFilter.kt`:
- Around line 22-35: The current doFilterInternal in
JwtAuthenticationProcessingFilter swallows all RuntimeException; change it to
only catch token-related exceptions (e.g., TokenNotFoundException and whatever
specific token validation exception your jwtTokenProvider.validate can throw),
log those, and continue the filter chain, but allow any other unexpected
exceptions to propagate (or be handled by the global exception handler) instead
of catching RuntimeException. Specifically update doFilterInternal to catch
TokenNotFoundException (and the token-validation-specific exception type), call
saveAuthentication on success, ensure filterChain.doFilter(request, response) is
always invoked for normal flow, and remove or rethrow the broad catch
(RuntimeException) so non-token errors surface.
In `@src/main/kotlin/com/weeth/global/auth/kakao/KakaoAuthService.kt`:
- Around line 29-37: The RestClient call in KakaoAuthService (the chain starting
with restClient.post().uri(kakaoProperties.tokenUri) and returning
body<KakaoTokenResponse>()) doesn't handle Kakao-specific error payloads; add a
.onStatus(...) handler on the ResponseSpec returned by .retrieve() to map
4xx/5xx responses into a meaningful exception (e.g., KakaoAuthException) by
reading the error body (into a DTO or String) and throwing that exception, so
authentication failures / token errors from Kakao are translated into clear,
actionable exceptions rather than generic WebClient errors.
In
`@src/main/kotlin/com/weeth/global/common/controller/ExceptionDocController.kt`:
- Around line 59-63: The TODO comment about adding SAS-related exceptions must
be resolved before release: either create a tracked issue and reference it from
the TODO or implement the SAS error codes now by adding the appropriate error
enum/class and annotating the controller endpoint; update
ExceptionDocController.authErrorCodes to include the SAS error code class (or
replace the TODO with an issue link), and ensure the new SAS error enum is
included in `@ApiErrorCodeExample` alongside JwtErrorCode so the /auth Operation
exposes those codes.
In `@src/main/kotlin/com/weeth/global/config/RedisConfig.kt`:
- Around line 33-39: redisTemplate() currently calls redisConnectionFactory()
directly which can create a duplicate LettuceConnectionFactory; change
redisTemplate to accept a RedisConnectionFactory (or LettuceConnectionFactory)
parameter and use that injected instance for connectionFactory instead of
calling redisConnectionFactory() so Spring provides the single shared bean;
update the method signature and remove the direct factory call, keeping
keySerializer/valueSerializer assignments on RedisTemplate<String,String>.
In
`@src/test/kotlin/com/weeth/global/auth/jwt/application/service/JwtTokenExtractorTest.kt`:
- Around line 43-52: Add a success-case unit test in JwtTokenExtractorTest for
the extractRefreshToken method: create a mock HttpServletRequest, stub
getHeader("Refresh") to return a sample token (e.g., "refresh-token"), call
jwtTokenExtractor.extractRefreshToken(request), and assert the returned value
equals that sample token; place this alongside the existing exception test in
the describe("extractRefreshToken") block using the same test style.
- Around line 32-41: Add edge-case unit tests to JwtTokenExtractorTest for
JwtTokenExtractor.extractAccessToken: add one test where
HttpServletRequest.getHeader("Auth") returns null and assert the method returns
null, and another where the header exists but does not start with "Bearer "
(e.g., returns "access-token" or "Bearer"), asserting null (or expected
behavior) to cover missing-prefix behavior; use mockk to stub HttpServletRequest
and keep test names consistent with existing style.
In
`@src/test/kotlin/com/weeth/global/auth/jwt/filter/JwtAuthenticationProcessingFilterTest.kt`:
- Around line 47-54: Replace the raw null/type checks in the test that inspects
SecurityContextHolder.getContext().authentication by using Kotest matchers: call
SecurityContextHolder.getContext().authentication.shouldNotBeNull(), then use
shouldBeInstanceOf<AuthenticatedUser>() on that authentication.principal (or on
the variable you cast) and assign the result to principal; keep subsequent
assertions (principal.id, principal.email, principal.role.name, and
authentication.authorities check) unchanged, and add the imports
io.kotest.matchers.nulls.shouldNotBeNull and
io.kotest.matchers.types.shouldBeInstanceOf to the test file.
| enum class ErrorMessage( | ||
| val code: Int, | ||
| val message: String, | ||
| ) { | ||
| UNAUTHORIZED(401, "인증 정보가 존재하지 않습니다."), | ||
| FORBIDDEN(403, "권한이 없습니다."), | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Find ErrorCodeInterface definitions and existing error code implementations
echo "=== ErrorCodeInterface 정의 및 사용 패턴 ==="
rg "interface.*ErrorCode|enum.*ErrorCode" --type=kt --type=java -B 2 -A 15 | head -150
echo -e "\n=== ErrorCode enum 인스턴스 예제 ==="
rg "enum class.*ErrorCode|ErrorCode\(" --type=kt --type=java -A 20 | head -100Repository: Team-Weeth/weeth-server
Length of output: 207
🏁 Script executed:
#!/bin/bash
# 프로젝트 내 에러 코드 범위 확인
echo "=== 프로젝트의 에러 코드 범위 패턴 ==="
rg "code.*[0-9]{3,4}" src/main --type=kt --type=java | grep -E "[2-4]xxx|[2-4][0-9]{3}" | head -50Repository: Team-Weeth/weeth-server
Length of output: 136
🏁 Script executed:
#!/bin/bash
# ResponseCodeInterface와 ErrorCodeInterface 구조 확인
echo "=== ResponseCodeInterface 및 ErrorCodeInterface 구조 ==="
fd "ResponseCode|ErrorCode" --type f --extension kt --extension java | head -20Repository: Team-Weeth/weeth-server
Length of output: 1690
🏁 Script executed:
#!/bin/bash
# Examine ErrorCodeInterface definition
echo "=== ErrorCodeInterface 정의 ==="
cat src/main/kotlin/com/weeth/global/common/exception/ErrorCodeInterface.kt
echo -e "\n=== JwtErrorCode 예제 (인증 관련) ==="
cat src/main/kotlin/com/weeth/global/auth/jwt/application/exception/JwtErrorCode.kt
echo -e "\n=== UserErrorCode 예제 ==="
cat src/main/java/com/weeth/domain/user/application/exception/UserErrorCode.java | head -50Repository: Team-Weeth/weeth-server
Length of output: 3878
🏁 Script executed:
#!/bin/bash
# Check if ErrorMessage.kt is used elsewhere
echo "=== ErrorMessage 사용 위치 ==="
rg "ErrorMessage" src/ --max-count=20Repository: Team-Weeth/weeth-server
Length of output: 733
🏁 Script executed:
#!/bin/bash
# Check ResponseCodeInterface pattern
echo "=== ResponseCodeInterface 정의 ==="
fd "ResponseCodeInterface" --type f | head -1 | xargs catRepository: Team-Weeth/weeth-server
Length of output: 290
ErrorCodeInterface 패턴을 따르도록 구현이 필요합니다.
이 enum은 프로젝트의 에러 코드 규칙을 따르지 않습니다. ErrorCodeInterface를 구현해야 하며, HTTP 상태 코드(401, 403) 대신 프로젝트 규칙에 따른 에러 코드(2xxx 범위)를 사용해야 합니다. 같은 인증 레이어의 JwtErrorCode를 참고하여 ANONYMOUS_AUTHENTICATION(2903) 같은 명확한 에러 코드로 정의하거나, 기존 JwtErrorCode로 통합하세요.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/main/kotlin/com/weeth/global/auth/authentication/ErrorMessage.kt` around
lines 3 - 9, The ErrorMessage enum must implement ErrorCodeInterface and replace
HTTP status numeric codes with project error codes in the 2xxx range (or reuse
the existing JwtErrorCode constants) to follow the project's error-code pattern;
update the enum declaration ErrorMessage to implement ErrorCodeInterface, change
members (e.g., UNAUTHORIZED -> ANONYMOUS_AUTHENTICATION with code 2903 and
FORBIDDEN -> a suitable 2xxx code or map to an existing JwtErrorCode entry), and
ensure the enum exposes the interface-required properties/methods so it conforms
to the same shape as JwtErrorCode.
src/main/kotlin/com/weeth/global/auth/kakao/dto/KakaoAccount.kt
Outdated
Show resolved
Hide resolved
| import jakarta.persistence.Converter | ||
|
|
||
| @Converter | ||
| abstract class JsonConverter<T>( |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# JsonConverter 구체 구현 및 `@Converter/`@Convert 사용 여부 확인
# 1) `@Converter` 사용 위치 확인
echo "=== `@Converter` 사용 위치 ==="
rg -n --type=kt '@Converter' -A 2
# 2) JsonConverter 상속 클래스 확인
echo -e "\n=== JsonConverter 상속 클래스 ==="
rg -n --type=kt 'class\s+\w+.*:\s*JsonConverter<'
# 3) 엔티티 필드에 `@Convert로` 지정된 곳 확인
echo -e "\n=== `@Convert` 사용 위치 ==="
rg -n --type=kt '@Convert' -A 2
# 4) 파일 구조 확인
echo -e "\n=== converter 디렉토리 구조 ==="
find . -path '*/converter/*' -name '*.kt' -type f | head -20Repository: Team-Weeth/weeth-server
Length of output: 507
🏁 Script executed:
#!/bin/bash
# 1) Read the JsonConverter file to see implementation
echo "=== JsonConverter.kt 전체 내용 ==="
cat -n ./src/main/kotlin/com/weeth/global/common/converter/JsonConverter.kt
# 2) Read BoardConfigConverter file
echo -e "\n=== BoardConfigConverter.kt 전체 내용 ==="
cat -n ./src/main/kotlin/com/weeth/domain/board/domain/converter/BoardConfigConverter.kt
# 3) Search for `@Converter` annotation (without type filter)
echo -e "\n=== `@Converter` 어노테이션 검색 ==="
rg '@Converter' -A 3
# 4) Search for JsonConverter usage
echo -e "\n=== JsonConverter 상속/사용 검색 ==="
rg 'JsonConverter' -A 2
# 5) Search for `@Convert` annotation
echo -e "\n=== `@Convert` 어노테이션 검색 ==="
rg '@Convert' -A 2Repository: Team-Weeth/weeth-server
Length of output: 4004
추상 클래스의 @Converter 제거 권장
현재 구현은 안전합니다. 구체 변환기 BoardConfigConverter가 @Converter로 올바르게 표시되어 있고, 엔티티 필드(config)에서 @Convert(converter = BoardConfigConverter::class)로 명시적으로 지정되어 있습니다. 그러나 추상 클래스 JsonConverter<T>에는 인스턴스화할 수 없으므로 @Converter 어노테이션이 불필요합니다. 추상 클래스의 어노테이션을 제거하여 의도를 더 명확히 하는 것을 권장합니다.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/main/kotlin/com/weeth/global/common/converter/JsonConverter.kt` around
lines 7 - 10, Remove the unnecessary `@Converter` annotation from the abstract
class JsonConverter<T>; locate the class declaration for JsonConverter<T> and
delete the `@Converter` above it so only concrete converters (e.g.,
BoardConfigConverter) remain annotated and the entity field (config) continues
to use `@Convert`(converter = BoardConfigConverter::class) as before.
| @ExceptionHandler(Exception::class) | ||
| fun handle(ex: Exception): ResponseEntity<CommonResponse<Void?>> { | ||
| val statusCode = if (ex is ErrorResponse) ex.statusCode.value() else 500 | ||
|
|
||
| log.warn("예외 처리(Exception)", ex) | ||
| log.warn(LOG_FORMAT, ex::class.simpleName, statusCode, ex.message) | ||
|
|
||
| val response = CommonResponse.createFailure(statusCode, ex.message ?: "") | ||
|
|
||
| return ResponseEntity | ||
| .status(statusCode) | ||
| .body(response) | ||
| } | ||
|
|
||
| companion object { | ||
| private const val INPUT_FORMAT_ERROR_MESSAGE = "입력 포맷이 올바르지 않습니다." | ||
| private const val LOG_FORMAT = "Class : {}, Code : {}, Message : {}" | ||
| } |
There was a problem hiding this comment.
일반 예외 응답에 내부 메시지 노출 위험이 있습니다.
ex.message를 그대로 내려주면 내부 구현 정보가 노출될 수 있습니다. 클라이언트에는 일반화된 메시지를 반환하고 상세 내용은 로그로만 남기는 편이 안전합니다.
🔒 노출 최소화 응답 제안
- val response = CommonResponse.createFailure(statusCode, ex.message ?: "")
+ val response = CommonResponse.createFailure(statusCode, INTERNAL_SERVER_ERROR_MESSAGE)
@@
companion object {
private const val INPUT_FORMAT_ERROR_MESSAGE = "입력 포맷이 올바르지 않습니다."
+ private const val INTERNAL_SERVER_ERROR_MESSAGE = "서버 오류가 발생했습니다."
private const val LOG_FORMAT = "Class : {}, Code : {}, Message : {}"
}📝 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.
| @ExceptionHandler(Exception::class) | |
| fun handle(ex: Exception): ResponseEntity<CommonResponse<Void?>> { | |
| val statusCode = if (ex is ErrorResponse) ex.statusCode.value() else 500 | |
| log.warn("예외 처리(Exception)", ex) | |
| log.warn(LOG_FORMAT, ex::class.simpleName, statusCode, ex.message) | |
| val response = CommonResponse.createFailure(statusCode, ex.message ?: "") | |
| return ResponseEntity | |
| .status(statusCode) | |
| .body(response) | |
| } | |
| companion object { | |
| private const val INPUT_FORMAT_ERROR_MESSAGE = "입력 포맷이 올바르지 않습니다." | |
| private const val LOG_FORMAT = "Class : {}, Code : {}, Message : {}" | |
| } | |
| `@ExceptionHandler`(Exception::class) | |
| fun handle(ex: Exception): ResponseEntity<CommonResponse<Void?>> { | |
| val statusCode = if (ex is ErrorResponse) ex.statusCode.value() else 500 | |
| log.warn("예외 처리(Exception)", ex) | |
| log.warn(LOG_FORMAT, ex::class.simpleName, statusCode, ex.message) | |
| val response = CommonResponse.createFailure(statusCode, INTERNAL_SERVER_ERROR_MESSAGE) | |
| return ResponseEntity | |
| .status(statusCode) | |
| .body(response) | |
| } | |
| companion object { | |
| private const val INPUT_FORMAT_ERROR_MESSAGE = "입력 포맷이 올바르지 않습니다." | |
| private const val INTERNAL_SERVER_ERROR_MESSAGE = "서버 오류가 발생했습니다." | |
| private const val LOG_FORMAT = "Class : {}, Code : {}, Message : {}" | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/main/kotlin/com/weeth/global/common/exception/CommonExceptionHandler.kt`
around lines 74 - 91, The handler function handle(Exception) currently uses
ex.message directly in the client response; change it to return a generic
message (e.g., INPUT_FORMAT_ERROR_MESSAGE for format errors or a fixed generic
string like "서버에서 오류가 발생했습니다.") when building CommonResponse.createFailure,
while still logging the detailed ex.message and stacktrace via log.warn (use the
existing LOG_FORMAT and the first log.warn that logs the exception). Keep the
statusCode determination logic (using ErrorResponse) and update only the
response body creation in handle and the logging lines so detailed internals
remain in logs but the response to clients contains a non-sensitive, generalized
message.
soo0711
left a comment
There was a problem hiding this comment.
고생 많으셨습니다!! 모두 잘 맞게 마이그레이션 된 것 같아요!!
JWT 구조가 다른 도메인과 통일되어 코드 흐름이 더 잘 읽힙니당 👍
📌 Summary
Global 도메인을 마이그레이션 했습니다
📝 Changes
What
Why
How
📸 Screenshots / Logs
💡 Reviewer 참고사항
✅ Checklist
Summary by CodeRabbit
인프라 개선
API 변경
테스트