이전 편에서 세션 기반 인증이 다중 서버 환경에서 부딪히는 문제를 살펴봤다. 핵심은 "서버가 상태를 보관한다"는 구조 자체였다. 이번 편에서는 그 전제를 뒤집는 Stateless 인증이 무엇인지, 어떤 원리로 확장성 문제를 풀어내는지 알아본다.
Stateless의 의미
Stateless란 "서버가 클라이언트의 상태를 저장하지 않는다"는 뜻이다. 각 요청이 완전히 독립적이어서, 서버는 이전 요청에서 무슨 일이 있었는지를 전혀 기억하지 않는다.
이게 인증과 무슨 관계가 있을까? 세션 방식에서 서버가 보관하던 "이 사용자는 로그인했다"는 상태를, 서버가 아닌 클라이언트가 직접 들고 다니게 하는 것이다.
비유하자면 이렇다. 세션 방식은 도서관에서 회원증 번호만 보여주면, 직원이 회원 명부를 뒤져서 대출 가능 여부를 확인하는 것과 같다. 명부가 한 권뿐이니, 직원이 여러 명이면 그 명부를 공유하느라 복잡해진다.
토큰 방식은 대출 가능 여부, 회원 등급, 만료일이 다 적힌 카드를 회원이 직접 들고 다니는 것이다. 직원은 카드를 보고 진짜인지만 확인하면 된다. 명부를 뒤질 필요가 없으니, 직원을 몇 명이든 늘릴 수 있다.
세션과 토큰의 인증 흐름 비교
두 방식의 차이를 흐름으로 비교해보자.
Cookie: SESSION=abc123 Server->>Store: abc123 조회 Store-->>Server: {user_id: 1, role: USER} Note over Server: 저장소에서 받은
정보로 사용자 식별 Server-->>Browser: 200 OK end
Authorization: Bearer eyJhbG... Note over Server: 토큰을 열어보면
user_id: 1, role: USER
→ 서버 메모리에서 바로 처리 Server-->>Browser: 200 OK end
세션 방식에서는 서버가 매번 외부 저장소를 조회해야 한다. 토큰 방식에서는 서버가 토큰 자체에서 정보를 꺼내기 때문에 외부 I/O가 없다. 이 차이가 확장성에서 결정적인 역할을 한다.
Self-contained 토큰
토큰 기반 인증의 핵심 개념이 Self-contained다. "토큰 안에 필요한 정보가 모두 들어있다"는 뜻이다.
세션 ID는 abc123 같은 의미 없는 문자열이다. 이 문자열만 봐서는 누구의 세션인지, 권한이 뭔지 전혀 알 수 없다. 반드시 저장소를 조회해야 한다.
반면 토큰에는 이런 내용이 담겨 있다.
{
"user_id": 1,
"email": "user@example.com",
"role": "USER",
"exp": 1670000000,
"iat": 1669996400
}
사용자 ID, 이메일, 권한, 만료 시간까지 토큰 안에 전부 있으니, 서버는 이것만 열어보면 바로 사용자를 식별하고 권한을 확인할 수 있다. DB나 Redis를 조회할 필요가 없다.
토큰은 JSON을 Base64URL로 인코딩한 문자열이다. 디코딩하면 원래 JSON이 나온다. 여기에 서명을 검증하면 변조 여부까지 확인할 수 있다. 구체적인 구조는 3 JWT의 구조 해부에서 자세히 다룬다.
이 특성이 왜 중요한지, 코드로 비교하면 더 명확하다.
// 세션 방식 — 매 요청마다 Redis 조회 (네트워크 I/O 발생)
@GetMapping("/profile")
public UserProfile getProfile(@CookieValue("SESSION") String sessionId) {
Session session = sessionRepository.findById(sessionId);
Long userId = session.getUserId();
return userRepository.findById(userId);
}
// 토큰 방식 — 서버 메모리에서 바로 파싱 (네트워크 I/O 없음)
@GetMapping("/profile")
public UserProfile getProfile(@RequestHeader("Authorization") String authHeader) {
String token = authHeader.substring(7);
Claims claims = jwtUtil.parseToken(token);
Long userId = claims.get("user_id", Long.class);
return userRepository.findById(userId);
}
sessionRepository.findById()— Redis 같은 외부 저장소를 네트워크로 조회한다jwtUtil.parseToken()— 토큰을 CPU 연산으로 파싱한다. 외부 호출이 전혀 없다
둘 다 마지막에는 userRepository.findById()로 DB를 조회하지만, 그건 프로필 데이터를 가져오기 위한 것이다. 인증 자체에 외부 I/O가 필요한가가 핵심 차이다.
확장성이 달라지는 이유
이 차이가 서버 확장에서 어떤 의미를 갖는지 보자.
세션 방식은 서버를 아무리 늘려도 Redis라는 공유 의존성이 남아 있다. 서버 100대가 되면 Redis 조회도 100배가 되니까, Redis도 함께 확장해야 한다.
토큰 방식은 각 서버가 독립적으로 토큰을 검증한다. 서명 검증에 필요한 건 Secret Key 하나뿐이고, 이건 서버 시작 시 환경 변수로 한 번만 로드하면 된다. 서버를 추가할 때 공유 인프라를 신경 쓸 필요가 없다.
| 항목 | 세션 방식 | 토큰 방식 |
|---|---|---|
| 서버 추가 시 | Redis 연결 설정 필요 | Secret Key만 있으면 끝 |
| 인증 처리 | 네트워크 I/O (Redis 조회) | 로컬 CPU 연산 |
| 공유 인프라 | Redis 클러스터 필수 | 불필요 |
| 장애 영향 | Redis 장애 → 전체 인증 마비 | 서버 개별 장애만 영향 |
토큰이 적합한 환경
모든 상황에 토큰이 답은 아니다. 토큰이 특히 빛을 발하는 환경이 있다.
RESTful API
REST의 핵심 원칙 중 하나가 Stateless다. 각 요청이 독립적이어야 한다는 원칙인데, 세션은 이 원칙을 위반한다. 서버가 세션이라는 상태를 보관하고 있으니까. 토큰 방식은 REST의 Stateless 철학과 자연스럽게 맞아떨어진다.
마이크로서비스 아키텍처
마이크로서비스에서 토큰의 장점이 가장 극적으로 드러난다.
인증 서비스가 토큰을 한 번 발급하면, API Gateway, 주문 서비스, 결제 서비스가 각각 독립적으로 토큰을 검증한다. 서비스들끼리 세션 저장소를 공유하거나, 인증 서비스에 매번 확인을 요청할 필요가 없다.
세션 방식이었다면? 모든 서비스가 동일한 Redis를 바라봐야 하고, Redis에 접근할 수 없는 서비스는 인증 자체가 불가능해진다.
SPA와 모바일 애플리케이션
SPA(React, Vue 등)는 JavaScript로 API를 직접 호출하는데, Authorization 헤더에 토큰을 실어 보내는 방식이 자연스럽다. 모바일 앱도 브라우저 쿠키에 의존할 수 없는 환경이라 토큰이 더 적합하다.
특히 프론트엔드와 API 서버의 도메인이 다를 때 차이가 크다. 쿠키는 도메인 제약이 있어서 www.example.com에서 받은 쿠키를 api.example.com으로 보내려면 별도 설정이 필요하다. 반면 Authorization 헤더는 CORS 설정만 되면 도메인에 상관없이 자유롭게 전송할 수 있다.
세션이 더 나은 경우
토큰의 한계도 분명하다. 다음 상황에서는 세션이 더 적합하다.
- 서버 사이드 렌더링 — Spring MVC + Thymeleaf 같은 전통적인 구성에서는 브라우저 쿠키가 자동으로 관리되므로 세션이 편하다
- 단일 서버, 소규모 서비스 — 확장 계획이 없다면 토큰의 관리 복잡도가 오히려 부담이다
- 즉시 로그아웃 필수 — 금융 서비스나 관리자 시스템처럼 권한 변경 시 즉시 세션을 무효화해야 하는 경우. 토큰은 발급 후 만료 전까지 서버에서 강제로 무효화하기 어렵다
실무에서는 두 방식을 섞어 쓰기도 한다. 웹 브라우저에는 세션을, 모바일 API에는 토큰을 사용하는 식이다. 정답은 하나가 아니라 상황에 맞는 선택이다.
토큰의 트레이드오프
토큰 방식이 얻는 것과 잃는 것을 정리하면 이렇다.
얻는 것
- 수평 확장 용이 — 서버 추가 시 공유 인프라 불필요
- 서비스 간 독립성 — 마이크로서비스에서 각 서비스가 독립적으로 인증 처리
- 인프라 단순화 — Redis 같은 세션 저장소 운영 부담 없음
- 크로스 플랫폼 — 웹, 모바일, IoT 등 다양한 클라이언트에서 동일하게 사용
잃는 것
- 토큰 크기 — 매 요청 헤더에 토큰이 포함되어 트래픽이 증가한다. 세션 ID는 수십 바이트지만, 토큰은 수백 바이트~수 킬로바이트가 될 수 있다
- 즉시 무효화 어려움 — 토큰은 만료 전까지 유효하므로, 강제 로그아웃이 까다롭다
- 정보 노출 가능성 — 토큰은 인코딩일 뿐 암호화가 아니라서, 디코딩하면 내용을 볼 수 있다
- 상태 동기화 불가 — 토큰 발급 후 사용자 권한이 변경돼도, 이미 발급된 토큰에는 반영되지 않는다
이런 트레이드오프를 이해한 상태에서, 다음 편에서는 이 토큰의 실체인 JWT가 구체적으로 어떻게 생겼는지 구조를 해부해본다.