Repository: ReTrip-Dev/ReTrip-api
Pull request #4: Feat: 카카오 로그인 및 회원가입 구현
Author: login: Bumnote
Labels: name: 🔥 feat
description: 기능 구현 이슈
Created: 2025-05-20T01:32:04Z | Merged: 2025-05-20T03:05:04Z

## #️⃣ 연관된 이슈

#3 

## 📝 작업 내용
- Spring Security OAuth2, Client 기능을 활용한 카카오 OIDC 방식 로그인 구현했습니다.
- RestClient 동기 통신으로 유저 정보를 가져올 수 있는 카카오 엔드포인트와 통신하여 유저 정보를 가져오는 기능을 구현했습니다.
- 로그인 성공 시, SuccessHandler에서 유저 식별값인 kakaoId 값을 통해서 유저 정보를 가져옵니다.
  - 만약, 처음 로그인 하는 경우에는 유저 정보를 DB에 저장합니다.  

<br/>

## 💬 리뷰 요구사항
> 리뷰어가 특별히 봐주었으면 하는 부분이 있다면 작성해주세요
- 유저 정보 저장 로직을 Service에서 해야할 지, Handler에서 해야할 지 고민하다가 카카오 유저 정보를 가져오는 Service의 의존성이 커질 것 같아 Handler에서 처리하도록 구현했습니다. 검토해주시면 감사하겠습니다. 
<br/>




