안녕하세요! 👋 최근에 Spring Boot 기반의 소셜 로그인 시스템을 구축하면서 정말 많은 것들을 배웠는데요, 오늘은 그 경험을 여러분과 공유해보려고 합니다.
요즘 웬만한 서비스에는 '구글로 로그인', '카카오로 로그인' 버튼이 기본이잖아요? 사용자 입장에서는 편리하고, 개발자 입장에서는... 음, 처음엔 좀 복잡하게 느껴질 수 있어요. 하지만 한 번 제대로 구현해보면 생각보다 재미있는 기능이에요! 😄

📋 목차
- 프로젝트 소개
- 기술 스택 선택 이유
- OAuth2 구조 설계
- Security 설정의 핵심
- 인증 플로우 완전 정복
- JWT 토큰 관리 전략
- 보안 강화 포인트
- 환경 설정 가이드
- 마무리 및 회고
🎯 프로젝트 소개
이번 프로젝트의 목표는 명확했어요. 사용자 경험은 편리하게, 보안은 탄탄하게! 🛡️
주요 기능들을 살펴보면:
- 다중 소셜 로그인 지원: Google, Kakao, Naver를 모두 지원해서 사용자가 선택의 폭이 넓어요
- JWT 기반 인증: Access Token과 Refresh Token을 활용한 안전한 토큰 관리
- 쿠키 기반 저장: XSS 공격을 방지하는 HttpOnly 쿠키 사용
- 자동 회원가입: 소셜 로그인 시 별도 가입 절차 없이 자동으로 계정 생성
- 약관 동의 프로세스: 법적 요구사항을 충족하는 약관 동의 시스템
처음에는 "소셜 로그인이 그렇게 어려울까?"라고 생각했는데, 막상 해보니 고려할 것들이 정말 많더라고요. 특히 보안 부분에서는 더욱 신경써야 할 것들이 많았어요.
상세 코드 (꼭 보고.. star..부탁)
🛠 기술 스택 선택 이유
기술 스택을 선택할 때는 각각 나름의 이유가 있었어요:
백엔드 기술
- Java 21: 최신 LTS 버전으로 성능 개선과 새로운 기능들을 활용
- Spring Boot 3.4.5: 최신 Spring Security와의 호환성, 향상된 OAuth2 지원
- Spring Security + OAuth2 Client: 소셜 로그인의 표준이자 가장 안정적인 선택
- JWT (jjwt 0.12.3): 무상태 인증을 위한 토큰 기반 시스템
데이터베이스 & 캐시
- MySQL: 관계형 데이터 저장을 위한 신뢰할 수 있는 선택
- Redis: Refresh Token 관리와 세션 캐싱을 위한 고성능 인메모리 DB
기타 도구들
- QueryDSL: 타입 안전한 쿼리 작성
- Lombok: 보일러플레이트 코드 제거
Spring Boot 3.x 버전을 선택한 가장 큰 이유는 OAuth2 관련 설정이 이전 버전보다 훨씬 직관적으로 개선되었기 때문이에요. 특히 OAuth2AuthorizedClientService와 관련된 설정들이 많이 간소화되었거든요! 🚀
🏗 OAuth2 구조 설계
OAuth2 구현에서 가장 중요한 건 각 컴포넌트의 역할을 명확히 하는 것이었어요.
핵심 컴포넌트들
- CustomAuthorizationRequestResolver: OAuth2 인증 요청을 사용자 정의로 처리해요. 여기서 state 파라미터나 추가 스코프를 설정할 수 있거든요.
- CustomOAuth2UserService: 각 소셜 플랫폼에서 받아온 사용자 정보를 우리 시스템에 맞게 변환하는 역할을 해요.
- OAuth2AuthenticationSuccessHandler: 로그인 성공 후의 모든 처리 로직이 들어가요. JWT 토큰 생성, 쿠키 설정, 리다이렉트 등등.
- OAuth2AuthenticationFailureHandler: 로그인 실패 시 사용자에게 적절한 피드백을 제공해요.
각 소셜 플랫폼마다 사용자 정보 형식이 달라서 이 부분에서 좀 고생했어요. Google은 sub, Kakao는 id, Naver는 response.id로 고유 식별자를 제공하거든요. 이런 차이점들을 추상화해서 처리하는 게 핵심이었어요.
// 예시: 소셜 플랫폼별 사용자 정보 추출
public String extractSocialId(String provider, OAuth2User oAuth2User) {
return switch (provider.toLowerCase()) {
case "google" -> oAuth2User.getAttribute("sub");
case "kakao" -> String.valueOf(oAuth2User.getAttribute("id"));
case "naver" -> {
Map<String, Object> response = oAuth2User.getAttribute("response");
yield response != null ? (String) response.get("id") : null;
}
default -> throw new IllegalArgumentException("지원하지 않는 소셜 로그인입니다.");
};
}
🔧 Security 설정의 핵심
Spring Security 설정에서 가장 신경쓴 부분은 REST API에 최적화된 구조를 만드는 것이었어요.
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
// CORS 설정
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
// CSRF 비활성화 (JWT 사용으로 불필요)
.csrf(AbstractHttpConfigurer::disable)
// 폼 로그인 비활성화
.formLogin(AbstractHttpConfigurer::disable)
// 세션을 사용하지 않음
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
// 요청별 권한 설정
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/api/public/**").permitAll()
.requestMatchers("/api/private/**").authenticated()
.anyRequest().denyAll())
// OAuth2 로그인 설정
.oauth2Login(oauth2 -> oauth2
.authorizationEndpoint(endpoint -> endpoint
.authorizationRequestResolver(customAuthorizationRequestResolver))
.userInfoEndpoint(userInfo -> userInfo
.userService(customOAuth2UserService))
.successHandler(oAuth2AuthenticationSuccessHandler)
.failureHandler(oAuth2AuthenticationFailureHandler))
.build();
}
}
여기서 중요한 포인트는 SessionCreationPolicy.STATELESS 설정이에요. 전통적인 세션 기반 인증이 아닌 JWT 토큰 기반으로 동작하기 때문에 서버가 세션 상태를 유지할 필요가 없거든요. 이렇게 하면 서버 확장성도 좋아지고 메모리 사용량도 줄일 수 있어요! 📈
🔄 인증 플로우 완전 정복
소셜 로그인 과정을 단계별로 살펴보면 생각보다 복잡한 과정을 거쳐요:
1단계: 로그인 시작 사용자가 프론트엔드에서 "Google로 로그인" 버튼을 클릭하면, /api/public/oauth2/authorization/google 엔드포인트로 요청이 전송돼요.
2단계: OAuth2 제공자로 리다이렉트 Spring Security가 자동으로 Google OAuth2 인증 페이지로 사용자를 리다이렉트시켜요. 이때 state 파라미터와 함께 콜백 URL도 전달됩니다.
3단계: 인증 및 콜백 사용자가 Google에서 로그인을 완료하면, Google이 인증 코드와 함께 우리 서비스의 콜백 URL로 리다이렉트해요.
4단계: 토큰 교환 및 사용자 정보 획득 서버에서는 받은 인증 코드를 Google Access Token으로 교환하고, 이를 사용해 사용자 정보를 가져와요.
5단계: JWT 토큰 생성 및 응답 사용자 정보를 바탕으로 우리 시스템의 JWT 토큰을 생성하고, HttpOnly 쿠키에 저장해서 응답해요.
신규 사용자 처리 만약 처음 로그인하는 사용자라면 자동으로 회원가입 프로세스가 진행돼요:
- 소셜 계정 정보로 새 사용자 계정 생성
- 기본 역할(ROLE_USER) 할당
- 약관 동의 페이지로 리다이렉트
- 동의 완료 후 메인 페이지로 이동
이 과정에서 사용자는 별도의 회원가입 폼을 작성할 필요가 없어서 정말 편리해요! 😊
🎫 JWT 토큰 관리 전략
JWT 토큰 관리에서는 Access Token과 Refresh Token의 이중 구조를 사용했어요.
토큰 설계 원칙
- Access Token: 30분의 짧은 만료 시간으로 실제 API 접근에 사용
- Refresh Token: 2일의 긴 만료 시간으로 Access Token 갱신에만 사용
토큰 저장 방식 모든 토큰은 HttpOnly 쿠키에 저장해서 XSS 공격으로부터 보호했어요. 또한 Refresh Token은 Redis에도 함께 저장해서 서버 측에서 토큰 유효성을 이중으로 검증할 수 있게 했습니다.
자동 토큰 갱신 프론트엔드에서는 API 요청 시 401 응답을 받으면 자동으로 토큰 갱신을 시도해요:
// 프론트엔드 토큰 갱신 로직 예시
async function refreshTokenIfNeeded(response) {
if (response.status === 401) {
const refreshResponse = await fetch('/api/auth/refresh', {
method: 'POST',
credentials: 'include'
});
if (refreshResponse.ok) {
// 원래 요청 재시도
return fetch(originalRequest);
} else {
// 로그인 페이지로 리다이렉트
window.location.href = '/login';
}
}
return response;
}
이렇게 하면 사용자는 토큰 만료를 전혀 신경 쓸 필요 없이 seamless한 경험을 할 수 있어요! ✨
🛡 보안 강화 포인트
보안은 타협할 수 없는 부분이라 여러 방면으로 신경썼어요:
1. HttpOnly 쿠키 사용 JavaScript에서 토큰에 접근할 수 없도록 해서 XSS 공격을 원천 차단했어요.
2. Redis 기반 토큰 검증 Refresh Token을 Redis에 저장해서 서버 측에서 토큰 유효성을 검증하고, 필요시 즉시 무효화할 수 있게 했어요.
3. 완벽한 로그아웃 처리 로그아웃 시에는 클라이언트 쿠키와 서버 Redis 저장소 모두에서 토큰을 제거해요:
@PostMapping("/logout")
public ResponseEntity<Void> logout(HttpServletResponse response,
HttpServletRequest request) {
// 쿠키 삭제
Cookie deletedAccessCookie = cookieProvider.generateDeletedAccessTokenCookie();
Cookie deletedRefreshCookie = cookieProvider.generateDeletedRefreshTokenCookie();
response.addCookie(deletedAccessCookie);
response.addCookie(deletedRefreshCookie);
// Redis에서 토큰 삭제
String refreshToken = cookieProvider.getRefreshTokenFromCookies(request);
if (refreshToken != null) {
redisService.deleteRefreshToken(refreshToken);
}
// SecurityContext 정리
SecurityContextHolder.clearContext();
return ResponseEntity.ok().build();
}
4. CORS 설정 허용된 도메인에서만 API 접근이 가능하도록 제한했어요.
5. 환경변수를 통한 민감 정보 관리 모든 시크릿 키와 설정 정보는 환경변수로 관리해서 코드에 노출되지 않도록 했어요.
⚙️ 환경 설정 가이드
이 프로젝트를 직접 실행해보고 싶으시다면 다음 단계를 따라주세요:
필수 환경변수 설정
# OAuth2 소셜 로그인 설정
GOOGLE_CLIENT_ID=${YOUR_GOOGLE_CLIENT_ID}
GOOGLE_CLIENT_SECRET=${YOUR_GOOGLE_CLIENT_SECRET}
KAKAO_CLIENT_ID=${YOUR_KAKAO_CLIENT_ID}
KAKAO_CLIENT_SECRET=${YOUR_KAKAO_CLIENT_SECRET}
NAVER_CLIENT_ID=${YOUR_NAVER_CLIENT_ID}
NAVER_CLIENT_SECRET=${YOUR_NAVER_CLIENT_SECRET}
# JWT 서명 키 (충분히 복잡한 랜덤 문자열 사용)
ACCESS_TOKEN_SECRET=${YOUR_ACCESS_TOKEN_SECRET}
REFRESH_TOKEN_SECRET=${YOUR_REFRESH_TOKEN_SECRET}
# 데이터베이스 설정
DATABASE_URL=jdbc:mysql://localhost:3306/oauth_demo
DATABASE_USERNAME=${YOUR_DB_USERNAME}
DATABASE_PASSWORD=${YOUR_DB_PASSWORD}
# Redis 설정
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_PASSWORD=${YOUR_REDIS_PASSWORD}
소셜 로그인 설정 각 플랫폼의 개발자 콘솔에서 OAuth2 애플리케이션을 등록해야 해요:
- Google: Google Cloud Console
- Kakao: Kakao Developers
- Naver: Naver Developers
콜백 URL은 다음과 같이 설정해주세요:
- Google: http://localhost:8080/login/oauth2/code/google
- Kakao: http://localhost:8080/login/oauth2/code/kakao
- Naver: http://localhost:8080/login/oauth2/code/naver
실행 방법
# 1. MySQL, Redis 서버 실행
docker-compose up -d
# 2. 애플리케이션 실행
./gradlew bootRun
# 3. 브라우저에서 테스트
open http://localhost:8080
🎯 마무리 및 회고
이번 소셜 로그인 프로젝트를 진행하면서 정말 많은 걸 배웠어요. 특히 사용자 경험과 보안 사이의 균형을 맞추는 것이 가장 도전적이었던 것 같아요.
잘했던 점들
- OAuth2와 JWT를 조합한 현대적인 인증 시스템 구축
- 다중 소셜 로그인 지원으로 사용자 선택권 확대
- Redis를 활용한 효율적인 토큰 관리
- HttpOnly 쿠키를 통한 XSS 방어
아쉬웠던 점들
- 초기 설정이 복잡해서 러닝커브가 있었음
- 각 소셜 플랫폼별 API 차이점 처리에 시간이 많이 소요됨
- 에러 처리 부분에서 좀 더 세밀한 대응이 필요함
앞으로의 계획 다음 단계로는 이런 기능들을 추가해보려고 해요:
- 소셜 계정 연동/해제 기능
- 권한 기반 접근 제어 (RBAC)
- 사용자 프로필 관리
- 2FA (Two-Factor Authentication) 지원
이 글이 소셜 로그인 구현을 고민하고 계신 분들에게 도움이 되었으면 좋겠어요! 혹시 궁금한 점이나 개선할 부분이 있다면 언제든 댓글로 알려주세요. 함께 성장하는 개발자가 되어요! 🚀
참고: 이 글의 코드는 학습 목적의 데모 애플리케이션입니다. 실제 프로덕션 환경에서는 추가적인 보안 검토와 테스트가 필요해요.
Tags: #SpringBoot #OAuth2 #JWT #소셜로그인 #Spring Security #Google로그인 #카카오로그인 #네이버로그인 #인증 #보안 #REST API #Redis #MySQL #Java21 #백엔드개발 #웹개발 #토큰관리 #쿠키보안
'💻 백엔드' 카테고리의 다른 글
| 🐬 MySQL Replication 구축하기: Docker로 간단하게 시작하는 데이터베이스 복제 (0) | 2025.06.12 |
|---|---|
| 🔍 JMX와 Datadog을 활용한 Java 애플리케이션 모니터링 가이드 (1) | 2025.06.11 |
| 🚀 SELECT FOR UPDATE SKIP LOCKED로 효율적인 작업 큐 구현하기 (5) | 2025.06.09 |
| 👀 Spring Boot에서 AWS S3에 이미지를 업로드하는 3가지 방법 완벽 비교 (1) | 2025.05.31 |
| 🔥 Locust! 첫 Locust 부하테스트 도전기 (0) | 2025.05.30 |