SSE와 Streamable HTTP 2부 — SSE만으로는 부족한가? 양방향 통신과 RPC의 문제

SSE와 Streamable HTTP 2부 — 양방향 통신과 RPC의 문제

1부에서는 SSE(Server-Sent Events)가 어떻게 하나의 HTTP Response를 이용해 서버에서 클라이언트로 데이터를 계속 전달할 수 있는지 살펴봤습니다.

핵심은 단순했습니다.

HTTP Response는 반드시 한 번에 전달될 필요가 없다.

하나의 HTTP Response를 종료하지 않고 데이터를 조금씩 전달하면 서버는 클라이언트에게 지속적으로 이벤트를 보낼 수 있습니다.

Client                         Server
   │                              │
   │──── HTTP Request ───────────>│
   │                              │
   │<──── data: message 1 ────────│
   │<──── data: message 2 ────────│
   │<──── data: message 3 ────────│
   │<──── data: done ─────────────│
   │                              │

LLM의 답변 스트리밍이나 작업 진행 상태를 전달하는 용도라면 이 구조만으로도 상당히 많은 문제를 해결할 수 있습니다.

그런데 시스템이 조금 더 복잡해지면 질문이 하나 생깁니다.

서버만 계속 데이터를 보내면 충분할까?

이번 글에서는 이 질문에서 시작해보겠습니다.


1. SSE의 방향은 기본적으로 Server → Client다

SSE의 데이터 흐름은 기본적으로 단방향입니다.

Server ─────────────────────> Client

서버가 이벤트를 생성하고 클라이언트가 이를 받습니다.

예를 들어 LLM 서비스라면 이런 구조가 자연스럽습니다.

Client
   │
   │ POST /question
   ▼
Server
   │
   │ LLM 실행
   │
   ├──── token
   ├──── token
   ├──── token
   └──── done
         │
         ▼
       Client

질문은 일반적인 HTTP Request로 보내고, 생성되는 결과는 스트림으로 받습니다.

파일 분석 서비스도 비슷합니다.

Client
   │
   │ PDF Upload
   ▼
Server
   │
   ├── parsing...
   ├── OCR...
   ├── analyzing...
   ├── extracting...
   └── completed
         │
         ▼
       Client

이런 상황에서는 서버에서 클라이언트 방향의 데이터 흐름이 대부분이기 때문에 SSE가 매우 잘 맞습니다.

하지만 모든 시스템이 이렇게 단순하지는 않습니다.


2. 클라이언트도 계속 메시지를 보내야 한다면?

이번에는 채팅 시스템을 생각해봅시다.

사용자가 메시지를 보내고 서버가 응답합니다.

그런데 응답이 끝나기 전에 사용자가 작업을 취소할 수도 있습니다.

Client                         Server
   │                              │
   │──── question ───────────────>│
   │                              │
   │<──── token ──────────────────│
   │<──── token ──────────────────│
   │                              │
   │──── cancel ─────────────────>│
   │                              │
   │<──── cancelled ──────────────│

조금 더 복잡한 Agent 시스템에서는 이런 흐름도 생길 수 있습니다.

Client                         Agent
   │                              │
   │──── task ───────────────────>│
   │                              │
   │<──── processing ─────────────│
   │                              │
   │<──── approval_required ──────│
   │                              │
   │──── approve ────────────────>│
   │                              │
   │<──── processing ─────────────│
   │                              │
   │<──── completed ──────────────│

이제 통신 방향이 한쪽만이 아닙니다.

Client <────────────────────> Server

이쯤 되면 자연스럽게 떠오르는 기술이 있습니다.

WebSocket입니다.


3. 그러면 그냥 WebSocket을 쓰면 되지 않을까?

WebSocket은 클라이언트와 서버가 하나의 연결을 유지하면서 양방향으로 메시지를 주고받을 수 있도록 합니다.

Client                         Server
   │                              │
   │<═══════ WebSocket ═════════>│
   │                              │
   │──── message ────────────────>│
   │<──── message ────────────────│
   │──── message ────────────────>│
   │<──── message ────────────────│

