
로그인을 구현할 때 세션과 JWT 중 무엇을 쓸지 정해야 한다. 검색해 보면 "JWT는 확장성이 좋다", "세션은 안전하다" 같은 요약이 나오는데, 둘 다 절반만 맞는 말이다. 이 글은 둘의 실제 차이와, 선택을 가르는 조건을 정리한다. 결론부터 말하면 로그아웃을 즉시 반영해야 한다면 세션 쪽이 훨씬 단순하다.
근본적인 차이는 상태를 어디에 두는가
세션 방식은 서버가 상태를 갖는다. 로그인하면 서버가 세션을 만들어 저장소에 넣고, 클라이언트에는 세션 ID만 쿠키로 준다. 요청이 올 때마다 그 ID로 저장소를 조회한다.
JWT 방식은 서버가 상태를 갖지 않는다. 로그인하면 사용자 정보를 담고 서명한 토큰을 준다. 요청이 오면 서명만 검증하면 되고, 저장소 조회가 없다.

JWT 의 장점은 조회가 없다는 것
JWT가 좋다고 할 때 실제로 말하는 이점은 이렇다.
저장소 왕복이 없다. 요청마다 Redis나 데이터베이스를 조회하지 않는다. 트래픽이 아주 많으면 이 차이가 의미 있다.
서버가 상태를 공유하지 않아도 된다. 여러 서버가 있어도 세션 저장소를 공유할 필요가 없다. 서버를 늘리기 쉽다.
서비스 경계를 넘기 쉽다. 마이크로서비스에서 각 서비스가 토큰을 자체 검증할 수 있다. 인증 서비스에 매번 물어보지 않아도 된다.
대가는 취소할 수 없다는 것
JWT의 근본 제약은 발급한 토큰을 회수할 수 없다는 것이다. 서버가 상태를 안 갖기 때문에, "이 토큰은 이제 무효"라고 기록할 곳이 없다. 만료 시각까지는 유효하다.
이게 문제가 되는 상황은 생각보다 많다.
사용자가 로그아웃했는데 토큰은 살아 있다. 비밀번호를 바꿨는데 기존 토큰으로 계속 접근된다. 관리자가 계정을 정지시켰는데 반영이 안 된다. 권한을 회수했는데 토큰에 담긴 옛 권한으로 동작한다.
흔한 대응은 토큰 수명을 짧게 하고 리프레시 토큰을 쓰는 것이다. 액세스 토큰은 15분, 리프레시 토큰은 2주로 두고, 리프레시 토큰은 서버에 저장한다.
그런데 여기서 역설이 생긴다. 리프레시 토큰을 서버에 저장하는 순간 상태를 갖게 된다. JWT를 쓴 이유였던 무상태성이 절반 사라진다. 그리고 여전히 최대 15분 동안은 회수된 권한으로 접근이 가능하다.
즉시 차단이 꼭 필요하면 블랙리스트를 둬야 하는데, 그러면 요청마다 블랙리스트를 조회하게 된다. 이 시점에서 세션 방식과 비용이 같아진다.
그 밖의 실무적 함정
토큰 크기. JWT는 쿠키나 헤더에 실려 매 요청마다 오간다. 권한 목록을 잔뜩 담으면 수 KB가 되고, 모든 요청에 그 오버헤드가 붙는다. 세션 ID는 수십 바이트다.
담긴 정보는 누구나 읽을 수 있다. JWT는 서명될 뿐 암호화되지 않는다. Base64를 풀면 내용이 그대로 보인다. 개인정보를 담으면 안 된다.
알고리즘 혼동 공격. alg 헤더를 none으로 바꾸거나 알고리즘을 바꿔치기하는 공격이 알려져 있다. 검증할 때 기대하는 알고리즘을 서버에서 고정해야 한다. 라이브러리 기본값에 의존하면 안 된다.
저장 위치. 브라우저에서 localStorage에 토큰을 넣으면 XSS 한 번에 탈취된다. HttpOnly 쿠키에 넣으면 자바스크립트가 못 읽지만, 그 순간 CSRF 대책이 필요해진다. "JWT를 쓰면 CSRF 걱정이 없다"는 말은 localStorage를 전제로 한 것이고, 그건 XSS 위험을 대신 지는 것이다.
선택 기준
정리하면 이렇게 고른다.
세션이 맞는 경우. 일반적인 웹 서비스, 관리자 화면, 로그아웃과 권한 변경이 즉시 반영돼야 하는 서비스. 서버가 몇 대이고 Redis 하나로 세션을 공유할 수 있다면 세션이 단순하고 안전하다. 대부분의 서비스가 여기 해당한다.
JWT가 맞는 경우. 서비스 간 인증(서버 대 서버), 외부에 API를 제공하면서 발급처와 검증처가 다른 경우, 짧은 수명의 일회성 접근 권한(파일 다운로드 링크 등). 그리고 OIDC 같은 표준 프로토콜을 따라야 할 때다.
섞어 쓰는 경우도 흔하다. 브라우저 사용자는 세션 쿠키로, 외부 API 호출자는 JWT로 인증한다. 각자 다른 요구를 가지므로 다른 방식을 쓰는 것이 자연스럽다.
유행보다 요구사항을 본다
JWT가 널리 쓰이게 된 배경에는 "확장성"이라는 단어가 있었다. 그런데 세션 저장소 하나로 감당 못 할 규모의 서비스는 많지 않다. Redis 한 대가 초당 수만 건을 처리한다.
반대로 로그아웃이 즉시 안 되는 것은 규모와 무관하게 문제가 된다. 보안 사고가 났을 때 "모든 세션을 끊어라"가 안 되면 곤란하다. 계정 탈취 신고를 받고 대응할 수 없는 인증 시스템은 곤란하다.
그래서 나는 기본값을 세션으로 두고, JWT가 필요한 이유가 분명할 때 쓰는 것을 권한다. 무상태성이 실제로 필요한 상황인지, 아니면 그냥 요즘 방식이라 고른 것인지 한 번 물어보면 대개 답이 나온다.
참고 자료
- OWASP Session Management Cheat Sheet — 세션 ID 관리와 쿠키 속성
- OWASP JSON Web Token Cheat Sheet — 알고리즘 고정, 토큰 취소, 저장 위치
- RFC 7519: JSON Web Token — JWT 구조와 클레임 정의
'IT' 카테고리의 다른 글
| 구조화 로깅, 장애 때 찾을 수 있는 로그 남기기 (0) | 2026.10.15 |
|---|---|
| 캐시 전략, 무효화와 스탬피드 대응 (0) | 2026.10.14 |
| N+1 쿼리 문제, 발견하고 고치는 법 (0) | 2026.10.13 |
| 인덱스를 만들었는데 쿼리가 느린 이유 (0) | 2026.10.12 |
| AI 코딩 에이전트 시대의 코드 리뷰 (0) | 2026.10.11 |
