87 lines
10 KiB
Markdown
87 lines
10 KiB
Markdown
---
|
|
title: Streaming Response Patterns (SSE vs WebSocket vs Long-Polling vs Chunked)
|
|
source_type: llm-generated
|
|
status: draft
|
|
confidence: medium
|
|
tags: [streaming, sse, websocket, http, backend]
|
|
related_projects: [ca-skeleton]
|
|
last_reviewed: 2026-06-04
|
|
---
|
|
|
|
# Streaming Response Patterns (SSE vs WebSocket vs Long-Polling vs Chunked)
|
|
|
|
> Layer: `wiki/concepts/` — 일반 개념. ca-skeleton 이 이 개념을 *미지원으로 결정하고 ArchUnit 으로 차단한* 사실은 [[wiki/projects/ca-tmpl/streaming-response-support]] 참조.
|
|
|
|
## Summary
|
|
|
|
HTTP 의 기본 통신 모델은 *request-response*(클라이언트가 묻고 서버가 한 번 답함)다. 이를 넘어 서버가 클라이언트로 데이터를 *지속적으로/능동적으로* 보내려면 별도 메커니즘이 필요하다 — 대표적으로 **SSE**(서버→클라이언트 단방향 push), **WebSocket**(양방향 full-duplex), **long-polling**(요청을 응답 없이 오래 붙잡아 둠), **chunked transfer encoding**(크기 미상 응답을 조각으로 흘려보냄)이 있다. 핵심 구분 축은 *통신 방향(단/양방향)* 과 *통신 모델이 request-response 를 유지하는가, server-push 로 바뀌는가* 다.
|
|
|
|
## Standard (공식 정의)
|
|
|
|
- **SSE (Server-Sent Events)**: MIME type `text/event-stream`, UTF-8 인코딩 필수. `data:` / `event:` / `id:` / `retry:` 필드를 가진 line-based text protocol. 클라이언트 측 API 는 `EventSource`(브라우저 `Window`/`Worker` context 전용 — 서버는 직접 `text/event-stream` 응답을 구현해야 함). 재연결 시 `Last-Event-ID` 헤더로 마지막 수신 event 를 서버에 전달. (WHATWG HTML §9.2)
|
|
- **WebSocket**: 단일 TCP 연결 위의 *full-duplex*(양방향) 통신 — 각 side 가 독립적으로 언제든 송신 가능. HTTP Upgrade handshake(`GET` + `Upgrade: websocket` → `101 Switching Protocols`)로 연결을 수립하고, handshake 이후 TCP 는 HTTP 가 아닌 WebSocket 프레임 전송에 쓰인다. HTTP 와의 *유일한* 관계는 handshake 가 HTTP Upgrade 로 해석되는 것뿐인 독립 프로토콜. (IETF RFC 6455 §1.2, §1.7)
|
|
- **Chunked transfer encoding**: *크기를 알 수 없는* content stream 을 length-delimited buffer 의 연속으로 전송 — 전체 크기 없이 connection 을 유지하며 메시지 완료를 수신자가 알 수 있게 함(`Transfer-Encoding: chunked`, last-chunk = size 0). HTTP/1.1 한정 (HTTP/2 는 DATA frame 으로 별도 framing, `Transfer-Encoding` 자체 금지). (IETF RFC 9112 §7.1)
|
|
- **Long-polling**: 클라이언트가 요청을 보내고 서버가 *이벤트가 생길 때까지* 응답을 지연시키는 패턴 — RFC 6455 는 WebSocket 의 탄생 배경으로 "HTTP polling/long-polling 은 HTTP 의 남용(abuse)이며 서버가 클라이언트마다 여러 TCP 연결을 유지해야 했다"고 기술한다. (RFC 6455 §1.1)
|
|
- **Spring MVC(servlet) 매핑**: `request.startAsync()` 로 Servlet/filter 는 exit 하고 response 만 열어 둠. 응답 타입별로 — `StreamingResponseBody`(message conversion 우회, `OutputStream` 직접 write, *파일 다운로드* 용), `ResponseBodyEmitter`(객체 stream emit, 각 객체를 `HttpMessageConverter` 로 직렬화), `SseEmitter`(`ResponseBodyEmitter` 의 subclass, W3C SSE 포맷). (Spring MVC vendor doc)
|
|
|
|
## 한계 / 주의점
|
|
|
|
- **"streaming" 이라는 단어가 두 개의 다른 것을 가리킨다**: ① *통신 모델 자체* 가 server-push 로 바뀌는 것(SSE/WebSocket) ② request-response 모델을 유지한 채 *응답 body 만 조각 전송* 하는 것(`StreamingResponseBody` / chunked 다운로드). 둘은 운영 부담·계약이 전혀 다르므로 묶어서 다루면 안 된다.
|
|
- **SSE 는 단방향**: 서버→클라이언트만. 클라이언트→서버 메시지는 별도 일반 HTTP 요청으로. 양방향이 필요하면 WebSocket.
|
|
- **WebSocket 은 기존 HTTP 인프라와 자동 호환되지 않는다**: HTTP 와 독립 프로토콜이라 reverse proxy(Nginx 등)에 Upgrade 처리 설정이 별도로 필요. envelope/필터/미들웨어 같은 기존 request-response 자산도 그대로 못 씀.
|
|
- **server-push 는 운영 비용을 키운다**: connection 수 관리, 서버 재시작 시 동시 재연결(thundering herd), 멀티 서버 fan-out, timeout/heartbeat/reconnect, load balancer sticky session 등. 단발 request-response 에는 없던 부담.
|
|
- **chunked 는 HTTP/1.1 전용**: HTTP/2·HTTP/3 에서 `Transfer-Encoding: chunked` 는 금지(별도 framing). 브라우저의 trailer section 지원도 일반화 보장 안 됨.
|
|
- **YAGNI 경계**: 실제 server-push use case 가 없으면 스트리밍 도입은 speculative generality — request-response + 비동기 우회(LRO polling, webhook)로 대부분 충분.
|
|
|
|
## Project Application
|
|
|
|
- [[wiki/projects/ca-tmpl/streaming-response-support]] — ca-skeleton 이 이벤트/server-push 스트리밍을 *미지원으로 결정* 하고 ArchUnit import-ban 3개(`no_sse_emitter` / `no_response_body_emitter` / `no_websocket_handler`)로 강제. `StreamingResponseBody`(다운로드)는 차단 제외.
|
|
|
|
## Claim-backed Knowledge
|
|
|
|
> 이 개념 문서의 핵심 설명은 raw source claim 으로 뒷받침된다. 공식 standard / vendor doc / 회사 사례를 분리한다.
|
|
|
|
| Knowledge Point | Supporting Claims | Confidence | Notes |
|
|
|---|---|---|---|
|
|
| SSE 는 `text/event-stream`(UTF-8) line-based protocol, `data:/event:/id:/retry:` 필드 | `raw/official-docs/whatwg-html-server-sent-events.md#WHATWG-SSE-C1`, `#WHATWG-SSE-C2` | `high` | WHATWG HTML (official-standard) |
|
|
| SSE 재연결은 `Last-Event-ID` 헤더로 마지막 event 전달, `retry:` 로 대기시간 설정 | `#WHATWG-SSE-C3`, `#WHATWG-SSE-C4` | `high` | 서버 활용은 구현 책임 (MAY 수준) |
|
|
| `EventSource` 는 브라우저 클라이언트 API — 서버는 `text/event-stream` 을 직접 구현 | `#WHATWG-SSE-C5` | `high` | Spring 서버 측에 EventSource 직접 적용 불가 |
|
|
| WebSocket 은 단일 TCP 위 full-duplex, 양 side 독립 송신 | `raw/official-docs/rfc6455-websocket.md#RFC6455-C1` | `high` | RFC 6455 (official-standard) |
|
|
| WebSocket 은 HTTP Upgrade handshake(101) 이후 HTTP 와 독립 프로토콜 | `#RFC6455-C3`, `#RFC6455-C5` | `high` | reverse proxy 자동 호환 아님 — 별도 설정 필요 |
|
|
| WebSocket 탄생 배경 = HTTP polling/long-polling 의 "HTTP 남용" + 클라이언트당 다중 TCP | `#RFC6455-C2` | `high` | "항상 polling 보다 우수" 는 아님 — 희소 업데이트엔 SSE/polling 적합 |
|
|
| chunked = 크기 미상 stream 을 length-delimited buffer 로, HTTP/1.1 한정 | `raw/official-docs/rfc9112-http-1-1-chunked-transfer.md#RFC9112-CHUNK-C1` | `high` | HTTP/2 에선 `Transfer-Encoding` 금지 |
|
|
| `SseEmitter` = `ResponseBodyEmitter` subclass, W3C SSE 포맷 / `StreamingResponseBody` = 파일 다운로드용 | `raw/official-docs/spring-mvc-async-streaming.md#SPRING-ASYNC-C4`, `#SPRING-ASYNC-C2`, `#SPRING-ASYNC-C3` | `high` | Spring vendor doc — server-push(SSE) vs 다운로드(StreamingResponseBody) 구분 |
|
|
| SSE 멀티서버 운영 시 thundering herd(재시작 시 동시 재연결 CPU spike), 해결로 random jitter | `raw/company-tech-blogs/sse-realtime-notification-woowahan.md#WOOWA-SSE-C2`, `#WOOWA-SSE-C3` | `medium` | 우아한형제들 사례 (company-case-study) — 공식 best practice 아님, 규모별 심각도 다름 |
|
|
|
|
## 내가 설명할 수 있어야 하는 것
|
|
|
|
- SSE / WebSocket / long-polling / chunked 각각의 공식 정의와 통신 방향(단/양방향).
|
|
- "streaming" 이 *통신 모델 변경(server-push)* 과 *응답 body 청크 전송(다운로드)* 두 개를 가리킨다는 점, 그리고 왜 둘을 구분해야 하는지.
|
|
- WebSocket 이 왜 기존 HTTP 인프라(envelope, proxy)와 자동 호환되지 않는가.
|
|
- 언제 스트리밍이 가치 있고(실시간 push, LLM token streaming), 언제 request-response + 비동기 우회(LRO polling, webhook)로 충분한가.
|
|
- 우아한형제들 SSE/WebSocket 운영 부담 사례를 *일반 법칙처럼* 말하면 안 되는 지점.
|
|
|
|
## Interview Questions
|
|
|
|
- SSE 와 WebSocket 의 차이는? 어떤 상황에 각각을 고르나?
|
|
- 서버가 클라이언트에 능동적으로 데이터를 보내야 할 때, 스트리밍 없이 해결하는 방법은? (LRO polling, webhook)
|
|
- `StreamingResponseBody` 와 `SseEmitter` 는 둘 다 "스트리밍" 인데 무엇이 다른가?
|
|
- WebSocket 을 도입하면 reverse proxy/load balancer 설정이 왜 달라지나?
|
|
- 스트리밍을 *도입하지 않기로* 결정한다면, 그 결정을 코드 레벨에서 어떻게 강제할 수 있나?
|
|
|
|
## Do Not Overclaim
|
|
|
|
- **회사 기술 블로그(우아한형제들) 사례 = 공식 best practice 아님.** thundering herd / jitter / fan-out 은 *그 회사 규모·스택*(WebFlux + Coroutine + Kafka 등) 특화이며 일반 법칙으로 단정 금지.
|
|
- **"WebSocket 이 polling 보다 항상 우월" → 금지.** RFC 6455 자체가 희소 업데이트엔 다른 선택이 적합할 수 있다고 시사.
|
|
- **"SseEmitter 가 Last-Event-ID replay 를 자동 지원" → 금지.** 서버 측 event store 를 별도 구현해야 함 (vendor doc 주의).
|
|
- **개념 문서는 구현 등급을 매기지 않는다.** 실제 구현/검증 여부는 [[wiki/projects/ca-tmpl/streaming-response-support]] 에서 판정.
|
|
|
|
## Sources
|
|
|
|
- [[raw/official-docs/whatwg-html-server-sent-events]] — WHATWG HTML SSE spec (`text/event-stream`, EventSource, Last-Event-ID, retry). official-standard.
|
|
- [[raw/official-docs/rfc6455-websocket]] — IETF RFC 6455 WebSocket (full-duplex, HTTP Upgrade handshake, masking). official-standard.
|
|
- [[raw/official-docs/rfc9112-http-1-1-chunked-transfer]] — HTTP/1.1 chunked transfer encoding (§7.1 framing). official-standard.
|
|
- [[raw/official-docs/spring-mvc-async-streaming]] — Spring MVC `SseEmitter` / `ResponseBodyEmitter` / `StreamingResponseBody`. official-vendor-doc.
|
|
- [[raw/company-tech-blogs/sse-realtime-notification-woowahan]] — 우아한형제들 SSE 운영 사례 (thundering herd, jitter, Kafka fan-out). company-case-study — 공식 best practice 아님.
|
|
- [[raw/company-tech-blogs/realtime-service-experience-woowahan-websocket]] — 우아한형제들 WebSocket 운영 사례 (이벤트 유실, 모바일 네트워크, 클러스터링). company-case-study.
|