SSE와 비교하면 차이가 분명합니다.

SSE

Server ─────────────────────> Client


WebSocket

Server <────────────────────> Client

실시간 채팅, 멀티플레이 게임, 공동 편집, 실시간 제어처럼 양쪽에서 지속적으로 메시지가 발생하는 시스템이라면 WebSocket은 매우 자연스러운 선택입니다.

그렇다면 여기서 이런 의문이 생깁니다.

MCP 같은 시스템도 Client와 Server가 계속 통신해야 하는데 그냥 WebSocket을 사용하면 되지 않을까?

기술적으로는 가능합니다.

하지만 여기에는 조금 다른 문제가 있습니다.


4. 모든 통신이 양방향 스트림일 필요는 없다

MCP Client가 MCP Server에게 Tool 목록을 요청한다고 생각해봅시다.

MCP Client                    MCP Server
    │                             │
    │──── tools/list ────────────>│
    │                             │
    │<──── tool list ─────────────│
    │                             │

이것은 전형적인 Request/Response입니다.

Tool을 호출하는 경우도 비슷합니다.

MCP Client                    MCP Server
    │                             │
    │──── tools/call ────────────>│
    │                             │
    │        Tool 실행            │
    │                             │
    │<──── result ────────────────│

이런 요청을 처리하기 위해 반드시 장시간 유지되는 완전한 양방향 메시징 채널이 필요한 것은 아닙니다.

HTTP가 이미 아주 잘하는 방식이 있습니다.

Request
   │
   ▼
Server
   │
   ▼
Response

그렇다면 기존 HTTP Request/Response 모델을 버릴 이유가 있을까요?

오히려 필요한 것은 이런 구조일 수 있습니다.

일반적인 요청

Client ─── Request ─────────> Server
Client <── Response ───────── Server


스트리밍이 필요한 요청

Client ─── Request ─────────> Server
Client <── event ──────────── Server
Client <── event ──────────── Server
Client <── event ──────────── Server
Client <── result ─────────── Server

즉,

평소에는 HTTP Request/Response를 사용하고, 필요할 때만 Response를 스트리밍할 수 있으면 어떨까?

여기서 중요한 관점의 변화가 시작됩니다.


5. MCP 통신은 단순한 채팅이 아니다

MCP Client와 Server 사이에서는 다양한 종류의 메시지가 오갈 수 있습니다.

예를 들어 다음과 같습니다.

initialize

tools/list

tools/call

resources/list

resources/read

그리고 모든 요청의 특성이 같지는 않습니다.

어떤 요청은 즉시 끝날 수 있습니다.

tools/list
    │
    ▼
200 OK

반면 어떤 Tool은 오래 걸릴 수 있습니다.

tools/call
    │
    ▼
PDF 분석
    │
    ▼
OCR
    │
    ▼
데이터 추출
    │
    ▼
검증
    │
    ▼
Result

또 경우에 따라 서버에서 클라이언트로 별도의 메시지를 전달해야 할 수도 있습니다.

즉 MCP 통신에는 다음과 같은 성격이 함께 존재합니다.

Request / Response

Notification

Streaming

이것이 단순한 SSE와 WebSocket의 선택 문제만으로 보기 어려운 이유입니다.


6. RPC 관점에서 생각해보자

여기서 RPC라는 개념을 잠깐 살펴볼 필요가 있습니다.

RPC는 Remote Procedure Call의 약자입니다.

이름 그대로 다른 프로세스나 다른 서버에 있는 기능을 마치 함수를 호출하듯 사용하는 개념입니다.

로컬 Python 함수라면 다음과 같이 호출할 수 있습니다.

result = analyze_invoice("invoice.pdf")

하지만 analyze_invoice()가 다른 서버에 있다면 직접 호출할 수 없습니다.

네트워크를 통해 요청해야 합니다.

Agent
  │
  │ analyze_invoice("invoice.pdf")
  ▼
Network
  │
  ▼
MCP Server
  │
  ▼
analyze_invoice()

논리적으로는 함수 호출처럼 보이지만 실제로는 네트워크 메시지가 오갑니다.

