← Posts

HTTP · Web · Session

상태를 안 갖는다는 것

서버는 방금 로그인한 사람도 다음 요청에서 못 알아본다. 그 불편을 일부러 감수한 이유와, 그래서 상태를 어디에 두게 됐는지.

목차
  1. 로그인했는데 다음 요청에서 누군지 모른다
  2. 서버는 요청 사이를 기억하지 않는다
  3. 왜 일부러 잊게 만들었나
  4. 그러면 매 요청이 자기소개를 한다
  5. 쿠키는 자기소개를 브라우저가 대신하는 것
  6. 상태를 어디에 두나
    1. 서버 메모리에 둔다
    2. 공용 저장소에 둔다
    3. 클라이언트가 들고 다닌다
  7. 무상태에도 대가가 있다
  8. 실무에서 이 선택이 갈리는 지점
  9. 정리

로그인 API를 만들어 성공까지 확인했다. 그런데 바로 다음에 부른 /me가 401을 준다. 코드에는 아무 문제가 없다.

로그인했는데 다음 요청에서 누군지 모른다

상황을 그대로 펼치면 이렇다.

http
POST /login HTTP/1.1
Host: example.com

{"id": "kim", "pw": "..."}
http
HTTP/1.1 200 OK

{"message": "환영합니다"}

여기까지는 성공이다. 이어서 내 정보를 부른다.

http
GET /me HTTP/1.1
Host: example.com
http
HTTP/1.1 401 Unauthorized

방금 로그인한 그 사람인데 서버가 모른다고 한다. 버그가 아니다. 두 요청은 서버 입장에서 아무 관계가 없다.

diagramdiagram

서버는 요청 사이를 기억하지 않는다

HTTP를 무상태(stateless) 프로토콜이라고 부른다. 뜻은 정확히 이거다. 서버는 요청 하나를 처리하고 나면 그 요청에 대해 알던 것을 다음 요청으로 이어가지 않는다.

여기서 “상태”는 저장된 데이터 전부를 말하는 게 아니다. DB에 회원 정보를 저장하는 건 얼마든지 한다. 무상태가 말하는 건 요청과 요청 사이를 잇는 대화의 맥락이다. “이 연결로 들어온 사람은 아까 로그인한 kim”이라는 기억을 서버가 안 갖는다.

전화 통화에 비유하면 이렇다. 상태를 갖는 통신은 전화다. 한 번 연결되면 “아까 말한 그거요”가 통한다. HTTP는 편지에 가깝다. 매 편지가 자기가 누구인지 다시 밝히지 않으면 받는 쪽은 모른다.

왜 일부러 잊게 만들었나

불편해 보이는데 왜 이렇게 설계했나. 서버를 늘릴 수 있게 하려고다.

사용자가 늘어 서버를 두 대로 늘렸다고 하자. 앞에 로드밸런서를 두고 요청을 나눠 보낸다.

diagramdiagram

서버가 대화 맥락을 자기 안에 들고 있다면, kim의 로그인 기억은 서버 A에만 있다. 다음 요청이 서버 B로 가면 B는 kim을 모른다. 그러면 로드밸런서는 “kim은 무조건 A로”라고 묶어야 하고, A가 죽는 순간 A에 묶인 사람들이 전부 로그아웃된다.

무상태면 이 문제가 통째로 사라진다. 모든 요청이 자기 정보를 다 들고 오니까 어느 서버가 받아도 똑같이 처리한다. 서버를 늘리고 줄이고 죽이고 살리는 게 자유로워진다.

참고

이게 오늘날 오토스케일링이나 컨테이너 재배포가 가능한 바탕이다. 서버 한 대를 언제든 버려도 되는 건, 그 서버가 사용자에 대해 버리면 안 되는 것을 안 들고 있기 때문이다.

그러면 매 요청이 자기소개를 한다

서버가 기억을 안 하기로 했으니 남은 방법은 하나다. 요청이 자기를 밝힌다.

diagramdiagram

