tech/project

[token] 토큰을 어떻게 저장하는 것이 좋을까?

sunm2n 2025. 12. 22. 05:44

우리가 프로젝트를 하면 보통 토큰 기반 방식의 로그인을 가장 많이 구현한다.

 

현재 가장 일반적으로 쓰는 방식은 리프레시 토큰 전략이다.

 

이때 내가 이쪽 파트 개발을 맡았다면 엑세스 토큰과 리프레시 토큰을 각각 어디에 저장해야 할지 고민해야 한다.

 

먼저 크게 저장 방식을 3가지로 나눠서 비교 분석을 해보겠다.

 

1. 토큰 저장 전략별 비교 분석

방식 1: 둘 다 DB(RDB)에 저장

가장 전통적이고 데이터의 지속성(Persistence)을 중시하는 방식이다.

  • 장점: 데이터가 안정적으로 보관되며, 사용자의 로그인 이력이나 기기 정보 등 상세한 감사(Auditing) 데이터를 함께 관리하기 쉽습니다. 갑작스러운 서버 장애에도 로그인이 유지된다.
  • 단점: 매 요청(Access Token 검증 시) 혹은 리프레시 요청 시마다 Disk I/O가 발생하므로 속도가 느리고 트래픽이 몰릴 경우 DB 병목 현상의 주범이 된다.
  • 용도: 사용자 수가 적거나, 강력한 보안 기록(Log)이 필요한 금융권 시스템 등

방식 2: 엑세스 토큰은 Redis, 리프레시는 DB에 저장

이 방식은 사실 JWT의 본래 목적(Stateless)과는 조금 거리가 있다. 보통 Access Token은 서버에 저장하지 않고 서명(Signature)만 확인하는 것이 원칙이기 때문이다. 하지만 '블랙리스트'나 '세션 제어'를 위해 저장한다면 다음과 같다.

  • 장점: 자주 사용되는 Access Token 검증은 메모리(Redis)에서 빠르게 처리하고, 상대적으로 유효기간이 긴 Refresh Token은 안전하게 DB에 보관하여 안정성을 챙기는 하이브리드 전략이다.
  • 단점: 두 개의 서로 다른 저장소를 관리해야 하므로 아키텍처가 복잡해진다.
  • 용도: 성능과 데이터 안정성을 동시에 챙기고 싶을 때 사용하나, 구현 복잡도 때문에 현재는 잘 쓰이지 않는 과도기적 방식이다.

방식 3: 둘 다 Redis(In-memory)에 저장 (DB 미사용)

최신 서비스들이 가장 많이 채택하는 성능 중심의 방식이다.

  • 장점: 응답 속도가 매우 빠르다(Sub-millisecond). Redis의 TTL(Time To Live) 기능을 사용하면 만료된 토큰을 별도의 배치 작업 없이 자동으로 삭제할 수 있어 관리가 매우 편리하다.
  • 단점: 인메모리 특성상 Redis 서버가 재시작되면 모든 토큰 데이터가 날아가 모든 사용자가 로그아웃될 수 있다. (물론 Redis의 영속성 옵션인 AOF/RDB로 보완은 가능)
  • 용도: 대규모 트래픽을 처리해야 하는 웹/앱 서비스

 

이 글만 본다면 방식3이 제일 좋네? 근데 왜 RDB에 저장하는 방식을 사람들이 생각한거지? 이점은 없나? 에 대해 고민을 해볼 필

요도 있다고 생각한다.

 

먼저 위의 글만 읽었을 때는 데이터의 영속성과 상대적으로 redis 보다는 RDB가 보안적으로 안전해 보인다? 라는 의견을 제시할 수 있다.

 

근데 사실 보안 때문이라면 RDB에 저장되기 전에 탈취 당하면 의미가 없는거 아닌가? 라는 고민도 해볼 수 있다.

 

결론부터 이야기 하자면

 

토큰을 DB(RDB/Redis)에 저장하는 목적은 '탈취를 막기 위해서'가 아니라, '탈취된 이후의 대응(무력화)'과 '데이터의 영속성' 때문

 

 

 

2. "탈취하면 의미 없는 거 아닐까?"에 대한 답변

 

