HTTP의 상태 저장

mongBrown·2026년 4월 28일

로그인 상태는 어떻게 유지되는 걸까

HTTP는 상태가 없는 통신 프로토콜이다. 네이버에 로그인하고 메일함으로 이동하면 브라우저는 서버에 이런 요청을 보낸다.

GET /mail HTTP/1.1
Host: mail.naver.com

이게 전부다. 이 요청 어디에도 "방금 로그인한 사람이 보낸 요청"이라는 정보가 없다. 서버는 이 요청을 받는 순간, 보낸 사람이 누구인지 알 방법이 없다. 그런데 메일함에는 내 메일이 떠 있다.

브라우저에 저장하고, 요청마다 전달한다

서버가 기억을 못 한다면, 결국 클라이언트가 들고 다녀야 한다. 매 요청마다 "나 이 사람이야"라고 알려주면 서버는 확인만 하면 되니까.

그런데 모든 정보를 매 요청마다 다 보낼 필요가 있을까? 인증 정보라면 서버가 매번 확인해야 하니 자동으로 붙여 보내는 게 편하겠지만, 그 외의 데이터라면 필요할 때만 꺼내 쓰면 충분하지 않을까.

이 차이에서 쿠키와 localStorage가 나뉜다.

쿠키는 같은 도메인으로 요청을 보낼 때마다 브라우저가 자동으로 포함시킨다. 서버가 로그인 응답에 Set-Cookie 헤더로 값을 심으면, 이후 모든 요청에 그 값이 따라붙는다.

# 서버 응답 (로그인 성공 시)
Set-Cookie: sessionId=abc123

# 이후 모든 요청에 자동 포함
Cookie: sessionId=abc123

localStorage는 자동으로 전송되지 않는다. JavaScript가 직접 읽어서 요청 헤더에 붙여야 한다. 매번 보낼 필요 없는 데이터를 브라우저에 저장해두고 필요할 때 꺼내 쓰는 용도에 맞는다.

쿠키localStorage
자동 전송HTTP 요청마다 자동 포함JS로 직접 읽어서 헤더에 넣어야 함
서버 설정Set-Cookie로 서버가 직접 심을 수 있음JS만 가능
만료만료 시간 설정 가능직접 삭제 전까지 영구 유지
용량~4KB~5-10MB

쿠키를 그냥 쓰면 뭐가 위험한가

bank.com에 로그인된 상태에서 다른 사이트를 열었다고 하자. 그 페이지 안에 이런 코드가 있다.

<!-- evil.com에 있는 코드 -->
<img src="https://bank.com/transfer?to=hacker&amount=1000000">

브라우저가 이 img 태그를 렌더링하면서 bank.com으로 요청을 보낸다. 이때 브라우저는 bank.com 쿠키를 자동으로 포함시킨다. 서버 입장에서는 정상 사용자가 보낸 요청처럼 보인다. 이게 CSRF(Cross-Site Request Forgery)다. 쿠키의 자동 전송을 악용한 공격이다.

다른 방향에서도 공격이 들어온다. 공격자가 페이지에 악성 스크립트를 심으면, 사용자가 그 페이지를 열 때 document.cookie로 쿠키 값을 읽어 공격자 서버로 전송한다. 이게 XSS(Cross-Site Scripting)다. localStorage도 같은 방식으로 털린다.

쿠키localStorage
XSS쿠키 값 탈취 가능마찬가지로 탈취 가능
CSRF자동 전송으로 취약자동 전송 없어서 안전

두 공격을 막으려면 쿠키에 속성을 붙여야 한다.

Set-Cookie: token=abc123; HttpOnly; SameSite=Strict; Secure

HttpOnly를 붙이면 JavaScript에서 document.cookie로 읽을 수 없다. XSS로 스크립트가 실행되더라도 쿠키 값을 가져가지 못한다. SameSite=Strict는 다른 도메인에서 시작된 요청에는 쿠키를 포함시키지 않는다. evil.com에서 bank.com으로 요청이 가도 쿠키가 붙지 않아 CSRF를 막는다. Secure는 HTTPS 연결에서만 쿠키를 전송하도록 강제한다.

그러면 쿠키에 실제로 뭘 담아야 할까 — Session vs JWT

쿠키를 안전하게 쓰는 방법을 알았다면, 이제 쿠키 안에 뭘 담을 것인지가 문제다.

한 가지 방법은 서버가 직접 상태를 기억하는 것이다. 로그인하면 서버가 세션 저장소(DB 또는 Redis)에 사용자 정보를 저장하고, 세션 ID만 쿠키로 내려준다. 이후 요청마다 세션 ID로 저장소를 조회해서 사용자를 확인한다.

