본문으로 건너뛰기
목록으로

분산 환경 인증: JWT를 왜 쓰나 — 세션으로도 되는데

Johny Cho
Software Engineer @ Kurly

분산 서버 환경에서 인증을 이야기하면 대개 이렇게 정리합니다. "서버가 여러 대라 세션을 한 서버에 두면 다음 요청이 다른 서버로 갈 때 로그인이 풀린다. 그래서 JWT를 쓴다." 그런데 여기엔 빈틈이 있습니다. 세션도 분산 환경에서 잘 씁니다. 실제로 면접에서 "분산이라 JWT를 썼다"고 답하면 "세션도 분산 환경에서 써 왔는데, 그럼에도 왜 JWT냐"는 되물음이 돌아옵니다. 이 글은 그 되물음에 답합니다.

"분산이라서 JWT"는 절반만 맞는 답입니다. 세션도 분산에서 동작하기 때문에, 진짜 이유는 상태를 어디에 두느냐의 트레이드오프에 있습니다.

세션 기반 인증이 분산에서 막히는 지점

세션 기반 인증은 서버가 상태(로그인 정보)를 들고 있는 방식입니다. 로그인에 성공하면 서버 메모리에 세션을 만들고, 클라이언트에는 그 세션을 가리키는 SESSIONID만 쿠키로 내려줍니다. 이후 요청마다 서버는 이 ID로 자기 메모리의 세션을 찾아 "누구인지"를 압니다. (쿠키·세션의 기초는 쿠키와 세션의 차이 문서를 참고하세요.)

문제는 서버가 여러 대일 때입니다. A 서버에서 로그인해 세션이 A 메모리에 생겼는데, 다음 요청이 로드밸런서를 거쳐 B 서버로 가면 B는 그 세션을 모릅니다. 로그인이 풀린 것처럼 보입니다.

세션이 특정 서버의 로컬 메모리에만 있으면, 요청이 다른 서버로 분산되는 순간 인증 상태가 끊깁니다.

그런데 세션도 분산에서 잘 됩니다

여기가 핵심입니다. 위 문제는 "세션이 로컬 메모리에만 있을 때"의 문제일 뿐, 세션 방식 자체의 한계가 아닙니다. 분산 환경에서 세션을 쓰는 정석 해법이 여럿 있습니다.

1) 스티키 세션 (세션 어피니티)

쉽게 말하면 은행 창구와 같습니다. 처음 방문한 손님에게 번호표를 주고, 다음에 와도 그 번호표를 보고 같은 창구(서버) 로 안내하는 것입니다. 손님(사용자)의 상담 기록(세션)이 그 창구에만 있어도, 늘 같은 창구로 가니 문제가 없습니다.

이 "번호표"를 무엇으로 삼느냐에 따라 두 방식이 있습니다.

방식 1. 쿠키를 번호표로 — 로드밸런서가 사용자의 첫 요청을 A 서버로 보낸 뒤, 응답에 "너는 A 담당" 이라는 쿠키를 붙여 줍니다. 이후 요청에는 브라우저가 그 쿠키를 자동으로 실어 보내고, 로드밸런서는 그걸 보고 다시 A로 보냅니다. 이 과정은 전부 로드밸런서가 하고, 우리 서버 코드는 손댈 게 없습니다.

AWS의 로드밸런서(ALB)라면 체크박스 하나 켜는 수준입니다.

스티키 세션    : 켜기(ON)
고정 기준 : 로드밸런서가 발급하는 쿠키
고정 유지 시간 : 1일

설정 파일로는 이렇게 표현됩니다(위와 같은 내용입니다).

stickiness.enabled                    = true      # 스티키 세션 켜기
stickiness.type = lb_cookie # 로드밸런서가 만든 쿠키로 고정
stickiness.lb_cookie.duration_seconds = 86400 # 하루 동안 같은 서버 유지

방식 2. 접속 IP를 번호표로 — 쿠키 없이, "같은 곳에서 온 요청(같은 IP)은 늘 같은 서버로" 라고 정하는 방식입니다. Nginx는 한 줄(ip_hash)이면 됩니다.

upstream app {
ip_hash; # 접속 IP가 같으면 항상 같은 서버로 보냄
server 10.0.0.11:8080; # 서버 A
server 10.0.0.12:8080; # 서버 B
}

쿠버네티스도 같은 아이디어를 옵션 한 줄로 제공합니다.

