https://github.com/TheOne-team-1/TheOne-Bottle-Shop
GitHub - TheOne-team-1/TheOne-Bottle-Shop: 주류 커머스 플랫폼 백엔드 구현하기
주류 커머스 플랫폼 백엔드 구현하기. Contribute to TheOne-team-1/TheOne-Bottle-Shop development by creating an account on GitHub.
github.com
Spring Boot 3 + Docker 환경에서 Google OAuth2 소셜 로그인 연동 트러블슈팅
1. 개요
Spring Boot 3 기반의 주류 커머스 플랫폼 TheOne-Bottle-Shop 프로젝트에 구글 소셜 로그인을 추가하던 중, 애플리케이션 기동 실패부터 런타임 예외까지 총 5가지 오류가 순차적으로 발생했다. 각 오류는 Spring Security OAuth2의 자동 구성 원리, 프로파일별 설정 파일 로드 순서, 제네릭 타입 처리 방식에 대한 이해 부족에서 비롯된 것으로, 해결 과정을 통해 관련 개념을 깊이 있게 학습할 수 있었다.
2. 문제 상황 요약
#오류발생 시점01ClientRegistrationRepository 빈 주입 실패 → 앱 기동 불가애플리케이션 시작 시02Active Profile(dev)과 OAuth2 설정 파일 불일치애플리케이션 시작 시03401 invalid_client — 구글 콘솔 redirect URI 미등록구글 로그인 시도 시04ClassCastException: String cannot be cast to char[]구글 계정 선택 후 콜백 처리 시05토큰이 존재하지 않습니다 (A005) — SecurityConfig permitAll 누락로그인 성공 후 리다이렉트 시
3. 원인 분석 및 해결 과정
3.1 ClientRegistrationRepository 빈 생성 실패
에러 로그
Parameter 5 of constructor in SecurityConfig required a bean of type
'org.springframework.security.oauth2.client.registration.ClientRegistrationRepository' that could not be found.
원인
SecurityConfig 생성자에서 ClientRegistrationRepository를 직접 필드 주입(@RequiredArgsConstructor)받고 있었다. Spring Boot는 application.yml에 spring.security.oauth2.client.registration 설정이 존재할 때만 해당 빈을 자동 생성한다. yml 설정이 없는 상태에서 빈을 주입받으려 하면 기동 시점에 NoSuchBeanDefinitionException이 발생하며 애플리케이션이 시작되지 않는다.
해결
1. SecurityConfig에서 불필요한 직접 주입 제거
java// 제거 — 직접 주입은 불필요
private final ClientRegistrationRepository clientRegistrationRepository;
// oauth2Login() 내부에서도 제거
.clientRegistrationRepository(clientRegistrationRepository)
// yml에 설정만 있으면 Spring Boot가 자동으로 빈 생성 및 연결
.oauth2Login(oauth2 -> oauth2
.userInfoEndpoint(userInfo -> userInfo.userService(customOAuth2UserService))
.successHandler(oAuth2SuccessHandler)
)
2. application-dev.yml에 OAuth2 설정 추가
yamlspring:
security:
oauth2:
client:
registration:
google:
client-id: ${GOOGLE_CLIENT_ID}
client-secret: ${GOOGLE_CLIENT_SECRET}
scope:
- email
- profile
provider:
google:
issuer-uri: https://accounts.google.com
Insight
Spring Boot의 OAuth2 자동 구성은 yml 설정의 존재 여부에 따라 조건부로 활성화된다(@ConditionalOnMissingBean). ClientRegistrationRepository를 직접 주입받는 것은 오히려 자동 구성을 방해할 수 있으며, yml 설정만 올바르게 존재하면 Spring이 알아서 빈을 생성하고 oauth2Login()에 연결해준다.
3.2 Active Profile과 설정 파일 불일치
원인
IntelliJ Run Configuration의 Active Profiles가 dev로 설정되어 있어 Spring Boot는 application-dev.yml을 로드한다. OAuth2 설정을 application-test.yml에만 추가했기 때문에 실제 실행 환경에서는 해당 설정이 로드되지 않았다.
파일로드 여부비고application.yml 항상 로드공통 설정application-dev.yml profile=dev 시 로드dev 전용 설정application-test.yml profile=dev 시 미로드test 전용 설정
해결
실제 실행 프로파일인 application-dev.yml에 OAuth2 설정을 추가하고, 민감한 값은 .env.dev를 통해 환경변수로 주입했다.
bash# .env.dev — 환경변수로 분리하여 보안 강화
GOOGLE_CLIENT_ID=your-client-id.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=GOCSPX-xxxxxxxxxxxxx
Insight
오류 발생 시 "어떤 yml 파일이 실제로 로드되고 있는가"를 가장 먼저 확인해야 한다. 애플리케이션 시작 로그의 `The following profiles are active:` 항목을 보면 현재 활성화된 프로파일을 바로 확인할 수 있다. client-id/secret 등 민감한 정보는 절대 코드에 하드코딩하지 않고 환경변수로 분리하며, `.env` 파일은 반드시 `.gitignore`에 등록해야 한다.
---
3.3 401 invalid_client — 구글 콘솔 redirect URI 미등록
**에러**
```
액세스 차단됨: 승인 오류 — The OAuth client was not found.
401 오류: invalid_client
```
원인
Google Cloud Console의 OAuth 2.0 클라이언트 설정에 **승인된 리디렉션 URI**가 등록되지 않았다. 구글은 OAuth2 인증 후 콜백을 전송할 때 사전에 등록된 URI로만 리다이렉트를 허용한다. Spring Security OAuth2의 기본 콜백 URI 패턴은 `/login/oauth2/code/{registrationId}`이므로, 이 경로가 콘솔에 등록되지 않으면 인증 요청 자체가 거부된다.
해결
Google Cloud Console → API 및 서비스 → 사용자 인증 정보에서 아래 URI를 등록했다.
| 항목 | 등록값 |
|------|--------|
| 승인된 JavaScript 원본 | http://localhost:8080 |
| 승인된 리디렉션 URI | http://localhost:8080/login/oauth2/code/google |
Insight
배포 환경에서는 로컬 URI 외에 실제 도메인 URI도 추가로 등록해야 한다. 또한 팀 프로젝트 협업 시 client-id/secret이 채팅이나 문서에 노출됐다면 즉시 재발급이 필요하다. 노출된 키는 보안 위협의 대상이 되며, 구글 콘솔에서 간단히 재생성할 수 있다.
---
3.4 ClassCastException: String cannot be cast to char[]
**에러 로그**
```
java.lang.ClassCastException: class java.lang.String cannot be cast to class [C
at OAuth2SuccessHandler.onAuthenticationSuccess(OAuth2SuccessHandler.java:31)
원인
Java에서 [C는 char[] 타입을 의미한다. OAuth2User.getAttribute(key)는 제네릭 반환 타입 <A>를 가지며, 컴파일 타임에 타입이 결정되지 않고 런타임에 캐스팅이 발생한다. String.valueOf()를 사용할 경우 일부 환경에서 내부적으로 char[]로의 캐스팅을 시도하며 ClassCastException이 발생할 수 있다.
java// 문제 코드
String email = String.valueOf(oAuth2User.getAttribute("email"));
해결
getAttributes()로 Map을 직접 참조한 후 toString()을 명시적으로 호출하는 방식으로 변경했다.
java// OAuth2SuccessHandler — null 안전 처리 포함
Object emailAttr = oAuth2User.getAttributes().get("email");
String email = emailAttr != null ? emailAttr.toString() : null;
// CustomOAuth2UserService — 명시적 toString() 호출
String email = attributes.get("email").toString();
String name = attributes.get("name").toString();
String providerId = attributes.get("sub").toString();
Insight
OAuth2User.getAttribute()의 반환 타입은 <A>로 런타임 시점에 타입이 결정된다. 소셜 로그인 속성값을 처리할 때는 String.valueOf() 대신 getAttributes().get(key).toString() 방식을 사용하는 것이 타입 안전성 측면에서 더 명확하다. 제네릭 메서드의 반환값을 다룰 때는 항상 런타임 타입 캐스팅 위험을 고려해야 한다.
3.5 로그인 성공 후 토큰 인증 오류 (A005)
에러
json{"success":false,"status":"A005","message":"토큰이 존재하지 않습니다.","data":null}
원인
OAuth2 인증 성공 후 OAuth2SuccessHandler가 /oauth2/redirect?accessToken=...로 리다이렉트를 수행하는데, 해당 경로가 SecurityConfig의 permitAll() 목록에 포함되지 않았다. JwtAuthenticationFilter가 모든 요청에 대해 토큰을 검사하므로, 리다이렉트 시점에 아직 클라이언트가 토큰을 보유하지 않은 상태에서 인증 실패가 발생했다.
해결
OAuth2 관련 경로와 테스트용 경로를 permitAll()에 추가했다.
java.requestMatchers(
"/api/signup", "/api/login", "/api/signup/admin",
"/login/oauth2/**", // OAuth2 콜백 경로
"/oauth2/redirect", // 로그인 성공 후 리다이렉트 대상
"/index.html", "/", // 테스트용 정적 페이지
"/h2-console/**" // 개발 환경 H2 Console
).permitAll()
H2 Console은 iframe을 사용하므로 frameOptions도 함께 비활성화했다.
javahttp
.csrf(csrf -> csrf.disable())
.headers(headers -> headers
.frameOptions(frame -> frame.disable())) // H2 Console iframe 허용
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
```
Insight
OAuth2 인증 플로우에서 리다이렉트 대상 URI는 JWT가 발급되기 이전 단계다. 따라서 `/login/oauth2/**`뿐만 아니라 성공 핸들러가 리다이렉트하는 최종 URI도 반드시 `permitAll()`에 포함해야 한다. 인증 필터의 동작 범위를 정확히 이해하고 설계하는 것이 중요하다.
---
4. 최종 동작 플로우 및 결과
```
1. GET /oauth2/authorization/google 요청
↓
2. 구글 로그인 페이지로 자동 리다이렉트
↓
3. CustomOAuth2UserService.loadUser() 실행
- DB에 해당 이메일 없음 → Member.createSocial() → 자동 회원가입
- DB에 해당 이메일 있음 → 기존 회원 조회 → 로그인
- SocialAuth 테이블에 provider/providerId 저장
↓
4. OAuth2SuccessHandler.onAuthenticationSuccess() 실행
- JWT accessToken, refreshToken 발급
↓
5. /index.html?accessToken=eyJ...&refreshToken=eyJ... 리다이렉트
항목결과구글 OAuth2 인증 및 콜백 처리
정상 동작신규 사용자 자동 회원가입
정상 동작기존 사용자 로그인 및 JWT 발급
정상 동작배포 환경 redirect URI 등록
배포 시 추가 필요프론트엔드 연동
연동 시 URL 변경 필요
5. 인사이트
5.1 Spring Boot 자동 구성에 대한 이해
이번 트러블슈팅을 통해 Spring Boot의 자동 구성(Auto Configuration)이 어떤 조건에서 활성화되는지 체감적으로 이해할 수 있었다. ClientRegistrationRepository는 yml 설정 존재 여부에 따라 조건부 빈으로 생성되며, 불필요한 직접 주입은 오히려 자동 구성을 방해할 수 있다.
5.2 보안 설계 관점
OAuth2 플로우에서 각 단계별로 어떤 경로가 인증을 필요로 하는지 명확하게 파악하고 SecurityConfig를 설계해야 한다. 또한 client-secret 등 민감한 정보를 환경변수로 분리하고 .gitignore로 관리하는 것은 협업 환경에서 기본 중의 기본이다.
6. 결론
OAuth2 소셜 로그인 연동은 단순히 라이브러리를 추가하는 것으로 끝나지 않는다. Spring Security의 필터 체인 동작 방식, 프로파일별 설정 파일 로드 원리, 구글 콘솔의 보안 정책, 제네릭 타입의 런타임 동작까지 다양한 레이어의 지식이 유기적으로 연결된다. 오류를 만날 때마다 에러 메시지를 단서로 삼아 원인을 계층적으로 분석하는 습관이 디버깅 속도를 결정한다는 것을 다시 한번 체감했다.
'spring_2기[본캠프] > 과제' 카테고리의 다른 글
| [과제] Spring K사 서버 개발 프로젝트 Day 1 (0) | 2026.04.03 |
|---|---|
| [과제] Spring 플러스 프로젝트 Day 11 (0) | 2026.03.24 |
| [과제] Spring 플러스 프로젝트 Day 9 (0) | 2026.03.20 |
| [과제] Spring 플러스 프로젝트 Day 8 (0) | 2026.03.19 |
| KODEKATA알고리즘 CODEKATA DAY41 (0) | 2026.03.18 |