Diff:
diff --git a/src/main/java/ssafy/retrip/api/service/oauth/CustomOAuth2UserService.java b/src/main/java/ssafy/retrip/api/service/oauth/CustomOAuth2UserService.java
new file mode 100644
index 0000000..10c0bbd
--- /dev/null
+++ b/src/main/java/ssafy/retrip/api/service/oauth/CustomOAuth2UserService.java
@@ -0,0 +1,53 @@
+package ssafy.retrip.api.service.oauth;
+
+
+import jakarta.transaction.Transactional;
+import java.util.Optional;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest;
+import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService;
+import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
+import org.springframework.security.oauth2.core.oidc.user.OidcUser;
+import org.springframework.stereotype.Service;
+import org.springframework.web.client.RestClient;
+import ssafy.retrip.api.service.oauth.info.KakaoUserInfo;
+import ssafy.retrip.api.service.oauth.request.KakaoUserCreateServiceRequest;
+
+@Service
+@Transactional
+@RequiredArgsConstructor
+public class CustomOAuth2UserService extends OidcUserService {
+
+  private final RestClient restClient;
+
+  private final String USER_INFO_URL = "https://kapi.kakao.com/v1/oidc/userinfo";
+  private final String HEADER_NAME = "Authorization";
+  private final String HEADER_VALUE = "Bearer ";
+
+  @Override
+  public OidcUser loadUser(OidcUserRequest userRequest) throws OAuth2AuthenticationException {
+
+    OidcUser oidcUser = super.loadUser(userRequest);
+
+    KakaoUserInfo info = getKakaoUserInfo(userRequest);
+
+    return CustomOidcUser.builder()
+        .oidcUser(oidcUser)
+        .info(info).build();
+  }
+
+  private KakaoUserInfo getKakaoUserInfo(OidcUserRequest userRequest) {
+
+    String accessToken = userRequest.getAccessToken().getTokenValue();
+
+    KakaoUserCreateServiceRequest request = restClient.get()
+        .uri(USER_INFO_URL)
+        .header(HEADER_NAME, HEADER_VALUE + accessToken)
+        .retrieve()
+        .body(KakaoUserCreateServiceRequest.class);
+
+    return Optional.ofNullable(request)
+        .map(KakaoUserCreateServiceRequest::toKakaoUserInfo)
+        .orElseThrow(IllegalArgumentException::new);
+  }
+}
diff --git a/src/main/java/ssafy/retrip/api/service/oauth/CustomOidcUser.java b/src/main/java/ssafy/retrip/api/service/oauth/CustomOidcUser.java
new file mode 100644
index 0000000..0e8d72d
--- /dev/null
+++ b/src/main/java/ssafy/retrip/api/service/oauth/CustomOidcUser.java
@@ -0,0 +1,54 @@
+package ssafy.retrip.api.service.oauth;
+
+import java.util.Collection;
+import java.util.Map;
+import lombok.Builder;
+import lombok.Getter;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.oauth2.core.oidc.OidcIdToken;
+import org.springframework.security.oauth2.core.oidc.OidcUserInfo;
+import org.springframework.security.oauth2.core.oidc.user.OidcUser;
+import ssafy.retrip.api.service.oauth.info.KakaoUserInfo;
+
+@Getter
+public class CustomOidcUser implements OidcUser {
+
+  private final OidcUser oidcUser;
+  private final KakaoUserInfo info;
+
+  @Builder
+  private CustomOidcUser(OidcUser oidcUser, KakaoUserInfo info) {
+    this.oidcUser = oidcUser;
+    this.info = info;
+  }
+
+  @Override
+  public String getName() {
+    return oidcUser.getName();
+  }
+
+  @Override
+  public OidcIdToken getIdToken() {
+    return oidcUser.getIdToken();
+  }
+
+  @Override
+  public OidcUserInfo getUserInfo() {
+    return oidcUser.getUserInfo();
+  }
+
+  @Override
+  public Map<String, Object> getClaims() {
+    return oidcUser.getClaims();
+  }
+
+  @Override
+  public Map<String, Object> getAttributes() {
+    return oidcUser.getAttributes();
+  }
+
+  @Override
+  public Collection<? extends GrantedAuthority> getAuthorities() {
+    return oidcUser.getAuthorities();
+  }
+}
diff --git a/src/main/java/ssafy/retrip/api/service/oauth/info/KakaoUserInfo.java b/src/main/java/ssafy/retrip/api/service/oauth/info/KakaoUserInfo.java
new file mode 100644
index 0000000..a27bac3
--- /dev/null
+++ b/src/main/java/ssafy/retrip/api/service/oauth/info/KakaoUserInfo.java
@@ -0,0 +1,25 @@
+package ssafy.retrip.api.service.oauth.info;
+
+import static lombok.AccessLevel.PROTECTED;
+
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+@Getter
+@NoArgsConstructor(access = PROTECTED)
+public class KakaoUserInfo {
+
+  private String sub;
+
+  private String email;
+
+  private String nickname;
+
+  @Builder
+  public KakaoUserInfo(String sub, String email, String nickname) {
+    this.sub = sub;
+    this.email = email;
+    this.nickname = nickname;
+  }
+}
diff --git a/src/main/java/ssafy/retrip/api/service/oauth/request/KakaoUserCreateServiceRequest.java b/src/main/java/ssafy/retrip/api/service/oauth/request/KakaoUserCreateServiceRequest.java
new file mode 100644
index 0000000..7c294da
--- /dev/null
+++ b/src/main/java/ssafy/retrip/api/service/oauth/request/KakaoUserCreateServiceRequest.java
@@ -0,0 +1,25 @@
+package ssafy.retrip.api.service.oauth.request;
+
+import static lombok.AccessLevel.PROTECTED;
+
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import ssafy.retrip.api.service.oauth.info.KakaoUserInfo;
+
+@Getter
+@NoArgsConstructor(access = PROTECTED)
+public class KakaoUserCreateServiceRequest {
+
+  private String sub;
+
+  private String email;
+
+  private String nickname;
+
+  public KakaoUserInfo toKakaoUserInfo() {
+    return KakaoUserInfo.builder()
+        .sub(sub)
+        .email(email)
+        .nickname(nickname).build();
+  }
+}
diff --git a/src/main/java/ssafy/retrip/config/RestClientConfig.java b/src/main/java/ssafy/retrip/config/RestClientConfig.java
new file mode 100644
index 0000000..0a008a7
--- /dev/null
+++ b/src/main/java/ssafy/retrip/config/RestClientConfig.java
@@ -0,0 +1,14 @@
+package ssafy.retrip.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.client.RestClient;
+
+@Configuration
+public class RestClientConfig {
+
+  @Bean
+  public RestClient restClient(RestClient.Builder builder) {
+    return builder.build();
+  }
+}
\ No newline at end of file
diff --git a/src/main/java/ssafy/retrip/config/SecurityConfig.java b/src/main/java/ssafy/retrip/config/SecurityConfig.java
new file mode 100644
index 0000000..4278957
--- /dev/null
+++ b/src/main/java/ssafy/retrip/config/SecurityConfig.java
@@ -0,0 +1,55 @@
+package ssafy.retrip.config;
+
+import lombok.RequiredArgsConstructor;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.config.Customizer;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.config.annotation.web.configurers.CsrfConfigurer;
+import org.springframework.security.config.annotation.web.configurers.FormLoginConfigurer;
+import org.springframework.security.config.annotation.web.configurers.HttpBasicConfigurer;
+import org.springframework.security.config.annotation.web.configurers.LogoutConfigurer;
+import org.springframework.security.config.http.SessionCreationPolicy;
+import org.springframework.security.web.SecurityFilterChain;
+import ssafy.retrip.api.service.oauth.CustomOAuth2UserService;
+import ssafy.retrip.handler.OAuth2AuthenticationFailureHandler;
+import ssafy.retrip.handler.OAuth2AuthenticationSuccessHandler;
+
+@Configuration
+@EnableWebSecurity
+@RequiredArgsConstructor
+public class SecurityConfig {
+
+  private final CustomOAuth2UserService customOAuth2UserService;
+  private final OAuth2AuthenticationSuccessHandler oAuth2AuthenticationSuccessHandler;
+  private final OAuth2AuthenticationFailureHandler oAuth2AuthenticationFailureHandler;
+
+  @Bean
+  public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
+
+    http
+        .csrf(CsrfConfigurer::disable)
+        .httpBasic(HttpBasicConfigurer::disable)
+        .formLogin(FormLoginConfigurer::disable)
+        .logout(LogoutConfigurer::disable)
+        .authorizeHttpRequests(auth -> auth
+            .anyRequest().permitAll()
+        );
+
+    http
+        .oauth2Client(Customizer.withDefaults())
+        .oauth2Login(oauth2 -> oauth2
+            .successHandler(oAuth2AuthenticationSuccessHandler)
+            .failureHandler(oAuth2AuthenticationFailureHandler)
+            .userInfoEndpoint(userInfo -> userInfo
+                .oidcUserService(customOAuth2UserService)
+            )
+        );
+
+    http.sessionManagement(
+        session -> session.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED));
+
+    return http.build();
+  }
+}
diff --git a/src/main/java/ssafy/retrip/domain/member/Member.java b/src/main/java/ssafy/retrip/domain/member/Member.java
index ad1863b..67caaf3 100644
--- a/src/main/java/ssafy/retrip/domain/member/Member.java
+++ b/src/main/java/ssafy/retrip/domain/member/Member.java
@@ -20,14 +20,14 @@ public class Member extends BaseEntity {
 
   private String kakaoId;
 
-  private String nickname;
+  private String email;
 
-  private String profileImageUrl;
+  private String nickname;
 
   @Builder
-  private Member(String kakaoId, String nickname, String profileImageUrl) {
+  private Member(String kakaoId, String email, String nickname) {
     this.kakaoId = kakaoId;
+    this.email = email;
     this.nickname = nickname;
-    this.profileImageUrl = profileImageUrl;
   }
 }
diff --git a/src/main/java/ssafy/retrip/domain/member/MemberRepository.java b/src/main/java/ssafy/retrip/domain/member/MemberRepository.java
index bd1e72a..273d15c 100644
--- a/src/main/java/ssafy/retrip/domain/member/MemberRepository.java
+++ b/src/main/java/ssafy/retrip/domain/member/MemberRepository.java
@@ -1,9 +1,11 @@
 package ssafy.retrip.domain.member;
 
+import java.util.Optional;
 import org.springframework.data.jpa.repository.JpaRepository;
 import org.springframework.stereotype.Repository;
 
 @Repository
 public interface MemberRepository extends JpaRepository<Member, Long> {
 
+  Optional<Member> findByKakaoId(String kakaoId);
 }
diff --git a/src/main/java/ssafy/retrip/handler/OAuth2AuthenticationFailureHandler.java b/src/main/java/ssafy/retrip/handler/OAuth2AuthenticationFailureHandler.java
new file mode 100644
index 0000000..f0a9bf1
--- /dev/null
+++ b/src/main/java/ssafy/retrip/handler/OAuth2AuthenticationFailureHandler.java
@@ -0,0 +1,41 @@
+package ssafy.retrip.handler;
+
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.web.DefaultRedirectStrategy;
+import org.springframework.security.web.RedirectStrategy;
+import org.springframework.security.web.authentication.AuthenticationFailureHandler;
+import org.springframework.stereotype.Component;
+import org.springframework.web.util.UriComponentsBuilder;
+
+@Component
+public class OAuth2AuthenticationFailureHandler implements AuthenticationFailureHandler {
+
+  private final RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
+
+  private static final String REDIRECT_URL = "/login/callback";
+  private static final String FRONT_SERVER = "http://localhost:5173";
+
+  @Override
+  public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
+      AuthenticationException exception) throws IOException, ServletException {
+
+    String errorMessage = URLEncoder.encode(exception.getMessage(), StandardCharsets.UTF_8);
+    String targetUrl = getTargetUrl(errorMessage);
+
+    redirectStrategy.sendRedirect(request, response, targetUrl);
+  }
+
+  private String getTargetUrl(String errorMessage) {
+    return UriComponentsBuilder
+        .fromUriString(FRONT_SERVER + REDIRECT_URL)
+        .queryParam("error", errorMessage)
+        .build().toUriString();
+
+  }
+}
diff --git a/src/main/java/ssafy/retrip/handler/OAuth2AuthenticationSuccessHandler.java b/src/main/java/ssafy/retrip/handler/OAuth2AuthenticationSuccessHandler.java
new file mode 100644
index 0000000..2f359ba
--- /dev/null
+++ b/src/main/java/ssafy/retrip/handler/OAuth2AuthenticationSuccessHandler.java
@@ -0,0 +1,55 @@
+package ssafy.retrip.handler;
+
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.oauth2.core.oidc.user.OidcUser;
+import org.springframework.security.web.DefaultRedirectStrategy;
+import org.springframework.security.web.RedirectStrategy;
+import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
+import org.springframework.security.web.savedrequest.HttpSessionRequestCache;
+import org.springframework.security.web.savedrequest.RequestCache;
+import org.springframework.stereotype.Component;
+import ssafy.retrip.domain.member.Member;
+import ssafy.retrip.domain.member.MemberRepository;
+
+@Component
+@RequiredArgsConstructor
+public class OAuth2AuthenticationSuccessHandler implements AuthenticationSuccessHandler {
+
+  private final RequestCache requestCache = new HttpSessionRequestCache();
+  private final RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
+  private final MemberRepository memberRepository;
+
+  private static final String REDIRECT_URL = "/";
+
+  @Override
+  public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
+      Authentication authentication) throws IOException, ServletException {
+
+    OidcUser oidcUser = (OidcUser) authentication.getPrincipal();
+
+    Member member = findMember(oidcUser);
+
+    redirectStrategy.sendRedirect(request, response, REDIRECT_URL);
+  }
+
+  private Member findMember(OidcUser oidcUser) {
+
+    String kakaoId = oidcUser.getSubject();
+    String email = oidcUser.getEmail();
+    String nickname = oidcUser.getAttribute("nickname");
+
+    return memberRepository.findByKakaoId(kakaoId).orElseGet(
+        () -> {
+          Member m = Member.builder()
+              .kakaoId(kakaoId)
+              .email(email)
+              .nickname(nickname).build();
+          return memberRepository.save(m);
+        });
+  }
+}
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
index 08998e8..2dc8300 100644
--- a/src/main/resources/application.yml
+++ b/src/main/resources/application.yml
@@ -25,6 +25,31 @@ spring:
       hibernate:
         format_sql: true
 