spec:
sessionAffinity: ClientIP # 같은 IP는 항상 같은 Pod(서버)로
  • 장점: 서버 코드를 안 바꿔도 됩니다. 로드밸런서·인프라 설정만으로 됩니다.
  • 한계: 특정 서버로 손님이 쏠릴 수 있고, 그 서버가 죽으면 거기 붙어 있던 세션이 통째로 사라져 재로그인해야 합니다. 배포·오토스케일로 서버가 교체돼도 마찬가지입니다. 특히 IP 방식은 회사·모바일처럼 여러 사람이 같은 IP로 나가는 환경에서 한 서버로 몰릴 수 있습니다.

2) 중앙 세션 스토어 (외부화)

세션을 서버 메모리가 아니라 모든 서버가 공유하는 외부 저장소(주로 Redis)에 둡니다. 어느 서버로 요청이 가든 같은 Redis를 조회하므로 로그인이 유지됩니다. Spring Session 같은 도구가 이 방식을 표준으로 지원합니다.

  • 장점: 서버는 무상태(stateless)에 가까워지고, 어느 서버로 붙어도 동일하게 동작합니다. 로그아웃·강제 만료가 즉시 반영됩니다.
  • 한계: 요청마다 세션 스토어를 조회하니 네트워크 왕복이 한 번 더 생기고, 세션 스토어가 죽으면 인증 전체가 흔들립니다. 스토어를 HA로 이중화해야 합니다.

3) 세션 클러스터링 (복제)

서버들끼리 세션을 서로 복제해 모든 노드가 같은 세션을 갖게 합니다. WAS 클러스터 기능으로 구현하는데, 노드가 늘수록 복제 트래픽이 커져 확장성이 떨어집니다. 요즘은 중앙 세션 스토어 방식에 밀려 잘 쓰지 않습니다.

스티키 세션·중앙 세션 스토어·세션 복제 — 분산 환경에서도 세션은 얼마든지 동작합니다. 따라서 "분산이라서 JWT"는 이유가 되지 못합니다.

그럼 진짜 이유는: 상태를 어디에 둘 것인가

세션과 JWT의 본질적 차이는 "분산이냐"가 아니라 인증 상태를 서버가 들고 있느냐, 토큰 자체에 담아 서버가 안 들고 있느냐입니다.

  • 세션: 상태를 서버(또는 공유 스토어)가 보관. 요청이 오면 그 상태를 찾아봐야 누구인지 안다. → 상태 저장(stateful)
  • JWT: 상태를 토큰 안에 서명해서 담아 클라이언트가 보관. 요청이 오면 서버는 서명만 검증하면 저장소 조회 없이 누구인지 안다. → 상태 비저장(stateless)

JWT는 헤더.페이로드.서명 세 부분으로 이뤄지고, 페이로드에 사용자 식별자 같은 클레임을 담습니다. 서버는 비밀키(또는 공개키)로 서명을 검증해 위·변조되지 않았음만 확인하면 되므로, 인증을 위해 DB나 세션 스토어를 조회하지 않아도 됩니다.

Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1LTEyMyJ9.SflKxwRJSMeKKF2QT4fw

토큰은 점(.)을 기준으로 세 부분으로 나뉩니다.

  • 헤더eyJhbGciOiJIUzI1NiJ9 : 서명 알고리즘 같은 메타데이터. 디코딩하면 {"alg":"HS256"}.
  • 페이로드eyJzdWIiOiJ1LTEyMyJ9 : 사용자 식별자 등 클레임(claim). 디코딩하면 {"sub":"u-123"}.
  • 서명SflKxwRJSMeKKF2QT4fw : 헤더·페이로드를 비밀키로 서명한 값. 위·변조 검증에 씁니다.

토큰 구조·서명 방식은 JWT (Json Web Token) 문서에서 더 자세히 다룹니다.

그래서 JWT를 선택하는 실질적 이유는 이렇게 정리됩니다.

  1. 인증 경로에서 저장소 조회 제거: 요청마다 세션 스토어를 왕복하지 않으니, 트래픽이 커질수록 이점이 큽니다. 세션 스토어가 단일 병목·단일 장애점이 되는 것도 피합니다.
  2. 서버 간 공유 인프라가 필요 없음: 공유 Redis나 세션 복제 없이, 각 서버가 서명키만 있으면 독립적으로 검증합니다. 서버 증설이 단순해집니다.
  3. 서비스·도메인 간 전파가 쉬움: MSA에서 게이트웨이가 검증한 신원을 토큰 그대로 하위 서비스에 넘기면, 각 서비스가 세션 공유 없이 신원을 확인할 수 있습니다. 모바일 앱·외부 연동처럼 쿠키/세션이 부자연스러운 클라이언트에도 잘 맞습니다.
JWT의 진짜 이점은 "분산"이 아니라 "인증을 무상태로 만들어 요청마다의 저장소 조회와 공유 세션 인프라를 없앤다"는 데 있습니다.

