💬 들어가며
PostDM 프로젝트에서 팀원분이 로그인 기능을 먼저 구현하셨고, 나는 추가적으로 이어서 로그아웃 기능을 맡게 되었다.
처음에는 단순히 “토큰을 지우면 로그아웃이지 않을까?” 생각했지만, 실제로 JWT 기반 인증 구조를 깊이 들여다보면서 “JWT에서는 로그아웃이 왜 까다로운가?”, “서버가 토큰을 저장하지 않는데 어떻게 로그아웃을 구현할 수 있을까?” 같은 궁금증이 하나씩 생겨났다.
그렇게 JWT의 원리부터 시작해 다양한 로그아웃 전략을 정리하면서 이번 프로젝트에 적합한 Access Token 블랙리스트 방식으로 구현할 수 있었다.
이 글에서는 JWT의 구조, JWT의 로그아웃 방식 그리고 프로젝트에서 로그아웃을 구현한 방법에 대해 자세히 공유하려고 한다.
1. JWT의 기본 구조
JWT(Json Web Token)는 서버가 클라이언트를 인증할 때 사용되는 Self-contained한 토큰 기반 인증 방식이다. 토큰은 서버에서 발급하고, 이후에는 클라이언트가 요청마다 이 토큰을 보내서 본인을 인증하게 된다.
JWT는 아래와 같이 3부분으로 구성된다.
Header.Payload.Signature
- Header: 토큰의 타입과 서명 알고리즘 정보 (alg, typ)
- Payload: 사용자 정보와 클레임(claims) (sub, exp, role 등)
- Signature: 서버의 secret key로 생성한 서명값 (위조 방지)
- 클라이언트가 Payload를 마음대로 수정할 수 없도록 하기 위한 것
- Payload는 인코딩된 것이지 암호화된 게 아니므로 누구나 디코딩 가능
- 대신 서버가 서명(Signature)을 검증하여 위조 여부를 판단함

