들어가며
JWT는 한 번 발급되면 만료될 때까지 유효합니다. “로그아웃” 버튼을 눌러도 서버에서 토큰을 무효화할 수 없습니다. 사용자가 비밀번호를 바꿔도, 관리자가 계정을 비활성화해도, 기존 토큰은 여전히 작동합니다. 이것이 JWT의 가장 큰 약점입니다. 그런데도 JWT는 널리 쓰입니다 — 서버가 토큰을 저장하지 않아도(무상태, stateless) 검증할 수 있기 때문입니다. 이 글은 그 설계의 우아함과, 그 우아함이 만들어내는 “철회의 어려움”이라는 딜레마를 따라갑니다.
JWT의 3부분 구조
JWT를 처음 보면 긴 문자열처럼 보이지만, 사실 점(.)으로 구분된 세 부분입니다. 구조 자체는 매우 단순합니다. Header는 “이 토큰이 무엇인지”, Payload는 “누구의 토큰인지”, Signature는 “이 내용이 변조되지 않았는지”를 담당합니다. 아래 그림에서 세 영역의 역할 구분에 주목하세요 — 앞의 두 부분은 Base64로 인코딩된 JSON일 뿐이고, 진짜 보안은 세 번째 Signature에 있습니다.
xxxxx.yyyyy.zzzzz
│ │ │
│ │ └─ Signature (서명)
│ │ 토큰이 변조되지 않았음을 증명
│ │
│ └─── Payload (페이로드)
│ 사용자 정보 + 권한 + 만료 시간
│
└───────── Header (헤더)
토큰 타입 + 서명 알고리즘
세 부분이 각각 디코딩하면 어떤 내용인지 확인해 보겠습니다. 아래 코드에서 Payload의 각 필드(claim)가 무엇을 의미하는지에 주목하세요. 특히 exp(만료 시간)과 mfa_verified(MFA 완료 여부)는 보안 정책과 직결됩니다.
// 1. Header — 토큰 타입과 서명 알고리즘
{ "alg": "HS256", "typ": "JWT" }
// 2. Payload — 사용자 정보와 권한
{
"sub": "Administrator",
"iat": 1781778642, // 발급 시간
"exp": 1781807442, // 만료 시간 (8시간 후)
"display_name": "Administrator",
"role": "admin",
"groups": ["Domain Admins", "Administrators"],
"mfa_verified": true // MFA 완료 여부
}
// 3. Signature — 서명 (비밀키로 계산)
HMAC-SHA256(
base64(header) + "." + base64(payload),
secret_key
) = IEMU52xsyEtri_1mMG4PdQL9oq1x6YanwX1xrL4gOzA
여기서 반드시 짚고 넘어가야 할 점이 있습니다. Payload는 암호화가 아니라 인코딩입니다. Base64로 풀면 누구나 내용을 읽을 수 있습니다. 그래서 주민등록번호나 비밀번호 힌트 같은 민감 정보는 절대 Payload에 넣으면 안 됩니다. JWT의 보안은 “내용을 숨기는 것”이 아니라 “내용이 변조되지 않았음을 증명하는 것”에 있습니다 — 그것이 Signature의 역할입니다.
핵심: JWT Payload는 암호화가 아닙니다. Base64 인코딩일 뿐이므로 누구나 디코딩할 수 있습니다. Signature는 “읽기 방지”가 아니라 “변조 방지”를 담당합니다.
이 구조가 간단해 보이지만, 서명 알고리즘을 선택하는 순간 첫 번째 분기점이 나옵니다. 대칭키로 서명할 것인가, 비대칭키로 서명할 것인가 — 이 선택은 토큰이 쓰이는 환경에 따라 달라집니다.
서명 알고리즘: HS256 vs RS256
Header의 alg 필드에 들어가는 값이 바로 서명 알고리즘입니다. JWT에서 가장 많이 쓰이는 두 가지는 HS256과 RS256입니다. 둘 다 “서명하고 검증한다”는 목적은 같지만, 키를 어떻게 관리하느냐에서 극명하게 갈립니다. 아래 표를 읽을 때 “키의 개수”와 “검증 주체” 열에 주목하세요 — 이것이 두 알고리즘의 선택 기준입니다.
| 알고리즘 | 방식 | 키 | 용도 |
|---|---|---|---|
HS256 | 대칭키 (HMAC) | 서버 비밀키 1개 | 내부 API |
RS256 | 비대칭키 (RSA) | 개인키(서명) + 공개키(검증) | OIDC, 외부 앱 |
ES256 | 타원곡선 (ECDSA) | 개인키 + 공개키 | 모바일, IoT |
왜 내부 API에는 HS256을 쓰는가
HS256은 대칭키 방식입니다. 서버 하나가 비밀키를 들고 있고, 서명도 검증도 같은 키로 합니다. 키가 하나뿐이므로 관리가 단순하고, 계산이 빠릅니다. 사내 시스템처럼 “토큰을 발급하는 서버”와 “검증하는 서버”가 같은 조직 안에 있을 때, 비밀키를 안전하게 공유할 수 있다면 HS256이 가장 실용적인 선택입니다. 다만 “비밀키를 검증자에게도 줘야 한다”는 점은 단점입니다 — 검증자가 키를 탈취당하면 서명까지 위조할 수 있습니다.
왜 OIDC·외부 앱에는 RS256을 쓰는가
RS256은 비대칭키 방식입니다. 개인키로 서명하고, 공개키로 검증합니다. OpenID Connect에서 IdP(Identity Provider)가 토큰을 발급하면, 수십 개의 외부 앱이 각자 공개키로 검증합니다. 핵심은 외부 앱이 개인키를 가질 필요가 없다는 점입니다. 공개키만으로 검증이 가능하므로, 앱이 해킹당해도 서명을 위조할 수 없습니다. 신뢰하는 발급자(IdP)와 신뢰할 수 없는 다수의 검증자가 공존하는 환경에서는 RS256이 필수입니다. OIDC의 /.well-known/jwks.json 엔드포인트가 바로 이 공개키를 배포하는 표준 방식입니다.
핵심: HS256은 “믿을 수 있는 내부”를 위한 선택이고, RS256은 “믿을 수 없는 외부”를 위한 선택입니다. 키를 하나만 쓰는 단순함과, 공개키만 배포하는 안전함 사이의 트레이드오프입니다.
서명 알고리즘까지 정했다면, 이제 이 토큰이 실제 인증 과정에서 어떻게 오가는지 흐름을 따라가야 합니다.
JWT 인증 플로우
구조와 알고리즘을 알았으니, JWT가 실제 시스템에서 어떻게 쓰이는지 전체 흐름을 봅니다. 아래 시퀀스는 사용자가 로그인하고 이후 API를 호출하는 과정을 한눈에 보여줍니다. 읽을 때 1~4단계(로그인)와 6~8단계(API 호출)의 경계에 주목하세요 — 로그인은 한 번이지만, API 호출은 매 요청마다 서명 검증이 일어납니다. 그리고 서버가 7단계에서 “DB 조회 없이” 검증을 끝낸다는 점이 바로 JWT의 핵심입니다.
JWT 인증 플로우 (전체 시퀀스)
클라이언트 서버
│ │
│ 1. POST /auth/login │
│ {username, password} │
│─────────────────────────────▶│
│ │
│ 2. LDAP bind 인증
│ 3. JWT 생성 (secret_key로 서명)
│ │
│ 4. JWT 반환 │
│◀─────────────────────────────│
│ │
│ 5. localStorage에 저장 │
│ 또는 HttpOnly 쿠키 │
│ │
│ 6. 모든 API 요청에 헤더 포함 │
│ GET /api/v1/users │
│ Authorization: Bearer eyJ │
│─────────────────────────────▶│
│ │
│ 7. JWT 검증
│ · 서명 확인
│ · 만료 시간 확인
│ · 권한 확인 (PBAC)
│ │
│ 8. 200 OK + 데이터 │
│◀─────────────────────────────│
7단계에서 서버가 하는 일을 자세히 보면 세 가지 검증이 순서대로 일어납니다. 먼저 서명 확인 — 토큰이 우리 비밀키로 서명된 것인지 확인합니다. 다음 만료 시간 확인 — exp 필드가 지났는지 봅니다. 마지막 권한 확인 — Payload의 role, groups로 접근 권한을 판단합니다(PBAC). 이 세 검증 모두 DB 조회가 필요 없습니다. 그것이 JWT의 매력입니다. 그런데 이 “DB 조회가 필요 없다”는 장점이, 다음 섹션에서 보게 될 가장 큰 약점과 동전의 양면이라는 사실을 곧 깨닫게 됩니다.
JWT vs 세션: stateless의 대가
JWT를 쓰는 가장 큰 이유는 무상태(stateless) 검증입니다. 서버가 세션 저장소(Redis 등)를 두지 않아도, 토큰 서명만으로 신원을 확인할 수 있습니다. 서버가 10대로 늘어도 세션을 공유할 필요가 없습니다. 이것은 확장성 측면에서 엄청난 이점입니다. 하지만 이 이점에는 명확한 대가가 따라옵니다. 아래 표를 읽을 때 “철회” 행에 주목하세요 — 이 한 줄이 JWT와 세션의 가장 결정적인 차이입니다.
| 특징 | 세션 (Session) | JWT (토큰) |
|---|---|---|
| 저장 위치 | 서버 메모리/DB | 클라이언트 |
| 검증 방법 | 서버에 조회 | 서명 검증만 (조회 불필요) |
| 확장성 | 서버 간 세션 공유 필요 (Redis) | Stateless — 어느 서버든 검증 |
| 철회 | 서버에서 세션 삭제 → 즉시 | 어려움 (만료까지 대기 또는 블랙리스트) |
| 크기 | 작음 (세션 ID) | 큼 (JSON 데이터 포함) |
세션은 서버가 세션을 들고 있으므로, 삭제하는 순간 즉시 무효화됩니다. 로그아웃, 비밀번호 변경, 계정 비활성화 — 어떤 이유든 세션을 지우면 당장 끝납니다. 반면 JWT는 서버가 토큰을 기억하지 않습니다. 토큰은 클라이언트에 있고, 서버는 요청이 올 때마다 “서명이 맞고 만료되지 않았는가”만 봅니다. 그래서 사용자가 로그아웃을 해도, 서버 입장에서는 “취소할 세션”이 존재하지 않습니다. 토큰은 만료 시간(exp)이 될 때까지 스스로 죽을 때까지 계속 유효합니다.
핵심: JWT의 statelessness는 확장성을 주지만, 철회를 어렵게 합니다. “서버가 토큰을 기억하지 않는다”는 장점이 그대로 “서버가 토큰을 무효화할 수 없다”는 단점이 됩니다.
그렇다면 이 딜레마를 실무에서 어떻게 다룰 수 있을까요. 답은 토큰의 수명을 짧게 만들고, 갱신의 순간에만 서버가 개입하는 구조에 있습니다.
Refresh Token Rotation: 철회 문제의 해법
JWT 자체는 철회를 제공하지 않습니다. 그래서 실무에서는 토큰을 둘로 쪼개어 이 한계를 우회합니다. Access Token(수명 15분)은 실제 API 호출에 쓰이는 JWT이고, Refresh Token(수명 7일)은 Access Token이 만료되었을 때 새 토큰을 받아오는 용도입니다. 핵심 설계 원리는 “Access Token은 짧아서 탈취되어도 15분뿐이고, Refresh Token은 서버가 기억하므로 언제든 폐기할 수 있다”는 것입니다.
왜 Access Token은 15분인가
15분은 “사용자가 자주 재로그인하지 않을 만큼 길면서, 탈취되었을 때 피해를 최소화할 만큼 짧은” 절충점입니다. Access Token은 stateless JWT이므로 서버가 폐기할 수 없습니다. 그렇다면 유일한 방어선은 수명을 짧게 두는 것뿐입니다. 8시간짜리 토큰이 탈취되면 8시간 동안 피해가 지속되지만, 15분짜리 토큰이면 최대 15분입니다. 물론 15분도 “즉시 철회”는 아닙니다 — 이 한계를 Refresh Token이 메꿉니다.
왜 Refresh Token은 JWT가 아닌 불투명(opaque) 토큰인가
여기가 설계의 핵심입니다. Access Token은 stateless JWT로 만듭니다 — 빠른 검증을 위해. 하지만 Refresh Token은 반대로 만듭니다. JWT가 아닌 의미 없는 무작위 문자열(opaque token)로 만들고, 서버 DB에 저장합니다. 이유는 단 하나, 철회 가능성입니다. 서버가 기억하는 토큰이어야 “이 토큰을 폐기하겠다”고 했을 때 실제로 폐기할 수 있습니다. JWT였다면 다시 “서명이 맞으니 유효하다”는 함정에 빠집니다. 즉, 체계 안에 stateless한 부분(Access Token)과 stateful한 부분(Refresh Token)이 의도적으로 공존하는 것입니다.
아래 그림은 Refresh Token Rotation이 어떻게 동작하는지 보여줍니다. 주목할 점은 “갱신할 때마다 이전 토큰을 폐기하고 새 토큰을 발급”한다는 것, 그리고 하단의 “재사용 감지” 시나리오입니다. 탈취된 토큰이 쓰이는 순간, 정당한 사용자의 토큰까지 함께 폐기되어 강제 재로그인이 일어납니다.
Refresh Token Rotation (회전):
시간 T0: RT-1 발급
시간 T0+15m: Access Token 만료
→ RT-1로 갱신 요청
→ RT-1 폐기, RT-2 발급 + 새 Access Token
시간 T0+30m: Access Token 만료
→ RT-2로 갱신 요청
→ RT-2 폐기, RT-3 발급 + 새 Access Token
만약 해커가 RT-1을 가로챈다면:
→ RT-1로 갱신 시도 → 이미 폐기됨 → 거부
→ 정당한 사용자의 RT-2도 폐기 (재사용 감지) → 강제 재로그인
이 설계가 만들어내는 실질적 효과를 정리하면 다음과 같습니다. 아래 정책 설정에서 Access Token과 Refresh Token의 차이 — 하나는 JWT, 하나는 opaque — 가 왜 의도적인 설계인지 확인하겠습니다.
이원화 토큰 정책 설정 (YAML)
Access Token:
alg: HS256 (내부) / RS256 (OIDC)
expiry: 15분
claims: sub, role, groups, mfa_verified, permissions
storage: 클라이언트 (stateless)
Refresh Token:
type: 불투명 문자열 (JWT 아님, stateful)
expiry: 7일
rotation: 사용 시마다 새 토큰 발급 (이전 토큰 폐기)
storage: DB refresh_tokens 테이블
revoke: 로그아웃 시 폐기
로그아웃:
→ Refresh Token 폐기 (즉시)
→ Access Token은 15분 후 자연 만료 (stateless이므로 폐기 불가)
핵심: 로그아웃은 “15분 보안 창”입니다. Refresh Token을 폐기하면 더 이상 갱신이 불가능해지지만, 이미 발급된 Access Token은 15분간 유효합니다. 즉시 철회가 필요하다면 Access Token 블랙리스트(Redis)를 도입해야 하지만, 대부분의 경우 15분 창이 허용 가능한 트레이드오프입니다.
Refresh Token Rotation까지 갖추면, JWT의 철회 문제를 실무 수준에서 다루는 체계가 완성됩니다. 이제 전체를 돌아보며, 이 설계가 풀려는 본질적 긴장이 무엇이었는지 정리하겠습니다.
마치며
JWT의 본질적 매력은 statelessness에 있습니다. 서버가 토큰을 저장하지 않아도 서명 검증만으로 신원을 확인할 수 있으니, 서버가 100대로 늘어도 세션을 공유할 필요가 없습니다. 이것이 RESTful API와 마이크로서비스 시대에 JWT가 널리 쓰이는 이유입니다. 하지만 이 우아함은 뒤집으면 가장 날카로운 단점이 됩니다. 서버가 토큰을 기억하지 않는다는 것은, 동시에 서버가 토큰을 무효화할 수 없다는 뜻입니다. 사용자가 로그아웃 버튼을 눌러도, 비밀번호를 바꿔도, 계정이 정지되어도, 토큰은 만료 시간이 될 때까지 스스로 죽기를 기다릴 수밖에 없습니다. statelessness가 주는 확장성과 철회 불가능성이 주는 위험 — 이 둘은 같은 동전의 양면입니다.
Refresh Token Rotation은 이 딜레마에 대한 실무적 답입니다. 체계 안에 stateless한 부분과 stateful한 부분을 의도적으로 섞어 두는 것입니다. API 호출마다 매번 검증되는 Access Token은 stateless JWT로 두어 빠르게 처리하고, 철회 권한이 필요한 Refresh Token은 서버가 기억하는 opaque 토큰으로 만듭니다. Access Token의 수명을 15분으로 짧게 잡으면, 설령 탈취되더라도 피해는 15분에 그치고, 그 사이 Refresh Token Rotation의 재사용 감지가 비정상 접근을 잡아냅니다. 완벽한 즉시 철회를 포기하는 대신, “충분히 짧은 시간 창”과 “갱신 시점의 서버 개입”으로 위험을 구조적으로 통제하는 것입니다. Kerberos가 “완벽을 추구하기보다 위험을 구조적으로 통제”했던 것과 같은 철학이 여기에도 흐릅니다.
돌아보면, JWT의 이야기는 “가장 좋은 기술을 고르는” 이야기가 아닙니다. HS256과 RS256 중 어느 것이 우월한가가 아니라, 내부 환경이냐 외부 환경이냐에 따라 어느 쪽이 맞는가의 문제입니다. Access Token과 Refresh Token 중 어느 하나만 쓸 수는 없는가가 아니라, stateless의 속도와 stateful의 통제력을 어디서 나눌 것인가의 문제입니다. 15분이라는 수명도 정답이 아니라 절충점입니다 — 짧을수록 안전하지만 사용자는 자주 갱신해야 합니다. 보안 설계란 결국 “위험을 없애는 것”이 아니라 “어떤 위험은 받아들이고, 어떤 위험은 구조로 막고, 그 경계를 어디에 그을 것인가”를 결정하는 일입니다. JWT는 그 경계선을 그리는 일이 얼마나 섬세한 설계 문제인지를 잘 보여줍니다.
이전 글: (5) Kerberos & SSO 처음으로: (1) IAM이란 무엇인가
게이트웨이 On-promise 제품 팀에서 시스템 모니터링 및 관리를 쉽게 다가갈 수 있도록 하기 위한 업무를 하고 있습니다.
Contact: lhjnano@gmail.com