티스토리 뷰

Backend

아직도 DB에 JWT 저장하세요?

seungwonlee 2025. 10. 29. 15:20

JWT란?

JWT(json web token)는 클라이언트와 서버 간 인증 정보를 토큰 형태를 안전하게 전달하기 위한 표준이다. 기존 세선/쿠키 방식과 달리 서버는 상태를 저장하지 않고 토큰 자체만으로 정보의 유효성을 검증할 수 있어 서버 자원 소모를 줄일 수 있다. 즉 서버가 세션을 들고 있지 않아도 클라이언트가 JWT만 가지고 있다면 인증이 가능하다. 그래서 Stateless 인증 방식이라고 부른다.

3가지 구성 요소

구분 설명
Header(헤더) 토콘의 타입과 해싱 알고리즘(HS256, HS512) 정보가 담김
Payload(정보) 유저 정보, 권한, 만료시간 등이 포함
Signature(서명) 위 두 정보를 서버의 비밀키로 서명하여 생성, 서버는 이 서명을 통해 토큰의 위변조 여부를 확인

JWT를 관리하는 대표적인 방법 두 가지

방식 설명
DB 저장 AccessToken/ RefreshToken을 RDB에 저장하고 상태를 관리
Redis 저장 인메모리 캐시(Redis)로 토큰, TTL, 블랙리스트를 관리

DB로 관리하는 경우

데이터가 영구적으로 저장되어 서버가 재시작해도 토큰 정보가 유지된다는 장점이 있지만 단점으론 토큰 검증 및 관리를 위해 매번 디스크 I/O 발생하는 DB에 접근해야 하고 TTL 처리에 별도 배치나 쿼리가 필요하다가 있습니다.

Redis로 관리하는 경우

인메모리 기반이라 읽기/쓰기가 매우 빠르다, 토큰 검증 시 부하가 적어 대규모 트래픽에 유리하다, TTL기능을 활용하여 토큰 만료를 자동으로 관리할 수 있다는 장점이 있지만 단점으론 메모리에 데이터가 저장되므로 레디스 서버가 다운되거나 재시작되면 모든 토큰 정보가 삭제됩니다.


블랙리스트

유효 기간이 남았음에도 강제로 무효화해야 하는 토큰 목록을 의미로 사용자가 로그아웃하면 액세스 토큰과 리프레시 토큰 모두 즉시 무효화되어야 한다. 보안상의 이유로 비밀번호 변경 시 기존의 모든 토큰을 무효화 때, 서버에서 특정 토큰이 해킹되었다고 판단하여 강제로 만료시켜야 할 때 사용한다.

JTI(JWT ID)의 역할

블랙리스트를 관리할 때 토큰 문자열 전체 대신 JTI라는 클레임을 사용한다. JTI는 토큰에 부여하는 고유 식별자로 서버는 토큰 전체를 저장하는 대신 로그아웃된 토큰의 JTI만 Redis에 저장한다. 이후 어떤 요청이 들어오든 토큰의 JTI를 추출해 Redis에서 해당 JTI가 존재하는지 확인하고 존재하면 유효 기간과 상관없이 무효 토큰으로 처리한다. 장점으론 데이터 저장 공간을 절약한다.

 

액세스 토큰을 생성할 때 페이로드에 jti 클레임을 추가한다.

redisTemplate.opsForValue().set(
    "bl:access:" + jti,
    "logout",
    remainingTime,
    TimeUnit.SECONDS
);
// 키 구조 bl:access:{jti} = "logout"

Redis의 진짜 강점은 TTL

토큰마다 만료 시간을 설정해 두면 만료 시점이 지나면 Redis가 자동으로 키를 삭제한다. 이제 배치나 스케줄러 없이도 TTL이 지나면 자동으로 데이터가 정리됩니다. 메모리 누수나 "고아 토큰" 걱정이 없어진다.👍

@RedisHash("token")
public class Token {
    @Id
    private String userId;

    private String token;

    @TimeToLive(unit = TimeUnit.SECONDS)
    private Long expiration;
}

