안녕하세요, 자바파커입니다.
로그아웃 버튼을 눌렀습니다. 그런데 아까 그 토큰으로 API를 호출하니 아직 됩니다.
로그아웃이 안 된 건가요?
로그아웃은 됐습니다. 토큰이 안 죽었을 뿐입니다.
결론부터 말씀드리면 — 세션과 JWT의 차이는 딱 하나입니다. 로그인 상태를 서버가 기억하느냐, 토큰이 들고 다니느냐. 나머지 차이는 전부 여기서 파생됩니다. 위 현상도 그 파생물 중 하나고요.
세션 — 서버가 기억한다
로그인에 성공하면 서버가 자기 쪽에 기록을 만듭니다.
1. 로그인 성공
2. 서버: 저장소에 { abc123 → 사용자 7번, 만료 30분 } 기록
3. 브라우저에 쿠키로 abc123 만 내려줌
4. 이후 요청마다 브라우저가 abc123 을 보냄
5. 서버: 저장소에서 abc123 을 찾아 "7번이구나" 확인쿠키에는 의미 없는 문자열 하나만 들어갑니다. 실제 정보는 전부 서버에 있습니다.
- 저장소가 서버 메모리면 서버를 재시작하면 전부 로그아웃됩니다
- 서버가 여러 대면 한 대에만 있는 기록을 다른 대가 모릅니다. 그래서 Redis 같은 공용 저장소로 뺍니다
JWT — 토큰이 들고 다닌다
로그인에 성공하면 서버가 정보를 담은 토큰을 만들어 서명해서 넘깁니다. 서버는 아무것도 저장하지 않습니다.
1. 로그인 성공
2. 서버: { 사용자 7번, 만료 시각 } 을 담아 서명한 토큰 생성
3. 브라우저에 토큰 자체를 내려줌
4. 이후 요청마다 브라우저가 토큰을 보냄
5. 서버: 서명만 검증. 조회할 곳이 없음서버는 조회하지 않습니다. 토큰에 이미 답이 들어 있고, 서명으로 "이거 내가 만든 게 맞다"만 확인하면 끝입니다. 그래서 서버를 늘려도 문제가 없습니다.
JWT는 세 조각입니다
eyJhbGciOiJIUzI1NiJ9 . eyJzdWIiOiI3IiwiZXhwIjoxNzY1NDMyMTAwfQ . dQw4w9Wg...
① header ② payload ③ signature| 조각 | 내용 |
|---|---|
| header | 어떤 알고리즘으로 서명했는지 |
| payload | 사용자 식별자, 만료 시각 같은 정보 |
| signature | 앞의 둘을 서버 키로 서명한 값 |
가장 흔한 오해 — payload는 암호화가 아닙니다
base64로 인코딩됐을 뿐입니다. 누구나 디코딩해서 읽을 수 있습니다.
# 두 번째 조각만 떼서 디코딩하면 그냥 보입니다
echo 'eyJzdWIiOiI3IiwiZXhwIjoxNzY1NDMyMTAwfQ' | base64 -d
# {"sub":"7","exp":1765432100}서명이 지켜 주는 건 "내용이 바뀌지 않았다"(무결성)이지 "아무도 못 본다"(기밀성)가 아닙니다.
그래서 payload에 민감한 정보를 넣으면 안 됩니다. 주민번호, 전화번호, 권한 상세, 내부 ID 체계 같은 것들요. 넣어도 동작은 하니까 사고가 나기 전까지 모릅니다.
내용까지 가려야 한다면 JWE라는 별도 규격이 있습니다. 흔히 말하는 JWT는 JWS(서명만)입니다.
결정적 차이 — 로그아웃
여기서 도입부 질문의 답이 나옵니다.
| 세션 | JWT | |
|---|---|---|
| 로그아웃 | 저장소에서 지우면 즉시 끝 | 토큰은 만료까지 살아 있음 |
| 강제 차단 | 해당 기록만 지우면 됨 | 방법이 없음 |
| 권한 변경 | 다음 요청부터 반영 | 토큰 재발급 전까지 옛 권한 |
서버가 기억하지 않기로 했으니, 취소할 방법도 없는 겁니다. 이건 JWT의 결함이 아니라 무상태를 선택한 대가입니다.
그래서 실무에서는 이렇게 우회합니다.
하나, Access Token을 짧게 가져갑니다. 15분쯤으로 두면 최악의 경우 15분만 뚫립니다. 대신 자주 재발급해야 하니 Refresh Token을 같이 씁니다.
둘, Refresh Token은 서버에 저장합니다. 로그아웃하면 이걸 지웁니다. Access Token은 못 죽여도 재발급을 막으면 15분 뒤엔 끝납니다.
셋, 급하면 블랙리스트를 둡니다. 차단할 토큰 목록을 서버가 들고 확인합니다.
눈치채셨을 텐데, 둘과 셋은 결국 서버가 상태를 갖는 것입니다. "JWT는 무상태라 좋다"는 말이 실무에서 반쯤만 맞는 이유가 이것입니다.
어디에 저장하나
이건 JWT냐 세션이냐와 별개 문제인데 자주 섞여서 이야기됩니다.
| 저장 위치 | 취약한 공격 | 비고 |
|---|---|---|
localStorage |
XSS — 스크립트가 그냥 읽어 갑니다 | JS로 읽어야 하니 어쩔 수 없음 |
httpOnly 쿠키 |
CSRF — 브라우저가 자동으로 붙여 보냅니다 | JS가 못 읽으니 XSS엔 안전 |
httpOnly 쿠키에 SameSite=Lax(또는 Strict)를 걸면 CSRF는 대부분 막힙니다. XSS는 막을 수단이 없다는 점에서, 웹이라면 httpOnly 쿠키가 기본값으로 낫습니다.
Set-Cookie: token=...; HttpOnly; Secure; SameSite=Lax; Path=/언제 무엇을
| 상황 | 선택 |
|---|---|
| 웹 서비스 하나, 서버 몇 대 | 세션 + Redis — 단순하고 로그아웃이 확실합니다 |
| 서버가 자주 늘었다 줄었다 | JWT — 공용 저장소 없이도 검증됩니다 |
| 여러 서비스가 인증을 공유 | JWT — 각자 서명만 검증하면 됩니다 |
| 모바일 앱 | JWT — 쿠키를 쓰기 번거롭습니다 |
| 즉시 차단이 중요 (금융·관리자) | 세션 또는 짧은 만료 + 블랙리스트 |
기본값은 세션입니다. JWT가 최신이라서 더 나은 게 아닙니다. 서버 한두 대짜리 서비스에 JWT를 쓰면, 얻는 것 없이 로그아웃 처리만 어려워집니다.
자주 묻는 질문 (FAQ)
JWT가 세션보다 더 안전한가요?
아닙니다. 보안 수준의 문제가 아니라 상태를 어디에 둘 것인가의 문제입니다. 오히려 즉시 차단이 안 된다는 점에서 JWT가 불리한 국면도 많습니다. 둘 다 토큰을 탈취당하면 뚫리는 건 똑같고, 그걸 막는 건 HTTPS와 저장 위치 선택입니다.
토큰을 훔쳐가면 어떻게 되나요?
만료 전까지 그 사람이 됩니다. 세션이든 JWT든 같습니다. 차이는 대응인데, 세션은 지우면 즉시 끝나고 JWT는 만료를 기다리거나 블랙리스트가 필요합니다. 그래서 Access Token을 짧게 두는 것입니다.
alg: none 이야기는 뭔가요?
header의 알고리즘을 none으로 바꾸고 서명을 지운 토큰을 보내면, 일부 구현이 이걸 "서명 검증 안 함"으로 받아들여 통과시키던 취약점입니다. 지금의 주요 라이브러리는 막혀 있지만, 검증할 알고리즘을 서버가 고정해 두는 게 원칙입니다. 토큰이 알려 주는 대로 검증하면 안 됩니다.
세션은 서버 메모리를 많이 먹지 않나요?
사용자당 수백 바이트 수준이라 보통 문제가 되지 않습니다. 진짜 문제는 용량이 아니라 서버가 여러 대일 때 공유입니다. Redis로 빼면 둘 다 해결됩니다.
정리하며
차이는 하나입니다. 로그인 상태를 서버가 기억하느냐, 토큰이 들고 다니느냐.
- 세션은 서버가 기억합니다 — 로그아웃이 즉시, 대신 공용 저장소가 필요합니다
- JWT는 토큰이 들고 다닙니다 — 서버 확장이 쉽고, 대신 취소할 방법이 없습니다
- payload는 암호화가 아닙니다. 누구나 읽습니다. 민감한 정보를 넣지 마세요
- Refresh Token과 블랙리스트를 붙이는 순간 결국 상태를 갖게 됩니다
- 저장 위치는 별개 문제입니다. 웹이면
httpOnly쿠키 +SameSite가 기본값입니다
로그아웃했는데 토큰이 살아 있는 걸 보신 적 있으신가요? 그때 어떻게 처리하셨나요?