보통 토큰이 탈취되는 시점은 서버에 저장되기 전이 아니라, 사용자의 브라우저(클라이언트)에 전달된 이후이다.

  • HTTPS(TLS)의 역할: 서버에서 토큰을 생성해 클라이언트로 보낼 때, 전송 구간은 암호화되어 있어 중간에 가로채는 것이 매우 어렵다.
  • 실제 탈취 경로: 대부분 사용자의 PC가 해킹당하거나, XSS(Cross-Site Scripting) 공격으로 브라우저 저장소(LocalStorage)에 있는 토큰을 탈취당하는 경우이다.

이때 서버가 토큰을 DB에 들고 있다면 다음과 같은 '사후 통제권'을 갖게 된다.

  1. 즉각적인 무효화(Revocation): 사용자가 "나 해킹당한 것 같아요!"라고 하거나, 이상 접근이 감지되면 서버 DB에서 해당 리프레시 토큰을 삭제해버리면 됩니다. 그럼 해커는 더 이상 토큰을 갱신할 수 없다.
  2. Stateless의 한계 극복: 토큰을 어디에도 저장하지 않으면(Pure Stateless), 그 토큰이 만료될 때까지 서버는 해커의 접근을 막을 방법이 전혀 없다.

 

3. 왜 Redis 대신 RDB(방식 1, 2)를 선택할까?

Redis가 훨씬 빠른데도 굳이 RDB를 쓰는 이유는 '보안성'보다는 '데이터의 신뢰성과 영속성' 때문이다.

RDB(방식 1, 2)를 쓰는 이유

  • 데이터 비휘발성: Redis는 메모리 기반이라 설정에 따라 서버가 꺼지면 데이터가 날아갈 위험이 있는 반면 RDB는 안정적인 디스크 저장을 보장한다.
  • 복합적인 정보 관리: 단순히 토큰 값만 저장하는 게 아니라, 어떤 기기(IP, Browser)에서, 언제 마지막으로 접속했는지 등의 상세한 이력을 남겨야 할 때 RDB의 관계형 구조가 훨씬 유리하다.
  • 트랜잭션 보장: 로그인 시 사용자 포인트 지급, 로그인 로그 기록 등과 토큰 발급을 하나의 트랜잭션으로 묶어 데이터 무결성을 보장하기 쉽다.

Redis(방식 3)가 대세인 이유

  • 사실 요즘은 Redis도 AOF/RDB 백업 기능이 잘 되어 있어 데이터 유실 위험이 매우 낮다.
  • 무엇보다 리프레시 토큰은 유효기간이 지나면 버려지는 휘발성 데이터의 성격이 강하므로, 굳이 무거운 RDB보다는 빠른 Redis를 쓰는 것이 현대적 아키텍처(MSA 등)에 더 적합하다.

 

좋다. 여기까지 보면 일단 당장은 방법3이 좋은거 같다.

 

하지만 여기서 끝내면 안된다.

 

내가 말한 방법3에는 허점이 존재한다.

 

JWT(JSON Web Token)의 탄생 목적 자체가 상태를 저장하지 않기 위해서(Stateless)인데 왜 redis에 저장한다는 거지?

 

라는 의문을 가져야 한다.

 

만약 가졌다면 당신은 이미 고수다.

 

현대적인 아키텍처(Spring Boot + MSA 등)에서 가장 권장되는 방식은  3번의 변형인 "Access Token은 Stateless하게, Refresh Token만 Redis에 저장"하는 방식이다.

 

구분 전략 이유
Access Token 저장 안 함 (Stateless) 서버는 토큰의 서명만 확인하여 DB/Redis 조회를 최소화함 (성능 극대화).
Refresh Token Redis 저장 만료 기간이 길고 보안상 탈취 시 즉시 무효화(Revoke)해야 하므로 서버가 상태를 관리함. Redis의 TTL로 자동 삭제 가능
Logout Redis Blacklist 로그아웃된 Access Token을 Redis에 잠시 등록하여 만료 전까지 재사용을 차단함

 

Access Token은 아예 저장하지 않는 것이 JWT의 핵심 이점(Stateless)을 살리는 길이다. 다만 보안 강화를 위해 리프레시 토큰 로테이션(RTR, Refresh Token Rotation) 기법을 Redis와 함께 조합하는 것이 2025년 현재의 표준이다.

 

 

