Kubernetes에서 상태 비저장(Stateless) MCP 구현: 더 이상 세션 고정(Sticky Sessions)이 필요 없습니다
요약
Kubernetes 환경에서 상태 비저장(Stateless) MCP 구현 방식을 소개합니다. 새로운 MCP 사양 개정판 덕분에 세션 어피니티 설정 없이도 장바구니 같은 복잡한 쇼핑 카트 세션을 여러 파드에 걸쳐 안정적으로 처리할 수 있게 되었습니다. 이는 기존의 까다로운 스케일링 제약 조건과 롤링 재시작 시 발생하던 신뢰성 문제를 근본적으로 해결합니다.
핵심 포인트
- 새 MCP 사양은 단일 요청으로 세션 초기화와 후속 작업을 통합했습니다.
- 세션 어피니티(Session Affinity) 설정 없이도 상태 비저장 아키텍처가 가능해졌습니다.
- HPA 및 롤링 재시작 시 발생하는 스케일링 제약 조건이 해소됩니다.
원래 2026-10-03에 webofmike.com에서 게시되었습니다. 데모 리포와 그 안의 모든 명령어는 게시 전에 실행되었습니다.
agentgateway를 프론트엔드로 하고, 세션 어피니티(session affinity)가 어디에도 설정되지 않은 MCP 서버 3개의 복제본입니다. 장바구니 쇼핑 카트 세션(장바구니 생성, 아이템 두 개 추가, 결제)이 다른 파드(pod)를 거쳐서도 올바르게 완료되며, 심지어 Deployment의 전체 롤링 재시작 과정 중에도 그렇습니다. 이것이 바로 MCP 2026-07-28 사양 개정판과 stateless-mcp-scale가 보여주는 실질적인 Kubernetes의 이점이며, 단순히 설명하는 대신 이를 증명하는 리포지토리가 바로 여기입니다.
Simon Willison은 이 사양 개정판이 발표된 주에 Stateless MCP has recaptured my interest라는 글을 작성했으며, 이는 Hacker News에서 386점을 받았습니다. 그의 게시물은 와이어 포맷(wire-format) 변경 사항을 명확하게 설명합니다: 이전의 두 번 요청하는 과정(initialize를 사용하여 Mcp-Session-Id를 얻고, 그 ID를 사용한 두 번째 요청)이 단일의 자체 포함된 요청으로 통합됩니다. 그의 게시물이 다루지 않은 부분은, 그것이 Kubernetes 운영에 관한 것이 아니기 때문에, 이 변경 사항이 인프라 팀에게 실제로 무엇을 가져다주는지입니다: 특정한 짜증 나는 스케일링 제약 조건의 종식입니다. 본 포스트는 이를 보여주는 가장 작은 클러스터를 구축합니다.
이전 사양에서 요구했던 것
이전 사양에서는 MCP 클라이언트가 initialize를 호출하여 Mcp-Session-Id 헤더를 받은 후, 세션 내의 모든 후속 요청에 이 헤더를 전송해야 했습니다. 서버는 이 세션 ID와 연결된 상태를 메모리에 유지할 것으로 예상되었습니다. Kubernetes 환경에서 이는 initialize를 처리한 파드(pod)가 해당 세션의 모든 후속 요청을 처리해야 함을 의미했습니다. 따라서 Service에 sessionAffinity: ClientIP를 설정하거나 그 앞에 쿠키 기반의 어피니티(affinity) 설정을 사용해야 했습니다. 세션 어피니티 자체가 생소한 기술은 아니지만, 비용이 따릅니다. 이는 수평 파드 자동 스케일링(horizontal pod autoscaling)과 충돌합니다 (새로운 복제본은 새로운 세션만 받으며 기존 세션을 공유하지 못함). 또한 롤링 재시작이나 축소 배포 시, 종료된 파드에 고정되었던 어떤 세션이든 신뢰성 이벤트가 발생할 수 있게 만듭니다.
새 사양이 제거하는 것들
spec의 발표는 이 변경 사항을 명확하게 설명합니다:
stateless-mcp-scale는 세 부분으로 구성되어 있습니다:
initialize,tools/list, 그리고 Streamable HTTP를 통해tools/call을 구현하는 최소한의 MCP 서버입니다 (server/stateless_mcp_server.py, Python 표준 라이브러리만 사용, 프레임워크 없음). 이 서버는 세 가지 도구(cart_create,cart_add_item,cart_checkout)를 노출하며, 이들의 상태는 프로세스 내에 있는 것이 아니라 호출자가 보유하는 불투명한cartToken안에 완전히 존재합니다. 또한 모든 응답은servedBy.pod와servedBy.podIP를 보고하여 데모가 어떤 복제본(replica)이 답변했는지 보여줄 수 있게 합니다.- 해당 서버의 3개 복제본을 가진 Kubernetes Deployment이며, 일반적인
ClusterIPService 뒤에 위치합니다. 아무것도sessionAffinity를 설정하지 않으므로 Kubernetes 기본값인None으로 유지됩니다. - 이 앞에 agentgateway v1.5.0이 배치되었으며,
statefulMode: stateless로 구성되었습니다:
gateways:
default:
port: 3000
...
statefulMode: stateless는 agentgateway의 설정 스키마에
이 모든 과정은 아직 엔드투엔드로 문서화되지 않았습니다. agentgateway 자체의 릴리스 노트에서도 이를 인정하고 있습니다("대부분의 내용은 전용 가이드로 다뤄지지 않았음"). 저는 순수한 요청(bare requests)을 보내고 agentgateway의 오류 메시지를 읽는 방식으로 정확한 요구사항들을 찾아냈는데, 이들이 직접 반복 테스트하기에 충분히 정밀했습니다:
400 mcp: invalid MCP protocol version header
HTTP 헤더 내부에 JSON-RPC 본문 안에만 추가하는 것이 아니라 MCP-Protocol-Version: 2026-07-28을 추가하여 수정했습니다.
400 invalid request parameters: _meta.protocolVersion is required for modern requests
JSON-RPC의 params에 _meta를 추가하고, 키는 단순히 protocolVersion이 아니라 역방향 DNS 형식인 io.modelcontextprotocol/protocolVersion으로 지정하여 수정했습니다.
400 invalid MCP routing header: Mcp-Method
자체 HTTP 헤더로 Mcp-Method: tools/call (또는 JSON-RPC 메서드와 일치하는 tools/list)를 추가하고, 도구 호출의 경우 Mcp-Name: cart_create을 추가하여 수정했습니다. 이들은 agentgateway 자체의 라우팅 헤더이며, JSON-RPC 엔벨로프와는 별개입니다.
그리고 표준 initialize 호출을 먼저 시도하면 다음과 같은 응답이 옵니다:
{"jsonrpc":"2.0","id":1,"error":{"code":-32601,"message":"method not found: initialize"}}
statefulMode: stateless를 사용하면, agentgateway는 핸드셰이크 자체를 거부합니다. 이는 스펙 자체가 가지고 있는 틀과 일치합니다. 즉, 현대적이고 상태 비저장(stateless)인 클라이언트는 초기화 요청을 보내지 않습니다. 이 모든 과정에서 agentgateway는 클라이언트나 백엔드 어느 쪽에도 Mcp-Session-Id를 전송하거나 기대하지 않습니다. 저장소의 scripts/mcp_client.py 파일은 네 가지 요구사항을 한 곳에 인코딩하여, 다른 사람이 오류 문자열을 읽고 다시 발견할 필요가 없게 만들었습니다.
증명(The proof)
make up 명령어는 데모 이미지를 빌드하고, kind 클러스터를 생성하며, 두 이미지를 모두 로드한 후 네임스페이스와 3개 복제본 배포(Deployment), 그리고 agentgateway를 적용하고 각 롤아웃이 완료될 때까지 기다립니다. make demo 명령어는 scripts/demo.py를 실행하여 라이브 클러스터에 대해 세 가지 작업을 수행하고 그 결과를 검증합니다. 이것은 실제 깨끗한 실행에서 나온 원본 출력입니다:
=== 1. Fan-out: 9개의 독립적인 도구/호출 요청, 세션 친화성(session affinity) 구성 안 함 ===
요청 1: stateless-mcp-demo-757875b499-z2zsg에 의해 처리됨
요청 2: stateless-mcp-demo-757875b499-w6mpr에 의해 처리됨
...
Step 1은 이 특정 9개 요청 샘플에서 3개의 복제본(replica) 중 2개에 걸쳐 착지했습니다. 개발 과정 중 별도의 실행에서는 처음 6개 요청 모두 3개 복제본 전체를 사용했습니다. 정확한 비율이 중요한 것이 아닙니다. 중요한 것은 연속된 두 호출이 어떤 메커니즘으로도 고정되지 않았음에도 불구하고, 모든 호출이 여전히 성공했다는 점입니다. Step 3은 제가 두 번 읽을 만한 부분입니다: cart_checkout은 배포(Deployment)의 이전 ReplicaSet의 파드에서 생성된 카트를 종료합니다. 이 파드는 체크아웃이 실행될 때쯤에는 더 이상 존재하지 않습니다. 체크아웃에 응답하는 복제본은 그 자체의 메모리에서 해당 카트를 본 적이 없지만, 카트의 상태가 그 메모리에 있었던 것이 아니기 때문에 여전히 올바르게 디코딩합니다. 2026-07-28 이전 모델에서는 이러한 정확한 시퀀스, 즉 친화성 없음(no affinity)과 세션 중간에 발생하는 롤링 재시작(rolling restart)이 MCP 클라이언트를 깨뜨리는 교과서적인 방법과 가깝습니다.
전제 수정하기
제가 처음에 생각했던 각도는 에이전트게이트웨이(agentgateway)가 새로운 스펙을 '도착한 당일' 지원한다는 것이었습니다. gh release list -R agentgateway/agentgateway와 gh release view v1.4.0를 확인해 보면 그게 정확하지 않으며, 실제 타임라인은 더 흥미롭습니다. 전체 2026-07-28 지원을 추가한 agentgateway v1.4.0은 스펙의 날짜가 지정된 개정일보다 하루 전인 2026-07-27 17:51 UTC에 게시되었습니다. 이 버전은 2026년 5월 21일부터 공개된 스펙의 릴리스 후보(release candidate)을 기반으로 구축되었으며, 구현자들에게 두 달 이상의 선행 시간을 주었습니다. v1.4.1은 2026-07-29에 MCP 호환성 수정 작업을 거쳐 출시되었는데, 여기에는 상위(upstream) MCP 응답을 예상된 타입과 검증하는 작업과 더 이상 합성 세션 식별자를 실제 Mcp-Session-Id 값처럼 로깅하지 않는 것이 포함되었습니다. Willison의 게시물과 Hacker News 토론은 그보다 며칠 뒤인 7월 31일과 8월 1일에 이루어졌습니다. '첫날' 지원이라는 정신 자체는 여전히 정확하지만, '같은 날'이라는 세부 사항은 잘못되었으며, 실제 순서(RC 기반 구축, 하루 일찍 배포, 이틀 뒤 패치)가 이렇게 큰 스펙 개정안이 실제로 어떻게 구현되는지에 대한 더 나은 이야기입니다.
저는 이전에 멀티테넌트 배포를 위해 agentgateway 뒤에 MCP 서버들을 페더레이션(federating)하는 것에 대해 글을 쓴 적이 있습니다. 그 경우 확장성의 축은 동일한 게이트웨이에서 다른 고객들이 서로 다른 도구를 얻는 것이었습니다. 이 게시물은 다른 축입니다: 동일한 서버의 동일한 복제본들로 구성되어 있으며, 질문은 그중 어느 것도 모든 요청에 응답할 수 있는지 여부입니다. 새로운 스펙 하에서는 추가적인 설정 없이도 '예'가 답입니다.
전체 레포지토리에는 서버, Kubernetes 매니페스트, agentgateway 설정, 그리고 위에서 출력된 결과를 생성한 스크립트가 포함되어 있으며, github.com/themsquared/stateless-mcp-scale에서 확인할 수 있습니다. make up && make demo를 실행하면 kind 클러스터에서 세 가지 실험을 모두 재현할 수 있습니다. 다음에 테스트해 볼 가치가 있는 것은 실제 HPA(Horizontal Pod Autoscaler)가 부하 조건 하에서 MCP 서버를 스케일하는 멀티 노드 클러스터입니다. 이를 통해 파드가 단지 하나의 노드의 ReplicaSet을 넘어 기기들 사이로 이동할 때도 동일한 비친화성(no-affinity) 속성이 유지되는지 확인할 수 있습니다.
자주 묻는 질문 (FAQ)
MCP 2026-07-28 스펙 개정에서 무엇이 변경되었나요?
이전 스펙에서는 의무적인 initialize/initialized 핸드셰이크와 클라이언트를 하나의 서버 프로세스에 고정(pin)하는 데 사용되던 Mcp-Session-Id 헤더를 폐지했습니다. 이제 각 요청은 프로토콜 버전과 클라이언트 ID를 자체 _meta 필드에 담고 있으므로, 어떤 복제본(replica)이든 모든 요청에 응답할 수 있습니다. 호출 간에 상태를 유지해야 하는 서버는 메모리에 보관하는 대신 호출자에게 명시적인 토큰을 전달하도록 기대됩니다.
MCP 서버가 Kubernetes에서 세션 고정(sticky sessions, session affinity)이 필요한가요?
2026-07-28 비상태(stateless) 와이어 포맷을 사용한다면 필요하지 않습니다. 본 게시물의 데모는 agentgateway 뒤에 3개의 복제본 MCP 서버를 배치하고, Services 양쪽 모두에서 sessionAffinity를 Kubernetes 기본값(None)으로 설정했습니다. 그 결과, 배포(Deployment)의 전체 롤링 재시작을 거치면서도 다른 파드들을 통해 4회 호출 세션이 올바르게 완료되었습니다.
비상태 MCP 요청에 agentgateway가 요구하는 HTTP 헤더는 무엇인가요?
MCP-Protocol-Version: 2026-07-28을 JSON-RPC 본문 내부가 아닌 자체 HTTP 헤더로 추가해야 합니다. 또한, Mcp-Method를 JSON-RPC 메서드를 명명하고, tools/call에 대해 Mcp-Name을 agentgateway의 자체 라우팅 헤더로, JSON-RPC 엔벨로프와 분리하여 추가해야 합니다. JSON-RPC 파라미터에는 io.modelcontextprotocol/protocolVersion이라는 이름의 _meta 키도 필요합니다. Mcp-Session-Id는 절대로 전송되거나 요구되지 않습니다.
agentgateway가 출시 당일에 새로운 MCP 스펙을 지원했나요?
거의: agentgateway v1.4.0은 2026년 7월 28일 전체 지원을 추가하여 2026년 7월 27일에 공개되었으며, 이는 스펙 자체의 날짜가 확정된 개정보다 하루 전이었습니다. 이 버전은 2026년 5월 21일부터 공개된 스펙의 릴리스 후보(release candidate)를 기반으로 구축되었습니다. v1.4.1은 MCP 호환성 버그 수정과 함께 2026년 7월 29일에 뒤따랐습니다.
정식 버전(Canonical version), 기계가 읽을 수 있는 마크다운 형식은 https://webofmike.com/stateless-mcp-scale/index.md에서 확인할 수 있습니다: https://webofmike.com/stateless-mcp-scale/
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기