Conversation
commit 218f057 Author: JunHyeong Park <64461926+joonhyong@users.noreply.github.com> Date: Thu Jan 8 15:49:29 2026 +0900 [URECA-21] Chore: domain/auth 및 global/security 구조 정립 (#8) * chore: domain/auth 구조 및 global/security 구조 분리 * chore: OAuth 공통부분 파일 작성 * chore: KakaoOAuthClient, GoogleOAuthClient 파일 생성 * chore: PR 템플릿 업로드 * chore: OAuthService 시그니처 변경, OAuthProvider Enum 도입, OAuthController 엔드포인트 정리, DTO 불변성 개선 * chore: OAuthController 입력 검증 추가, GlobalExceptionHandler 추가 * chore: GlobalExceptionHandler 에러 발생 시 내부 구현 노출 문제 및 로깅 누락 해결
commit c6aef4f Author: JunHyeong Park <64461926+joonhyong@users.noreply.github.com> Date: Fri Jan 9 15:44:50 2026 +0900 [URECA-27] Feat: Ureca 27/feat/oauth client (#14) * Feat: User 엔티티 생성 * Feat OAuthClient 생성 및 Google, Kakao, Naver OAuthClient 구현 * Feat: OAuthServiceImpl 및 UserMapper.xml 구현 -> 로그인 요청 확인 및 DB 연동 * Fix: GoogleOAuthClient, NaverOAuthClient에 name에 대한 fallback 추가 commit 218f057 Author: JunHyeong Park <64461926+joonhyong@users.noreply.github.com> Date: Thu Jan 8 15:49:29 2026 +0900 [URECA-21] Chore: domain/auth 및 global/security 구조 정립 (#8) * chore: domain/auth 구조 및 global/security 구조 분리 * chore: OAuth 공통부분 파일 작성 * chore: KakaoOAuthClient, GoogleOAuthClient 파일 생성 * chore: PR 템플릿 업로드 * chore: OAuthService 시그니처 변경, OAuthProvider Enum 도입, OAuthController 엔드포인트 정리, DTO 불변성 개선 * chore: OAuthController 입력 검증 추가, GlobalExceptionHandler 추가 * chore: GlobalExceptionHandler 에러 발생 시 내부 구현 노출 문제 및 로깅 누락 해결
📝 WalkthroughWalkthrough카카오 OAuth 연동을 위한 세 가지 변경을 추가합니다. KakaoUserResponse DTO를 도입해 카카오 사용자 응답(id, 이메일, 닉네임)을 매핑합니다. KakaoOAuthClient는 액세스 토큰 교환을 폼 인코딩 HTTP 호출로 재구성하고 TokenResponse DTO를 추가했으며, 사용자 정보 조회는 KakaoUserResponse를 사용해 매핑하도록 변경되었습니다. WebMvcConfig를 추가해 프로퍼티로 지정된 허용 출처로 CORS를 설정합니다. Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes 🎓 교육적 피드백✅ 잘된 점
🔍 개선 제안
짧게 한 마디: 타입 정리는 잘하셨습니다 — 이제 실패 케이스와 보안 경계선에 방어벽을 더 쌓아주세요. 🚥 Pre-merge checks | ✅ 2 | ❌ 3❌ Failed checks (2 warnings, 1 inconclusive)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
Comment |
There was a problem hiding this comment.
Actionable comments posted: 7
🤖 Fix all issues with AI agents
In @src/main/java/com/ureca/unity/domain/auth/dto/KakaoUserResponse.java:
- Around line 9-12: KakaoUserResponse currently exposes a nullable kakaoAccount
which can cause NPEs when callers in KakaoOAuthClient do
body.getKakaoAccount().getEmail()/getProfile().getNickname(); add null-safe
accessors on KakaoUserResponse such as getEmail() and getNickname() that return
Optional<String> by mapping through kakaoAccount (and profile for nickname), and
update KakaoOAuthClient to use these Optionals (or check isPresent()) instead of
chaining getters; alternatively, mark kakaoAccount with @JsonProperty(required =
true) if you want deserialization to enforce presence, but prefer adding the
Optional-returning methods to avoid NPEs.
In
@src/main/java/com/ureca/unity/domain/auth/service/oauth/KakaoOAuthClient.java:
- Line 6: KakaoOAuthClient defines a duplicate inner TokenResponse class (lines
~93-106) while com.ureca.unity.domain.auth.dto.TokenResponse already exists;
remove the inner class and update usages in KakaoOAuthClient (e.g., methods
parsing/returning token payload) to import and use
com.ureca.unity.domain.auth.dto.TokenResponse instead, or map to a service-level
DTO like ServiceTokenResponse if you need a different shape, ensuring
constructors/parsers (e.g., whereTokenResponseIsBuilt or parseTokenResponse) are
adjusted to instantiate or convert to the existing DTO.
- Around line 34-40: getUserInfo currently throws a generic RuntimeException
when accessToken is missing; create a custom OAuthTokenException (e.g., in
domain/auth/exception) that accepts an OAuthProvider and message, and replace
the RuntimeException in getUserInfo with throwing new
OAuthTokenException(OAuthProvider.KAKAO, "Failed to get Kakao access token");
also update getAccessToken to throw OAuthTokenException on error cases so
callers get a specific exception type, and ensure your GlobalExceptionHandler
maps OAuthTokenException to an appropriate client response.
- Around line 85-90: The code in KakaoOAuthClient that builds the OAuthUserInfo
is unsafe because body.getKakaoAccount() and its nested
getEmail()/getProfile().getNickname() can be null; add null checks (or use
Optional/guard clauses) before accessing these nested fields in the method that
returns new OAuthUserInfo so you pass a safe email and nickname (e.g., null or
empty string) when absent, and complement this by adjusting KakaoUserResponse
annotations/mapping (e.g., remove required=true or document optional fields) to
reflect that email/profile may be missing.
- Around line 34-67: The OAuth flow lacks state parameter handling: update the
authorization initiation and callback flow so a cryptographically random state
is generated and stored server-side (e.g., session or secure cookie) when
building the auth URL, then require and validate the incoming state on callback
handlers; specifically, modify OAuthController to accept both code and state,
have OAuthServiceImpl's start/login method emit and persist the state, and
update OAuth client implementations (KakaoOAuthClient.getUserInfo and analogous
methods in Google/Naver clients) to accept the state, compare it against the
stored value and throw a clear exception (rejecting the request) on mismatch or
missing state; ensure storage is tied to the user session and that invalid state
leads to an immediate error response to prevent CSRF.
In @src/main/java/com/ureca/unity/global/config/WebMvcConfig.java:
- Around line 11-14: The CORS origin is hard-coded in WebMvcConfig
(registry.addMapping(...).allowedOrigins("http://localhost:3000")), so
externalize it to a config property and read it at runtime: add a
cors.allowed-origins property (default via application.yml and overridable by
env var CORS_ALLOWED_ORIGINS), inject that property into the WebMvcConfig class
(e.g., via @Value or a @ConfigurationProperties bean) and call
registry.addMapping(...).allowedOrigins(...) with the injected value(s) (split
into an array if multiple origins are supported); ensure allowCredentials(true)
and allowedMethods remain unchanged.
🧹 Nitpick comments (1)
src/main/java/com/ureca/unity/domain/auth/service/oauth/KakaoOAuthClient.java (1)
57-57: 리팩토링 권장: API URL을 설정 파일로 외부화하세요.현재 카카오 API URL이 하드코딩되어 있습니다. 향후 API 버전 변경이나 테스트 환경 구성 시 코드 수정이 필요합니다.
♻️ 설정 외부화 예시
application.yml:oauth: kakao: token-uri: https://kauth.kakao.com/oauth/token user-info-uri: https://kapi.kakao.com/v2/user/me코드:
+@Value("${oauth.kakao.token-uri}") +private String tokenUri; + +@Value("${oauth.kakao.user-info-uri}") +private String userInfoUri; + private String getAccessToken(String code) { // ... TokenResponse response = restTemplate.postForObject( - "https://kauth.kakao.com/oauth/token", + tokenUri, request, TokenResponse.class );Also applies to: 76-76
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
src/main/java/com/ureca/unity/domain/auth/dto/KakaoUserResponse.javasrc/main/java/com/ureca/unity/domain/auth/service/oauth/KakaoOAuthClient.javasrc/main/java/com/ureca/unity/global/config/WebMvcConfig.java
🧰 Additional context used
🧬 Code graph analysis (2)
src/main/java/com/ureca/unity/domain/auth/dto/KakaoUserResponse.java (1)
src/main/java/com/ureca/unity/domain/auth/service/oauth/KakaoOAuthClient.java (1)
Getter(93-106)
src/main/java/com/ureca/unity/domain/auth/service/oauth/KakaoOAuthClient.java (3)
src/main/java/com/ureca/unity/domain/auth/dto/KakaoUserResponse.java (3)
Getter(6-24)Getter(14-18)Getter(20-23)src/main/java/com/ureca/unity/domain/auth/dto/OAuthUserInfo.java (1)
Getter(7-16)src/main/java/com/ureca/unity/domain/auth/dto/TokenResponse.java (1)
Getter(6-12)
🔇 Additional comments (1)
src/main/java/com/ureca/unity/domain/auth/service/oauth/KakaoOAuthClient.java (1)
25-26: client_secret은 Kakao OAuth에서 필수 파라미터입니다 — 현재 구현이 올바릅니다.공식 Kakao OAuth 문서에 따르면, 토큰 엔드포인트(
/oauth/token)에client_secret을 전송하는 것이 필수입니다. 이 기능은 앱 설정에서 기본적으로 활성화되어 있습니다. 따라서 현재 코드에서@Value로clientSecret을 주입하고 요청 파라미터에 포함하는 방식은 정확합니다.JavaScript 앱이나 네이티브 앱에서는 PKCE 흐름을 사용하여
client_secret없이 동작하지만, 이 코드처럼 백엔드 서버에서 authorization code를 교환하는 경우(confidential client flow)에는client_secret이 반드시 필요합니다.수정할 필요가 없습니다.
Also applies to: 46-46
| public OAuthUserInfo getUserInfo(String code) { | ||
| String accessToken = getAccessToken(code); | ||
| if (accessToken == null || accessToken.isEmpty()) { | ||
| throw new RuntimeException("Failed to get Kakao access token"); | ||
| } | ||
| return getUser(accessToken); | ||
| } | ||
|
|
||
| /* 1. Authorization Code → Access Token */ | ||
| private String getAccessToken(String code) { | ||
| MultiValueMap<String, String> params = new LinkedMultiValueMap<>(); | ||
| params.add("grant_type", "authorization_code"); | ||
| params.add("client_id", clientId); | ||
| params.add("client_secret", clientSecret); | ||
| params.add("redirect_uri", redirectUri); | ||
| params.add("code", code); | ||
|
|
||
| HttpHeaders headers = new HttpHeaders(); | ||
| headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); | ||
|
|
||
| MultiValueMap<String, String> body = new LinkedMultiValueMap<>(); | ||
| body.add("grant_type", "authorization_code"); | ||
| body.add("client_id", clientId); | ||
| body.add("redirect_uri", redirectUri); | ||
| body.add("code", code); | ||
| HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(params, headers); | ||
|
|
||
| // Client Secret을 쓰는 경우만 포함 | ||
| if (clientSecret != null && !clientSecret.isBlank()) { | ||
| body.add("client_secret", clientSecret); | ||
| } | ||
|
|
||
| HttpEntity<MultiValueMap<String, String>> request = | ||
| new HttpEntity<>(body, headers); | ||
|
|
||
| ResponseEntity<Map> response = restTemplate.exchange( | ||
| tokenUri, | ||
| HttpMethod.POST, | ||
| // TokenResponse는 Jackson이 인식할 수 있도록 기본 생성자 필요 | ||
| TokenResponse response = restTemplate.postForObject( | ||
| "https://kauth.kakao.com/oauth/token", | ||
| request, | ||
| Map.class | ||
| TokenResponse.class | ||
| ); | ||
|
|
||
| if (response.getBody() == null || response.getBody().get("access_token") == null) { | ||
| throw new IllegalArgumentException("Failed to retrieve Kakao access token"); | ||
| if (response == null || response.getAccessToken() == null) { | ||
| throw new RuntimeException("Kakao token response is null or invalid"); | ||
| } | ||
|
|
||
| return response.getBody().get("access_token").toString(); | ||
| return response.getAccessToken(); | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# KakaoOAuthClient.java 전체 파일 확인
cat src/main/java/com/ureca/unity/domain/auth/service/oauth/KakaoOAuthClient.javaRepository: Ureca3/Ureca-server
Length of output: 3682
🏁 Script executed:
# 다른 OAuth 클라이언트 파일 확인
fd "OAuthClient" src/main/java --type fRepository: Ureca3/Ureca-server
Length of output: 370
🏁 Script executed:
# state 파라미터 관련 코드 검색
rg "state" src/main/java/com/ureca/unity/domain/auth/ -i --type java -B2 -A2Repository: Ureca3/Ureca-server
Length of output: 45
🏁 Script executed:
# OAuth 관련 설정이나 인터페이스 확인
fd -e java src/main/java/com/ureca/unity/domain/auth/ | head -20Repository: Ureca3/Ureca-server
Length of output: 536
🏁 Script executed:
# OAuth 콜백 관련 컨트롤러 검색
fd -e java src/main/java/com/ureca/unity/domain/auth/ --type fRepository: Ureca3/Ureca-server
Length of output: 536
🏁 Script executed:
# GoogleOAuthClient와 NaverOAuthClient에서 state 처리 확인
cat src/main/java/com/ureca/unity/domain/auth/service/oauth/GoogleOAuthClient.javaRepository: Ureca3/Ureca-server
Length of output: 3349
🏁 Script executed:
# state 관련 코드 검색 (다양한 형식으로)
rg "state" src/main/java/com/ureca/unity/domain/auth/ --type java -iRepository: Ureca3/Ureca-server
Length of output: 45
🏁 Script executed:
# 인증 관련 서비스나 컨트롤러 파일 목록
fd -e java src/main/java/com/ureca/unity/domain/auth/ -type fRepository: Ureca3/Ureca-server
Length of output: 233
🏁 Script executed:
# 올바른 fd 문법으로 auth 디렉토리의 모든 파일 찾기
fd . src/main/java/com/ureca/unity/domain/auth/Repository: Ureca3/Ureca-server
Length of output: 1184
🏁 Script executed:
# OAuth 콜백/인증 관련 컨트롤러 찾기
find src/main/java/com/ureca/unity/domain/auth/ -name "*Controller*" -o -name "*Service*"Repository: Ureca3/Ureca-server
Length of output: 276
🏁 Script executed:
# state 파라미터 검색 (대소문자 구분 없이)
rg -i "state" src/main/java/com/ureca/unity/domain/auth/Repository: Ureca3/Ureca-server
Length of output: 45
🏁 Script executed:
# NaverOAuthClient도 확인
cat src/main/java/com/ureca/unity/domain/auth/service/oauth/NaverOAuthClient.javaRepository: Ureca3/Ureca-server
Length of output: 3564
🏁 Script executed:
# OAuthController.java 확인
cat src/main/java/com/ureca/unity/domain/auth/controller/OAuthController.javaRepository: Ureca3/Ureca-server
Length of output: 985
🏁 Script executed:
# OAuthServiceImpl.java 확인
cat src/main/java/com/ureca/unity/domain/auth/service/OAuthServiceImpl.javaRepository: Ureca3/Ureca-server
Length of output: 2419
🏁 Script executed:
# OAuthService 인터페이스도 확인
cat src/main/java/com/ureca/unity/domain/auth/service/OAuthService.javaRepository: Ureca3/Ureca-server
Length of output: 342
OAuth 보안: state 파라미터 검증 필수 구현
현재 OAuth 흐름에서 CSRF 공격을 방지하는 state 파라미터가 완전히 빠져있습니다. OAuthController는 code만 받고 있으며, OAuthServiceImpl과 모든 OAuthClient(Kakao, Google, Naver)에서도 state 처리가 없습니다.
개선 방향 (RFC 6749 Section 10.12):
- 인가 요청 시 클라이언트가 생성한 랜덤 state를 세션/쿠키에 저장
- 콜백 핸들러에서 요청 파라미터의 state와 저장된 state 비교 검증
- 불일치 시 요청 거부 및 예외 처리
현재 상태로는 공격자가 사용자를 강제로 다른 계정으로 인가하도록 유도할 수 있습니다. 다음 리팩토링 단계에서 반드시 추가하세요.
🤖 Prompt for AI Agents
In
@src/main/java/com/ureca/unity/domain/auth/service/oauth/KakaoOAuthClient.java
around lines 34 - 67, The OAuth flow lacks state parameter handling: update the
authorization initiation and callback flow so a cryptographically random state
is generated and stored server-side (e.g., session or secure cookie) when
building the auth URL, then require and validate the incoming state on callback
handlers; specifically, modify OAuthController to accept both code and state,
have OAuthServiceImpl's start/login method emit and persist the state, and
update OAuth client implementations (KakaoOAuthClient.getUserInfo and analogous
methods in Google/Naver clients) to accept the state, compare it against the
stored value and throw a clear exception (rejecting the request) on mismatch or
missing state; ensure storage is tied to the user session and that invalid state
leads to an immediate error response to prevent CSRF.
| TokenResponse response = restTemplate.postForObject( | ||
| "https://kauth.kakao.com/oauth/token", | ||
| request, | ||
| Map.class | ||
| TokenResponse.class | ||
| ); |
There was a problem hiding this comment.
예외 처리 누락: RestTemplate 호출 실패 시나리오를 처리하세요.
RestTemplate은 네트워크 오류, 4xx/5xx 응답 시 RestClientException을 던지는데, 현재 코드는 이를 처리하지 않습니다. OAuth 플로우 중 외부 API 실패는 충분히 예상 가능한 상황입니다.
🔧 예외 처리 추가
+import org.springframework.web.client.RestClientException;
+
private String getAccessToken(String code) {
// ... params 설정 ...
- TokenResponse response = restTemplate.postForObject(
- "https://kauth.kakao.com/oauth/token",
- request,
- TokenResponse.class
- );
+ TokenResponse response;
+ try {
+ response = restTemplate.postForObject(
+ "https://kauth.kakao.com/oauth/token",
+ request,
+ TokenResponse.class
+ );
+ } catch (RestClientException e) {
+ throw new OAuthTokenException(OAuthProvider.KAKAO,
+ "카카오 토큰 발급 API 호출 실패: " + e.getMessage(), e);
+ }
if (response == null || response.getAccessToken() == null) {getUser() 메서드도 동일하게 적용하세요.
Spring Security OAuth2 Client 라이브러리를 사용하면 재시도, 타임아웃 등 더 견고한 처리를 기본 제공합니다.
Also applies to: 75-80
- KakaoUserResponse 중첩 객체 null 처리 - KakaoOAuthClient 중첩 필드 null 검증 추가 - CORS origin 환경변수로 외부화
…/Ureca-server into URECA-22/Feat/kakao-login
There was a problem hiding this comment.
Actionable comments posted: 5
🤖 Fix all issues with AI agents
In
@src/main/java/com/ureca/unity/domain/auth/service/oauth/KakaoOAuthClient.java:
- Around line 38-40: Replace the generic RuntimeException thrown in
KakaoOAuthClient when the access token is missing with a specific custom
exception (e.g., OAuthTokenException or OAuthAuthenticationException): create
the custom exception class (extending RuntimeException or a project base
exception) with message and cause constructors, then change the throw in the
method that obtains the token inside KakaoOAuthClient (the accessToken
null/empty check) to throw the new OAuthTokenException with a descriptive
message; update any callers or method signatures if they expect this specific
exception for clearer error handling.
- Line 33: The RestTemplate in KakaoOAuthClient is created without timeouts
(private final RestTemplate restTemplate = new RestTemplate()), which can cause
blocking on external Kakao calls; replace this direct instantiation by injecting
a configured RestTemplate bean (or build one with RestTemplateBuilder) that sets
connection timeout ~3000ms and read timeout ~5000ms (or use
SimpleClientHttpRequestFactory with setConnectTimeout and setReadTimeout) and
update KakaoOAuthClient to accept the RestTemplate via constructor injection and
remove the direct new RestTemplate() field initialization.
- Around line 71-103: In getUser, avoid repeating null-unsafe calls to
body.getKakaoAccount() and directly accessing nested fields; compute local
variables for kakaoAccount, email and nickname using Optional (or null checks)
once (e.g., var account = body.getKakaoAccount(); String email =
Optional.ofNullable(account).map(KakaoUserResponse.KakaoAccount::getEmail).orElse(null);
String nickname =
Optional.ofNullable(account).map(KakaoUserResponse.KakaoAccount::getProfile).map(KakaoUserResponse.Profile::getNickname).orElse("Unknown");)
and then pass those locals into the OAuthUserInfo constructor instead of calling
body.getKakaoAccount().getEmail() and
body.getKakaoAccount().getProfile().getNickname(), ensuring null-safety and
removing the duplicated optional chains and the stray extra semicolon.
- Line 6: Remove the dead inner static TokenResponse class (the one defined
around lines 108-118) so the code relies solely on the imported
com.ureca.unity.domain.auth.dto.TokenResponse; delete that inner class
declaration and any redundant getAccessToken() method, leaving the existing
import on line 6 and usages like TokenResponse.class intact, and then verify the
external DTO has Jackson annotations (e.g., @JsonProperty("access_token")) or
proper naming strategy so the access_token field maps correctly.
🧹 Nitpick comments (4)
src/main/java/com/ureca/unity/domain/auth/dto/KakaoUserResponse.java (2)
6-12: Jackson 역직렬화를 위한 기본 생성자가 필요합니다.
RestTemplate.postForObject()또는exchange()로 응답을 매핑할 때 Jackson은 기본 생성자를 사용합니다. 현재 명시적 생성자가 없어 Lombok의 암묵적 처리에 의존하고 있지만, 명확성을 위해@NoArgsConstructor를 추가하는 것이 좋습니다.또한
@JsonProperty(required = true)를 설정했지만, 실제 필드가 null일 경우 런타임에NullPointerException이 발생할 수 있습니다.@NotNull같은 validation 어노테이션 추가를 고려하세요.♻️ 제안하는 개선안
+import lombok.NoArgsConstructor; + @Getter +@NoArgsConstructor public class KakaoUserResponse { private Long id;
14-23: 중첩 클래스에도 기본 생성자 명시가 필요합니다.
KakaoAccount와Profile클래스도 Jackson 역직렬화 대상이므로@NoArgsConstructor를 추가해야 합니다. 특히 외부 API 응답 구조가 변경되거나 필드가 누락될 경우를 대비해 명시적으로 선언하는 것이 안전합니다.♻️ 제안하는 개선안
@Getter +@NoArgsConstructor public static class KakaoAccount { private String email; private Profile profile; } @Getter +@NoArgsConstructor public static class Profile { private String nickname; }src/main/java/com/ureca/unity/domain/auth/service/oauth/KakaoOAuthClient.java (2)
44-69: 하드코딩된 URL을 properties로 외부화하세요.Line 59의
https://kauth.kakao.com/oauth/token이 하드코딩되어 있습니다. 개발/운영 환경 분리나 테스트 시 유연성을 위해application.yml에 설정하는 것이 좋습니다.또한 Line 64-66의 예외 처리도 앞서 언급한 커스텀 예외로 개선이 필요합니다.
⚙️ 제안하는 개선안
application.yml 추가:
oauth: kakao: token-uri: https://kauth.kakao.com/oauth/token user-info-uri: https://kapi.kakao.com/v2/user/me코드 수정:
@Component("kakao") @RequiredArgsConstructor public class KakaoOAuthClient implements OAuthClient { @Value("${oauth.kakao.client-id}") private String clientId; @Value("${oauth.kakao.client-secret}") private String clientSecret; @Value("${oauth.kakao.redirect-uri}") private String redirectUri; + + @Value("${oauth.kakao.token-uri}") + private String tokenUri; // ... private String getAccessToken(String code) { // ... TokenResponse response = restTemplate.postForObject( - "https://kauth.kakao.com/oauth/token", + tokenUri, request, TokenResponse.class );
105-118: 중복된 getter 메서드를 제거하세요.
@Getter어노테이션이 클래스 레벨에 있으므로, Line 115-117의 명시적getAccessToken()메서드는 불필요합니다. Lombok이 자동으로getAccess_token()메서드를 생성하며, Line 116에서access_token필드를 그대로 반환하고 있어 중복입니다.필드명을 Java 관례에 맞게
accessToken으로 변경하고@JsonProperty("access_token")을 사용하는 것을 권장합니다.♻️ 개선안
@Getter @Setter @NoArgsConstructor public static class TokenResponse { + @JsonProperty("access_token") - private String access_token; + private String accessToken; + + @JsonProperty("token_type") private String token_type; + + @JsonProperty("refresh_token") private String refresh_token; + + @JsonProperty("expires_in") private Long expires_in; + private String scope; - - public String getAccessToken() { - return access_token; - } }이렇게 하면 Line 68에서
response.getAccessToken()을 그대로 사용할 수 있습니다.
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
src/main/java/com/ureca/unity/domain/auth/dto/KakaoUserResponse.javasrc/main/java/com/ureca/unity/domain/auth/service/oauth/KakaoOAuthClient.javasrc/main/java/com/ureca/unity/global/config/WebMvcConfig.java
🚧 Files skipped from review as they are similar to previous changes (1)
- src/main/java/com/ureca/unity/global/config/WebMvcConfig.java
| import com.ureca.unity.domain.auth.constant.OAuthProvider; | ||
| import com.ureca.unity.domain.auth.dto.KakaoUserResponse; | ||
| import com.ureca.unity.domain.auth.dto.OAuthUserInfo; | ||
| import com.ureca.unity.domain.auth.dto.TokenResponse; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# KakaoOAuthClient.java 파일 확인
fd -t f "KakaoOAuthClient.java" -x cat -n {} | head -120Repository: Ureca3/Ureca-server
Length of output: 4920
내부 TokenResponse 클래스를 삭제하세요 - 사용되지 않는 코드입니다.
Line 6의 import는 실제로 Line 61에서 TokenResponse.class로 사용되고 있습니다. 하지만 Lines 108-118의 내부 static class TokenResponse는 Java의 import 우선순위 규칙에 의해 코드에서 전혀 참조되지 않는 dead code입니다.
이 내부 클래스는:
- 불필요한 getAccessToken() 메서드를 정의하고 있음 (getter annotation이 이미 있음)
- 유지보수 시 혼란을 야기함
- 코드 중복을 초래함
해결책: Lines 108-118의 내부 TokenResponse 클래스를 완전히 삭제하고, 외부 DTO인 com.ureca.unity.domain.auth.dto.TokenResponse만 사용하세요. 외부 DTO에서 Jackson이 snake_case 필드명(access_token)을 제대로 매핑할 수 있도록 @JsonProperty 애너테이션 설정이 되어있는지 확인하세요.
🤖 Prompt for AI Agents
In
@src/main/java/com/ureca/unity/domain/auth/service/oauth/KakaoOAuthClient.java
at line 6, Remove the dead inner static TokenResponse class (the one defined
around lines 108-118) so the code relies solely on the imported
com.ureca.unity.domain.auth.dto.TokenResponse; delete that inner class
declaration and any redundant getAccessToken() method, leaving the existing
import on line 6 and usages like TokenResponse.class intact, and then verify the
external DTO has Jackson annotations (e.g., @JsonProperty("access_token")) or
proper naming strategy so the access_token field maps correctly.
| @Value("${oauth.kakao.redirect-uri}") | ||
| private String redirectUri; | ||
|
|
||
| private final RestTemplate restTemplate = new RestTemplate(); |
There was a problem hiding this comment.
RestTemplate에 타임아웃 설정이 필요합니다.
외부 API(카카오) 호출 시 네트워크 지연이나 장애 상황에서 무한 대기할 수 있습니다. RestTemplate에 connection timeout과 read timeout을 설정하지 않으면 스레드가 블로킹되어 서비스 전체에 영향을 줄 수 있습니다.
Spring의 RestTemplateBuilder를 사용하거나, SimpleClientHttpRequestFactory로 타임아웃을 설정하세요. 일반적으로 connection timeout 3초, read timeout 5초가 권장됩니다.
⚙️ 타임아웃 설정 예시
Config 클래스에서 빈으로 등록하는 방법:
@Configuration
public class RestTemplateConfig {
@Bean
public RestTemplate oAuthRestTemplate() {
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
factory.setConnectTimeout(3000);
factory.setReadTimeout(5000);
return new RestTemplate(factory);
}
}그리고 KakaoOAuthClient에서 주입받아 사용:
@Component("kakao")
@RequiredArgsConstructor
public class KakaoOAuthClient implements OAuthClient {
// ...
- private final RestTemplate restTemplate = new RestTemplate();
+ private final RestTemplate oAuthRestTemplate;Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In
@src/main/java/com/ureca/unity/domain/auth/service/oauth/KakaoOAuthClient.java
at line 33, The RestTemplate in KakaoOAuthClient is created without timeouts
(private final RestTemplate restTemplate = new RestTemplate()), which can cause
blocking on external Kakao calls; replace this direct instantiation by injecting
a configured RestTemplate bean (or build one with RestTemplateBuilder) that sets
connection timeout ~3000ms and read timeout ~5000ms (or use
SimpleClientHttpRequestFactory with setConnectTimeout and setReadTimeout) and
update KakaoOAuthClient to accept the RestTemplate via constructor injection and
remove the direct new RestTemplate() field initialization.
| public OAuthUserInfo getUserInfo(String code) { | ||
| String accessToken = getAccessToken(code); | ||
| if (accessToken == null || accessToken.isEmpty()) { | ||
| throw new RuntimeException("Failed to get Kakao access token"); | ||
| } | ||
| return getUser(accessToken); | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check for state validation logic in OAuth/Auth related files
rg -n "state" --type java -g "**/*OAuth*" -g "**/*Auth*" -A 3 -B 3
echo "---"
# Search for redirect URI validation
rg -n "redirectUri\|redirect_uri" --type java -A 2 -B 2 | head -100Repository: Ureca3/Ureca-server
Length of output: 66
🏁 Script executed:
#!/bin/bash
# Check the full KakaoOAuthClient implementation
find . -type f -name "KakaoOAuthClient.java" | xargs cat -nRepository: Ureca3/Ureca-server
Length of output: 4920
🏁 Script executed:
#!/bin/bash
# Search for OAuth related configuration or parameter handling
rg -n "state\|redirectUri" --type java -g "**/*Controller*" -g "**/*Config*" -A 2 -B 2 | head -80Repository: Ureca3/Ureca-server
Length of output: 45
🏁 Script executed:
#!/bin/bash
# Find Controller files that might handle OAuth
find . -type f -name "*Controller.java" | xargs grep -l -i "oauth\|kakao" | head -10Repository: Ureca3/Ureca-server
Length of output: 138
🏁 Script executed:
#!/bin/bash
# Search for state generation or storage logic
rg -n "new.*state\|generateState\|saveState\|UUID.*generate" --type java | head -50Repository: Ureca3/Ureca-server
Length of output: 45
🏁 Script executed:
#!/bin/bash
# Look for OAuth related services
find . -type f -name "*OAuthService*" -o -name "*AuthService*" | xargs cat -n 2>/dev/null | head -200Repository: Ureca3/Ureca-server
Length of output: 3203
🏁 Script executed:
#!/bin/bash
# Check for session or storage of state values
rg -n "session\|state" --type java -g "**/*Auth*" -g "**/*Oauth*" | grep -i "session\|store\|save\|put" | head -40Repository: Ureca3/Ureca-server
Length of output: 45
🏁 Script executed:
#!/bin/bash
# Read OAuthController to see full OAuth flow implementation
cat -n ./src/main/java/com/ureca/unity/domain/auth/controller/OAuthController.javaRepository: Ureca3/Ureca-server
Length of output: 1202
🏁 Script executed:
#!/bin/bash
# Search for any state-related logic in entire auth directory
find ./src/main/java/com/ureca/unity/domain/auth -type f -name "*.java" | xargs grep -l "state" 2>/dev/nullRepository: Ureca3/Ureca-server
Length of output: 47
🏁 Script executed:
#!/bin/bash
# Check issue #6 or related documentation
find . -type f \( -name "*.md" -o -name "*.txt" \) | xargs grep -l "issue.*6\|#6\|state.*검증\|CSRF" 2>/dev/null | head -10Repository: Ureca3/Ureca-server
Length of output: 45
OAuth 보안 검증이 완전히 누락되었습니다.
OAuth 흐름 전체에서 필수 보안 검사가 부재합니다:
- State 검증 없음: Authorization 요청 시 state를 생성하거나 콜백에서 검증하지 않아 CSRF 공격에 취약합니다.
- Redirect URI 검증 없음: 콜백 핸들러에서 redirectUri를 명시적으로 검증하지 않습니다(설정에서만 사용 중).
- Authorization 요청 생성 로직 누락: 현재 구현은 코드 교환 단계만 있으며, 사용자를 카카오 인증 페이지로 리다이렉트하는 단계가 없습니다.
카카오 공식 문서(보안 가이드)에 따라:
- Authorization 요청 시 UUID 기반 state 생성 및 세션/캐시 저장
- 콜백 수신 시 code와 함께 전달된 state를 저장된 값과 비교
- 요청된 redirectUri가 등록된 값과 일치하는지 확인
| if (accessToken == null || accessToken.isEmpty()) { | ||
| throw new RuntimeException("Failed to get Kakao access token"); | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
더 구체적인 예외 타입을 사용하세요.
RuntimeException은 너무 포괄적이어서 호출부에서 적절한 에러 핸들링이 어렵습니다. OAuth 인증 실패, 네트워크 오류, API 응답 오류 등을 구분할 수 있는 커스텀 예외를 정의하세요.
예: OAuthAuthenticationException, OAuthTokenException 등
♻️ 커스텀 예외 예시
// domain/auth/exception/OAuthTokenException.java
public class OAuthTokenException extends RuntimeException {
public OAuthTokenException(String message) {
super(message);
}
public OAuthTokenException(String message, Throwable cause) {
super(message, cause);
}
}사용:
if (accessToken == null || accessToken.isEmpty()) {
- throw new RuntimeException("Failed to get Kakao access token");
+ throw new OAuthTokenException("Failed to get Kakao access token");
}Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In
@src/main/java/com/ureca/unity/domain/auth/service/oauth/KakaoOAuthClient.java
around lines 38 - 40, Replace the generic RuntimeException thrown in
KakaoOAuthClient when the access token is missing with a specific custom
exception (e.g., OAuthTokenException or OAuthAuthenticationException): create
the custom exception class (extending RuntimeException or a project base
exception) with message and cause constructors, then change the throw in the
method that obtains the token inside KakaoOAuthClient (the accessToken
null/empty check) to throw the new OAuthTokenException with a descriptive
message; update any callers or method signatures if they expect this specific
exception for clearer error handling.
| private OAuthUserInfo getUser(String accessToken) { | ||
| HttpHeaders headers = new HttpHeaders(); | ||
| headers.setBearerAuth(accessToken); | ||
|
|
||
| HttpEntity<Void> request = new HttpEntity<>(headers); | ||
|
|
||
| ResponseEntity<Map> response = restTemplate.exchange( | ||
| userInfoUri, | ||
| ResponseEntity<KakaoUserResponse> response = restTemplate.exchange( | ||
| "https://kapi.kakao.com/v2/user/me", | ||
| HttpMethod.GET, | ||
| request, | ||
| Map.class | ||
| KakaoUserResponse.class | ||
| ); | ||
|
|
||
| if (response.getBody() == null || response.getBody().get("id") == null) { | ||
| throw new IllegalArgumentException("Failed to retrieve Kakao user info"); | ||
| } | ||
| KakaoUserResponse body = response.getBody(); | ||
| if (body == null) throw new RuntimeException("Kakao user response is null"); | ||
|
|
||
| Map<String, Object> body = response.getBody(); | ||
| Map<String, Object> kakaoAccount = | ||
| (Map<String, Object>) body.get("kakao_account"); | ||
| String email = Optional.ofNullable(body.getKakaoAccount()) | ||
| .map(KakaoUserResponse.KakaoAccount::getEmail) | ||
| .orElse(null); | ||
|
|
||
| String email = null; | ||
| String nickname = null; | ||
| String nickname = Optional.ofNullable(body.getKakaoAccount()) | ||
| .map(KakaoUserResponse.KakaoAccount::getProfile) | ||
| .map(KakaoUserResponse.Profile::getNickname) | ||
| .orElse("Unknown");; | ||
|
|
||
| if (kakaoAccount != null) { | ||
| email = (String) kakaoAccount.get("email"); | ||
|
|
||
| Map<String, Object> profile = | ||
| (Map<String, Object>) kakaoAccount.get("profile"); | ||
| if (profile != null) { | ||
| nickname = (String) profile.get("nickname"); | ||
| } | ||
| } | ||
| return new OAuthUserInfo( | ||
| OAuthProvider.KAKAO.value(), | ||
| String.valueOf(body.getId()), | ||
| body.getKakaoAccount().getEmail(), | ||
| body.getKakaoAccount().getProfile().getNickname() | ||
| ); | ||
| } |
There was a problem hiding this comment.
중복된 Optional 체인과 일관성 없는 null 처리를 개선하세요.
Lines 87-94에서 body.getKakaoAccount()를 두 번 반복 조회하고 있습니다. 또한 Lines 97-102의 생성자에서는 Optional 없이 직접 접근하여 Lines 87-94의 null 체크가 무의미해집니다.
Line 100에서 body.getKakaoAccount().getEmail()은 getKakaoAccount()가 null일 경우 NPE가 발생합니다.
🔧 개선된 null-safe 구현
private OAuthUserInfo getUser(String accessToken) {
// ... (request 생성 부분 동일)
KakaoUserResponse body = response.getBody();
if (body == null) throw new RuntimeException("Kakao user response is null");
- String email = Optional.ofNullable(body.getKakaoAccount())
- .map(KakaoUserResponse.KakaoAccount::getEmail)
- .orElse(null);
-
- String nickname = Optional.ofNullable(body.getKakaoAccount())
- .map(KakaoUserResponse.KakaoAccount::getProfile)
- .map(KakaoUserResponse.Profile::getNickname)
- .orElse("Unknown");;
-
+ KakaoUserResponse.KakaoAccount account = body.getKakaoAccount();
+ if (account == null) {
+ throw new OAuthUserInfoException("Kakao account information is missing");
+ }
+
+ String email = account.getEmail();
+ String nickname = Optional.ofNullable(account.getProfile())
+ .map(KakaoUserResponse.Profile::getNickname)
+ .orElse("Unknown");
return new OAuthUserInfo(
OAuthProvider.KAKAO.value(),
String.valueOf(body.getId()),
- body.getKakaoAccount().getEmail(),
- body.getKakaoAccount().getProfile().getNickname()
+ email,
+ nickname
);
}Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In
@src/main/java/com/ureca/unity/domain/auth/service/oauth/KakaoOAuthClient.java
around lines 71 - 103, In getUser, avoid repeating null-unsafe calls to
body.getKakaoAccount() and directly accessing nested fields; compute local
variables for kakaoAccount, email and nickname using Optional (or null checks)
once (e.g., var account = body.getKakaoAccount(); String email =
Optional.ofNullable(account).map(KakaoUserResponse.KakaoAccount::getEmail).orElse(null);
String nickname =
Optional.ofNullable(account).map(KakaoUserResponse.KakaoAccount::getProfile).map(KakaoUserResponse.Profile::getNickname).orElse("Unknown");)
and then pass those locals into the OAuthUserInfo constructor instead of calling
body.getKakaoAccount().getEmail() and
body.getKakaoAccount().getProfile().getNickname(), ensuring null-safety and
removing the duplicated optional chains and the stray extra semicolon.
Key Changes
카카오, 구글, 네이버 로그인 동작 확인 완료
작업 내역
close: #6
💬 공유사항 to 리뷰어
비고
Summary by CodeRabbit
새로운 기능
리팩터
✏️ Tip: You can customize this high-level summary in your review settings.