JWT 시리즈

1. 토큰 기반 인증의 개념과 필요성 (현재 글)

2. JWT의 구조와 원리

세션 기반 인증은 단일 서버에서는 잘 동작하지만, 서버를 여러 대로 확장하는 순간 골치 아픈 문제가 터진다. 토큰 기반 인증은 이 확장성 문제를 근본적으로 해결하기 위해 등장했다. 왜 세션이 한계에 부딪히는지, 토큰이 그걸 어떻게 풀어내는지 살펴보자.

세션 기반 인증의 한계

세션 인증의 기본 동작부터 다시 짚어보자.

sequenceDiagram participant Client as 클라이언트 participant Server as 서버 participant Store as 세션 저장소 Client->>Server: POST /api/login {email, password} Note over Server: 인증 확인 Server->>Store: 세션 생성 (session_id: abc123) Store-->>Server: 저장 완료 Server-->>Client: Set-Cookie: SESSION=abc123 Note over Client: 브라우저가 쿠키 저장 Client->>Server: GET /api/profile (Cookie: SESSION=abc123) Server->>Store: 세션 조회 (abc123) Store-->>Server: user_id: 1 Note over Server: 사용자 식별 Server-->>Client: 200 OK (프로필 데이터)

핵심은 서버가 세션 저장소에 상태를 보관한다는 점이다. 클라이언트는 세션 ID만 쿠키로 들고 다니고, 실제 사용자 정보는 서버 쪽에 있다. 단일 서버에서는 아무 문제가 없다.

다중 서버 환경의 문제

서버를 여러 대로 늘리면 상황이 달라진다.

graph TD LB[로드 밸런서] S1[서버 1] S2[서버 2] SS1[세션 저장소] SS2[세션 저장소] LB --> S1 LB --> S2 S1 --> SS1 S2 --> SS2 SS1 -.- |"abc123 존재"| SS1 SS2 -.- |"abc123 없음"| SS2 style S1 fill:#4CAF50,stroke:#333,stroke-width:2px,color:#fff style S2 fill:#f44336,stroke:#333,stroke-width:2px,color:#fff

사용자가 서버 1에서 로그인하면 서버 1의 메모리에 세션이 생긴다. 그런데 다음 요청이 로드 밸런서에 의해 서버 2로 라우팅되면? 서버 2에는 해당 세션이 없으니 인증 실패가 발생한다.

분산 환경의 해결책과 그 한계

이 문제를 해결하는 방법이 두 가지 있는데, 둘 다 근본적인 한계를 안고 있다.

Sticky Session

같은 사용자의 요청을 항상 같은 서버로 보내는 방식이다. 하지만 해당 서버에 장애가 나면 그 서버의 모든 세션이 날아가고, 특정 서버에 부하가 몰릴 수도 있다.

세션 클러스터링 (Redis)

모든 서버가 Redis 같은 공유 저장소를 사용하는 방식이다.

# application.yml
spring:
  session:
    store-type: redis
  redis:
    host: localhost
    port: 6379

얼핏 깔끔해 보이지만, 결국 Redis라는 추가 인프라에 의존하게 된다. Redis가 죽으면 모든 서버의 인증이 불가능해지는 단일 장애점(Single Point of Failure)이 생기고, 모든 API 요청마다 네트워크를 타고 Redis를 조회해야 하니 응답 시간도 늘어난다.

세션 방식의 근본적 문제

서버 수가 10대에서 100대로 늘어나면 세션 동기화 복잡도, Redis 부하, 네트워크 트래픽이 모두 함께 증가한다. 서버를 더 투입할수록 인증 인프라 비용도 선형으로 올라가는 구조다.

Stateless 아키텍처와 토큰

세션의 근본 문제는 "서버가 상태를 들고 있다"는 점에서 비롯된다. 토큰 기반 인증은 이 전제를 뒤집는다.

Stateless란 서버가 클라이언트의 상태를 저장하지 않는 아키텍처다. 인증에 필요한 모든 정보를 클라이언트가 토큰에 담아서 매 요청마다 제시한다. 서버는 받은 토큰의 유효성만 검증하면 된다.

영화관 비유

세션 방식은 영화관 직원이 관객의 티켓을 보관하고, 관객이 올 때마다 보관함을 뒤지는 것과 같다. 토큰 방식은 관객이 직접 티켓을 들고 다니고, 직원은 티켓을 보고 좌석을 안내하기만 하면 된다. 보관함이 필요 없으니 직원을 몇 명이든 늘릴 수 있다.

세션 vs 토큰의 코드 차이

두 방식의 차이를 코드로 비교해보면 핵심이 명확해진다.

// 세션 방식
@GetMapping("/api/profile")
public UserProfile getProfile(@CookieValue("SESSION") String sessionId) {
    Session session = sessionRepository.findById(sessionId);
    Long userId = session.getUserId();
    return new UserProfile(userRepository.findById(userId));
}
  • sessionRepository.findById() — Redis 같은 외부 저장소를 네트워크로 조회한다. 이게 모든 요청마다 발생한다.
// 토큰 방식
@GetMapping("/api/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 new UserProfile(userRepository.findById(userId));
}
  • jwtUtil.parseToken() — 토큰을 서버 메모리에서 바로 파싱한다. 외부 저장소 조회가 없으므로 네트워크 I/O도 없다.

Self-contained 토큰

토큰 기반 인증의 핵심 특성은 Self-contained, 즉 토큰 자체에 필요한 정보가 모두 들어있다는 것이다.