로그인에 성공하면 서버가 쪽지 하나를 준다. 이후 요청마다 그 쪽지를 같이 보낸다. 서버는 매번 쪽지를 확인해 누군지 알아낸다.

여기서 중요한 건, 서버가 여전히 아무것도 기억하지 않는다는 점이다. 매 요청을 처음 보는 요청처럼 처리한다. 다만 그 요청 안에 신원이 들어 있을 뿐이다.

쿠키는 자기소개를 브라우저가 대신하는 것

쪽지를 매 요청에 손으로 붙이는 건 번거롭다. 브라우저는 이걸 자동으로 해준다. 그 장치가 쿠키다.

서버는 응답에 Set-Cookie를 붙여 “이걸 보관해라”라고 말한다.

http
HTTP/1.1 200 OK
Set-Cookie: SESSIONID=a1b2c3; HttpOnly; Secure; SameSite=Lax

그러면 브라우저는 그 뒤로 같은 사이트에 보내는 요청에 알아서 붙인다.

http
GET /me HTTP/1.1
Host: example.com
Cookie: SESSIONID=a1b2c3

쿠키는 인증 방식이 아니라 그냥 보관·자동첨부 장치다. 무엇을 담을지는 서버 마음이다. 뒤에 붙은 옵션들이 이 자동첨부의 범위를 좁힌다.

옵션하는 일
HttpOnlyJavaScript에서 못 읽게 한다
SecureHTTPS 요청에만 붙인다
SameSite다른 사이트에서 시작된 요청에 붙일지 정한다
Max-Age / Expires언제까지 보관할지
주의

이 속성들은 쿠키가 어떻게 악용되는지를 알아야 왜 필요한지 보인다. 각각이 무엇을 막는지는 XSSCSRF가 따로 다룬다. 여기서는 쿠키에 이런 조절 장치가 붙는다까지만 본다.

상태를 어디에 두나

서버가 안 기억하기로 했다면, 그 기억은 어딘가에는 있어야 한다. 무상태는 상태를 없애는 게 아니라 옮기는 것이다. 둘 곳은 크게 셋이다.

diagramdiagram

서버 메모리에 둔다

가장 단순하다. 서버가 a1b2c3 → kim이라는 표를 자기 메모리에 들고, 쿠키에는 a1b2c3만 담아 보낸다.

빠르고 만들기 쉽다. 대신 앞에서 본 문제가 그대로 돌아온다. 서버가 여러 대면 표를 공유하지 못하고, 서버를 재시작하면 표가 날아간다. 서버는 무상태인데 앱이 상태를 들고 있는 셈이다.

공용 저장소에 둔다

같은 표를 서버 밖으로 꺼내 Redis 같은 저장소에 둔다. 서버 A든 B든 같은 곳을 보니 어느 쪽이 받아도 kim을 알아본다.

diagramdiagram

앱 서버는 진짜로 버려도 되는 물건이 된다. 대신 요청마다 저장소를 한 번 더 다녀와야 하고, 저장소가 죽으면 전부가 로그아웃된다. 상태를 한 곳으로 모았으니 그 한 곳이 급소가 됐다.

클라이언트가 들고 다닌다

발상을 뒤집는다. 표를 서버 어디에도 두지 않고, 쪽지 안에 내용을 다 적어서 클라이언트에게 준다. “이 사람은 kim이고 권한은 이것이다”를 통째로 담고, 위조를 막기 위해 서버가 서명을 얹는다.

서버는 요청이 올 때마다 서명만 검증하면 된다. 조회할 저장소가 없다. 대신 대가가 붙는다.

  • 한 번 발급하면 되돌리기 어렵다. 서버에 목록이 없으니 “이 쪽지 무효”라고 선언할 데가 없다. 그래서 유효기간을 짧게 두거나, 결국 무효 목록을 서버에 두게 된다.
  • 내용이 바뀌어도 안 따라온다. 권한을 회수해도 이미 발급된 쪽지에는 옛 권한이 적혀 있다.
  • 크기가 커진다. 매 요청에 실려 나간다.
