본문 바로가기
웹개발 이모저모

[실무 웹 인증과 보안 완벽 가이드 #02] 모던 웹 인증의 핵심: JWT(JSON Web Token) 구조와 원리, 그리고 한계

by 코드메이트 2026. 4. 7.

안녕하세요! 지난 1편에서는 웹 서버의 '금붕어 같은 기억력(무상태성)'을 극복하기 위해 등장한 쿠키와 세션, 그리고 사용자가 많아졌을 때 서버를 확장(Scale-out)하면서 겪는 세션 불일치 문제와 Redis를 활용한 해결책까지 깊게 알아보았습니다.

"서버 공통 저장소인 Redis를 쓰면 다 해결되는 거 아니야?" 라고 생각하셨을 수도 있습니다. 네, 확실히 세션 불일치 문제는 완벽하게 해결됩니다. 하지만, 진짜 문제는 '비용과 성능'에서 터지기 시작합니다.

 

1. 세션의 한계와 토큰(Token) 기반 인증의 화려한 등장

여러분의 서비스가 폭풍 성장해서 넷플릭스나 쿠팡처럼 동시 접속자가 수백만 명이 되었다고 상상해 봅시다. 사용자가 페이지를 이동할 때마다, API를 호출할 때마다 서버는 매번 Redis(세션 저장소)에 연결해서 "이 Session ID 가진 사람 있어? 유효한 로그인 맞아?" 라고 물어봐야 합니다.

아무리 Redis가 빠르다고 해도, 수백만 번의 네트워크 통신이 발생하면 데이터베이스에 엄청난 병목(Bottleneck) 현상이 생깁니다. 매번 명부를 뒤적거려야 하는 경비원(서버)은 과로로 쓰러지기 일보 직전이 되는 거죠.

그래서 개발자들은 발상을 완전히 뒤집었습니다. "서버가 유저 상태를 기억하지 않게 만들자! 유저한테 '위조가 불가능한 출입증'을 쥐여주고, 서버는 그 출입증이 진짜인지만 확인하면 되잖아?"

이 천재적인 아이디어가 바로 토큰(Token) 기반 인증의 시작이며, 그중에서도 현대 웹 개발의 사실상 표준(De facto standard)으로 자리 잡은 것이 바로 JWT(JSON Web Token)입니다.

 

서버 부하를 줄이는 JWT 토큰 기반 인증과 기존 세션 기반 인증의 아키텍처 비교

 

2. 해부학 시간: JWT의 3단 구조 완벽 분석

JWT는 겉보기에는 aaaaa.bbbbb.ccccc 처럼 점(.)을 기준으로 나뉜 의미를 알 수 없는 긴 알파벳과 숫자의 조합입니다. 하지만 이를 분해해 보면 세 가지의 명확한 구역으로 나뉩니다.

① Header (헤더): 어떤 토큰이고, 어떤 암호화 알고리즘을 썼는가? 헤더에는 이 토큰의 타입이 무엇인지(보통 JWT), 그리고 뒤에 나올 Signature(서명)를 만들 때 어떤 알고리즘(예: HS256, RS256 등)을 사용했는지가 적혀 있습니다. 일종의 '편지 봉투' 역할을 한다고 보시면 됩니다.

② Payload (페이로드): 내가 서버에게 전달하고 싶은 데이터 (Claim) 가장 중요한 부분입니다. 페이로드에는 유저의 고유 ID, 토큰의 발급 시간(iat), 토큰의 만료 시간(exp), 유저의 권한 등 실제로 서버가 알아야 할 정보들이 JSON 형태로 담겨 있습니다. 이곳에 담긴 정보 하나하나를 클레임(Claim)이라고 부릅니다.

③ Signature (서명): 이 토큰이 위조되지 않았음을 증명하는 도장 JWT의 핵심이자 꽃입니다. 해커가 Payload의 내용을 내 맘대로 조작하지 못하게 막아주는 강력한 자물쇠 역할을 하죠. 서버만 알고 있는 '비밀키(Secret Key)'를 사용해서 Header와 Payload의 내용을 믹서기에 넣고 갈아버린(해싱한) 결과물입니다.

클라이언트가 JWT를 보내면, 서버는 자신이 가진 비밀키로 서명을 다시 만들어 봅니다. 그리고 토큰에 적힌 서명과 자신이 방금 만든 서명이 일치하는지 비교하죠. 만약 해커가 Payload의 회원 등급을 '일반'에서 '관리자'로 슬쩍 바꿨다면? 내용물이 바뀌었기 때문에 서명이 완전히 달라져서 서버는 단칼에 요청을 거절해 버립니다.

 

점(Dot)으로 구분되는 JSON Web Token(JWT)의 3가지 핵심 구조 분석

 

3. 주니어 개발자의 흔한 실수: JWT는 암호화가 아닙니다!

여기서 정말 많은 개발자, 특히 주니어 분들이 면접에서 치명적인 실수를 합니다. "JWT는 암호화되어 있어서 안전하니까, Payload에 이메일이나 비밀번호 같은 걸 넣어도 되죠?"

절대 안 됩니다! 큰일 납니다!

JWT의 Header와 Payload는 암호화(Encryption)된 것이 아니라, 단순히 컴퓨터가 읽기 편하게 Base64로 인코딩(Encoding)된 것에 불과합니다. 즉, 누구든지 구글에 'JWT 디코더'라고 검색해서 복사 붙여넣기만 하면, 1초 만에 Payload 안에 들어있는 JSON 데이터를 훤히 들여다볼 수 있습니다.

따라서 Payload에는 user_id나 role처럼 유저를 식별할 수 있는 최소한의 정보만 넣어야 하며, 주민등록번호, 비밀번호, 전화번호 같은 민감한 개인정보는 절대, 네버, 무조건 넣으시면 안 됩니다. 서명(Signature)은 '위조'를 막아줄 뿐, '내용 유출'을 막아주는 기능이 아니라는 점을 꼭 명심하세요.

 

4. 무적 같아 보였던 JWT의 치명적인 아킬레스건 (한계점)

서버 메모리도 안 차지하고, 위조도 불가능하고, 모바일 앱이나 다른 도메인 서버와 통신할 때도 쿠키 없이 헤더에 쏙 넣어서 보내면 되니까 너무 완벽해 보입니다. (이것을 Stateless 하다고 하죠).

하지만 모든 기술은 트레이드오프(Trade-off)가 있습니다. JWT가 서버의 기억상실증을 이용해 성능을 얻었다면, 그로 인해 잃은 것은 바로 '통제권'입니다.

한 번 발급된 JWT는 유효기간이 끝날 때까지 서버가 절대 통제할 수 없습니다.

만약 여러분이 PC방에서 로그인을 한 뒤 자리를 비웠는데, 옆자리 해커가 여러분의 JWT 토큰을 USB에 복사해서 훔쳐 갔다고 가정해 봅시다. 해커는 자신의 집에 가서 그 토큰으로 서버에 요청을 보냅니다. 서버는 서명을 확인해 보니 완벽한 정상 토큰입니다. 이 토큰이 진짜 주인이 보낸 건지, 해커가 훔쳐서 보낸 건지 확인할 방법이 없습니다. 서버가 유저의 상태(세션)를 기억하지 않기로 했으니까요!

세션 방식이었다면 해킹 사실을 안 즉시 서버 관리자가 Redis에서 해당 유저의 세션을 강제로 지워버리면(강제 로그아웃) 끝납니다. 하지만 JWT는 서버에 저장된 게 없으니, 토큰을 지울 수도 막을 수도 없습니다. 그저 그 토큰의 수명이 다해서 자연사(?)하기만을 손가락 빨며 기다려야 하는 엄청난 보안 취약점이 발생합니다.

 

 

통제 불가능한 JWT의 치명적인 단점인 토큰 탈취 및 보안 취약점 시나리오

 

마치며: 이 딜레마를 어떻게 해결할까?

서버의 부하를 줄이려면 JWT를 써야 하는데, JWT를 쓰자니 한 번 털리면 속수무책으로 당해야 합니다. 수명을 아주 짧게 5분으로 만들면 보안은 좋아지겠지만, 사용자는 5분마다 로그인을 다시 해야 하니 짜증 나서 서비스를 탈퇴해 버릴 겁니다.

성능, 보안, 그리고 사용자 경험(UX). 이 세 마리 토끼를 한 번에 잡을 수는 없는 걸까요?

개발자들은 포기하지 않았습니다. 그리고 아주 기발하고 안전한 '토큰 이원화 전략'을 만들어냈죠. 바로 다음 시간, [3편] 완벽한 방어를 위한 토큰 전략: Access Token과 Refresh Token 도입기에서 이 치명적인 단점을 어떻게 극복하고 실무에 적용하는지 그 마법 같은 아키텍처를 공개합니다. 다음 편에서 뵙겠습니다!

 

[실무 웹 인증과 보안 완벽 가이드 #03] Access Token과 Refresh Token 도입기

안녕하세요! 지난 2편에서는 서버의 부담을 획기적으로 줄여주는 마법의 기술, JWT(JSON Web Token)에 대해 알아보았습니다. 하지만 JWT에는 너무나도 치명적인 단점이 하나 있었죠. 바로 '한 번 발급

code-bricks.tistory.com