팀 프로젝트로 진행한 소셜 네트워크 서비스(SNS)와 유사한 뉴스피드 API 서버 구현에서, 회원 기능과 인증/인가 로직을 담당했다. 초기에는 세션 기반 인증으로 구성했지만, 이후 JWT 기반 인증 방식으로 리팩토링 하는 과정에서 문제를 마주하게 됐다.
JWT에 대한 사전 지식이 부족한 상태에서 공부함과 동시에 실시간으로 구현을 하면서, 세션과 다른 토큰의 특성때문에 어쩔 수 없는 한계가 있다는 걸 깨닫게 되었고 그 한계점 해결 과정을 정리해보려 한다.
JWT 기반으로 로그아웃을 처리한 후에도, 여전히 이전의 토큰 값만 알고 있다면 API 요청이 정상적으로 처리되는 현상이 발생했다.
JWT는 무상태(stateless) 방식이다. 즉, 사용자 인증 정보를 클라이언트 쪽에서 자체적으로 보관한다.
반면, 세션 기반 인증은 서버가 사용자 상태를 유지하기 때문에 언제든지 무효화할 수 있는 상태 유지(stateful) 방식이다.
JWT는 한 번 발급되면 서버가 일방적으로 만료시키기 어렵다. 이게 바로 내가 고민한 "JWT 로그아웃 문제"였다.
이 문제를 해결하기 위해, Redis
를 활용한 토큰 블랙리스트 등록 방식을 선택했다.
Redis
는 메모리(RAM) 기반 저장소라서 일반 관계형 데이터베이스보다 읽고 쓰는 속도가 훨씬 빠르다.
평균 응답 속도:
Redis
: 평균 0.1~1ms 이하RDB
: 평균 10~100ms 이상(쿼리, 인덱스에 따라 다름)하지만 메모리에 데이터를 저장하는 구조이기 때문에 대량의 데이터가 쌓이면 메모리 부족 이슈가 발생할 수 있다. 따라서 데이터 저장 시 보통 TTL(Time To Live)을 설정해 일정 시간이 지나면 자동으로 데이터를 삭제하도록 구성한다.
블랙리스트는 "토큰이 만료되기 전까지만 유효하면 된다"는 특성이 있고, 복잡한 데이터 구조가 필요하지 않다. 따라서 간단한 키-값 구조를 사용하고 TTL 설정 기능을 제공하는 Redis
를 사용하는 것이 가장 실용적인 방식이라고 생각했다.
// 1. 액세스 토큰 유효성 검사
if (!jwtProvider.validate(accessToken)) {
throw new CustomException(ExceptionCode.INVALID_ACCESS_TOKEN);
}
// 2. 사용자 ID 추출 및 사용자 조회
Long userId = jwtProvider.getUserId(accessToken);
User user = userRepository.findUserByIdOrElseThrow(userId);
// 3. DB에 저장된 리프레시 토큰
String refreshToken = user.getRefreshToken();
// 4. 액세스 토큰 블랙리스트 등록
blackListService.addAccessTokenToBlacklist(accessToken);
// 5. 리프레시 토큰 블랙리스트 등록
blackListService.addRefreshTokenToBlacklist(refreshToken);
// 6. 사용자 엔티티에서 리프레시 토큰 제거
user.deleteRefreshToken();
/**
* Access Token 블랙리스트 등록
* @param accessToken 등록할 accessToken
*/
protected void addAccessTokenToBlacklist(String accessToken) {
// 1. 액세스 토큰 만료 시간 계산
long expiration = jwtProvider.getExpiration(accessToken);
// 2. 액세스 토큰 블랙리스트 등록
redisTemplate.opsForValue().set(
"blacklist:access:" + accessToken,
"logout",
expiration,
TimeUnit.MILLISECONDS
);
}
1. 액세스 토큰 유효성 검사
2. 사용자 식별 및 조회
3. DB에 저장된 리프레시 토큰 조회
MySQL
에서 조회한다.4. 토큰 TTL 계산
토큰 만료 시각 - 현재 시각
으로 남은 유효 시간(Time To Live, TTL)을 계산한다.5. Redis 블랙리스트 등록
6. 토큰 블랙리스트 필터링
관계형 데이터 베이스로도 블랙리스트 기능 구현은 할 수 있지만 속도, 관리 측면에서 효율성이 떨어진다.
Redis
서버가 죽으면 블랙리스트도 다 사라지지 않나?
맞다. Redis
는 메모리 기반이라 휘발성이다.
그래서 운영 환경에서는 RDB 스냅샷 방식, AOF 방식등을 사용해 영속성을 확보한다고 한다.
RDB 스냅샷: 일정 시점의 Redis
메모리 데이터를 통째로 덤프
AOF: Redis
에서 수행된 모든 쓰기 명령을 파일에 기록,
서버가 재시작되면 파일을 순서대로 다시 실행해서 복원한다.
이건 운영 환경에 따라 달라질 수 있는 문제다.
블랙리스트는 어차피 "토큰 만료 시점까지만 유효" 하면 되기 때문에,
장기 보관보다는 TTL을 기반으로 한 자동 만료가 더 실용적일 수 있다.
대부분의 경우에는 Redis
에서 제공하는 기본적인 TTL 관리만으로도 충분하다.
나는 이런 특성 때문에, 블랙리스트에 한해서는 영속성 확보가 반드시 필요한 건 아니라고 생각한다.
JWT는 분산 시스템에서 유용한 인증 수단이지만, 한 번 발급되면 서버가 제어할 수 없다는 점에서 관리의 어려움이 있다.
Redis
를 활용한 블랙리스트 방식으로 토큰 무효화를 처리한 경험은 보안적인 관점과 서버 인증 관련 역량을 한 단계 성장시켜줬다는 생각이 든다.
Redis는 하루 빨리 윈도우를 지원하도록 하라...