https://www.youtube.com/watch?v=2z_FRqOes4s

 

2025년 최신 환경에서 Spring Security와 Redis를 활용해 엑세스 및 리프레시 토큰을 구현하는 실전 가이드 영상이다.

참고하면 더 좋을거 같다.

 

4. RTR이 작동하는 과정 (Flow)

  1. 사용자가 엑세스 토큰(만료됨) + 리프레시 토큰(RT1)을 서버에 보낸다.
  2. 서버는 Redis에 저장된 RT1이 유효한지 확인한다.
  3. [RTR의 핵심] 서버는 RT1을 즉시 폐기(Delete)하고, 새로운 리프레시 토큰(RT2)과 엑세스 토큰을 발급한다.
  4. 클라이언트는 이제 RT2를 저장하고 다음 갱신 때 사용한다.

5. 왜 RTR을 쓰면서도 엑세스 토큰은 저장 안 해?

RTR을 사용하는 가장 큰 이유는 리프레시 토큰 탈취에 대비하기 위해서이다.

  • 탈취 감지: 만약 해커가 RT1을 훔쳐서 먼저 새 토큰을 받아갔다면, 진짜 사용자가 RT1로 요청했을 때 서버는 "어? 이미 폐기된 RT1인데 왜 또 요청이 오지?"라고 판단할 수 있다.
  • 보안 조치: 이 경우 서버는 해당 사용자와 연관된 모든 리프레시 토큰을 무효화하여 해커의 접근을 차단할 수 있다.

이 모든 과정은 리프레시 토큰의 유효성을 체크하는 로직일 뿐이며, 엑세스 토큰은 여전히 서버 메모리나 DB를 거치지 않고 그 자체로 인증 수단이 되는 JWT의 장점을 그대로 가져간다.

 

6. 예외: 엑세스 토큰을 Redis에 저장하는 유일한 경우 (Blacklist)

 

기존 방법3 처럼 엑세스 토큰을 Redis에 저장하는 경우가 있다면, 그것은 보통 RTR 때문이 아니라 '로그아웃' 때문이다.

블랙리스트(Blacklist) 전략: 사용자가 로그아웃을 하면, 아직 만료 시간이 남은 엑세스 토큰을 무효화해야 한다. 이때 해당 엑세스 토큰의 ID(JTI) 등을 Redis에 잠시 저장해두고, 요청이 올 때마다 "이거 로그아웃된 토큰인가?"를 대조한다.

 

정리하자면

  • RTR 기법: 리프레시 토큰만 Redis에 저장하고 관리 (엑세스 토큰은 저장 X)
  • RTR의 목적: 리프레시 토큰이 한 번만 사용되게 함으로써 보안성을 극대화

아키텍처를 설계한다면, "Access Token은 Stateless하게(저장 X), Refresh Token은 Redis에서 RTR 방식으로 관리"하는 조합이 현재 가장 표준적이고 강력한 보안 모델이라고 이해하면 될 거 같다.

 

7. 그럼 왜 리프레시 토큰(RT)은 저장을 해?

여기서 의문이 생길 수 있다. "그럼 리프레시 토큰도 Stateless하게 하면 안 돼?" 좋은 질문이다.

결론부터 말하자면 그럴 수 없는 이유는 '통제권' 때문이다.

  • AcessToken: 너무 자주 쓰이기 때문에 성능을 위해 통제권을 포기하고 Stateless하게 둔다. (대신 수명을 아주 짧게 가져감)
  • RefreshToken: 수명이 길기 때문에(예: 2주), 만약 탈취당했을 때 서버가 "이 토큰은 이제 못 써!"라고 강제로 무효화(Revoke)할 수 있는 수단이 반드시 필요하다. 그래서 서버 저장소(Redis)에 상태를 기록해두는 것이다.

 

정리하자면 "AT는 속도를 위해 자유를 주고(Stateless), RT는 보안을 위해 감옥(Redis)에 가두어 관리한다"

 

 

8. AcessToken을 저장하지 않으면 로그아웃 처리는 어떻게 해야해?

