앞선 글에서는 멀티 디바이스 환경에서 기기별 세션을 분리하고, 모든 기기 로그아웃과 refresh 재사용 탐지를 어떻게 설계해야 하는지를 다뤘습니다. 구조 자체는 명확하지만, 실제 트래픽이 늘어나기 시작하면 또 다른 문제가 바로 드러납니다.
바로 “세션 검증 비용”입니다. JWT만 검증하면 빠르지만 통제가 어렵고, 매 요청마다 DB에서 세션 상태를 조회하면 통제는 되지만 성능과 비용이 부담됩니다. 운영 단계에서는 이 두 가지를 동시에 만족시켜야 합니다.
다음 질문에 대한 실무적인 답을 정리합니다.
- JWT 기반 인증에서 왜 Redis가 필요한가
- Redis를 세션 저장소가 아니라 “검증 캐시”로 쓰는 기준
- Redis 장애, TTL 만료, 캐시 유실 상황을 어떻게 다뤄야 하는가
- 운영 중 실제로 문제가 되는 지점과 대응 전략
개념/배경 설명: JWT 인증만으로는 왜 부족한가
JWT의 가장 큰 장점은 서버가 상태를 가지지 않아도 된다는 점입니다. 서명 검증과 만료 시간 확인만으로 인증이 가능하기 때문에, 수평 확장에 매우 유리합니다.
하지만 실무에서는 다음과 같은 요구사항이 반드시 등장합니다.
- 모든 기기에서 즉시 로그아웃
- 보안 사고 발생 시 특정 세션 강제 차단
- 비밀번호 변경 후 기존 로그인 상태 무효화
JWT만 사용하는 구조에서는 이미 발급된 access token을 서버가 통제할 수 없습니다. 결국 “JWT는 인증 수단이고, 세션은 통제 수단”이라는 관점으로 역할을 분리해야 합니다. 문제는 이 통제 정보를 어디에서, 어떤 비용으로 확인하느냐입니다.
여기서 Redis는 “세션의 진실을 저장하는 곳”이 아니라, “세션이 아직 유효한지만 빠르게 확인하는 캐시 계층”으로 사용됩니다. 이 관점을 놓치면 설계가 바로 무거워집니다.
핵심 설계 1: DB, Redis, JWT의 역할 분리
먼저 각 구성 요소의 책임을 명확히 나누는 것이 중요합니다.
- JWT: 요청 인증, 사용자 식별, 빠른 검증
- DB: 세션의 진실(source of truth)
- Redis: 세션 유효성 검증 캐시
Redis를 DB처럼 쓰기 시작하면 다음 문제가 발생합니다.
- Redis 장애 시 인증 전체가 멈춘다
- TTL 설정 실수로 예기치 않은 전체 로그아웃이 발생한다
- 세션 상태가 Redis와 DB에서 불일치한다
따라서 기본 원칙은 단순합니다. “Redis에 세션이 없으면, 최종 판단은 DB로 간다.” Redis는 빠른 경로일 뿐, 최종 권한은 DB가 가져야 합니다.
실무 포인트 정리
- Redis는 통제 캐시이지, 세션 저장소가 아니다
- JWT 검증 이후에만 Redis를 조회한다
- Redis 실패 시 DB로 fallback 가능한 구조를 유지한다
핵심 설계 2: Redis 세션 키 설계와 TTL 기준
Redis에 저장할 정보는 최소화해야 합니다. 실무에서 가장 많이 사용하는 형태는 다음과 같습니다.
Key: session:{sessionId}
Value: userId
TTL: refresh token 만료 시점과 동일
여기서 핵심은 TTL 기준입니다. access token 만료 시간에 맞추는 것이 아니라, refresh token 만료 시간에 맞추는 것이 일반적입니다.
이유는 명확합니다. access token은 짧은 수명을 가지므로 Redis에 굳이 맞출 필요가 없고, refresh token이 살아있는 동안은 세션 자체가 유효한 상태로 간주되기 때문입니다.
“모든 기기 로그아웃”이나 보안 사고 발생 시에는, DB에서 세션을 revoke한 뒤 Redis 키를 삭제하면 즉시 반영됩니다.
실무 포인트 정리
- Redis에는 세션 존재 여부만 저장한다
- TTL은 refresh token 만료 기준으로 설정한다
- 세션 revoke 시 Redis 키 삭제를 반드시 포함한다
핵심 설계 3: 요청 인증 흐름에서 Redis를 쓰는 위치
Redis를 언제 조회하느냐에 따라 성능과 안정성이 크게 달라집니다. 권장 흐름은 다음과 같습니다.
- 1단계: JWT 서명 및 만료 시간 검증
- 2단계: JWT payload에서 sessionId 추출
- 3단계: Redis에서 sessionId 존재 여부 확인
- 4단계: Redis 미스 시 DB 조회 후 보정
async function authenticate(
accessToken: string,
): Promise<{ userId: string; sessionId: string }> {
const payload = verifyJwt(accessToken);
const key = `session:${payload.sessionId}`;
const cached = await redis.get(key);
if (cached) {
return { userId: payload.userId, sessionId: payload.sessionId };
}
// Redis miss → DB fallback
const session = await sessionRepository.findBySessionId(payload.sessionId);
if (!session || session.status !== 'ACTIVE') {
throw new Error('Session revoked');
}
// 캐시 복구
await redis.set(
key,
payload.userId,
'EX',
calcTTLFromSession(session),
);
return { userId: payload.userId, sessionId: payload.sessionId };
}
이 구조의 장점은 명확합니다. 평상시에는 Redis로 빠르게 처리하고, Redis 장애나 캐시 유실 상황에서도 인증이 완전히 깨지지 않습니다.
운영/실무에서 자주 겪는 문제
1) Redis flush로 인한 전체 로그아웃
운영 중 Redis flush, 재시작, 메모리 압박으로 키가 날아가는 상황은 생각보다 자주 발생합니다. 이때 Redis만 믿는 구조라면 모든 요청이 인증 실패로 이어집니다.
DB fallback 구조가 있으면, 일시적인 성능 저하는 발생하더라도 서비스는 유지됩니다. 이 차이가 운영 안정성을 크게 좌우합니다.
2) TTL 불일치로 인한 세션 혼란
Redis TTL이 refresh token 만료보다 짧으면, 정상 사용자도 갑자기 로그아웃되는 현상이 발생합니다. 반대로 너무 길면, revoke된 세션이 살아있는 것처럼 보일 수 있습니다.
TTL 기준은 반드시 문서로 남기고, 토큰 정책이 바뀔 때 함께 점검해야 합니다.
3) Redis를 신뢰 소스로 사용하는 설계
Redis를 세션의 진실로 사용하면, 데이터 불일치가 발생했을 때 복구가 어렵습니다. 특히 보안 사고 상황에서는 어떤 세션이 유효했는지 판단할 수 없게 됩니다.
실무 권장 체크리스트
- JWT, Redis, DB의 역할이 명확히 분리되어 있는가
- Redis 장애 시 DB fallback 경로가 존재하는가
- Redis TTL과 refresh token 만료 정책이 일치하는가
- 세션 revoke 시 Redis 무효화가 즉시 반영되는가
- Redis를 세션 저장소처럼 사용하고 있지는 않은가
JWT 기반 인증 구조에서 Redis를 사용하는 목적은 단순합니다. “빠르게 차단하고, 안전하게 복구하기 위함”입니다.
Redis를 세션의 진실로 착각하지 않고, DB를 기준으로 한 검증 캐시로만 사용하면 성능, 보안, 운영 안정성을 동시에 확보할 수 있습니다. 결국 좋은 인증 설계는 빠르면서도, 문제가 생겼을 때 즉시 통제 가능한 구조입니다.