Client                           Server
   │                                │
   │──── method: analyze_invoice ──>│
   │                                │
   │            실행                │
   │                                │
   │<──── result ───────────────────│

MCP 역시 이런 RPC적인 성격을 가지고 있습니다.

Client가 Server에게 특정 기능의 실행을 요청하고 결과를 받습니다.


7. 요청과 응답을 어떻게 구분할까?

네트워크에서는 여러 요청이 동시에 발생할 수도 있습니다.

Client                         Server
   │                              │
   │──── Request A ──────────────>│
   │──── Request B ──────────────>│
   │──── Request C ──────────────>│
   │                              │

그런데 처리 시간이 모두 같다는 보장은 없습니다.

Request A ───────────────── Result A

Request B ─── Result B

Request C ───────── Result C

Request B가 A보다 늦게 들어왔지만 먼저 끝날 수도 있습니다.

그래서 RPC에서는 요청을 식별할 수 있는 ID가 중요합니다.

예를 들어 JSON-RPC에서는 개념적으로 다음과 같은 요청을 보낼 수 있습니다.

{
  "jsonrpc": "2.0",
  "id": 10,
  "method": "tools/call",
  "params": {
    "name": "analyze_invoice"
  }
}

서버의 응답에는 같은 ID가 포함됩니다.

{
  "jsonrpc": "2.0",
  "id": 10,
  "result": {
    "status": "completed"
  }
}

그러면 Client는 알 수 있습니다.

Request id=10
      │
      ▼
  MCP Server
      │
      ▼
Response id=10

즉,

이 Response는 내가 보냈던 id=10 요청의 결과구나.

이렇게 Request와 Response를 연결할 수 있습니다.


8. Notification은 조금 다르다

모든 메시지가 Response를 요구하는 것도 아닙니다.

상대방에게 어떤 사실을 알려주기만 하면 되는 메시지도 있습니다.

Client                         Server
   │                              │
   │<──── Notification ───────────│
   │                              │

Notification은 일반적인 RPC Request와 달리 응답을 요구하지 않습니다.

따라서 통신 구조는 점점 다양해집니다.

Request
   │
   └────────> Response


Notification
   │
   └────────> No Response


Long-running Request
   │
   ├────────> Progress
   ├────────> Progress
   ├────────> Progress
   └────────> Result

이제 단순히

Server → Client

또는

Server ↔ Client

만으로 설명하기에는 부족합니다.


9. 중요한 것은 방향만이 아니라 메시지의 성격이다

SSE와 WebSocket을 비교하다 보면 흔히 통신 방향에만 집중하게 됩니다.

SSE       : Server → Client

WebSocket : Server ↔ Client

물론 중요한 차이입니다.

하지만 MCP 같은 RPC 시스템에서는 또 다른 질문이 중요합니다.

이 메시지는 무엇을 의미하는가?

예를 들어 다음을 구분해야 합니다.

Request인가?

Response인가?

Notification인가?

중간 진행 상황인가?

최종 결과인가?

즉 통신 채널뿐만 아니라 메시지의 의미와 생명주기도 관리해야 합니다.


10. Agent 시스템에서는 더 복잡해진다

AI Agent를 생각하면 이 문제가 더욱 명확해집니다.

예를 들어 사용자가 인보이스 PDF를 업로드했다고 해봅시다.

User
 │
 │ invoice.pdf
 ▼
Agent

Agent는 MCP Tool을 호출합니다.

Agent
 │
 │ tools/call
 ▼
MCP Server
 │
 ▼
Invoice Tool

그런데 분석 과정에서는 여러 단계가 발생할 수 있습니다.

Invoice Tool
     │
     ▼
  Parsing
     │
     ▼
    OCR
     │
     ▼
Field Extraction
     │
     ▼
 Validation
     │
     ▼
   Result

어떤 작업은 100ms 만에 끝날 수도 있고, 어떤 작업은 20초가 걸릴 수도 있습니다.

심지어 중간에 사용자 확인이 필요할 수도 있습니다.

