들어가며
로그아웃 기능을 구현하려고 보니, 생각보다 간단하지 않았습니다. JWT는 stateless라 서버가 따로 "이 토큰은 무효"라고 기록해두지 않으면, 로그아웃을 눌러도 만료 시간 전까지는 계속 유효한 토큰으로 남습니다. 이 문제를 어떻게 풀지 — Redis와 Postgres 사이에서 고민하고, 실제로 인프라를 도입한 과정을 정리합니다.
배경
로그아웃 이슈를 구현하던 중 "쿠키에 발급된 JWT를 실제로 회수(무효화)해야 한다"는 요구사항이 나왔습니다. 당시 구현 상태는 다음과 같았습니다.
- JWT는
OAuthSuccessHandler에서 HttpOnly 쿠키(accessToken)로 발급되고,JwtAuthFilter는 Authorization 헤더와 쿠키를 모두 인식합니다. - Access Token 유효기간은 1일이고, Refresh Token은 아직 미구현 상태였습니다.
- JWT 클레임에 JTI(토큰 고유 ID)가 없었습니다. subject(memberId), issuedAt, expiration만 있어서, "이 토큰 하나"를 서버가 특정할 방법이 없는 상태였습니다.
JwtAuthFilter.isValid()는 서명과 만료만 검사할 뿐, "무효화된 토큰인지"는 전혀 확인하지 않았습니다.- Redis는 프로젝트에 전혀 없었습니다.
build.gradle,application.yml,compose.yml어디에도 없어서, 이번에 처음 도입하는 인프라였습니다.
Redis vs Postgres, 무엇으로 저장할까
로그아웃 블랙리스트는 "토큰이 자연 만료될 때까지만 유효하면 되는" 전형적인 TTL(Time-To-Live) 데이터입니다.
| Redis | Postgres 테이블 | |
|---|---|---|
| 만료 처리 | EXPIRE로 자동 삭제, 정리 배치 불필요 | 만료된 행을 지우는 배치/스케줄러를 별도로 구현해야 함 |
| 조회 성능 | 인메모리라 빠름, 매 요청마다 도는 인증 경로에 부하가 적음 | 매 요청(JwtAuthFilter)마다 DB 조회가 하나 더 추가됨 |
| 도입 비용 | 새 인프라 구성 필요 | 테이블 하나 추가로 즉시 가능 |
당장 필요한 것만 보면 Postgres에 테이블 하나 추가하는 쪽이 더 빠른 것도 사실이었습니다. 하지만 남아있는 작업인 리프레시 토큰 저장소도 결국 같은 종류의 TTL 데이터라, 로그아웃에서 Redis를 먼저 들여놓으면 리프레시 토큰 작업에서도 그대로 재사용할 수 있다는 점이 결정적이었습니다. 인프라를 새로 추가하는 비용은 있지만 한 번만 치르면 되고, 이후 작업에서 중복 투자가 없다고 판단해 Redis로 결정했습니다.
Redis 도입하며 추가한 것들
인프라
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
# compose.yml
redis:
image: redis:7-alpine
ports:
- "6379:6379"
# application.yml
spring:
data:
redis:
host: ${REDIS_HOST:localhost}
port: ${REDIS_PORT:6379}
테스트 환경
기존 Postgres 컨테이너(PostgisContainerConfiguration)와 동일한 패턴으로 RedisContainerConfiguration을 작성했습니다. Redis는 전용 Testcontainers 모듈이 없어서, GenericContainer + @ServiceConnection("redis")로 연결하는 방식을 사용해 새 의존성 추가 없이 해결했습니다. StringRedisTemplate으로 값 저장 + TTL을 확인하는 연결 테스트도 함께 추가해 기존 포함 전체 60개 테스트가 통과하는 것을 확인했습니다.
앞으로 남은 작업
Redis 연결까지만 이번 단계에서 마쳤고, 실제 블랙리스트 로직은 다음 단계로 남겨두었습니다.
- JWT에 JTI(고유 ID) 클레임 추가
- 블랙리스트 저장/조회 서비스(
TokenBlacklistService) 작성 — 로그아웃 시redisTemplate.opsForValue().set(jti, "logout", 토큰_남은_유효기간)으로 저장, 인증 시redisTemplate.hasKey(jti)로 조회 JwtAuthFilter가 서명 검증 통과 후에도 JTI가 블랙리스트에 있으면 인증 처리하지 않도록 분기 추가DELETE /api/v1/auth/logout엔드포인트 — JTI를 블랙리스트에 등록하고, 쿠키를 발급 시와 동일한 속성으로maxAge=0재설정해 삭제
정리
- JWT의 stateless한 특성은 인증 경로를 가볍게 만들어주지만, "즉시 회수"가 필요한 로그아웃 같은 기능에서는 오히려 걸림돌이 됩니다.
- TTL이 핵심인 데이터는 자동 만료를 지원하는 저장소(Redis)가 정리 배치 없이도 자연스럽게 해결해줍니다.
- 당장의 구현 비용만 볼 게 아니라, 뒤에 이어질 비슷한 요구사항(리프레시 토큰)까지 고려하면 인프라 도입 결정이 달라질 수 있습니다.