훌륭한 질문이다.

 

Stateless(무상태) 방식의 최대 약점인 "이미 발급된 토큰은 유효기간이 끝날 때까지 서버가 통제할 수 없다"는 문제를 해결하는 방법은 크게 두 가지 관점으로 나뉜다.

 

그리고 보통 실무에서는 보안 요구사항에 따라 이 두 가지를 적절히 섞어서 사용합니다.

 

8-1. 블랙리스트(Blacklist) 방식 (가장 확실한 보안)

사용자가 로그아웃을 하면, 서버는 해당 사용자의 Access Token(AT)을 수거하여 "이 토큰은 이제 못 쓰는 토큰이다"라고 Redis에 등록해버리는 방식이다.

  • 작동 원리:
    1. 사용자가 로그아웃 요청을 보낸다.
    2. 서버는 요청에 포함된 AT의 남은 유효시간을 계산한다.
    3. Redis에 key: AT값 / value: logout 형태로 저장하고, 유효시간만큼 TTL(만료시간)을 설정한다.
    4. 이후 모든 API 요청 시, 서버는 Redis에 이 토큰이 블랙리스트에 있는지 먼저 확인한다.
  • 장점: 로그아웃 즉시 토큰이 무효화되어 가장 안전하다.
  • 단점: "DB 조회를 하지 않겠다"는 Stateless의 장점을 일부 포기해야 한다. (요청마다 Redis를 확인해야 하므로)

 

8-2. Refresh Token 삭제 방식 (성능과 보안의 타협)

 

대부분의 일반적인 서비스(커뮤니티, 쇼핑몰 등)에서 채택하는 방식이다. 엑세스 토큰은 그냥 두고, 리프레시 토큰(RT)만 서버(Redis)에서 지워버린다.

  • 작동 원리:
    1. 사용자가 로그아웃하면 서버 Redis에 저장된 RT를 즉시 삭제한다.
    2. 클라이언트에 저장된 AT와 RT도 삭제하라고 응답한다
  • 결과:
    • 이미 발급된 AT는 남은 유효기간(예: 5~10분) 동안은 여전히 사용 가능하다.
    • 하지만 AT가 만료되는 순간, 서버에 RT가 없으므로 재발급이 불가능해져 결국 로그아웃 상태가 된다.
  • 장점: 매 요청마다 Redis를 조회할 필요가 없어 Stateless의 장점(성능)을 그대로 유지한다.
  • 단점: 로그아웃 후에도 AT가 살아있는 5~10분 동안은 보안 취약점이 존재한다.

 

8-3. 그래서 어떤 걸 골라야 하지?

구분 블랙리스트 방식 RT 삭제 방식
보안성 매우 높음 (즉시 차단) 보통 (AT 만료 전까지 유효)
성능 약간 저하 (Redis 매번 조회) 매우 빠름 (Stateless 유지)
구현 난이도 조금 복잡함 매우 간단함
추천 서비스 금융, 관리자 페이지, 개인정보 민감 서비스 일반적인 웹/앱 서비스

 

결론: 현대적인 하이브리드 전략

현재 가장 많이 쓰이는 최적의 조합은 다음과 같다.

  1. 평상시: RT 삭제 방식을 기본으로 사용한다. (성능 최적화)
  2. 보안 강화: AT의 유효기간을 아주 짧게(5~15분) 설정하여, 로그아웃 후 발생할 수 있는 보안 공백을 최소화한다.
  3. 특수 상황: 계정 해킹 신고나 관리자에 의한 강제 로그아웃이 필요한 경우에만 블랙리스트 로직을 가동한다.

 

 

학부 수준의 프로젝트라면 RT 삭제 방식으로 구현하되, "보안을 위해 AT 만료 시간을 짧게 잡았다"라고 논리를 세우는 것만으로도 충분히 훌륭한 설계가 될 것이다. (물론 나도 학부생이기 때문에 100% 장담은 못한다.)

 

만약 블랙리스트까지 구현하게 된다면  메모리 낭비를 줄이기 위해 어떤 값을 Key로 쓰는 게 효율적인지(토큰 전체 vs 토큰의 JTI)에 대해서도 고민을 해보면 더더욱 좋을거 같다.