로그인 화면의 주도권을 백과 프론트 중 어디에서 가지고 있을 것인가에 대해서 auth2 로그인을 할 때 구현 방식이 달라진다.
서버 사이드 리다이렉트(302 응답)과 JSON으로 URL 반환(프론트에서 이동) 이렇게 2가지의 방식으로 분류할 수 있다.
최근 SPA(React, vue등)에서는 후자의 방식이 더 좋다고 한다. 왜 그런지 알아보자
1. 두 방식의 작동 원리 비교
1-1. 서버 사이드 리다이렉트 (Server-Side Redirect) - 전통적인 방식
Spring Security의 기본 설정(oauth2-client)이 주로 이 방식이다.
- Front: 사용자가 '구글 로그인' 버튼( <a> 태그)을 클릭한다.
- Back: 요청을 받자마자 HTTP 302 Redirect 응답과 함께 Location: https://accounts.google.com/... 헤더를 내려보낸다.
- Browser: 브라우저가 자동으로 해당 Location으로 이동한다.
1-2. URL 반환 방식 (JSON Response) - SPA 친화적 방식
REST API 설계 철학에 더 가깝다.
- Front: 사용자가 버튼을 클릭하면 axios나 fetch로 백엔드 API를 호출한다.
- GET /api/auth/login-urls
- Back: 리다이렉트 대신 JSON 데이터를 응답한다.
- { "google": "https://accounts.google.com/...", "github": "https://github.com/login/..." }
- Front: 응답받은 URL을 이용해 자바스크립트로 페이지를 이동시킨다.
- window.location.href = response.data.google;
2. 왜 SPA에서는 URL 반환 방식이 더 유연한가?
SPA(Single Page Application) 환경에서 JSON 반환 방식이 선호되는 핵심 이유는 "제어권(Control)"이 프론트엔드에 있기 때문이다.
2-1. 매끄러운 UX (로딩 처리 등)
- Redirect 방식: 사용자가 링크를 누르는 순간 브라우저가 하얗게 변하거나, 구글 페이지로 넘어가는 동안 잠시 멈춘 듯한 느낌을 받을 수 있다.
- JSON 방식: 프론트엔드에서 API를 호출하는 동안 "로그인 페이지로 이동 중..." 같은 로딩 스피너를 띄울 수 있다. 사용자는 시스템이 반응하고 있다고 느낀다.
2-2. 로그인 전 상태 저장 (State Preservation)
- 사용자가 보고 있던 페이지, 장바구니 상태, 혹은 "로그인 후 돌아올 페이지 경로(redirectUrl)" 등을 localStorage나 sessionStorage에 저장하는 로직을 이동 직전에 수행하기 편하다.
- 예: saveCurrentPath(); window.location.href = url;
2-3. 팝업(Popup) 로그인 구현 가능
- JSON으로 URL만 받아오면, 프론트엔드 개발자는 이를 window.location.href로 현재 창에서 열지, window.open()으로 팝업 창에서 열지 선택할 수 있다.
- 서버가 강제로 302를 보내면 팝업 처리가 까다로워진다.
2-4. 에러 핸들링의 통일성
- 만약 OAuth 공급자(구글, 깃허브) 서버 설정 문제로 URL 생성에 실패했다면?
- Redirect: 서버 에러 페이지(Whitelabel Error Page)가 뜰 수 있다.
- JSON: 백엔드가 500 에러와 메시지를 JSON으로 주면, 프론트엔드는 "일시적인 오류입니다."라는 Toast 메시지나 모달을 띄워 아름답게 대처할 수 있다.
3. 비교 요약표
| 특징 | 서버 사이드 리다이렉트 (302) | URL 반환 (JSON) |
| 구현 난이도 | 쉬움 (Spring Security 기본 동작) | 중간 (URL 생성 컨트롤러 별도 작성 필요) |
| 제어권 | 브라우저/백엔드 | 프론트엔드 (React/Vue) |
| 페이지 이동 | 즉시 이동 (깜빡임 발생 가능) | JS로 제어 (window.location) |
| 용도 | SSR(Thymeleaf, JSP), 간단한 프로젝트 | SPA(React, Vue), 앱(Flutter, iOS) |
| 확장성 | 낮음 | 높음 (팝업, 모바일 웹뷰 등 대응 유리) |
4. 코드 예시 (Spring Boot)
만약 JSON 방식을 채택한다면, Spring Security가 자동으로 해주는 리다이렉트를 막고, 직접 URL을 생성해서 내려주는 컨트롤러가 필요하다.
@RestController
@RequestMapping("/api/auth")
@RequiredArgsConstructor
public class AuthController {
private final ClientRegistrationRepository clientRegistrationRepository;
// 로그인 링크 목록을 JSON으로 반환
@GetMapping("/login-urls")
public ResponseEntity<Map<String, String>> getLoginUrls() {
Map<String, String> loginUrls = new HashMap<>();
// Google URL 생성
String googleUrl = getAuthorizationUrl("google");
loginUrls.put("google", googleUrl);
// Github URL 생성
String githubUrl = getAuthorizationUrl("github");
loginUrls.put("github", githubUrl);
return ResponseEntity.ok(loginUrls);
}
private String getAuthorizationUrl(String provider) {
ClientRegistration clientRegistration = clientRegistrationRepository.findByRegistrationId(provider);
// 실제로는 state, nonce 등 파라미터 구성 로직이 더 필요할 수 있습니다.
// Spring Security 내부 로직을 참고하여 URL을 빌드합니다.
return "https://accounts.google.com/o/oauth2/v2/auth?client_id="
+ clientRegistration.getClientId()
+ "&response_type=code&scope=..."
+ "&redirect_uri=" + clientRegistration.getRedirectUri();
}
}
만약 리다이렉트 방식을 선택한다면?
// AuthController.java
// 코드 없음. 텅 비어 있음.
// 스프링 시큐리티가 알아서 하니까!
- 리다이렉트 방식: "아 귀찮아, 로그인 페이지 가는 건 그냥 스프링 네가 알아서 넘겨." (구현 개꿀, 하지만 커스텀 힘듦)
- JSON 방식: "프론트야, 내가 URL 조립해서 줄 테니까, 네가 띄우고 싶은 타이밍에 띄워." (구현 귀찮음, 하지만 프론트랑 협업하기 좋고 API 명세가 깔끔함)
좀 더 쉽게 풀어쓰자면
리다이렉트 방식 (Spring Security 기본)
"스프링 필터가 알아서 납치한다"
- 누가 조립하나? : 스프링 시큐리티의 내부 필터(OAuth2AuthorizationRequestRedirectFilter)가 알아서 조립한다.
- 어떻게 주나? : 백엔드 로직(Controller)으로 들어오기도 전에, 필터 단계에서 프론트엔드에게 "여기로 당장 이동해!(HTTP 302)" 하고 명령을 내려버린다.
- 흐름: 구글 → 백엔드(직접 받음)
- 구글이 로그인이 끝나면, 프론트를 거치지 않고 백엔드 서버 주소로 바로 들어온다.
- 프론트엔드는 중간에 끼어들 틈이 없다. 백엔드가 혼자 북 치고 장구 치고 다 한다.
URL 반환 방식 (JSON 응답)
"내가(개발자가) 직접 조립해서 데이터로 건네준다"
- 누가 조립하나? : 작성자님이 만든 Service나 Controller에서 코드(Java)로 직접 문자열을 붙여서 조립한다.
- 어떻게 주나? : 명령(302)이 아니라 정보(JSON)로 준다. "자, 이게 구글 로그인 주소야. 이걸로 이동시키든 말든 프론트 네가 알아서 해~" 하고 텍스트만 던져준다.
- Front: 구글 로그인 후 받은 '인증 코드(Code)'를 백엔드로 던져준다. (POST /api/login/google)
- Back: 받은 코드를 가지고 구글 서버에 가서 "이거 진짜냐?" 하고 물어본다.
- Back: 구글이 "어, 진짜네" 하고 확인해주면(토큰 발급), 그때 우리 서비스의 로그인(JWT 발급)을 완료한다.
5. JSON 방식이 보안적으로 더 우수한 이유
해당 방식이 보안측면에서도 더 우수하다.
정확히 말하면 SPA(React/Vue) 환경에서 발생할 수 있는 보안 구멍(CORS, 쿠키 문제)을 막기에 훨씬 유리하다.
리다이렉트 방식을 SPA에 억지로 끼워 맞추려다 보면 오히려 보안 설정을 해제해야 하는 상황이 올 수 있다.
리다이렉트 방식을 A방식, JSON 반환 방식을 B방식이라 하면
5-1. "쿠키 지옥(Cookie Hell)"과 CORS 이슈 회피
사실 이게 가장 큰 이유이다. A 방식을 쓰면 세션 쿠키에 의존해야 하는데, 여기서 보안 구멍이 뚫리기 쉽다.
- A 방식의 문제점:
- 스프링 시큐리티의 기본 리다이렉트 방식은 과정 중에 JSESSIONID 같은 쿠키를 사용해서 사용자를 임시로 식별
- 그런데 프론트(localhost:3000)와 백엔드(localhost:8080)의 도메인이나 포트가 다르면, 브라우저의 강력한 보안 정책(SameSite, Secure 등) 때문에 쿠키가 차단
- 위험한 해결책: 개발자들이 이걸 해결하겠다고 SameSite=None으로 설정을 풀거나, 보안 수준을 낮춰버리는 경우가 생김 → 해킹에 취약해짐.
- B 방식의 장점:
- 쿠키를 안씀
- 프론트가 코드를 받아서 백엔드에 POST 요청의 Body(JSON)로 담아 보냄
- 브라우저의 까다로운 쿠키 정책과 싸울 필요가 없으니, 보안 설정을 억지로 낮출 필요도 없음
5-2. "코드 탈취(Interception)" 방지: PKCE 활용 가능
B 방식을 쓰면 PKCE(Pixie, 픽시)라는 강화된 보안 기술을 적용하기가 구조적으로 매우 자연스럽다.
- 시나리오: 해커가 사용자의 브라우저를 엿보고 있다가, 구글에서 돌아오는 '인증 코드'를 중간에서 가로챘다고 가정해 보자
- A 방식: 서버가 주도하므로 프론트엔드 단에서 추가적인 검증 로직을 넣기가 까다로움
- B 방식 (PKCE 적용 시):
- 출발 전: 프론트가 랜덤한 암호(Verifier)를 생성해서 들고 있음
- 도착 후: 백엔드에 코드를 줄 때, 아까 만든 암호도 같이 줌
- 검증: 백엔드는 "이 코드가 아까 그 브라우저가 요청한 게 맞는지" 암호로 대조
- 결과: 해커가 '인증 코드'를 훔쳐도, 프론트가 메모리에 숨겨둔 '암호'가 없어서 로그인을 못함
요약: B 방식은 프론트엔드에서 인증 흐름을 제어하므로, 최신 보안 표준인 PKCE를 구현하여 "중간에 코드를 도둑맞아도 안전한 상태"를 만들기 좋다.
5-3. CSRF(위조 요청) 공격 방어의 용이성
- A 방식: 전통적인 방식은 쿠키 기반이므로 CSRF 공격(해커가 사용자 몰래 로그인된 상태를 이용해 요청을 날리는 것)에 취약할 수 있다. 그래서 CSRF Token 설정을 복잡하게 해야 한다.
- B 방식:
- 최종적으로 발급받은 JWT(액세스 토큰)를 프론트엔드가 변수(메모리)에 저장하거나, API 요청 시 Header(Authorization: Bearer ...)에 담아 보냄
- 헤더에 담는 방식은 쿠키 방식보다 CSRF 공격을 하기가 기술적으로 훨씬 어렵다. (해커가 스크립트로 헤더를 조작하기 어렵기 때문)
6. 결론
- 간단하고 빠르게 구현하고 싶다면: 리다이렉트 방식 (Spring 기본 사용)
- 프론트엔드에서 세밀한 UX 제어가 필요하다면: JSON 방식
필자도 지금까지 리다이렉트 방식으로 구현을 해왔다.(사실 JSON 방식을 몰랐음)
하지만 추후 확장성이나 보안적인 측면에서도 JSON 방식이 더 좋은거 같다.
그러나 모든 것은 trade-off가 존재한다.
만약에 빠르게 MVP를 출시해야 한다면 기존에 구현해봤던 리다이렉트 방식을 채택하는 것도 무조건 나쁘다고 할 수 없다.
다만 로그인을 구현할 때 이러한 방식도 있으니 고려해보고 한다면 더 훌륭한 로그인이 완성되지 않을까 하는 생각이다.
'tech > project' 카테고리의 다른 글
| [Auth] 로그인 구현 대신 SaaS 서비스 도입에 대한 고찰 (0) | 2026.01.22 |
|---|---|
| [Test] redis(valkey)도 Testcontainers 도입하기 (0) | 2026.01.17 |
| [Test] H2 대신 Testcontainers 도입하기 (0) | 2026.01.13 |
| [postgreSQL] varchar VS text 무엇이 더 좋을까 (0) | 2026.01.07 |
| [token] 토큰을 어떻게 저장하는 것이 좋을까? (1) | 2025.12.22 |