이 문서를 통해 아래 질문들에 답할 수 있게 됩니다.
- Stateless 서버는 어떤 상태를 로컬 메모리에 두지 않는다는 뜻인가?
- Spring 인스턴스에서 요청 간 상태를 제거하면 배포, 확장, 장애 격리가 어떻게 바뀌는가?
- 세션 외부화 전에 어떤 상태를 분류하고 어떤 API 계약을 정해야 하는가?
Stateless의 의미
Stateless 서버는 상태를 전혀 쓰지 않는 서버가 아니다.
요청 사이에 유지되어야 하는 상태를 특정 Spring 인스턴스의 heap, static field, local file, worker memory에 묶지 않는다는 뜻이다.
사용자가 로그인한 뒤 다음 요청이 다른 Pod로 가도 같은 사용자로 인식되어야 한다.
결제 요청이 timeout 후 재시도되어도 다른 인스턴스에서 중복 주문이 생기면 안 된다.
이 조건을 만족하면 autoscaling, rolling deploy, failover가 쉬워진다.
대신 상태의 owner, TTL, 복구 경로, 외부 저장소 장애 대응을 명시해야 한다.
이 문서가 바꾸는 설계 축
| 축 | 바뀌는 내용 |
|---|---|
| 성능 | 로컬 메모리 접근은 줄고 외부 저장소 조회가 늘 수 있다. |
| 정합성 | 인스턴스가 달라도 같은 세션, 권한, 중복 방지 판단을 적용한다. |
| 장애 격리 | Pod 장애가 곧 사용자 상태 유실로 이어지는 일을 줄인다. |
| 운영 복잡도 | 상태별 owner, TTL, serialization, 복구 절차를 운영해야 한다. |
Stateless는 성능 최적화가 아니라 배포와 장애 복구를 단순하게 만드는 구조 선택이다.
성능은 local memory가 더 좋아 보일 수 있지만, 장애와 scale out 비용을 함께 봐야 한다.
요청 내 상태와 요청 간 상태
가장 먼저 상태의 수명을 나눈다.
| 상태 | 수명 | 로컬 보관 가능성 |
|---|---|---|
| validation result | 요청 처리 중 | 가능하다. |
| request id | 요청 처리 중 | 가능하나 로그 context 정리가 필요하다. |
| 로그인 principal | 여러 요청 | 로컬 보관 부적합 |
| 장바구니 draft | 여러 요청 | 유실 허용 범위에 따라 외부화 필요 |
| idempotency key | retry 기간 | 로컬 보관 부적합 |
| 결제 진행 상태 | 완료 또는 취소까지 | 영속 owner 필요 |
| local reference cache | 짧은 TTL | 재생성 가능할 때만 제한적으로 가능 |
요청이 끝나면 사라져도 되는 값은 로컬에 둘 수 있다.
요청을 넘어 사용자 경험이나 비즈니스 결과를 바꾸는 값은 인스턴스 바깥에 owner가 있어야 한다.
Spring 인스턴스에서 확인할 위치
숨은 상태는 HttpSession에만 있지 않다.
다음 위치를 함께 확인한다.
static Map이나 singleton bean field@SessionScope,@ApplicationScopebeanThreadLocalrequest context- in-memory cache와 Caffeine cache
- local file upload 임시 경로
- scheduled job의 cursor
- in-memory lock
- WebSocket connection registry
이 값들이 요청 간 의미를 가지면 scale out 이후 동작이 달라진다.
특히 singleton bean field는 Spring에서 자연스러워 보여도 여러 Pod 사이에서는 공유되지 않는다.
대표 시나리오
주문 API를 1대에서 6대 Pod로 늘린다고 하자.
로그인 세션 = HttpSession
checkout draft = server memory
idempotency key = local Caffeine cache
order write = RDB이 구조에서는 같은 사용자의 재시도 요청이 다른 Pod로 가면 idempotency key를 찾지 못할 수 있다.
checkout draft도 사용자가 어느 Pod에 붙었는지에 따라 다르게 보인다.
주문 write는 RDB에 있어도 주문 직전 상태가 로컬에 있으면 최종 결과가 흔들린다.
scale out 전에는 API별로 어떤 로컬 상태가 응답과 write 결과를 바꾸는지 확인해야 한다.
상태 외부화 원칙
외부화는 무조건 Redis로 옮긴다는 뜻이 아니다.
상태의 성격에 맞는 owner를 고른다.
| 상태 | 가능한 owner | 판단 기준 |
|---|---|---|
| 인증 세션 | session store, token, auth service | logout, 권한 변경, 장애 허용 |
| checkout draft | DB, Redis, client storage | 유실 비용과 재개 경험 |
| idempotency key | DB unique constraint, idempotency store | 중복 실행 비용 |
| upload progress | object storage, DB row | 재시도와 정리 절차 |
| rate limit counter | Redis, gateway | fail-open/closed 정책 |
상태를 외부화하면 해당 저장소가 새 의존성이 된다.
따라서 저장소 장애 때 사용자가 무엇을 보는지 API 계약으로 남겨야 한다.
API 계약
세션이나 요청 간 상태가 사라질 수 있다면 응답도 정해야 한다.
POST /orders
Idempotency-Key: user-10:cart-91:req-7{
"status": "PENDING",
"orderId": "ord-1001",
"message": "주문 처리 상태를 다시 조회해 주세요."
}로컬 상태를 제거하면 사용자가 항상 즉시 성공을 보는 구조가 아닐 수 있다.
대신 재조회 API, pending 상태, 명확한 401/409/422 응답이 필요하다.
상태 저장 위치 변경은 API 계약 변경으로 이어질 수 있다.
배포와 failover 관점
Stateless 구조의 핵심 검증은 배포 중에도 같은 요청이 처리되는지다.
다음 상황을 테스트한다.
1. 사용자가 로그인한다.
2. checkout draft를 만든다.
3. Pod A를 종료한다.
4. 다음 요청이 Pod B로 간다.
5. 사용자는 같은 상태를 보거나 명확한 복구 안내를 받는다.이 테스트를 통과하지 못하면 서버 수가 늘어날수록 장애 반경이 커진다.
rolling deploy 때 old/new code가 같은 외부 상태를 읽을 수 있는지도 확인해야 한다.
serialization format이나 key schema가 바뀌면 배포 중 일부 요청만 실패할 수 있다.
관측 지표
Stateless 전환 후에는 다음 지표를 본다.
- 인스턴스별 session 관련 memory
- 외부 상태 저장소 read/write latency
- 상태 조회 실패율
- idempotency conflict count
- pending 응답 비율
- Pod 재시작 후 사용자 오류율
- old/new version serialization error
상태 외부화는 코드 구조만 바꾸는 일이 아니다.
운영자는 인스턴스 장애와 외부 저장소 장애를 분리해 볼 수 있어야 한다.
개인 프로젝트 기준
- 요청을 넘어 유지되는 상태 목록을 표로 적는다.
static Map,HttpSession, local cache가 있다면 scale out 때 영향 범위를 적는다.- 결제, 주문, 쿠폰처럼 중복이 위험한 상태는 DB 제약이나 idempotency key로 보강한다.
- 단일 서버라면 단일 서버를 선택한 이유와 전환 조건을 남긴다.
- Pod 재시작 또는 서버 재시작 후 사용자가 무엇을 잃는지 확인한다.
기업 운영 기준
- 상태별 owner와 저장 위치를 architecture decision record에 남긴다.
- session, idempotency, draft, upload, rate limit 상태의 TTL과 cleanup 정책을 관리한다.
- rolling deploy 전 old/new state compatibility를 확인한다.
- 인증 장애와 주문 장애가 같은 저장소 장애로 묶이지 않도록 격리한다.
- scale-out 검증에는 failover, retry, serialization 실패 테스트를 포함한다.
위험 신호
- stateless라고 말하지만 singleton bean field에 사용자 상태가 있다.
- 서버를 한 대 더 늘렸을 때 같은 사용자의 상태가 달라진다.
- idempotency key가 local cache에만 있다.
- Pod 재시작 후 사라지는 상태와 복구 가능한 상태를 구분하지 않는다.
- 외부화한 저장소 장애 시 API 응답 계약이 없다.
- 배포 중 old/new code의 상태 호환성을 확인하지 않는다.
확인 질문
확인 질문
- 확인 질문: Stateless 서버가 로컬에 두면 안 되는 상태는 무엇인가?
- 답변: 요청을 넘어 사용자 경험이나 비즈니스 결과를 바꾸고, 다른 인스턴스에서도 같은 판단이 필요해지는 상태다.
- 확인 질문: 상태를 외부화하면 무엇이 새로 생기는가?
- 답변: 외부 저장소 latency, 장애 대응, TTL, serialization, owner 관리라는 운영 복잡도가 생긴다.
- 확인 질문: Stateless 전환이 성공했는지 확인하는 가장 직접적인 테스트는 무엇인가?
- 답변: 요청 중간에 인스턴스를 교체해도 같은 상태를 보거나 명확한 복구 응답을 받는지 확인하는 것이다.