공짜는 아니다 — JWT의 대가: 무효화

무상태의 대가는 한 번 발급한 토큰을 만료 전에 취소하기 어렵다는 점입니다. 세션은 서버가 상태를 쥐고 있으니 세션 하나만 지우면 즉시 무효가 되지만, JWT는 상태를 서버가 안 들고 있으니 "이 토큰 이제 무효"라고 서버가 일방적으로 선언할 방법이 없습니다. 서명이 유효하고 만료 시간이 남아 있으면 서버는 그대로 통과시킵니다.

이게 문제가 되는 상황:

  • 로그아웃했는데 토큰은 만료 전까지 계속 유효
  • 회원 탈퇴·비밀번호 변경·계정 정지가 일어났는데 이미 나간 토큰은 살아 있음
  • 토큰이 탈취됐는데 만료까지 막을 방법이 없음

현실적인 완화책은 결국 "무상태를 조금 포기" 하는 것입니다.

짧은 액세스 토큰 + 리프레시 토큰

액세스 토큰의 수명을 짧게(수십 분~수 시간) 두고, 만료되면 수명이 긴 리프레시 토큰으로 재발급합니다. 탈취돼도 유효 기간이 짧아 피해 창이 줄고, 재발급 시점에 계정 유효성을 다시 점검할 수 있습니다.

블랙리스트 (거부 목록)

토큰마다 고유 ID(jti)를 부여하고, 무효화해야 할 토큰의 jti를 Redis에 올려 둡니다. 검증할 때 블랙리스트에 있으면 거부합니다. 이때 Redis 항목의 TTL을 토큰 남은 만료 시간에 맞추면, 어차피 만료로 사라질 토큰만 그때까지 막으면 되므로 저장 비용이 한정됩니다.

// 로그아웃/강제 만료 시
SET blacklist:{jti} 1 EX {토큰 남은 초}

// 검증 시: 블랙리스트에 있으면 거부
EXISTS blacklist:{jti} → 1이면 401

단, 블랙리스트를 쓰는 순간 검증 경로에 다시 저장소 조회가 생깁니다. "무상태로 부하를 줄인다"는 JWT의 이점을 일부 반납하는 셈이라, 어느 데이터를 얼마나 검사할지는 서비스 민감도에 따라 정합니다. 결제·금융처럼 탈취 피해가 큰 서비스는 조회 비용을 감수하고 즉시 무효화를 택하고, 그렇지 않은 서비스는 짧은 만료로 대신하기도 합니다.

JWT는 무상태로 이점을 얻지만, 즉시 무효화가 필요한 순간 블랙리스트 같은 상태를 다시 끌어들여야 합니다. 결국 무상태의 이점과 무효화의 필요 사이의 저울질입니다.

토큰에 무엇을 담을까

JWT 페이로드는 서명은 되지만 암호화는 아니어서 누구나 디코딩해 내용을 볼 수 있습니다(Base64URL). 그래서 담을 것과 담지 말 것을 구분해야 합니다.

  • 담기 좋은 것: 사용자 식별자(UUID), 잘 바뀌지 않는 기기 식별자, 역할·권한 같은 최소한의 인가 정보.
  • 피할 것: 비밀번호·주민번호 같은 민감 정보, 그리고 자주 바뀌는 값(닉네임·잔액 등). 자주 바뀌는 값을 넣으면 값이 바뀔 때마다 토큰이 낡아, 결국 서버가 최신값을 다시 조회해야 하므로 무상태 이점이 사라집니다.

정리

  • "서버가 분산이라 JWT를 쓴다"는 절반의 답입니다. 세션도 스티키 세션·중앙 세션 스토어(Redis)·세션 복제로 분산에서 잘 동작합니다.
  • 진짜 차이는 상태를 서버가 들고 있느냐(세션·stateful) vs 토큰에 담아 서버가 안 들고 있느냐(JWT·stateless) 입니다.
  • JWT의 실질적 이점은 인증 경로에서 저장소 조회와 공유 세션 인프라를 없애는 것 — 트래픽 확장과 서비스 간 신원 전파에 유리합니다.
  • 대가는 즉시 무효화의 어려움입니다. 짧은 만료 + 리프레시 토큰, jti 블랙리스트(TTL을 만료에 맞춤)로 완화하되, 이는 무상태 이점을 일부 되돌리는 트레이드오프임을 인지하고 서비스 민감도에 맞춰 선택합니다.

세션이 무조건 구식이고 JWT가 정답인 것이 아닙니다. 요청마다의 조회 비용을 줄이는 무상태의 이점과, 상태를 쥐고 있어야 가능한 즉시 제어 사이에서 서비스 특성에 맞게 고르는 문제입니다.