웹 서비스에서 인증을 구현할 때 세션과 JWT 중 어느 쪽을 선택할지 결정해야 한다. 세션은 서버가 상태를 관리하여 즉시 제어가 가능하고, JWT는 서버에 상태를 두지 않아 수평 확장에 유리하다. 핵심은 “인증 상태를 어디에 둘 것인가"다.
로그인한 사용자가 다음 페이지를 요청하면, 서버는 이 사용자가 누구인지 모른다. HTTP가 stateless이기 때문이다. 인증 상태를 유지하려면 상태를 어딘가에 저장해야 한다.
세션 인증
세션 인증은 서버가 사용자의 인증 상태를 직접 관리하는 방식이다.
사용자가 로그인하면 서버는 세션 데이터를 생성하고, 고유한 세션 ID를 발급한다. 이 세션 ID는 쿠키를 통해 클라이언트에 전달된다. 이후 클라이언트가 요청을 보낼 때마다 쿠키에 담긴 세션 ID가 함께 전송되고, 서버는 이 ID로 세션 저장소를 조회해 사용자를 식별한다.
세션 데이터는 서버 메모리, 파일 시스템, 또는 Redis 같은 외부 저장소에 보관된다.
장점
서버가 세션을 직접 관리하기 때문에 제어가 용이하다. 특정 사용자의 세션을 즉시 무효화할 수 있다. 강제 로그아웃이나 동시 접속 제한 같은 기능을 구현하기 쉽다.
클라이언트에는 세션 ID만 전달되므로, 사용자 정보가 네트워크를 통해 노출될 위험이 적다.
한계
서버가 상태를 저장하므로 수평 확장에 제약이 생긴다. 서버 인스턴스가 여러 대일 때, 사용자가 다른 인스턴스로 요청을 보내면 세션을 찾을 수 없다. 이를 해결하려면 sticky session을 설정하거나, Redis 같은 공유 세션 저장소를 도입해야 한다.
사용자 수가 늘어나면 세션 저장소의 부하도 함께 증가한다.
JWT
JWT(JSON Web Token)는 인증 정보를 토큰 자체에 담는 방식이다. 서버가 상태를 저장하지 않는다.
구조
JWT는 세 부분으로 구성된다. 점(.)으로 구분한다.
header.payload.signature
header는 토큰의 타입과 서명 알고리즘을 명시한다. payload는 사용자 식별 정보와 만료 시간 등의 클레임을 담는다. signature는 header와 payload를 비밀 키로 서명한 값이다.
서버는 토큰을 받으면 signature를 검증한다. payload가 변조되었으면 signature가 일치하지 않으므로, 토큰이 위조되었는지 판별할 수 있다. 세션 저장소를 조회할 필요가 없다.
장점
서버가 상태를 저장하지 않으므로 수평 확장이 자유롭다. 어떤 서버 인스턴스가 요청을 받아도 토큰만 검증하면 된다. 별도의 세션 저장소가 필요 없다.
한계
토큰이 발급되면 만료 전까지 무효화하기 어렵다. 서버에 토큰 상태가 없기 때문이다. 강제 로그아웃이 필요하면 별도의 블랙리스트 저장소를 운영해야 하고, 이 경우 stateless 이점이 줄어든다.
payload는 Base64로 인코딩될 뿐, 암호화되지 않는다. 민감한 정보를 payload에 담으면 안 된다.
토큰 크기가 세션 ID보다 크다. 매 요청마다 전송되므로 네트워크 오버헤드가 세션 방식보다 크다.
access token과 refresh token
JWT를 사용할 때, 토큰을 하나만 발급하면 유효 기간 설정에서 트레이드오프가 발생한다. 길게 잡으면 탈취 시 위험하고, 짧게 잡으면 사용자가 자주 다시 로그인해야 한다.
이 문제를 해결하기 위해 토큰을 둘로 나눈다.
access token은 API 요청 시 인증에 사용된다. 유효 기간을 짧게 설정한다. 탈취되더라도 짧은 시간 내에 만료된다.
refresh token은 access token을 재발급받는 데 사용된다. 유효 기간이 상대적으로 길다. 서버 측에서 저장하고 관리할 수 있어 필요 시 무효화가 가능하다.
갱신 흐름은 다음과 같다.
- 클라이언트가 access token으로 API를 요청한다.
- access token이 만료되면 서버가 401을 반환한다.
- 클라이언트가 refresh token으로 새 access token을 요청한다.
- 서버가 refresh token을 검증하고, 새 access token을 발급한다.
저장 전략
세션 ID는 쿠키에 저장하는 것이 표준이다. JWT는 선택지가 여러 개이고, 어디에 저장하는가에 따라 보안 특성이 달라진다.
메모리
JavaScript 변수에 저장한다. 페이지를 새로고침하면 사라진다. XSS 공격에 노출되지 않지만, 새로고침마다 다시 인증해야 한다.
localStorage
브라우저 저장소에 보관한다. 새로고침해도 유지된다. 그러나 JavaScript로 접근할 수 있으므로, XSS 공격에 취약하다. XSS가 발생하면 토큰이 탈취될 수 있다.
cookie(HttpOnly)
HttpOnly 속성을 설정하면 JavaScript에서 접근할 수 없다. XSS로 토큰을 직접 탈취하는 것을 막을 수 있다. 다만 CSRF 공격에는 별도 대응이 필요하다. SameSite 속성과 CSRF 토큰을 함께 사용하는 것이 일반적이다.
| 저장 위치 | 새로고침 유지 | XSS 내성 | CSRF 내성 |
|---|---|---|---|
| 메모리 | X | 노출 없음 | 해당 없음 |
| localStorage | O | 취약 | 해당 없음 |
| cookie(HttpOnly) | O | 노출 없음 | 별도 대응 필요 |
자주 사용되는 조합은 access token을 메모리에, refresh token을 HttpOnly cookie에 저장하는 방식이다. access token은 짧은 수명으로 노출 위험을 줄이고, refresh token은 HttpOnly로 XSS를 차단한다.
비교
| 항목 | 세션 | JWT |
|---|---|---|
| 상태 저장 | 서버 | 클라이언트(토큰) |
| 수평 확장 | 공유 저장소 필요 | 자유로움 |
| 즉시 무효화 | 쉬움 | 어려움(블랙리스트 필요) |
| 네트워크 크기 | 작음(세션 ID) | 큼(토큰 전체) |
| 서버 부하 | 저장소 조회 | 서명 검증(CPU) |
서비스 구조와 요구사항이 선택을 결정한다. 즉시 무효화가 필수인지, 서버 간 상태 공유 비용을 감당할 수 있는지가 핵심 분기점이다. 단일 서버 환경에서 즉시 제어가 중요하면 세션이 적합하다. 분산 환경에서 서버 간 상태 공유 없이 인증을 처리해야 하면 JWT가 적합하다.