
Spring Boot JWT는 지금 검색 수요가 뚜렷하지만, 단어만 외우면 실제 판단 기준이 남지 않는 주제입니다. 이 글에서는 Spring Boot JWT 인증 구현 책임 분리를 목표로 핵심 개념과 실무 판단 기준을 차근차근 정리합니다.
대상 독자는 Spring Boot에서 로그인과 토큰 인증을 직접 구현하려는 백엔드 개발자입니다. 처음부터 세부 구현이나 상품명만 따라가기보다, 왜 이 주제가 필요한지와 어디서 실수하는지를 먼저 잡겠습니다.

JWT 인증은 로그인 구현과 요청 인증을 나눠야 한다
JWT 인증을 처음 구현할 때 가장 흔한 실수는 로그인 API, 토큰 발급, 요청 인증, 재발급 정책을 한 덩어리로 보는 것입니다. 이렇게 만들면 코드가 빨리 돌아가도 나중에 실패 처리와 보안 정책을 고치기 어려워집니다.
구조를 단순하게 보려면 로그인은 토큰을 발급하는 흐름이고, 이후 요청 인증은 이미 발급된 토큰을 검증하는 흐름이라고 나누면 됩니다.
- 로그인 API: 사용자 자격 증명을 확인하고 access token과 refresh token을 발급한다
- JWT 필터: 요청의 Authorization header를 읽고 access token을 검증한다
- SecurityContext: 검증된 사용자를 현재 요청의 인증 정보로 보관한다
- Refresh Token 서비스: 만료된 access token을 재발급할지 판단한다
SecurityFilterChain은 보안 흐름의 입구다
Spring Security에서 SecurityFilterChain은 어떤 요청이 어떤 필터를 통과하고, 어떤 URL은 인증 없이 열어둘지 정하는 위치입니다. JWT 인증을 넣는다고 해서 모든 로직을 이 설정 클래스에 몰아넣으면 안 됩니다.
설정 클래스는 경로와 필터 순서를 조립하고, 실제 토큰 파싱과 사용자 조회는 별도 컴포넌트로 빼는 편이 유지보수하기 쉽습니다.
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
.csrf(csrf -> csrf.disable())
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll()
.anyRequest().authenticated()
)
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
.build();
}JWT 필터의 책임은 작게 유지한다
JWT 필터는 요청에서 토큰을 꺼내고, 토큰이 유효하면 Authentication을 만들어 SecurityContext에 넣는 역할에 집중하는 편이 좋습니다. 회원가입, 로그인, refresh token 저장 같은 로직이 필터에 들어가면 요청마다 불필요한 책임이 늘어납니다.
토큰이 없으면 그냥 다음 필터로 넘길지, 토큰이 있는데 잘못됐으면 401로 끊을지 같은 정책도 명확히 정해야 합니다.
- Authorization header에서 Bearer token을 읽는다
- 서명, 만료, issuer, audience 같은 검증을 수행한다
- 사용자 식별자를 기반으로 인증 객체를 만든다
- 인증 실패와 권한 부족을 401/403으로 분리한다
Refresh Token은 저장소와 정책이 핵심이다
Access token은 짧게 가져가고 refresh token으로 재발급하는 구조가 흔합니다. 하지만 refresh token은 단순히 긴 JWT 하나를 더 발급하는 문제가 아닙니다. 탈취, 재사용, 로그아웃, 기기별 세션을 어떻게 처리할지가 더 중요합니다.
실무에서는 refresh token을 DB나 Redis에 저장하고 회전 전략을 두는 경우가 많습니다. 재발급 때마다 새 refresh token을 발급할지, 기존 토큰을 유지할지에 따라 보안성과 사용자 경험이 달라집니다.
- 로그인 성공 시 refresh token을 저장한다
- 재발급 요청 때 저장된 토큰과 요청 토큰을 비교한다
- 만료, 폐기, 기기 정보를 확인한다
- 정책에 따라 refresh token을 회전하거나 유지한다
- 로그아웃 시 저장된 refresh token을 폐기한다
직접 구현과 Resource Server 중 무엇을 고를까
자체 로그인과 토큰 발급까지 모두 서비스 안에서 처리한다면 직접 JWT 필터를 구현하는 흐름이 자연스러울 수 있습니다. 반대로 인증 서버가 따로 있고 API 서버는 토큰 검증만 하면 된다면 OAuth2 Resource Server 구성이 더 단순합니다.
중요한 것은 어떤 방식이 더 최신인가가 아니라, 현재 서비스가 토큰을 발급하는 주체인지 검증만 하는 주체인지 구분하는 것입니다.
- 직접 구현: 로그인, 발급, 재발급 정책을 서비스가 책임진다
- Resource Server: 외부 authorization server가 발급한 JWT를 API 서버가 검증한다
- 혼합 구조: 소셜 로그인 후 자체 토큰을 발급하는 경우 책임 경계를 더 명확히 해야 한다
조금 더 깊게 보면
초안 단계에서 핵심 개념만 잡으면 글이 얕게 끝날 수 있습니다. 그래서 여기서는 실제 적용할 때 확인해야 할 기준과 자주 생기는 오해를 한 단계 더 내려가 보겠습니다.