+  # Spring Security OAuth2, Client
+  security:
+    oauth2:
+      client:
+        provider:
+          kakao:
+            authorization-uri: ${AUTHORIZATION_URI}
+            token-uri: ${TOKEN_URI}
+            user-info-uri: ${USER_INFO_URI}
+            jwk-set-uri: ${OIDC_JWK_SET_URI}
+            user-name-attribute: sub
+        registration:
+          kakao:
+            authorization-grant-type: ${GRANT_TYPE}
+            redirect-uri: ${LOGIN_REDIRECT_URI}
+            client-name: Kakao
+            client-authentication-method: client_secret_post
+            client-id: ${REST_API_KEY}
+            client-secret: ${CLIENT_SECRET_KEY}
+            scope:
+              - openid              # OIDC
+              - name                # 이름
+              - profile_nickname    # 닉네임
+              - account_email       # 이메일
+
 ---
 spring:
   # Profile Config
@@ -48,3 +73,28 @@ spring:
       hibernate:
         format_sql: true
     database-platform: org.hibernate.dialect.H2Dialect
+
+  # Spring Security OAuth2, Client
+  security:
+    oauth2:
+      client:
+        provider:
+          kakao:
+            authorization-uri: ${AUTHORIZATION_URI}
+            token-uri: ${TOKEN_URI}
+            user-info-uri: ${USER_INFO_URI}
+            jwk-set-uri: ${OIDC_JWK_SET_URI}
+            user-name-attribute: sub
+        registration:
+          kakao:
+            authorization-grant-type: ${GRANT_TYPE}
+            redirect-uri: ${LOGIN_REDIRECT_URI}
+            client-name: Kakao
+            client-authentication-method: client_secret_post
+            client-id: ${REST_API_KEY}
+            client-secret: ${CLIENT_SECRET_KEY}
+            scope:
+              - openid              # OIDC
+              - name                # 이름
+              - profile_nickname    # 닉네임
+              - account_email       # 이메일


