-
MCP는 왜 Streamable HTTP로 갈아탔나 — 엔드포인트 하나로 스트림을 다스리는 법IT 2026. 7. 1. 22:00
MCP(Model Context Protocol, AI 모델을 외부 도구·데이터와 잇는 표준 규약)의 원격 트랜스포트는 처음에 HTTP + SSE 조합이었다. 동작은 잘 했다. 그런데 2025년 3월, MCP 명세는 트랜스포트를 Streamable HTTP라는 새 방식으로 갈아탔다. 잘 굴러가던 걸 왜 바꿨을까? 이 글은 그 이유를 따라간다 — 옛 방식이 남긴 어떤 숙제 때문에, Streamable HTTP가 무엇을 어떻게 바꿨고, 그래서 MCP에 무엇이 좋아졌는지를 순서대로 본다.
(이 글은 SSE가 무엇이고 MCP가 왜 그걸 골랐는지를 다룬 앞 글의 후속이다. SSE의 기본 — 서버가 HTTP 위에서 클라이언트에게 데이터를 밀어 넣는 단방향 스트림 — 은 안다고 보고 출발한다.)
출발점: 잘 돌아가던 두 엔드포인트 방식의 숨은 비용
먼저 옛 방식을 한 장으로 떠올려 보자. 초기 MCP는 통로를 둘로 갈랐다 — 서버가 보내는 메시지를 받는
GET /sse(받는 귀), 클라이언트가 보내는 메시지를 부치는POST /messages(말하는 입)다.다이어그램 설명. 옛 방식의 구조와 그 약점을 함께 보여준다. 클라이언트가
GET /sse를 계속 열어 둔 채 여러 요청을 POST하면, 모든 응답이 그 단 하나의 SSE 채널로 섞여 내려온다. 그림에서 ②의 결과가 ①보다 먼저 도착할 수 있다는 점에 주목하자 — 한 채널에 응답이 뒤섞이니, 클라이언트는 "이 결과가 어느 요청 것인지"를 매번 id로 맞춰야 한다. 여기서 세 가지 숨은 비용이 드러난다. 이게 다음 섹션의 출발점이다.문제: 옛 방식이 남긴 세 가지 숙제
두 엔드포인트 방식은 동작은 했지만, 규모가 커지면 아픈 세 가지 약점을 안고 있었다.
다이어그램 설명. 옛 방식의 세 숙제를 모았다. 첫째 stateful — 서버가 접속한 클라이언트마다 SSE 연결을 메모리에 붙잡고 있어야 해서, 서버를 여러 대로 늘려 부하를 나누는(수평 확장) 게 어렵다. 클라이언트 A의 SSE가 1번 서버에 붙어 있는데 A의 POST가 2번 서버로 가면 응답을 어디로 보낼지 길이 끊긴다. 둘째 단일 채널 혼선 — 모든 응답이 한 SSE로 섞여 내려오니 요청과 응답을 짝짓는 부담이 클라이언트에 쌓인다. 셋째 재연결 손실 — 그 하나뿐인 SSE가 끊기면 진행 중이던 모든 작업의 통로가 한꺼번에 사라진다. 세 약점이 모이는 종착지는 맨 아래 — 확장과 로드밸런싱이 어렵다. 클라우드에서 서버를 여러 대로 굴려야 하는 실제 운영에서 이건 치명적이다. 놓치기 쉬운 점: 이 약점들은 "한 명이 쓸 때"는 안 보이고, 여러 클라이언트를 여러 서버로 감당할 때 비로소 터진다.
해결의 핵심 발상: 엔드포인트를 하나로, 응답 형태는 서버가 고른다
Streamable HTTP의 아이디어는 단순하다. "통로를 둘로 가르지 말고 하나로 합치자. 대신 서버가 매 요청마다 응답 형태를 골라 짧으면 JSON 한 방, 길면 SSE 스트림으로 돌려주게 하자."
다이어그램 설명. 개편된 트랜스포트의 핵심이다. 이제 통로가 단일 엔드포인트(
/mcp) 하나뿐이고, 서버가 매 요청마다 응답 형태를 동적으로 고른다. 짧고 즉시 끝나는 요청에는 그냥 JSON을 한 번에 돌려주고(굳이 스트림을 열 필요 없음), 오래 걸리거나 진행 알림이 필요한 요청에는 응답을 SSE 스트림으로 승격시킨다. 같은 엔드포인트, 같은 POST인데 서버가Content-Type으로 "이번엔 한 방 / 이번엔 스트림"을 결정한다. 왜 이게 좋은가? 옛 방식은 "받는 통로를 미리 깔아 두고" 시작했지만, 새 방식은 필요할 때만 스트림을 연다 — 대다수의 짧은 요청은 평범한 HTTP 요청-응답으로 끝나니 서버가 연결을 붙잡고 있을 일이 확 줄어든다.핵심 개선 1: 요청마다 자기만의 응답 스트림을 가진다
옛 방식의 "단일 채널 혼선"을 정면으로 푸는 변화가 여기 있다. Streamable HTTP에서는 각 POST가 독립된 HTTP 트랜잭션이고, 그 응답 스트림은 그 요청에만 묶인다. 그래서 한 스트림이 흐르는 도중에 새 요청을 보내도 아무 충돌이 없다.
다이어그램 설명. ①의 SSE 스트림이 흐르는 도중에 ②를 POST해도 문제가 없는 이유를 보여준다. 두 박스는 동시에 진행되지만 서로 완전히 분리된 트랜잭션이다 — 같은
/mcp엔드포인트를 쓸 뿐, ①과 ②는 각자 독립된 HTTP 요청이고 각자 자기만의 응답 통로를 가진다. ①의data: 진행 50%는 ①번 요청의 응답 본문일 뿐이라, ②는 그것과 무관한 새 HTTP 트랜잭션으로 따로 나간다. 옛 방식과 비교하면 차이가 분명하다 — 옛 방식은 두 응답이 하나의 SSE 채널로 합류해 "이게 누구 응답이냐"를 id로 맞춰야 했지만, 새 방식은 애초에 통로가 갈라져 있어 ①의 결과가 ①의 응답으로, ②의 결과가 ②의 응답으로 각자의 트랜잭션 안에 깔끔히 묶인다. 짝짓기 부담이 사라지는 것이다.핵심 개선 2: HTTP/2 멀티플렉싱으로 동시 스트림의 한계를 푼다
"요청마다 스트림을 연다"는 발상에는 현실적 함정이 하나 있다. 동시에 열어 둘 수 있는 연결 수에 한계가 있다는 것이다. 이 한계가 어디서 오고 어떻게 풀리는지를 두 경우로 나눠 보자.
▲ HTTP/1.1 — 동시 스트림이 커넥션 수에 묶인다
▲ HTTP/2 — 멀티플렉싱으로 한계를 푼다
두 다이어그램 설명. 위 두 블록은 같은 문제 — "스트림을 여러 개 동시에 열 수 있나"를 HTTP 버전별로 대비한다. HTTP/1.1에서는 커넥션 하나가 스트림 하나라, 브라우저가 호스트당 약 6개로 막아 둔 커넥션 한도에 금세 걸린다. 긴 SSE 스트림을 5~6개 열면 그 뒤 요청은 앞 스트림이 끝날 때까지 대기한다. HTTP/2는 멀티플렉싱(multiplexing) — 한 TCP 커넥션 안에 여러 논리적 스트림을 다중화하는 기능 — 으로 이 벽을 없앤다. 커넥션은 하나인데 그 안에서 수십~수백 개의 요청·스트림이 동시에 흐른다. 그래서 Streamable HTTP는 HTTP/2 위에서 특히 빛난다 — "요청마다 스트림"이라는 설계가 커넥션 고갈 없이 그대로 성립한다. 놓치기 쉬운 점: HTTP/1.1에서도 동작은 하지만, 동시 스트림이 많은 워크로드라면 HTTP/2가 사실상 전제다.
핵심 개선 3: 세션 ID와 재개 — stateless에 가까워지다
옛 방식의 "stateful·재연결 손실"을 푸는 장치가 두 개 더 있다. 세션 ID와 재개(resumability)다.
다이어그램 설명. Streamable HTTP가 어떻게 여러 서버로 부하를 나누는지 보여준다. 서버는 첫 응답에서
Mcp-Session-Id라는 세션 식별자를 발급하고, 클라이언트는 이후 모든 요청에 이 ID를 붙인다. 핵심은 그 다음이다 — 두 번째 요청이 로드밸런서를 거쳐 다른 서버 인스턴스(B)로 가도, B가 세션 ID로 상태를 공유 저장소(예: Redis)에서 복원하면 대화가 이어진다. 옛 방식은 SSE 연결이 특정 서버에 물리적으로 붙어 있어 이게 불가능했지만, 새 방식은 연결이 아니라 ID로 세션을 식별해 서버를 갈아타도 된다. 여기에 SSE의id+Last-Event-ID재연결까지 더해져, 스트림이 끊겨도 끊긴 지점부터 이어받는다(재개). 그래서 무거운 stateful 구조에서 수평 확장이 쉬운 구조로 옮겨 간다. 함정 하나: 완전한 stateless는 아니다 — 상태를 공유 저장소에 두는 설계가 받쳐 줘야 이 그림이 성립한다.효과: MCP가 Streamable HTTP로 얻은 것
세 개선을 합치면, MCP는 "SSE의 실시간 스트리밍은 유지하면서, 옛 방식의 무거움은 덜어 낸" 트랜스포트를 얻었다.
다이어그램 설명. Streamable HTTP가 MCP에 가져온 효과를 모았다. 네 갈래 — 가벼운 단일 엔드포인트, 요청별 독립 스트림, 세션 기반 수평 확장, HTTP/2 친화 — 가 모여 클라우드에서 여러 서버로 굴리기 좋은 트랜스포트를 만든다. 핵심 메시지는 "버린 게 아니라 정리했다"는 것이다. SSE가 주던 실시간 서버 푸시는 그대로 살리되(긴 작업은 여전히 SSE 스트림으로 응답), 옛 방식이 강요하던 "항상 SSE를 붙잡고 있어야 하는 무거움"만 걷어 냈다. 짧은 요청은 평범한 HTTP로, 긴 요청은 SSE로 — 상황에 맞춰 응답 형태를 고르는 유연함이 이 전환의 본질이다. 놓치기 쉬운 점: Streamable HTTP는 SSE를 대체한 게 아니라 SSE를 더 똑똑하게 쓰는 방식이다. SSE는 여전히 그 안에서 핵심 부품으로 살아 있다.
정리
MCP가 두 엔드포인트 방식에서 Streamable HTTP로 갈아탄 이유는 한 문장으로 줄면 이렇다 — "한 명이 쓸 때는 안 보이던 stateful·혼선·확장의 약점이, 여러 클라이언트를 여러 서버로 감당해야 하는 실제 운영에서 터졌기 때문"이다.
해법은 통로를 하나로 합치고, 서버가 요청마다 응답 형태(JSON이냐 SSE냐)를 고르게 하고, 세션을 연결이 아닌 ID로 식별하게 한 것이다. 그 결과 SSE의 실시간성은 지키면서 클라우드 확장성을 얻었다. SSE가 "서버가 먼저 말하게 하는 법"이었다면, Streamable HTTP는 "그 말하기를 여러 서버에 흩어 놓고도 흐트러지지 않게 다스리는 법"이다.
이 글은 생성형 AI의 도움을 받아 작성되었습니다. 원본 자료를 기반으로 AI가 초안을 생성하고, 작성자가 검토·편집하였습니다.
'IT' 카테고리의 다른 글
MCP는 왜 SSE를 골랐나 — HTTP 위에서 서버가 먼저 말하게 하는 법 (0) 2026.07.01 서브 에이전트를 @tool로 감싸는 멀티 에이전트 패턴 (0) 2026.06.30 @tool이 내부에서 하는 일 — Pydantic BaseModel이 LLM의 호출 인터페이스가 되는 과정 (0) 2026.06.30 Pydantic BaseModel이란 무엇인가 — 타입 힌트를 진짜 검증으로 바꾸는 도구 (0) 2026.06.29 LangGraph가 Annotated를 쓰는 이유 — 덮어쓰기 문제와 리듀서의 등장 (0) 2026.06.29 RAG 에이전트 완전 조립 — create_agent부터 동작 추적까지 (1) 2026.06.29 검색 결과를 에이전트 도구로 — build_context와 @tool 패턴 (0) 2026.06.28 RAG의 배경과 make_retriever — LLM이 모르는 문서를 검색하는 방법 (0) 2026.06.28 생성과 검증의 분리 — generator 노드와 validator 노드 설계 (0) 2026.06.28 LangGraph 자기 수정 패턴의 State 설계 — 루프를 위한 5가지 필드 (0) 2026.06.27