StudyNote

OAuth2 Code Grant에서의 프론트/백엔드 책임 분리와 JWT 발급 고민기

DEOKJAE KWON 2025. 5. 31. 17:08
반응형

카카오테크 부트캠프 파이널 프로젝트를 진행하면서 카카오 로그인을 구현하게 되었다. 이렇게 OAuth2 Code Grant 방식으로 인증을 구현하기 전에 한 가지 고민에 빠졌는데, 바로 Access Token과 JWT 발급 시 프론트와 백엔드의 책임을 어디까지 분리할 것인가에 관한 문제였다. 이 글에서는 내가 OAuth2 인증 흐름을 구축하며 겪은 고민과, 보안과 유지보수성을 고려해 최종적으로 선택한 아키텍처를 정리해보려 한다.

OAuth2 Code Grant 기본 흐름

  1. 로그인 요청 → 인가 코드(Authorization Code) 발급
  2. 발급된 인가 코드를 Authorization Server에 전달 → Access Token 발급
  3. Access Token으로 Resource Server에서 유저 정보 획득
  4. 사용자 정보 획득 완료 → 백엔드단에서 JWT 발급

OAuth2 인증에서 사용자는 소셜 로그인 페이지를 통해 로그인한다. 보통 프론트는 로그인 경로를 하이퍼링크로 연결하고, 사용자가 로그인하면 리디렉션이 일어난다. 문제는 여기서 발생한다.

책임 분리와 잘못된 구현 사례

OAuth2 Code Grant 방식에서 프론트와 백엔드의 역할 분리를 어떻게 할지가 관건이다.

  • 프론트와 백엔드가 책임을 나누어 가지는 방식
    • 프론트단에서 (로그인 → 인가 코드 발급) 후 코드를 백엔드로 전송하고, 백엔드단에서 (인가 코드 → 토큰 발급 → 유저 정보 획득 → JWT 발급)
      • 인가 코드가 네트워크 상에 노출되며 탈취 위험 발생, 표준 흐름 위반 가능성 존재
    • 프론트단에서 (로그인 → 코드 발급 → Access 토큰) 후 Access 토큰을 백엔드로 전송하고, 백엔드단에서 (Access 토큰 → 유저 정보 획득 → JWT 발급)
      • Client Secret 노출 위험과 Access Token 탈취 가능성, 유저 정보 신뢰성 저하 문제 발생
  • 프론트가 모든 책임을 가지는 방식
    • 프론트에서 (로그인 → 코드 발급 → Access 토큰 → 유저 정보 획득) 과정을 모두 수행한 뒤 백엔드에 (유저 정보 → JWT 발급) 방식
      • Client Secret 노출 위험과 Access Token 탈취 가능성, 유저 정보 위·변조 가능성 발생
  • 백엔드가 모든 책임을 가지는 방식
    • 프론트단에서 백엔드의 OAuth2 로그인 경로로 하이퍼링킹을 진행 후 백엔드단에서 (로그인 페이지 요청 → 코드 발급 → Access 토큰 → 유저 정보 획득 → JWT 발급) 방식
      • 초기 구현 복잡도가 높고, 서버에 인증 처리 부담이 집중되며 프론트 UX 유연성이 제한된다.

결론적으로 인가 코드, Access Token, Client Secret이 프론트에 노출되면 탈취 위험이 발생한다. 백엔드에서 모든 처리를 전담하면 이러한 위험을 원천 차단할 수 있으며, 백엔드가 인증과 유저 정보 조회까지 관리하면 로직 일관성을 유지하고 신뢰성을 확보할 수 있다.

또한 OAuth 공급자 추가, 토큰 재발급 로직 변경 시 백엔드만 수정하면 되므로 유지보수가 편하고 확장성도 높아지며, OAuth2는 Confidential Client(백엔드)에서 민감한 처리를 수행하도록 권장한다.

특히, 카카오 공식 개발자 포럼(Dev Talk)에서도 백엔드가 OAuth 인증 및 토큰 발급 책임을 모두 지는 것을 권장하고 있다. (카카오 dev 톡에 적혀있는 프론트와 백엔드가 책임을 나눠 가지는 질문에 대한 카카오 공식 답변)

이러한 이유 때문에 백엔드가 모든 책임을 가지는 방식으로 구현을 시작했다.

Spring Security OAuth2 로그인 공식 설명과 커스텀 설정

OAuth2 로그인과 JWT 발급 흐름은 Spring Security의 oauth2Login() DSL을 활용해 구성했다. 기본적으로 Spring Security는 OAuth2 인증의 각 단계(인가 요청, 인가 코드 처리, Access Token 발급, 유저 정보 요청)를 자동으로 처리할 수 있게 해준다.