Reviews:
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR implements Kakao login and signup using Spring Security OAuth2 with OIDC by introducing configuration changes, custom authentication handlers, and a tailored OAuth2 user service.  
- Added OAuth2 client configuration for Kakao in application.yml  
- Implemented success and failure handlers for OAuth2 authentication  
- Introduced domain, repository, and custom OAuth2 user service classes to support Kakao-based authentication

### Reviewed Changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 2 comments.

<details>
<summary>Show a summary per file</summary>

| File                                                               | Description                                                         |
|--------------------------------------------------------------------|---------------------------------------------------------------------|
| src/main/resources/application.yml                                 | Added OAuth2 client configuration for Kakao; note duplicate sections  |
| src/main/java/ssafy/retrip/handler/OAuth2AuthenticationSuccessHandler.java | Added custom success handler that creates or retrieves a Member record  |
| src/main/java/ssafy/retrip/handler/OAuth2AuthenticationFailureHandler.java | Added custom failure handler generating redirect URLs with error messages  |
| src/main/java/ssafy/retrip/domain/member/MemberRepository.java       | Added repository method for retrieving members by Kakao ID              |
| src/main/java/ssafy/retrip/domain/member/Member.java                  | Updated domain model reflecting changes in attributes                   |
| src/main/java/ssafy/retrip/config/SecurityConfig.java                | Configured security filter chain for OAuth2 login using custom handlers     |
| src/main/java/ssafy/retrip/config/RestClientConfig.java              | Introduced RestClient bean configuration                              |
| src/main/java/ssafy/retrip/api/service/oauth/request/KakaoUserCreateServiceRequest.java | Added a request mapper for converting incoming Kakao user data             |
| src/main/java/ssafy/retrip/api/service/oauth/info/KakaoUserInfo.java    | Added user info model for handling Kakao user attributes                    |
| src/main/java/ssafy/retrip/api/service/oauth/CustomOidcUser.java         | Implemented a custom OIDC user wrapper for additional Kakao attributes       |
| src/main/java/ssafy/retrip/api/service/oauth/CustomOAuth2UserService.java | Customized OAuth2 user service to retrieve additional user info from Kakao API |
</details>