{
  "user_id": 1,
  "email": "user@example.com",
  "role": "USER",
  "exp": 1670000000,
  "iat": 1669996400
}

사용자 ID, 이메일, 권한 정보가 토큰 안에 있으니, 단순한 인가 판단은 DB 조회 없이도 가능하다. 예를 들어 관리자 권한 확인은 토큰의 role 값만 보면 된다.

확장성 비교

토큰 방식이 확장에 유리한 이유를 구조로 보면 이렇다.

graph TD subgraph "세션 방식" LB1[로드 밸런서] --> S1[서버 1] LB1 --> S2[서버 2] LB1 --> S3[서버 3] S1 --> R[Redis 클러스터] S2 --> R S3 --> R style R fill:#f44336,stroke:#333,stroke-width:2px,color:#fff end subgraph "토큰 방식" LB2[로드 밸런서] --> T1[서버 1] LB2 --> T2[서버 2] LB2 --> T3[서버 3] style T1 fill:#4CAF50,stroke:#333,stroke-width:2px,color:#fff style T2 fill:#4CAF50,stroke:#333,stroke-width:2px,color:#fff style T3 fill:#4CAF50,stroke:#333,stroke-width:2px,color:#fff end

세션 방식은 서버를 아무리 늘려도 결국 Redis 클러스터가 병목이 된다. 토큰 방식은 각 서버가 독립적으로 토큰을 검증하기 때문에, 서버 추가 시 공유 인프라를 신경 쓸 필요가 없다.

항목세션 방식토큰 방식
서버 추가 시Redis 연결 설정 필요서버만 추가하면 끝
인증 처리네트워크 I/O (Redis 조회)로컬 CPU 연산
인프라 의존Redis 클러스터 필수불필요

토큰의 트레이드오프

토큰이 만능은 아니다. 영화관 비유를 이어가면, 관객이 직접 티켓을 들고 다니니까 티켓을 잃어버리면 영화를 못 본다. 마찬가지로 토큰에도 분명한 한계가 있다.

장점

  • 수평 확장 용이 — 공유 저장소 불필요
  • 서버 부하 감소 — 세션 저장소 조회 없음
  • 마이크로서비스에 적합 — 각 서비스가 독립적으로 검증
  • 모바일 앱 친화적 — 쿠키에 의존하지 않음

단점

  • 토큰 크기 — 모든 요청 헤더에 토큰이 포함되어 트래픽 증가
  • 즉시 무효화 어려움 — 토큰은 만료 전까지 유효하므로, 강제 로그아웃이 까다로움
  • 민감 정보 포함 불가 — 토큰은 암호화가 아닌 인코딩이라 내용이 노출됨
  • 탈취 시 대응 어려움 — 서버에서 토큰을 삭제할 방법이 없음
하이브리드 전략

두 방식의 장점을 모두 취하기 위해, 웹 브라우저에는 세션을, 모바일 API에는 토큰을 사용하는 하이브리드 방식도 실무에서 많이 쓰인다.

토큰이 적합한 상황

모든 상황에 토큰이 답은 아니다. 어떤 경우에 토큰을 선택해야 하는지 정리해보자.

RESTful API

REST의 핵심 원칙 중 하나가 Stateless다. 각 요청이 독립적이어야 하므로, 서버가 상태를 보관하는 세션보다 토큰이 REST 철학에 부합한다.

마이크로서비스 아키텍처

마이크로서비스에서 세션을 쓰면 모든 서비스가 Redis에 의존하게 된다. 하나의 공유 저장소가 전체 시스템의 단일 장애점이 되는 셈이다.

sequenceDiagram participant User as 사용자 participant Auth as 인증 서비스 participant GW as API Gateway participant Order as 주문 서비스 participant Pay as 결제 서비스 User->>Auth: 로그인 요청 Auth-->>User: JWT 발급 User->>GW: 주문 요청 + JWT GW->>Order: JWT 전달 Note over Order: 독립적으로
토큰 검증 Order->>Pay: 결제 요청 + JWT Note over Pay: 독립적으로
토큰 검증 Pay-->>Order: 결제 완료 Order-->>GW: 주문 완료 GW-->>User: 응답

토큰 방식에서는 인증 서비스가 JWT를 한 번 발급하면, 각 서비스가 공유 저장소 없이 독립적으로 토큰을 검증한다.

SPA와 모바일 애플리케이션

SPA(React, Vue 등)는 JavaScript로 API를 직접 호출하기 때문에, Authorization 헤더에 토큰을 실어 보내는 방식이 자연스럽다. 모바일 앱도 마찬가지로, 브라우저 쿠키에 의존할 수 없는 환경이라 토큰이 더 적합하다.

특히 프론트엔드와 API 서버의 도메인이 다를 때(예: www.example.comapi.example.com), 쿠키는 도메인 제약이 있지만 Authorization 헤더는 CORS 설정만 되면 자유롭게 전송할 수 있다.

세션이 더 나은 경우

반대로 다음 상황에서는 세션이 더 적합하다.

  • 전통적인 서버 사이드 렌더링 — Spring MVC + Thymeleaf 같은 구성에서는 브라우저 쿠키가 자동으로 관리되므로 세션이 편하다
  • 단일 서버, 소규모 서비스 — 확장 계획이 없다면 토큰의 복잡한 관리가 오히려 오버헤드다
  • 즉시 로그아웃이 필수 — 관리자 시스템이나 금융 서비스처럼 권한 변경 즉시 세션을 무효화해야 하는 경우