서버가 상태를 직접 관리하니 강제 로그아웃이 쉽다. 저장소에서 세션을 지우면 즉시 무효화된다. 반면 서버가 여러 대라면 각 서버가 세션을 공유해야 하는 문제가 생긴다. 특정 서버에만 세션이 있으면 다른 서버로 요청이 가면 인증이 풀린다.

다른 방법은 서버가 기억하는 대신 토큰에 담아 클라이언트한테 넘기는 것이다. 서버가 사용자 정보를 토큰에 담아 서명한 뒤 내려주면, 클라이언트가 그걸 들고 다니다가 요청마다 포함시킨다. 서버는 저장소를 조회하지 않고 서명만 검증한다. 이게 JWT다.

서버가 아무것도 저장하지 않으니 여러 서버로 확장이 자유롭다. 대신 서버는 이 토큰을 어떻게 믿을 수 있을까.

JWT 구조 — Payload는 누구나 읽을 수 있다

JWT는 점(.)으로 구분된 세 부분으로 이루어진다.

Header.Payload.Signature
eyJhbGci...  .eyJ1c2VyX2lkIjo...  .SflKxwRJSMeKKF...

Header는 서명에 사용한 알고리즘 정보를, Payload는 실제 담긴 데이터를 가지고 있다. 둘 다 Base64로 인코딩되어 있는데, 이건 암호화가 아니다. 누구나 디코딩해서 내용을 볼 수 있다. 비밀번호나 개인정보를 Payload에 넣으면 안 되는 이유다.

Signature는 HMAC(Header + Payload, 서버 비밀키)로 만든 값이다. 서버는 요청이 들어오면 Header와 Payload를 비밀키로 다시 서명해서 Signature와 비교한다. 일치하면 변조 없음, 불일치하면 누군가 토큰을 건드린 것이다.

이 구조 덕분에 서버는 저장소 없이도 토큰의 무결성을 검증할 수 있다. 그런데 여기서 문제가 생긴다. 토큰이 탈취되면 만료 전까지 막을 방법이 없다.

탈취된 JWT를 어떻게 감지하나 — Refresh Token Rotation

이 문제를 완전히 해결하지는 못하지만 피해를 줄이는 방법이 Refresh Token 패턴이다.

토큰을 두 개 발급한다. 유효기간이 짧은 access token(5분)과 긴 refresh token(7일)이다. API 호출에는 access token을 쓰고, 만료되면 refresh token으로 새 access token을 재발급받는다. access token이 탈취돼도 5분 후면 쓸 수 없다.

여기에 Rotation을 더하면 refresh token 탈취까지 감지할 수 있다. refresh token을 한 번 쓰면 새 refresh token을 발급하고 기존 건 폐기한다. 서버 DB에는 현재 유효한 refresh token 하나만 저장한다.

1. 로그인
   → access_token (5분) + refresh_token_A (7일) 발급
   → 서버 DB에 refresh_token_A 저장

2. access_token 만료 → refresh_token_A로 재발급
   → 새 access_token 발급
   → refresh_token_A 폐기, refresh_token_B 발급

3. 해커가 refresh_token_A를 탈취했다면
   → 정상 사용자가 먼저 써서 refresh_token_B로 교체됨
   → 해커가 refresh_token_A로 요청 → 서버: 이미 폐기된 토큰
   → 즉시 해당 유저 토큰 전체 무효화 → 재로그인 필요

이미 사용된 refresh token으로 요청이 온다는 건, 정상 사용자라면 절대 발생하지 않는 상황이다. 서버가 이 신호를 받는 즉시 세션 전체를 종료한다.

그런데 refresh token을 서버 DB에 저장하는 순간, JWT의 장점인 "서버가 아무것도 저장하지 않는다"는 게 이미 무너졌다. 그럼 세션이랑 뭐가 다른 걸까.

어떤 방식을 선택할까

대부분은 access token은 stateless JWT로, refresh token만 서버 DB에 저장하는 방식을 쓴다. API 요청마다 DB 조회를 피하면서도, refresh token 수준에서 강제 무효화를 지원할 수 있어서다.

강제 로그아웃이 반드시 필요하거나 보안 민감도가 높다면 세션이 낫다. 서버를 수평 확장해야 하고 Redis 같은 공유 세션 저장소 관리가 부담이라면 JWT + Refresh Token Rotation이 현실적이다. 어느 쪽도 완벽하지 않고, 서비스 요구사항에 따라 어디서 상태를 관리할지 결정하는 문제다.

profile
화이팅!

0개의 댓글