state: COMMENTED
author_association: NONE
submitted_at: 2025-05-20T01:33:52Z

Review comments:
diff_hunk: @@ -48,3 +73,28 @@ spring:
       hibernate:
         format_sql: true
     database-platform: org.hibernate.dialect.H2Dialect
+
+  # Spring Security OAuth2, Client
+  security:
+    oauth2:
+      client:
+        provider:
+          kakao:
+            authorization-uri: ${AUTHORIZATION_URI}
+            token-uri: ${TOKEN_URI}
+            user-info-uri: ${USER_INFO_URI}
+            jwk-set-uri: ${OIDC_JWK_SET_URI}
+            user-name-attribute: sub
+        registration:
+          kakao:
+            authorization-grant-type: ${GRANT_TYPE}
+            redirect-uri: ${LOGIN_REDIRECT_URI}
+            client-name: Kakao
+            client-authentication-method: client_secret_post
+            client-id: ${REST_API_KEY}
+            client-secret: ${CLIENT_SECRET_KEY}
+            scope:
+              - openid              # OIDC
+              - name                # 이름
+              - profile_nickname    # 닉네임
+              - account_email       # 이메일
path: src/main/resources/application.yml
user: login: Copilot
body: The OAuth2 client configuration is duplicated in this file. Consider consolidating the configuration into a single block to improve maintainability and reduce potential inconsistencies.
```suggestion

```
created_at: 2025-05-20T01:33:51Z
author_association: NONE
diff_hunk: @@ -0,0 +1,53 @@
+package ssafy.retrip.api.service.oauth;
+
+
+import jakarta.transaction.Transactional;
+import java.util.Optional;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest;
+import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService;
+import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
+import org.springframework.security.oauth2.core.oidc.user.OidcUser;
+import org.springframework.stereotype.Service;
+import org.springframework.web.client.RestClient;
+import ssafy.retrip.api.service.oauth.info.KakaoUserInfo;
+import ssafy.retrip.api.service.oauth.request.KakaoUserCreateServiceRequest;
+
+@Service
+@Transactional
+@RequiredArgsConstructor
+public class CustomOAuth2UserService extends OidcUserService {
+
+  private final RestClient restClient;
+
+  private final String USER_INFO_URL = "https://kapi.kakao.com/v1/oidc/userinfo";
+  private final String HEADER_NAME = "Authorization";
+  private final String HEADER_VALUE = "Bearer ";
+
+  @Override
+  public OidcUser loadUser(OidcUserRequest userRequest) throws OAuth2AuthenticationException {
+
+    OidcUser oidcUser = super.loadUser(userRequest);
+
+    KakaoUserInfo info = getKakaoUserInfo(userRequest);
+
+    return CustomOidcUser.builder()
+        .oidcUser(oidcUser)
+        .info(info).build();
+  }
+
+  private KakaoUserInfo getKakaoUserInfo(OidcUserRequest userRequest) {
+
+    String accessToken = userRequest.getAccessToken().getTokenValue();
+
+    KakaoUserCreateServiceRequest request = restClient.get()
+        .uri(USER_INFO_URL)
+        .header(HEADER_NAME, HEADER_VALUE + accessToken)
+        .retrieve()
+        .body(KakaoUserCreateServiceRequest.class);
+
+    return Optional.ofNullable(request)
+        .map(KakaoUserCreateServiceRequest::toKakaoUserInfo)
+        .orElseThrow(IllegalArgumentException::new);
path: src/main/java/ssafy/retrip/api/service/oauth/CustomOAuth2UserService.java
user: login: Copilot
body: Consider throwing a more descriptive exception with an informative error message instead of a generic IllegalArgumentException to aid in debugging and clarity.
```suggestion
        .orElseThrow(() -> new IllegalArgumentException("Failed to convert KakaoUserCreateServiceRequest to KakaoUserInfo: request is null."));
```
created_at: 2025-05-20T01:33:52Z
author_association: NONE

PR comments:
user: login: Oilwoo
created_at: 2025-05-20T01:55:11Z
body: 지금 구조에서는 Handler에서 유저를 생성하는게 맞는 것 같습니다!
application.yml 파일에 필요한 변수내용을 노션에 업데이트해주세요! ㅎㅎ
author_association: CONTRIBUTOR
user: login: Bumnote
created_at: 2025-05-20T01:57:42Z
body: > 지금 구조에서는 Handler에서 유저를 생성하는게 맞는 것 같습니다! application.yml 파일에 필요한 변수내용을 노션에 업데이트해주세요! ㅎㅎ

리뷰 감사합니다:> env 내용들에 대해서 notion에 업데이트 시켜놨습니다 😄
author_association: MEMBER