
칭구칭구 프로젝트에서 기존의 ID/PW 로그인 방식과 함께 OAuth2 기반 소셜 로그인(Google, Kakao)을 도입했다.
OAuth2는 사용자의 비밀번호를 직접 관리하지 않고, 외부 인증 제공자(OAuth Provider) 에게 인증을 위임하는 방식으로 칭구칭구에서는 이 인증 결과만 받아 JWT를 발급하여 내부 인증에 활용하는 구조로 설계했다.
OAuth2는 인증(Authentication) 과 인가(Authorization) 를 분리하여 처리할 수 있도록 하는 표준 프로토콜이다.
즉, 서비스가 직접 사용자 비밀번호를 검증하지 않고, 신뢰할 수 있는 외부 서비스(Google, Kakao 등)에 인증을 위임하는 방식이다.
| 구성 요소 | 설명 |
|---|---|
| Resource Owner | 로그인하려는 사용자 |
| Client | 인증을 요청하는 애플리케이션 (우리 서비스) |
| Authorization Server | 인증을 처리하는 서버 (Google, Kakao 등) |
| Resource Server | 사용자 정보를 보유한 서버 |
| Access Token | 인증 후 클라이언트에게 부여되는 접근 권한 토큰 |
이 구조를 통해 서비스는 사용자 비밀번호를 직접 다루지 않아도 되고,
Access Token을 기반으로 필요한 정보에 접근할 수 있다.
보안성 강화
사용자 경험 향상
유지보수 및 확장성
OAuth2의 기본적인 동작 절차는 다음과 같다.
사용자가 “소셜 로그인” 버튼을 클릭하면, 클라이언트(우리 서비스)는
인증 요청을 해당 Provider(Google, Kakao)로 리다이렉트한다.
사용자는 Provider 로그인 페이지에서 인증을 수행한다.
인증이 성공하면, Provider는 Authorization Code를 우리 서버로 전달한다.
서버는 Authorization Code를 이용해 Access Token을 요청한다.
Access Token을 이용해 Provider의 사용자 정보 API(UserInfo) 에 접근한다.
응답받은 사용자 정보를 기반으로 신규 사용자는 회원가입 처리, 기존 사용자는 로그인 처리를 수행한다.
이후 서비스 내부에서는 자체 JWT 토큰을 발급하여 인증 상태를 유지한다.
즉, OAuth2는 외부 인증을 담당하고,
JWT는 내부 인증을 담당하는 구조로 역할이 분리 된다.
[사용자] → [프론트엔드] → [백엔드(Spring Boot)] → [OAuth2 Provider]
↓ ↓
JWT 발급 및 응답 <--- Access Token + UserInfo
칭구칭구 프로젝트에서는 Spring Security를 기반으로 OAuth2 Login 기능과 JWT 인증을 결합하여 구현했다.
Authorization: Bearer <JWT> 헤더로 접근 제어이 구조를 사용하면 로그인 상태 유지가 간단해지고,
OAuth Provider가 만료되더라도 내부 세션(JWT)은 독립적으로 관리된다.
OAuth2 로그인 구조를 적용한 뒤, 기존의 ID/PW 로그인 로직과 충돌하는 부분이 있어 일부 수정이 필요했다.
더 좋은 대처 방법이 있을 수 있겠지만 당장 생각나는 방법으로 진행했다.
리팩토링할 때 더 좋은 방법이 있는지 찾아보기..⭐
| 구분 | OAuth2 | JWT |
|---|---|---|
| 역할 | 외부 서비스의 인증 위임 | 내부 서비스의 인가(Authorization) |
| 토큰 발급 주체 | Google, Kakao 등 Provider | 우리 서버 |
| 사용 목적 | 사용자 신원 확인 | API 접근 권한 검증 |
| 수명 | Provider 정책에 따라 짧음 | 서비스 설정에 따라 조정 가능 |
| 보안 관리 | Provider가 담당 | 우리 서버가 담당 |
어리바리스타하면서 연동을 진행한 덕분에 중간중간 캡쳐하는 것을 잊었다.. 하하하! 간략하게 작성하자면 아래와 같다.
id, email, nickname 등을 수신 후 내부 DB에 저장profile, email 스코프 요청https://www.googleapis.com/oauth2/v2/userinfo 호출카카오 소셜 로그인은 다행스럽게도 무난하게 적용 되었는데, Google 소셜 로그인 연동할 때는 꽤 많은 오류를 겪었다.. (눈에서 땀이..)
| 구분 | 증상 | 원인 | 해결 방법 |
|---|---|---|---|
| ① | 500 Internal Server Error (Vercel 프론트 요청 시) | 백엔드 서버에서 Google 인증 후 리디렉션 URI 불일치 (redirect_uri_mismatch) | Google Cloud Console의 OAuth2 클라이언트 설정에서 승인된 리디렉션 URI를 실제 프론트 배포 주소(https://chinguchingu.vercel.app/login/oauth2/code/google)로 수정 |
| ② | "invalid_grant" 또는 "invalid_client" 오류 | - 환경 변수(Google Client ID/Secret) 불일치 - 이미 사용된 Authorization Code를 재요청 | Google OAuth 환경 변수를 동일하게 맞추고, 인증 과정에서 한 번 사용된 Code를 재요청하지 않도록 수정 |
| ③ | Security FilterChain에서 인증이 중단됨 | JwtAuthenticationFilter가 OAuth2LoginFilter보다 먼저 실행되어 OAuth2 요청도 JWT 검증 대상으로 처리함 | SecurityConfig에서 addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) 로 위치 재조정 및 permitAll() 경로에 /oauth2/** 추가 |
| ④ | 로그인 성공 후 리디렉션 실패 | OAuth2SuccessHandler 에서 Redirect URL이 하드코딩되거나 환경변수 누락 | frontend.url을 application.properties 환경 변수로 등록하고, response.sendRedirect(frontendUrl + "/oauth-success?token=" + jwtToken) 형식으로 수정 |
| ⑤ | UserInfo 응답 파싱 에러 (NullPointerException) | Google과 Kakao의 응답 필드명이 달라 공통 파서가 null 필드를 참조함 (Google→ sub, Kakao→ id, 중첩 JSON 구조 차이) | Provider별 DTO(GoogleUserInfo, KakaoUserInfo) 구현 후 OAuth2UserInfoFactory로 추상화. 이때 응답을 공통 형태로 매핑하는 CustomOAuth2User 를 도입하여 provider, providerId, email, nickname, profileImage 필드를 표준화. 이 구조로 NPE 및 필드 불일치 문제 해결 |
이 중 가장 복잡했던 문제는 ⑤ UserInfo 파싱 에러였다.
Google 응답은sub,name,picture같이 단일 필드로 오지만,
Kakao는kakao_account.email및properties.nickname처럼 중첩 JSON 구조를 사용한다.
두 Provider를 같은 로직으로 처리하면서NullPointerException이 발생했으나,
이후CustomOAuth2User를 설계하여 안정적으로 사용자 정보를 통합할 수 있게 되었다.
처음에는 OAuth2를 깊게 생각하지 않고 단순히 소셜 로그인 기능 을 적용할 때 사용 하는 것으로 여겼었지만 이번 프로젝트를 통해 그것이 “인증 책임을 위임하는 구조적 설계” 임을 이해할 수 있게 되었다.
Spring Security의 인증 흐름을 따라가며
OAuth2 → Access Token 교환 → 사용자 정보 → JWT 발급까지의 과정을 직접 구성하면서,
인증과 인가를 분리하는 것이 서비스 안정성과 확장성을 높이는 방법이라는 것을 알 수 있었다.
Kakao와 Google처럼 응답 스펙이 다른 Provider를 통합하기 위해
공통 인터페이스를 설계하면서, 유지보수성과 유연성을 동시에 고려하는 설계 능력이 중요하다는 것을 깨달을 수 있었다.
이번에는 오류를 수정하는데 급급했지만 다음에 또 기회가 된다면, 이론적으로 좀 더 공부하며 차분하게 진행해보고 싶다.