Agent
  │
  │ tools/call
  ▼
MCP Server
  │
  ├── OCR
  │
  ├── Extraction
  │
  └── Human Review Required
              │
              ▼
             User
              │
           Approve
              │
              ▼
           Continue

이런 시스템에서는 단순히 “연결 하나를 열어놓자”만으로 문제가 해결되지 않습니다.

요청과 응답의 관계, 상태, 이벤트, 스트리밍 등을 함께 고려해야 합니다.


11. 그렇다면 우리가 원하는 통신 방식은?

지금까지의 요구사항을 정리해보겠습니다.

우리는 기존 HTTP의 장점을 활용하고 싶습니다.

POST
GET
HTTP Status
Authentication
Proxy
Load Balancer

단순한 요청이라면 평범하게 처리하고 싶습니다.

Request
   │
   ▼
Response

하지만 필요한 경우에는 스트리밍도 하고 싶습니다.

Request
   │
   ▼
Response
   │
   ├── event
   ├── event
   ├── event
   └── result

그리고 서버에서 발생하는 Notification도 처리할 수 있어야 합니다.

결국 우리가 원하는 모습은 대략 이렇습니다.

             HTTP

Client ───── POST ───────────> Server
Client <──── JSON ──────────── Server


       필요할 때 Streaming

Client ───── Request ────────> Server
Client <──── Event ─────────── Server
Client <──── Event ─────────── Server
Client <──── Result ────────── Server

여기서 재미있는 질문이 하나 생깁니다.

HTTP Request/Response의 단순함은 유지하면서 필요할 때만 스트리밍할 수는 없을까?

바로 이 질문이 Streamable HTTP를 이해하는 출발점입니다.


12. Streaming HTTP가 아니라 Streamable HTTP

이름을 자세히 보면 재미있는 부분이 있습니다.

Streaming HTTP가 아니라 Streamable HTTP입니다.

이 차이가 꽤 중요합니다.

모든 통신을 항상 스트리밍한다는 의미가 아닙니다.

필요하다면 일반적인 HTTP Response를 사용할 수 있습니다.

Request
   │
   ▼
JSON Response

그리고 필요한 경우에는 스트리밍 Response를 사용할 수도 있습니다.

Request
   │
   ▼
Streaming Response
   │
   ├── event
   ├── event
   ├── event
   └── end

즉 핵심은 항상 Streaming이 아니라 Streamable, 다시 말해 필요하면 스트리밍할 수 있다는 것입니다.

그리고 이것이 다음 편에서 살펴볼 핵심 주제입니다.


마치며

1부에서는 SSE를 통해 이런 사실을 확인했습니다.

하나의 HTTP Response는 데이터를 조금씩 전달할 수 있다.

이번 2부에서는 여기서 한 단계 더 나아갔습니다.

SSE는 Server → Client 스트리밍에 매우 유용하지만, MCP 같은 RPC 시스템에서는 단순한 데이터 방향만으로는 충분하지 않습니다.

우리가 다뤄야 하는 것은 다음과 같은 것들입니다.

Request
Response
Notification
Streaming
Session

WebSocket처럼 완전한 양방향 채널을 만드는 것도 하나의 방법입니다.

하지만 모든 요청이 지속적인 양방향 통신을 필요로 하는 것은 아닙니다.

오히려 이런 질문을 해볼 수 있습니다.

HTTP의 단순한 Request/Response 구조는 그대로 사용하면서, 정말 필요한 순간에만 스트리밍할 수는 없을까?

이 질문에 대한 답이 Streamable HTTP입니다.

다음 3부에서는 드디어 Streamable HTTP의 구조를 본격적으로 살펴보겠습니다.

왜 MCP가 이 방식을 사용하게 되었는지, 일반 HTTP 요청과 SSE가 어떻게 하나의 Transport 안에서 공존하는지, 그리고 기존 SSE 기반 MCP Transport와 무엇이 달라졌는지를 구조적으로 살펴보겠습니다.


다음 편

SSE와 Streamable HTTP 3부 — MCP는 왜 Streamable HTTP를 사용하는가