인증 실패 처리를 먼저 정해야 한다
JWT 인증 구현에서 의외로 자주 빠지는 부분은 실패 응답입니다. 토큰이 없을 때, 토큰이 만료됐을 때, 서명이 틀렸을 때, 권한이 부족할 때 모두 같은 403을 던지면 프론트엔드와 모바일 앱이 원인을 알기 어렵습니다.
인증이 안 된 요청은 401, 인증은 됐지만 권한이 부족한 요청은 403으로 나누는 것이 기본입니다. 이 구분이 되어야 access token 재발급을 시도할지, 로그인 화면으로 보낼지, 권한 안내를 보여줄지 결정할 수 있습니다.
- Authorization header가 없으면 401 후보
- access token이 만료됐으면 401과 재발급 흐름
- 토큰은 유효하지만 role이 부족하면 403
- 서명 검증 실패는 재발급이 아니라 재로그인 또는 차단
토큰 유틸과 사용자 조회를 분리한다
JWT 구현 예제에서 JwtUtil 하나가 토큰 생성, 파싱, 사용자 조회, 예외 변환까지 모두 담당하는 경우가 많습니다. 작은 예제에서는 편하지만 서비스가 커지면 테스트와 변경이 어려워집니다.
토큰 자체를 다루는 컴포넌트와 사용자 정보를 가져오는 컴포넌트를 나누면 책임이 선명해집니다. 토큰 유틸은 claims와 서명만 알고, 사용자 조회는 UserDetailsService나 별도 member service가 맡는 식입니다.
public interface TokenProvider {
String createAccessToken(Long memberId, List<String> roles);
JwtClaims parse(String token);
boolean isExpired(String token);
}
public interface CurrentUserLoader {
UserDetails loadByMemberId(Long memberId);
}Refresh Token 저장 전략 비교
Refresh Token을 stateless JWT로만 두면 서버 저장소가 단순해집니다. 하지만 로그아웃, 기기별 세션 종료, 탈취 토큰 폐기 같은 요구사항이 생기면 제어가 어렵습니다.
반대로 DB나 Redis에 저장하면 제어권은 좋아지지만 저장소 비용과 만료 정책 관리가 필요합니다. 중요한 서비스라면 refresh token은 서버에서 폐기할 수 있는 형태로 설계하는 편이 안전합니다.
- DB 저장: 감사 로그와 기기 관리에 유리하지만 I/O가 늘어난다
- Redis 저장: TTL 관리가 쉽지만 영속성 정책을 확인해야 한다
- JWT only: 단순하지만 강제 폐기가 어렵다
- rotation: 탈취 대응에 유리하지만 재사용 감지 로직이 필요하다
실무 구현 순서
처음부터 모든 보안 요구사항을 한 번에 넣으려 하면 구조가 흐려집니다. 먼저 access token 요청 인증을 안정화하고, 그 다음 refresh token과 로그아웃, 마지막으로 기기 관리와 재사용 감지를 붙이는 순서가 좋습니다.
각 단계마다 통합 테스트를 하나씩 남기면 리팩터링 때 인증 흐름이 깨지는 것을 막을 수 있습니다.
- permitAll 경로와 인증 필요 경로를 SecurityFilterChain에 명확히 둔다
- JWT 필터에서 access token 검증과 SecurityContext 저장을 구현한다
- 401/403 실패 응답을 API 형식으로 통일한다
- refresh token 저장소와 재발급 API를 분리한다
- 로그아웃과 refresh token 폐기 테스트를 추가한다
실전 적용 시나리오
이 주제를 실제 작업에 적용한다면 먼저 작은 범위에서 시작하는 편이 좋습니다. Spring Boot JWT를 알고 있다는 것과, 프로젝트나 학습 문제 안에서 올바른 판단을 내리는 것은 다른 문제입니다.
Spring Boot에서 로그인과 토큰 인증을 직접 구현하려는 백엔드 개발자라면 처음부터 모든 세부 선택지를 외우기보다, 현재 상황에서 무엇을 확인해야 하는지 체크리스트로 좁혀야 합니다. 그래야 글을 읽고 바로 다음 행동으로 옮길 수 있습니다.
- 먼저 Spring Boot JWT가 필요한 상황인지 문제를 한 문장으로 적는다
- 현재 방식이 실패하는 지점을 로그, 예제, 표, 작은 입력 중 하나로 확인한다
- Spring Security JWT, SecurityFilterChain, Refresh Token 중 이번 문제와 직접 관련 있는 기준만 고른다
- 작은 예제로 먼저 검증하고, 그 다음 실제 프로젝트나 긴 입력으로 확장한다
- 결과가 좋아졌는지 주관적 느낌이 아니라 비교 가능한 기준으로 확인한다
이 순서를 따르면 글에서 배운 개념이 단순한 용어 정리로 끝나지 않습니다. 특히 개발 글은 예제 코드가 돌아가는지만 보지 말고, 왜 그 구조를 골랐는지 설명할 수 있어야 합니다. ETF 글도 마찬가지로 분배율이나 수익률 숫자 하나가 아니라 역할과 위험을 함께 설명할 수 있어야 합니다.
검토 체크리스트
마지막으로 실제 적용 전에 아래 항목을 확인하면 실수를 줄일 수 있습니다. 체크리스트는 정답을 대신하지 않지만, 빠뜨리기 쉬운 질문을 다시 꺼내는 데 도움이 됩니다.
- 이 주제를 써야 하는 문제가 정말 맞는가
- 비슷한 대안과 비교했을 때 장점과 비용을 함께 설명할 수 있는가
- 공식 문서나 신뢰할 수 있는 자료로 핵심 전제를 확인했는가
- 작은 예제나 제한된 데이터에서 먼저 검증했는가
- 실패했을 때 되돌릴 수 있는 기준과 로그를 남겼는가
- 초보자가 오해하기 쉬운 지점을 본문에서 따로 짚었는가
정리하면 Spring Boot JWT 글의 목적은 용어를 하나 더 외우게 하는 것이 아니라, 독자가 다음 선택을 덜 흔들리게 만드는 것입니다. 그래서 본문에서는 개념, 예시, 실수, 체크리스트를 함께 묶어 읽는 편이 좋습니다.
정리
Spring Boot JWT 인증은 필터 하나를 추가하는 일이 아니라 인증 흐름을 책임별로 나누는 일입니다. SecurityFilterChain은 조립, JWT 필터는 요청 인증, Refresh Token 서비스는 재발급 정책에 집중시키면 구조가 훨씬 안정됩니다.
관련해서 내부 글은 Spring Security JWT와 OAuth2 Resource Server는 어떻게 다를까, Spring AI는 무엇을 해결하려는 걸까와 함께 보면 좋습니다.
외부 기준은 Spring Security Reference – Servlet Security, Spring Security Reference – OAuth2 Resource Server JWT, RFC 7519 – JSON Web Token를 기준으로 확인했습니다.