무상태
고친 사람 github-actions[bot]
무상태는 서버가 앞 요청에서 있었던 일을 기억하지 않게 만듭니다. 대신 요청 하나에 그 요청을 처리하는 데 필요한 것을 전부 실어 보냅니다. 그래서 같은 사람이 보낸 요청을 매번 다른 서버가 받아도 답이 같습니다.
쉽고 빠른 이해
무상태는 서버가 앞 요청을 기억하지 않게 하는 성질입니다. 로그인한 사람이라는 것을 요청마다 토큰으로 다시 알리는 웹 서비스가 그렇습니다.
기억해 두면 다음 요청도 그 기억을 가진 한 대로 보내야 합니다. 그 한 대가 죽으면 하던 대화가 같이 사라집니다.
- 요청에 필요한 것을 전부 담아 보냅니다
- 서버는 그 요청만 읽고 처리한 뒤 아무것도 남기지 않습니다
- 남겨야 할 것은 클라이언트나 모든 서버가 함께 보는 저장소가 맡습니다
대가는 같은 정보를 요청마다 되풀이해 싣는다는 것입니다. 서버 대수를 늘리기는 쉬워지지만 요청 하나가 커집니다.
상세
구청 민원 창구를 떠올려 봅시다. 서류 한 벌을 갖춰 내밀면 어느 창구에 줄을 서도 일이 끝납니다. 직원이 나를 기억하고 있을 필요가 없습니다.
서버를 이렇게 만드는 것이 무상태입니다. 요청 하나가 혼자 설 수 있게 만듭니다. 서버에는 그 요청에 대해 아무것도 남기지 않습니다.
서버에 남는 대화 상태
앞뒤 요청을 잇는 정보를 대화 상태라고 부릅시다. 이 사람이 로그인을 마쳤는지, 장바구니에 무엇을 담았는지, 결제 화면 몇 단계까지 왔는지가 대화 상태입니다.
이런 정보를 서버 메모리에 남겨 두고 다음 요청에서 꺼내 쓰면 상태 유지입니다. 꺼내 쓸 것을 아예 안 남기면 무상태입니다. 요청 하나와 그 응답이 끝나는 순간, 서버에게 그 사람은 처음 보는 사람이 됩니다.
데이터베이스에 오래 보관하는 데이터는 대화 상태가 아닙니다. 주문을 받아 표에 한 줄 적는 서버도 무상태입니다. 가르는 선은 「다음 요청이 앞 요청을 이어받아야 하는가」입니다.
무상태는 대화 상태만 두고 하는 말입니다. 오래 보관하는 데이터를 없애자는 말이 아닙니다.
요청 하나에 전부 싣기
서버가 대화 상태를 안 남기면 그 몫은 요청이 집니다. 누가 보냈는지, 무엇을 원하는지, 어디서부터 이어서 보고 싶은지를 요청 하나가 다 들고 있어야 합니다.
HTTP(HyperText Transfer Protocol, 하이퍼텍스트 전송 프로토콜)로 도는 서비스는 이것을 헤더와 쿼리 스트링에 나눠 싣습니다. 주문 목록의 둘째 쪽을 달라는 요청은 이런 모양입니다.
GET /orders?page=2
Authorization: Bearer <토큰>
몇 쪽을 볼지도 누구인지도 이 요청 안에 들어 있습니다. 서버는 앞 요청을 안 봐도 이 요청만으로 답을 만듭니다. 바로 앞에 첫 쪽을 보내 준 서버가 아니어도 됩니다.
sequenceDiagram
participant 클라이언트
participant 서버1
participant 서버2
클라이언트->>서버1: 토큰 + page=1
서버1-->>클라이언트: 첫 쪽
클라이언트->>서버2: 토큰 + page=2
서버2-->>클라이언트: 둘째 쪽
Note over 서버1,서버2: 서버는 앞 요청을 안 본다
「아까 드린 그 번호로 이어서」 같은 요청은 무상태에서 성립하지 않습니다. 이어 갈 지점을 서버가 기억하지 않으므로 그 번호도 요청이 싣고 와야 합니다.
대화 상태를 옮겨 두는 두 곳
무상태는 대화 상태를 없애는 것이 아니라 옮기는 것입니다. 장바구니도 어딘가 남아야 합니다. 로그인을 마쳤다는 것도 마찬가지입니다.
옮겨 둘 곳은 둘입니다. 하나는 클라이언트입니다. 클라이언트가 대화 상태를 들고 다니다가 요청마다 함께 실어 보냅니다. 다른 하나는 모든 서버가 함께 보는 저장소입니다. 요청이 올 때마다 서버가 거기서 꺼내 봅니다.
견주려고 상태 유지인 서버 메모리도 첫 줄에 함께 넣습니다. 첫 줄이 상태 유지이고 아래 두 줄이 무상태입니다.
| 대화 상태를 두는 곳 | 어떻게 꺼내나 | 대신 치르는 것 |
|---|---|---|
| 서버 메모리 | 그 서버가 자기 안에서 꺼낸다 | 다음 요청도 그 서버로만 가야 한다 |
| 클라이언트 | 요청에 실려 함께 온다 | 요청이 커지고 위조를 막을 전자 서명이 든다 |
| 공용 저장소 | 어느 서버든 같은 곳을 본다 | 요청마다 저장소를 한 번 더 다녀온다 |
무상태 서버 두 대가 같은 답을 내는 까닭은 볼 곳이 서버 밖에 있기 때문입니다. 세 줄이 각각 어떤 모양인지 그리면 이렇습니다.
flowchart TD
subgraph G1["서버 메모리에 둔다"]
C1["클라이언트"] -->|요청| S1["서버1"]
S1 --- M1["대화 상태"]
end
subgraph G2["클라이언트가 들고 다닌다"]
C2["클라이언트"] -->|"요청 + 대화 상태"| S2["서버1"]
C2 -->|"요청 + 대화 상태"| S3["서버2"]
end
subgraph G3["공용 저장소에 둔다"]
C3["클라이언트"] --> S4["서버1"]
C3 --> S5["서버2"]
S4 --> D["공용 저장소"]
S5 --> D
D --- M2["대화 상태"]
end
어느 서버가 받아도 되는 구조
무상태를 고르는 까닭이 이 대목에 있습니다. 서버가 대화 상태를 남기면 다음 요청은 그것을 가진 한 대로 가야 합니다. 요청을 특정 서버에 묶어 보내는 이 방식을 세션 어피니티라고 부릅니다.
묶어 두면 두 가지가 걸립니다. 그 한 대가 죽으면 하던 대화가 같이 사라집니다. 대수를 늘려도 이미 묶인 요청은 새 서버로 안 갑니다.
무상태로 만들어 두면 부하 분산이 어느 서버를 골라도 됩니다. 부하 분산은 들어온 요청을 남는 서버에 나눠 주는 장치입니다. 앞 요청이 어디로 갔는지 볼 것 없이 한가한 대를 고르면 됩니다.
대수를 늘리는 일이 목록에 한 줄 더하는 일이 됩니다. 이것이 수평 확장의 전제입니다.
그래서 배포도 쉬워집니다. 서버를 한 대씩 내렸다 올려도 요청은 남은 대가 받습니다.
무상태 프로토콜과 무상태 서버
무상태라는 말은 두 곳에 붙습니다. 프로토콜에 붙을 때와 서버에 붙을 때입니다.
프로토콜이 무상태라는 말은, 요청 하나하나를 앞선 요청과 무관하게 처리하도록 규칙이 정해져 있다는 뜻입니다. HTTP는 요청과 응답 한 쌍으로 일이 끝나게 규칙을 정해 두었습니다. 반면 TCP(Transmission Control Protocol, 전송 제어 프로토콜)는 연결을 맺고 끊을 때까지의 단계를 양쪽이 기억합니다.
서버가 무상태라는 말은, 그 프로토콜 위에 얹은 서비스가 요청 사이에 아무것도 안 남긴다는 뜻입니다. 둘은 따로 놉니다. 무상태 프로토콜 위에서 돌아도 로그인 정보를 서버 메모리에 담아 두는 웹 서비스가 있습니다. 그런 서비스는 프로토콜만 무상태입니다.
REST(Representational State Transfer, 표현 상태 전달)는 이 중 서버 쪽 무상태를 제약으로 못 박습니다. 요청마다 그 요청을 이해하는 데 필요한 정보가 전부 들어 있어야 합니다. 서버에 쌓아 둔 맥락을 끌어다 쓸 수 없습니다.
무상태가 치르는 대가
같은 정보가 요청마다 되풀이해 실립니다. 인증 토큰과 이어 볼 지점을 매번 보내야 하므로 요청이 커집니다. 서버는 매번 그것을 다시 검증합니다.
클라이언트가 상태를 들고 다니면 그 값을 믿을 수 없습니다. 값을 고쳐 보내는 것을 막으려면 서명을 붙이고 만료를 두어야 합니다. 그 검사도 요청마다 듭니다.
공용 저장소에 두면 그 저장소가 모든 요청의 길목이 됩니다. 서버는 여러 대여도 저장소는 한 대입니다. 단일 장애점이 그리로 옮겨 온 것입니다.
서버마다 쌓아 둔 캐시도 덜 맞습니다. 같은 사용자의 다음 요청이 다른 서버로 가면 앞 서버가 데워 둔 값은 안 쓰입니다.
상태 유지가 맞는 때
주고받는 도중의 단계를 양쪽이 지켜야 하는 일에는 상태 유지가 맞습니다. 전송 계층은 연결을 맺고 순서를 세어 가며 데이터를 나릅니다. WebSocket은 한 번 연결해 놓고 그 연결로 계속 밀어 보냅니다. 둘 다 양쪽이 어디까지 왔는지를 기억하고 있어야 합니다.
한 요청에 담기에 너무 큰 진행 상황도 받는 쪽이 맡는 편이 낫습니다. 커다란 파일을 나눠 올리는 도중이라면 어디까지 받았는지를 받는 쪽이 기억하는 편이 간단합니다.
무상태와 상태 유지는 한 시스템 안에 겹쳐 씁니다. 서버는 무상태여도 그 서버가 올라탄 연결은 상태 유지입니다. 오래 보관할 데이터는 서버 밖 저장소가 맡습니다.
관련 항목
무상태와 맞세워지는 대립 개념
상태 유지 · 세션 · 세션 어피니티 · 스티키 세션 · 상태 기계 · 대화 상태
무상태를 제약으로 못 박는 표준과 구조
REST · HTTP · 클라이언트-서버 · 마이크로서비스 · 서버리스 · 무공유 아키텍처
요청에 상태를 실어 나르는 수단
쿠키 · 토큰 · JWT · Authorization 헤더 · ETag · 조건부 요청 · 커서 페이지네이션 · 쿼리 스트링
옮겨 둔 상태를 맡아 두는 저장소
세션 저장소 · Redis · Memcached · 데이터베이스 · 분산 캐시 · 캐싱
무상태 서버로 굴리는 운영 방식
수평 확장 · 오토스케일링 · 부하 분산 · 로드 밸런서 · 롤링 업데이트 · 블루-그린 배포 · 장애 조치 · 헬스 체크 · 컨테이너
무상태 서버가 지키기 쉬워지는 성질
멱등성 · 순수 함수 · 안전한 메서드 · 재시도 · 가용성 · 장애 내성
무상태로 옮기며 새로 생기는 문제
다른 이름: stateless · 스테이트리스 · 무상태성