@EnableRedisRepositories(enableKeyspaceEvents = ON_STARTUP)

Spring Data Redis에서 @RedisHash를 사용해 엔티티를 저장할 때 Redis 내부에는 두 가지 키가 함께 생성됩니다.

키 이름 설명 TTL 여부
token:{userId} 실제 토큰 데이터 (엔티티 본문) ✅ TTL 적용
token 엔티티의 ID 목록을 담은 인덱스(Set 형태) ❌ TTL 미적용

문제 상황

TTL이 설정된 키는 시간이 지나면 레디스가 자동으로 삭제한다. 하지만 인덱스용 Set은 TTL이 없기 때문에 삭제된 ID가 그대로 남게 되어 phantom key 현상이 발생한다. 즉 실제 데이터는 사라졌지만 인덱스에는 여전히 존재하는 불일치 상태가 생긴다. 

해결 방법

Spring Data Redis는 이 문제를 해결하기 위해 설정을 제공합니다. 이 설정은 레디스에서 TTL이 만료되거나 키가 삭제될 때 발생하는 이벤트를 감지하고 Spring해당 이벤트를 받아 내부 인덱스를 자동 정리해 준다. 
@EnableRedisRepositories(enableKeyspaceEvents = RedisKeyValueAdapter.EnableKeyspaceEvents.ON_STARTUP)

 

설정하지 않을 경우

 

설정했을 경우 자동으로 phantom key 생성

 

결과적으로 TTL 만료와 함께 인덱스까지 자동 정리되므로 "고아 인덱스 없이" 항상 정합성 있는 Redis Repository 상태를 유지할 수 있다.


RedisTemplate vs RedisRepository

Redis에 데이터를 저장하고 접근하는 Spring Data Redis의 두 가지 주요 방법은 RedisTemplate과 RedisRepository입니다. 

 

RedisTemplate

Redis 명령어를 실행하기 위해 제공하는 핵심 클래스입니다. Redis와의 통신을 하여 개발자가 데이터 타입별 연산을 쉽게 수행할 수 있도록 한다. 사용법은 opsForValue().set(), opsForHash().put()) 코드를 직접 작성하여 데이터를 조작합니다. 모든 데이터 조작을 수동으로 처리해야 한다. 

 

RedisRepository

Spring Data의 Repository 추상화를 Redis에 적용한 방식이다. JPA처럼 메서드 이름 규칙을 따르면 Spring이 자동으로 CRUD 구현체를 생성해 준다. TokenRepository extends CrudRepository<Token, String>와 같이 인터페이스를 선언하고 tokenRepository.findById 메서드를 호출하여 사용한다. 개발 편의성과 생산성이 매우 높다.

 

RedisTemplate 타입 권장 사항 <String, Object> vs. <String, String>

String, Object 보다 String, String이 권장되는 이유는 직렬화 문제와 데이터 호환성 때문이다.

 

직렬화 문제 및 데이터 비효율성 

Object 사용 시 Spring은 기본적으로 JDK 직렬화를 사용한다. JDK 직렬화는 Java 객체의 클래스 정보까지 바이트로 저장한다. 레디스에 저장되는 데이터 크기가 불필요하게 커지고(비효율적) 메모리를 낭비한다.

 

String 사용 시 Java객체를 레디스에 저장하기 전에 json 문자열로 명시적으로 변환하여 저장하게 됩니다. 레디스는 기본적으로 문자열 기반 저장소입니다. 저장된 데이터가 json이라는 표준 포맷이 되므로 자바 외의 다른 언어로 구성한 MSA 환경에서 데이터를 쉽게 읽고 처리할 수 있어 호환성을 극대화한다. 


결론적으로 JWT 관리는 DB보다 Redis가 빠르고 TTL 기반 자동 만료로 유지보수가 쉽다고 생각됩니다. 서비스 규모나 비용 등을 고려하면 DB로 관리하는 방식 또한 여러 정책을 가지고 사용한다면 굳이 비용을 들어서 사용할 필요가 없을 수 있다 생각됩니다.

 

728x90
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/11   »
1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30
글 보관함