참고

클라이언트가 들고 다니는 이 쪽지의 대표적인 형식이 JWT다. 남의 서비스 권한을 위임받아 올 때 그 쪽지를 어떻게 받아오는지를 정한 규격으로 OAuth 2.0이 따로 있는데, 형식과 절차는 서로 다른 이야기다. 둘 다 인증 쪽 주제라 여기서는 다루지 않는다. 이 글이 말하는 건 **“상태를 클라이언트에 두는 선택지가 있고, 그 선택에는 취소가 어렵다는 대가가 붙는다”**까지다.

무상태에도 대가가 있다

무상태의 이득만 봤으니 값도 봐야 한다.

같은 정보를 반복해서 보낸다. 쿠키든 토큰이든 매 요청에 실린다. 요청 하나에는 사소하지만, 페이지 하나가 요청 수십 개를 만든다는 걸 떠올리면 곱해진다. 그래서 이미지나 정적 파일을 다른 도메인에 두어 쿠키가 안 붙게 하는 방법을 쓰기도 한다.

매번 다시 확인한다. 요청마다 서명을 검증하거나 저장소를 다녀온다. 한 번 확인하고 연결 내내 믿는 방식보다 일이 많다.

진짜로 연결이 필요한 기능과 안 맞는다. 채팅처럼 서버가 먼저 말을 걸어야 하는 것은 이 모델 위에서 억지로 만들 수밖에 없다. 그 억지를 푸는 게 WebSocket 글이다.

정리하면 무상태는 서버를 가볍게 하는 대신 요청을 무겁게 하는 교환이다.

실무에서 이 선택이 갈리는 지점

셋 중 무엇을 고르냐는 대체로 아래로 갈린다.

상황대체로 가는 쪽
서버 한 대, 사내 도구서버 메모리 - 더 복잡하게 만들 이유가 없다
서버 여러 대, 일반 웹 서비스공용 저장소 세션 + 쿠키
서비스가 여러 개로 쪼개져 있고 서로 신원을 넘겨야 함클라이언트가 들고 다니는 토큰

두 가지를 덧붙인다.

“무상태니까 토큰”은 아니다. 저장소 세션도 앱 서버 자체는 무상태다. 상태가 앱 밖에 있으면 서버는 여전히 버려도 되는 물건이다. 토큰의 진짜 이점은 무상태 자체가 아니라 조회 없이 검증된다는 것이고, 그 대가가 취소의 어려움이다.

한쪽만 쓰는 경우가 오히려 드물다. 실무에서는 둘을 섞어 쓰는데, 어떻게 섞고 무엇을 얻는지는 세션이냐 토큰이냐가 축을 나눠 다룬다.

그리고 미들웨어 글에서 본 그 길목이 이 검증이 실제로 사는 자리다. 쿠키를 읽고 서명을 확인하는 일은 핸들러가 아니라 앞단이 한다.

정리

무상태란서버가 요청 사이의 대화 맥락을 안 갖는다
어느 서버가 받아도 같아서, 늘리고 죽이는 게 자유롭다
그래서매 요청이 자기를 밝힌다. 쿠키는 그걸 브라우저가 자동으로 해주는 장치
상태는없어지는 게 아니라 옮겨진다 - 서버 메모리 · 공용 저장소 · 클라이언트
클라이언트에 두면조회가 없어지는 대신 취소가 어려워진다
대가같은 정보를 매번 보내고 매번 검증한다

무상태는 서버를 자유롭게 했지만, 매 요청이 자기 짐을 다 지고 오게 만들었다. 그런데 짐을 지고 오는 것보다 더 비싼 게 남아 있다. 요청을 보내려면 먼저 서버와 연결부터 맺어야 하는데, 그 연결을 매번 새로 맺으면 요청 자체보다 준비가 오래 걸린다. 다음 글이 그 이야기다.

관련 글