💻 코드 구현
저번 application.properties 설정에 이어서 이번에는 본격적으로 코드를 구현해보도록 하겠습니다.
이제 전체적인 코드 구조를 살펴보겠습니다. 각 파일의 역할을 이해하면서 전체 흐름을 파악하는 것이 좋습니다.
1. KakaoProperties: 설정 정보 관리
application.yml에 작성한 카카오 관련 설정들을 자바 객체로 안전하게 가져오기 위한 클래스입니다. @ConfigurationProperties 어노테이션이 이 역할을 수행합니다.
package com.duck.moodflix.test.config;
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Getter
@Setter
@Component
@ConfigurationProperties(prefix = "kakao") // yml 파일에서 "kakao" prefix를 가진 값들을 매핑
public class KakaoProperties {
private String clientId;
private String redirectUri;
}
2. SecurityConfig: 보안 설정
스프링 시큐리티 설정을 담당합니다. 이번 예제에서는 카카오 로그인만 사용하므로 모든 요청을 허용하고 기본적인 form 로그인은 비활성화하는 간단한 설정을 사용합니다.
package com.duck.moodflix.test.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
public class SecurityConfig {
// 비밀번호 암호화 (로컬 회원가입 구현 시 재사용 가능)
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable()) // CSRF 보호 비활성화 (세션 방식에서는 권장되지 않지만, 테스트 편의를 위해)
.authorizeHttpRequests(auth -> auth
.anyRequest().permitAll() // 모든 경로에 대해 인증 없이 접근 허용
)
.formLogin(form -> form.disable()) // 기본 폼 로그인 비활성화
.logout(logout -> logout
.logoutUrl("/auth/logout")
.logoutSuccessUrl("/")
.permitAll()
);
return http.build();
}
}
💡 Tip: .anyRequest().permitAll()은 개발 초기 단계에서 편리하지만 실제 서비스에서는 각 API의 성격에 맞게 접근 권한을 설정해야 합니다.
3. KakaoDto: 카카오 API 응답 데이터 처리
카카오 API는 JSON 형태로 응답을 보내줍니다. 이 데이터를 자바 객체로 쉽게 변환하기 위해 DTO(Data Transfer Object)를 정의합니다.
package com.duck.moodflix.test.dto;
import lombok.Getter;
public class KakaoDto {
// 1. 토큰 받기 응답 DTO
@Getter
public static class OAuthToken {
private String access_token;
private String token_type;
private String refresh_token;
private int expires_in;
private String scope;
private int refresh_token_expires_in;
}
// 2. 사용자 정보 받기 응답 DTO
@Getter
public static class KakaoProfile {
private Long id;
private String connected_at;
private Properties properties;
private KakaoAccount kakao_account;
@Getter
public class Properties {
private String nickname;
}
@Getter
public class KakaoAccount {
private String email;
private Profile profile;
@Getter
public class Profile {
private String nickname;
}
}
}
}
KakaoProfile 내부 클래스 구조는 카카오가 보내주는 JSON 응답의 구조와 정확히 일치해야 데이터 매핑이 정상적으로 이루어집니다.
4. KakaoUtil: 카카오 API 통신 담당
실제로 카카오 서버와 HTTP 통신을 하는 로직을 담은 유틸리티 클래스입니다. WebClient를 사용해 비동기적으로 API를 호출합니다.
package com.duck.moodflix.test.util;
import com.duck.moodflix.test.config.KakaoProperties;
import com.duck.moodflix.test.dto.KakaoDto;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Component;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.client.WebClient;
@Component
@RequiredArgsConstructor
public class KakaoUtil {
private final KakaoProperties kakaoProperties;
// 1. 인가 코드로 Access Token 요청
public KakaoDto.OAuthToken requestToken(String code) {
WebClient webClient = WebClient.builder().baseUrl("https://kauth.kakao.com").build();
MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
params.add("grant_type", "authorization_code");
params.add("client_id", kakaoProperties.getClientId());
params.add("redirect_uri", kakaoProperties.getRedirectUri());
params.add("code", code);
// ★★★ 디버깅 포인트: 요청 파라미터 확인 ★★★
System.out.println("KAKAO TOKEN REQUEST PARAMS");
System.out.println("client_id: " + kakaoProperties.getClientId());
System.out.println("redirect_uri: " + kakaoProperties.getRedirectUri());
return webClient.post()
.uri("/oauth/token")
.header(HttpHeaders.CONTENT_TYPE, "application/x-www-form-urlencoded")
.body(BodyInserters.fromFormData(params))
.retrieve() // 응답을 받기 시작
.bodyToMono(KakaoDto.OAuthToken.class) // 응답 본문을 OAuthToken 객체로 변환
.block(); // 비동기 처리를 동기적으로 대기
}
// 2. Access Token으로 사용자 프로필 정보 요청
public KakaoDto.KakaoProfile requestProfile(KakaoDto.OAuthToken oAuthToken) {
WebClient webClient = WebClient.builder().baseUrl("https://kapi.kakao.com").build();
return webClient.get()
.uri("/v2/user/me")
.header(HttpHeaders.AUTHORIZATION, "Bearer " + oAuthToken.getAccess_token())
.retrieve()
.bodyToMono(KakaoDto.KakaoProfile.class)
.block();
}
}
5. KaKaoService: 로그인 비즈니스 로직
가장 핵심적인 로직이 담긴 서비스 클래스입니다. 컨트롤러로부터 인가 코드를 받아 전체 로그인 과정을 처리합니다.
로그인 처리 순서:
- 받아온 인가 코드로 KakaoUtil을 통해 카카오 서버에 Access Token을 요청합니다.
- 발급받은 Access Token으로 KakaoUtil을 통해 사용자 프로필 정보를 요청합니다.
- 받아온 사용자 이메일로 우리 DB에서 회원을 조회합니다.
- 만약 회원이 없다면, 받아온 프로필 정보(이메일, 닉네임)로 신규 회원가입 처리를 합니다.
- 조회 또는 생성된 회원 정보를 세션(Session)에 저장하여 로그인 상태를 유지시킵니다.
package com.duck.moodflix.test.service;
import com.duck.moodflix.test.dto.KakaoDto;
import com.duck.moodflix.test.util.KakaoUtil;
import com.duck.moodflix.users.domain.entity.User;
import com.duck.moodflix.users.repository.UserRepository;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpSession;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class KaKaoService {
private final KakaoUtil kakaoUtil;
private final UserRepository userRepository;
public User oAuthLogin(String accessCode, HttpServletRequest request) {
// 1. 카카오로부터 Access Token 발급받기
KakaoDto.OAuthToken oAuthToken = kakaoUtil.requestToken(accessCode);
// 2. Access Token으로 사용자 프로필 정보 가져오기
KakaoDto.KakaoProfile profile = kakaoUtil.requestProfile(oAuthToken);
String email = profile.getKakao_account().getEmail();
String nickname = profile.getKakao_account().getProfile().getNickname();
// 3. 우리 DB에 회원 정보가 있는지 확인 (없으면 신규 가입)
User user = userRepository.findByEmail(email)
.orElseGet(() -> userRepository.save(
User.builder()
.email(email)
.name(nickname)
.provider("kakao") // 소셜 로그인 제공자 정보
.build()
));
// 4. 세션에 사용자 정보 저장 (로그인 처리)
HttpSession session = request.getSession(true); // 세션이 없으면 새로 생성
session.setAttribute("loginUser", user);
return user;
}
}
6. AuthController: API 엔드포인트
마지막으로, 클라이언트(브라우저)와 상호작용할 API 엔드포인트를 정의합니다.
package com.duck.moodflix.test.controller;
import com.duck.moodflix.test.service.KaKaoService;
import com.duck.moodflix.users.domain.entity.User;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
@RequiredArgsConstructor
@RequestMapping("/auth")
public class AuthController {
private final KaKaoService kakaoService;
// 카카오 로그인 콜백 (Redirect URI)
// GET /auth/login/kakao?code=...
@GetMapping("/login/kakao")
public ResponseEntity<?> kakaoLogin(@RequestParam("code") String code, HttpServletRequest request) {
User user = kakaoService.oAuthLogin(code, request);
// 로그인 성공 후 프론트엔드로 유저 정보를 반환하거나, 리다이렉트할 수 있음
return ResponseEntity.ok(user);
}
// 로그인 상태 확인용 API (세션 기반)
@GetMapping("/me")
public ResponseEntity<?> getLoginUser(HttpServletRequest request) {
User loginUser = (User) request.getSession().getAttribute("loginUser");
if (loginUser == null) {
return ResponseEntity.status(401).body("로그인이 필요합니다.");
}
return ResponseEntity.ok(loginUser);
}
// 로그아웃 (세션 무효화)
@PostMapping("/logout")
public ResponseEntity<?> logout(HttpServletRequest request) {
request.getSession().invalidate();
return ResponseEntity.ok("로그아웃 되었습니다.");
}
}
✅ 최종 테스트
이제 애플리케이션을 실행하고 테스트하는 단계가 남았습니다. 저는 포트스맨으로 테스트 했습니다.
https://kauth.kakao.com/oauth/authorize?response_type=code&client_id={REST_API_키}&redirect_uri={Redirect_URI}
- {여러분의_REST_API_키}: 카카오 디벨로퍼스에서 발급받은 REST API 키
- {여러분의_Redirect_URI}: application.yml에 설정한 http://localhost:8080/auth/login/kakao
위 주소로 접속하면 카카오 로그인 및 동의 화면이 나타납니다. 동의하고 계속하기를 누르면, 설정한 Redirect URI로 이동하면서 code 파라미터가 함께 전달됩니다.
서버의 AuthController가 이 요청을 받아 로그인 처리를 수행하고 성공적으로 사용자 정보가 반환되면 구현이 완료된 것입니다.
로그인 후 http://localhost:8080/auth/me로 접속하여 로그인 정보가 잘 유지되는지 확인하고 /auth/logout으로 로그아웃도 테스트해보세요.
'백엔드 스터디' 카테고리의 다른 글
| 스프링 OAuth2 로그인 구현하기 4 - JWT (2) | 2025.08.17 |
|---|---|
| 스프링 OAuth2 로그인 구현하기 3 - JWT (1) | 2025.08.17 |
| 스프링 OAuth2 로그인 구현하기 1 (2) | 2025.08.04 |
| 디스패처 서블릿이란? (0) | 2025.04.02 |
| 서블릿이란? (0) | 2025.04.01 |