2. JWT 인증 흐름
JWT 기반 인증은 stateless(무상태) 인증 방식이다. 즉, 서버는 로그인 상태를 저장하지 않고, 토큰 자체가 인증 정보를 담고 있다.
클라이언트가 로그인을 하면 서버는 JWT를 발급해주고, 클라이언트는 이후 요청 시 Authorization: Bearer <token> 헤더에 실어 보낸다. 서버는 해당 토큰의 유효성과 서명을 검증하고, 인증 처리를 진행한다.
1) 사용자가 로그인 요청을 보냄
2) 서버가 인증 정보를 확인하고 JWT 토큰을 발급
- 보통 Access Token + Refresh Token 세트로 발급
- Access Token은 사용자 정보를 포함하고, 유효기간은 짧음
- Refresh Token은 주기적으로 Access Token을 재발급할 때 사용
3) 클라이언트가 토큰을 저장
- 보통 Access Token은 localStorage 또는 Authorization 헤더로 전송
- Refresh Token은 HttpOnly Cookie에 저장 (보안상)
4) 사용자는 요청마다 Access Token을 Authorization 헤더에 담아서 전송
Authorization: Bearer eyJhbGciOiJIUzI1...
5) 서버는 토큰을 파싱하고 유효성 검사
- 서명(Signature) 검증
- 만료 시간(exp) 체크
- Payload에서 사용자 ID, role 추출
6) 정상적인 경우 SecurityContext에 인증 객체 등록 → 요청 처리
- Spring Security에서는 UsernamePasswordAuthenticationToken을 생성해서 등록
3. JWT는 로그아웃이 안 되는 거 아닌가?
A. 맞다.
JWT는 "서버가 인증 상태를 기억하지 않는 구조(stateless)"이기 때문에,
로그아웃 처리를 위해서는 추가적인 무효화 로직을 직접 구현해야 한다.
세션 기반 인증 vs JWT 기반 인증
| 세션 기반 인증 | JWT 기반 인증 | |
| 인증 수단 저장 위치 | 서버 (메모리, Redis 등) | 클라이언트 (로컬스토리지, 쿠키 등) |
| 서버 상태 관리 | 상태 유지 (stateful) | 상태 없음 (stateless) |
| 로그아웃 처리 방식 | 서버에서 세션 삭제 | 별도 구현 필요 (토큰 무효화 등) |
세션 기반 인증
- 사용자가 로그인하면 서버가 세션 ID를 생성하고 이를 저장함 (예: 서버 메모리, Redis)
- 클라이언트는 이 세션 ID를 쿠키에 담아 보냄
- 로그아웃 시 서버는 해당 세션 ID를 삭제하면 인증이 종료됨
- 즉, 서버가 "사용자 상태를 기억"하고 있기 때문에 제어 가능함
JWT 기반 인증
- 사용자가 로그인하면 서버는 토큰(JWT)을 발급하고, 클라이언트가 이를 보관함
- 서버는 토큰을 저장하지 않고, 매 요청마다 토큰을 복호화해 유효성만 검증함 (장점: 확장성)
- 로그아웃 시 클라이언트가 토큰을 삭제해도, 서버 입장에서는 여전히 유효한 토큰으로 간주됨
- 서버가 이 토큰이 "로그아웃된 것인지" 판단할 수단이 없음
❗따라서 JWT에서는 로그아웃을 위해 아래 중 하나가 필요
- Access Token이 자연스럽게 만료되기를 기다리기 : Access Token 만료시간을 짧게 설정 + Refresh Token으로 재발급 제어
- 서버가 해당 토큰을 명시적으로 차단(= 저장) : 로그아웃된 토큰을 저장(블랙리스트) 하여 인증 시 차단
4. JWT 로그아웃 방식
| 방식 | 설명 | 장점 | 단점 |
| 클라이언트에서 토큰 삭제 | localStorage / 쿠키에서 토큰 제거 | 구현 간단 | - 클라이언트 기준, 서버는 토큰 무효화 불가 - 보안상 완전한 로그아웃 아님 |
| 토큰 만료시간 짧게 설정 | Access Token 수명 10분 등으로 제한 | 토큰 자동 만료 | UX 저하, 재발급 구현 필요 |
| ✅ 블랙리스트 방식 | 로그아웃된 토큰을 서버에 저장하여 차단 | 서버에서 제어 가능 | DB 또는 Redis 필요 |
블랙리스트 방식이 가장 적합한 이유
1) 실제 "로그아웃" 상태를 서버가 제어할 수 있음
- 클라이언트가 토큰을 삭제하는 방식은 서버가 그 상태를 모름 (위험)
- 토큰 만료 시간 기다리는 방식은 즉시 차단이 불가능 (느림)
- 반면, 블랙리스트 방식은 서버가 능동적으로 차단 가능
2) 강제 로그아웃, 관리자 강제 종료 등에 적합
- 예시로 보안상 문제 발생 시 관리자 강제로 로그아웃 시켜야 할 때, 블랙리스트 방식은 DB/Redis에서 해당 토큰만 삭제/추가하면 즉시 반영됨
3) Refresh Token이 필요 없는 경우에도 사용 가능
- 토큰 재발급을 구현하지 않아도, Access Token 단독으로 블랙리스트 방식 사용 가능
블랙리스트 방식
로그아웃 요청 시, 해당 Access Token을 서버에 저장해 두고, 이후 모든 인증 요청 시 해당 토큰이 블랙리스트에 포함되어 있는지를 검사하여 포함되어 있으면 차단하는 방식이다. 즉, 서버는 직접 토큰을 저장하지 않지만, “이 토큰은 쓰지 말아야 한다”는 정보만 저장해서 인증 흐름을 제어하는 전략이다.
장점
- 서버 주도 제어: 서버가 인증 상태를 직접 무효화할 수 있음
- 보안성 강화: 탈취된 토큰도 블랙리스트로 즉시 차단 가능
- UX 유지: Access Token 만료를 기다리지 않고도 즉시 로그아웃 가능
단점
- 상태 저장이 필요
- stateless 시스템에 상태 저장 도입됨
- 해결 방안: Redis나 TTL을 통해 제한된 저장으로 해결
- 저장 공간 증가
- 모든 로그아웃 토큰을 저장해야 함
- 해결 방안: 만료 시각 기반 스케줄러로 주기적 삭제 (스케줄링)
- 성능 오버헤드
- 인증 필터마다 DB 조회 필요
- 해결 방안: Redis 사용, 캐싱 전략 도입 가능
5. 프로젝트에 적용하기: 블랙리스트 기반 로그아웃
우리 프로젝트에서는 JWT의 단점을 보완하기 위해 Access Token 블랙리스트 방식으로 로그아웃을 구현했다. 이는 사용자가 로그아웃했을 때 해당 토큰을 서버에 기록해두고, 다음 요청부터 인증 필터에서 차단하는 방식이다.
구현 흐름
- 클라이언트가
POST /api/v1/auth/sign-out(로그아웃) 요청 시Authorization헤더에 Access Token 포함 - 서버는 Authorization 헤더에서 Access Token을 추출
- Access Token의 만료 시간(exp)을 추출하여, DB(
token_blacklist테이블)에 해당 토큰과 만료시각 저장 - Refresh Token 삭제
- 모든 요청에서 인증 필터가 토큰의 유효성을 검사할 때, 블랙리스트 테이블을 조회하여 해당 토큰이 있으면 차단 및 401 Unauthorized로 인증 실패 처리
블랙리스트 테이블 구조
| 필드명 | 설명 |
| id | 자동 증가 PK |
| token | 로그아웃 처리된 Access Token (Lob) |
| expiration | 해당 토큰의 만료 시간 |
| creatAt | 블랙리스트에 등록된 시각 |
JWT 로그아웃을 블랙리스트 방식으로 구현할 때 expiration 필드가 반드시 필요하다. expiration은 "이 토큰을 언제까지 기억해야 하는가"를 판단하는 기준이 된다. 즉, "기억의 유효기간" 같은 개념이다.
인증 필터: JwtAuthenticationFilter
JwtAuthenticationFilter에서 블랙리스트로 차단합니다. 토큰이 유효하더라도, 블랙리스트에 있으면 인증 실패 처리를 한다.
이는 토큰의 “기술적 유효성”이 아니라 “신뢰성”을 체크하는 방식이다.
if (tokenBlacklistService.isBlacklisted(token)) {
throw new JwtException("이미 로그아웃된 토큰입니다.");
}
만료된 토큰 정리 (스케줄링)
블랙리스트 DB가 계속 쌓이는 것을 방지하기 위해, 만료된 토큰은 주기적으로 삭제한다. 아래와 같이 cron을 설정하면 매시 정각마다 만료된 토큰을 정리하게 된다.
블랙리스트 테이블에 저장해둔 토큰의 expiration 시각을 기준으로 스케줄러(@Scheduled) 를 통해 해당 시각이 지난 토큰을 주기적으로 삭제한다. 이를 통해 DB 용량을 효율적으로 관리할 수 있다.
@Scheduled(cron = "0 0 * * * *") // cron: 매시 정각
@Transactional
public void cleanExpiredTokens() {
repository.deleteAllByExpirationBefore(LocalDateTime.now());
}
💬 마무리하며
JWT는 stateless 인증 방식이라는 점에서 확장성과 성능에 강점이 있지만, 반대로 "토큰을 잊지 못하는 기능"은 개발자가 직접 설계해야 한다는 책임도 함께 따른다.
이번 로그아웃 기능을 블랙리스트 방식으로 구현하면서, JWT 인증 흐름 전반에 대한 깊은 이해와 함께, 인증 필터, DB, 스케줄러, 예외 응답까지 모든 컴포넌트가 협력해야 보안적으로 안정적인 시스템이 완성된다는 것을 경험할 수 있었다.
또한, 통합 테스트를 통해 실제 사용 시나리오에 맞춰 흐름을 검증하며 예외 상황까지 생각하며 서비스의 신뢰성과 보안성을 높이는 경험을 할 수 있었다.
'프로젝트' 카테고리의 다른 글
| Redis란 무엇인가?: Spring Boot에 캐시로 적용해보기 (1) | 2025.07.10 |
|---|---|
| 페이지네이션: 성능 최적화를 위한 첫걸음 (0) | 2025.06.23 |
| JMeter를 활용한 성능 테스트 (1) | 2025.06.18 |
| Access Token 재발급: Refresh Token의 역할 (0) | 2025.05.13 |
| 팀 디스코드 봇 만들기(feat. Docker 활용) (1) | 2025.01.15 |