Spring Security의 oauth2Login() DSL은 OAuth2 로그인 흐름을 커스터마이징할 수 있는 다양한 설정을 제공한다.

엔드포인트 기본 경로 / 역할
Authorization Endpoint /oauth2/authorization/{registrationId} – 클라이언트(사용자)가 인가 요청
Redirection Endpoint /login/oauth2/code/{registrationId} – 인가 서버가 인가 코드 전달
Token Endpoint Access Token 발급 (Spring Security 내부 처리)
UserInfo Endpoint 사용자 정보 요청, OAuth2UserService에 위임
Login Page /login (기본), 또는 loginPage()로 커스터마이징 가능

나는 이를 기반으로, 프론트는 로그인 버튼 클릭 시 /api/oauth/{provider}/redirection으로 요청만 보내도록 하고,
이 요청을 백엔드의 OAuthController에서 받아 Spring Security의 기본 인가 요청 엔드포인트(/oauth2/authorization/{provider})로 리다이렉트하도록 처리했다. 즉, 프론트는 소셜 로그인 요청만 하고, 이후 인가 코드 교환, 토큰 발급, 유저 정보 요청, JWT 발급까지 모든 처리는 백엔드에서 맡도록 했다.

시퀀스 다이어그램

관련 코드 및 설정은 다음과 같다.

OAuthController

package com.tebutebu.apiserver.controller;

import com.tebutebu.apiserver.service.oauth.OAuthService;
import com.tebutebu.apiserver.util.exception.CustomValidationException;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

import java.io.IOException;

@RestController
@RequiredArgsConstructor
@Log4j2
@RequestMapping("/api/oauth")
public class OAuthController {

    private final OAuthService oAuthService;

    @GetMapping("/{provider}/redirection")
    public void redirectToProvider(@PathVariable String provider, HttpServletResponse response) throws IOException {
        try {
            oAuthService.validateProvider(provider);
            response.sendRedirect("/oauth2/authorization/" + provider);
        } catch (CustomValidationException e) {
            response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
            response.setContentType("application/json; charset=UTF-8");
            response.getWriter().write("{\"message\": \"" + e.getMessage() + "\"}");
        }
    }

}

CustomSecurityConfig

// 이외 코드 생략
http.oauth2Login(oauth2 -> oauth2
                .redirectionEndpoint(redir -> redir
                        .baseUri("/api/oauth/{registrationId}")
                )
                // ...

카카오 디벨로퍼스 Redirect URI

깃허브

이외의 자세한 설정은 깃허브 참고

 

GitHub - 100-hours-a-week/8-pumati-be: 카테부 판교 2기 8조 백엔드

카테부 판교 2기 8조 백엔드. Contribute to 100-hours-a-week/8-pumati-be development by creating an account on GitHub.

github.com

카카오테크 부트캠프 판교 2기의 파이널 프로젝트가 궁금하다면?

품앗이 서비스 바로가기: https://tebutebu.com/

 

품앗이

카카오테크 부트캠프 교육생들을 위한 트래픽 품앗이 플랫폼, 품앗이는 프로젝트 홍보를 통해 교육생들이 서로의 성공을 함께 만들어가는 공간입니다.

tebutebu.com


결론

부트캠프를 진행하면서 단순히 기능을 구현하는 것에서 끝나는 게 아니라 어떤 코드가 어떤 책임을 가져야 하고, 그에 따라 유지보수성과 보안은 어떻게 달라지는지에 대한 생각이 많아진 것 같다.

OAuth2 Code Grant 흐름을 구현하면서도 마찬가지였는데, 프론트와 백엔드의 역할을 어떻게 나눌지, 그리고 보안과 유지보수성을 어떻게 확보할지를 계속 고민했다.

이 과정에서 Spring Security 공식 흐름과 카카오 공식 개발자 포럼(devTalk) 권장 사항을 참고하고 문서로 정리하며, 나만의 방식으로 설계 흐름을 구체화할 수 있었다.

단순히 정답을 찾는 것이 아니라, 다양한 가능성을 고민하고, 때로는 실패하면서도 나만의 해답을 찾아가는 경험을 쌓을 수 있었던 좋은 시간이었던 것 같다. 다음 글에서는 이 흐름을 기반으로 고민한 Refresh Token 관리, 토큰 회전 처리(RTR), 직접 겪은 쿠키 관련 문제와 트러블슈팅까지 더 깊이 다뤄보려 한다.

반응형