9.7 KiB
title, source_type, status, confidence, tags, related_projects, last_reviewed, canonical_sources, audience, target_publish, status_label
| title | source_type | status | confidence | tags | related_projects | last_reviewed | canonical_sources | audience | target_publish | status_label | |||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Streaming Response를 지원하지 않는 결정도 계약이다 | blog | verified | high |
|
|
2026-07-03 |
|
backend-engineer | ready |
Streaming Response를 지원하지 않는 결정도 계약이다
Parent / 부모 (필수)
- 핵심 canonical: wiki/projects/ca-tmpl/streaming-response-support
- 관련 개념 문서: wiki/concepts/streaming-response-patterns - SSE/WebSocket/long-polling/chunked transfer의 일반 trade-off. 현재 이 글의 구현 사실 근거는 verified project canonical에 둔다.
타깃 독자 / Target reader
- 독자 profile: template skeleton에서 streaming/SSE/WebSocket response를 언제 열지 고민하는 백엔드 엔지니어.
- 이미 안다고 가정하는 것:
SseEmitter,ResponseBodyEmitter, WebSocket,StreamingResponseBody. - 처음 듣는다고 가정하는 것: “지원하지 않음”도 문서 문장이 아니라 build-time rule로 고정할 수 있다는 관점.
도입 / Hook
기술 선택은 보통 “무엇을 지원할 것인가”로 기록됩니다. 그런데 skeleton에서는 “지금은 열지 않을 것”도 중요한 결정입니다. SSE나 WebSocket을 한 번 열면 응답 envelope, timeout, heartbeat, reconnect, observability, connection cap, reverse proxy 설정까지 같이 따라옵니다. use case가 없는데 surface만 열면 템플릿은 빨리 무거워집니다.
ca-tmpl은 streaming response를 지원하지 않는다는 결정을 그냥 README에 쓰지 않았습니다. production code가 SseEmitter, ResponseBodyEmitter, Spring/Jakarta WebSocket surface를 import하면 ArchUnit rule이 잡도록 했습니다. 단, 대용량 다운로드용 StreamingResponseBody는 차단하지 않았습니다. 이 글은 “미지원도 계약이 될 수 있다”는 관점과, 어디까지가 실제 구현인지 정리합니다.
본문 outline / Body outline
- 미지원도 설계 결정이다.
- streaming이 깨뜨리는 기존 request-response baseline.
- 차단 대상과 제외 대상 -
SseEmitter/WebSocket은 막고StreamingResponseBody는 막지 않는다. - ArchUnit rule과 violations-as-data fixture로 검증한다.
- 나중에 streaming을 열려면 필요한 선행 계약.
본문 / Body
Streaming은 매력적인 기능입니다. LLM token streaming, 실시간 알림, export 진행률처럼 server가 client에게 계속 event를 보내야 하는 use case가 생기면 request-response만으로는 답답합니다. 하지만 skeleton의 default surface로 넣기에는 비용이 큽니다. event envelope을 어떻게 만들지, error를 mid-stream에서 어떻게 표현할지, trace id는 connection 단위인지 event 단위인지, proxy timeout과 heartbeat는 어떻게 둘지 정해야 합니다.
ca-tmpl은 현재 sample fixture에 server-push use case가 없기 때문에 streaming을 기본 지원하지 않기로 했습니다. 여기서 핵심은 “아직 안 만들었다”가 아니라 “지금은 열지 않는다는 결정을 build-time rule로 고정했다”입니다. production code가 SseEmitter를 import하거나, ResponseBodyEmitter를 쓰거나, Spring/Jakarta WebSocket package에 의존하면 ArchUnit rule이 실패합니다.
차단 대상은 이벤트/server-push streaming입니다. SseEmitter는 Server-Sent Events surface이고, ResponseBodyEmitter는 incremental object emit surface이며, WebSocket은 full-duplex connection model입니다. 이 셋은 request-response API baseline과 다른 운영 계약을 요구합니다. ca-tmpl은 이 표면을 기본 skeleton에 열지 않았습니다.
반대로 StreamingResponseBody는 차단하지 않습니다. 이름은 비슷하지만, ca-tmpl project canonical은 이를 대용량 파일 다운로드나 chunked body처럼 request-response 모델을 유지하는 관심사로 봅니다. 이벤트를 계속 push하는 계약과, 하나의 요청에 대해 body를 stream으로 쓰는 계약은 다릅니다. 그래서 over-block guard fixture가 있습니다. StreamingResponseBodyAllowedFixture는 streaming ban rule이 이 허용 케이스를 잡지 않아야 통과합니다.
이 구조가 좋은 이유는 “금지 rule이 진짜로 작동하는가”까지 테스트한다는 점입니다. ArchitectureViolationFixtureTest는 SseEmitterUsingFixture, ResponseBodyEmitterUsingFixture, Spring WebSocket fixture, Jakarta WebSocket fixture를 의도적 위반 데이터로 둡니다. 각 rule이 이 fixture를 잡는지 확인하고, WebSocket은 Spring glob과 Jakarta glob을 따로 가져와 vacuous pass를 줄입니다. 동시에 StreamingResponseBody는 잡지 않는지 확인합니다.
나중에 streaming을 열 수 없는 것은 아닙니다. 다만 그때는 단순히 controller return type을 바꾸는 일이 아닙니다. SSE인지 WebSocket인지, event envelope을 기존 { success, data, meta }와 어떻게 맞출지, per-event trace를 만들지, reconnect와 timeout, connection cap, reverse proxy 설정을 어떻게 둘지 결정해야 합니다. ca-tmpl 문서는 이 지원 계약을 planned/open 범위로 남겨 두고 있습니다.
따라서 이 글의 결론은 “streaming은 나쁘다”가 아닙니다. ca-tmpl의 결론은 더 좁습니다. 현재 skeleton의 sync request-response baseline에서는 server-push streaming을 기본 surface로 열지 않고, 그 미지원 상태가 우연히 깨지지 않도록 ArchUnit으로 막습니다. 운영 streaming endpoint, connection load, SSE/WebSocket 장애 대응은 이 글에서 말할 수 있는 범위가 아닙니다.
코드 예제 / Code samples (있다면)
// 출처: [[wiki/projects/ca-tmpl/streaming-response-support]]
// 실제 파일: app-bootstrap/.../CleanArchitectureTest.java, ca-tmpl @f6fbd4e196b4
static final ArchRule NO_SSE_EMITTER =
noClasses()
.that()
.resideInAPackage("dev.caskeleton..")
.should()
.dependOnClassesThat()
.haveFullyQualifiedName("org.springframework.web.servlet.mvc.method.annotation.SseEmitter");
// 출처: [[wiki/projects/ca-tmpl/streaming-response-support]]
// 실제 파일: app-bootstrap/.../CleanArchitectureTest.java, ca-tmpl @f6fbd4e196b4
static final ArchRule NO_WEBSOCKET_HANDLER =
noClasses()
.that()
.resideInAPackage("dev.caskeleton..")
.should()
.dependOnClassesThat()
.resideInAnyPackage("org.springframework.web.socket..", "jakarta.websocket..");
// 출처: [[wiki/projects/ca-tmpl/streaming-response-support]]
// 실제 파일: app-bootstrap/.../ArchitectureViolationFixtureTest.java, ca-tmpl @f6fbd4e196b4
void noWebsocketHandlerCatchesJakartaWebsocketFixture() {
EvaluationResult result =
CleanArchitectureTest.NO_WEBSOCKET_HANDLER.evaluate(JAKARTA_WEBSOCKET_FIXTURE_ONLY);
assertThat(result.hasViolation()).isTrue();
}
// 출처: [[wiki/projects/ca-tmpl/streaming-response-support]]
// 실제 파일: app-bootstrap/.../StreamingResponseBodyAllowedFixture.java, ca-tmpl @f6fbd4e196b4
public class StreamingResponseBodyAllowedFixture {
public StreamingResponseBody allowed() {
return outputStream -> outputStream.write("data".getBytes());
}
}
Sources / 근거 (canonical 인용 필수, derived layer 의무)
- wiki/projects/ca-tmpl/streaming-response-support - 이 글의 1차 canonical. streaming 미지원 결정, ArchUnit import-ban rule, violations-as-data fixture,
StreamingResponseBody제외 경계, local verification, 운영 미검증 범위를 따른다. - wiki/concepts/streaming-response-patterns - 관련 개념 문서. SSE/WebSocket/long-polling/chunked transfer의 일반 trade-off 배경으로만 둔다.
사실 vs 의견 / Fact vs opinion 구분
- 사실: ca-tmpl에는
NO_SSE_EMITTER,NO_RESPONSE_BODY_EMITTER,NO_WEBSOCKET_HANDLERArchUnit rule과 streaming violation fixtures,StreamingResponseBodyover-block guard fixture가 존재한다. 근거: wiki/projects/ca-tmpl/streaming-response-support - 사실: 구현된 것은 streaming 지원이 아니라 streaming 미지원을 강제하는 build-time guard다. 근거: wiki/projects/ca-tmpl/streaming-response-support
- 사실: 실제 SSE/WebSocket endpoint, connection load test, production streaming metric은 없다. 근거: wiki/projects/ca-tmpl/streaming-response-support
- 의견: skeleton 초기 surface에서는 real-time use case가 나타나기 전까지 server-push streaming을 닫아두는 편이 계약을 단순하게 유지한다.
- 알지 못하는 것: 실제 streaming workload 요구사항, proxy timeout tuning, per-event tracing 운영 효과.
답할 수 있는 범위 / Answer boundary
- 자신 있게 답할 수 있는 후속 질문:
- 왜 streaming을 지금 열지 않았는가?
- 어떤 Spring/Jakarta streaming surface를 ArchUnit으로 막았는가?
- 왜
StreamingResponseBody는 차단하지 않았는가? - violations-as-data fixture가 vacuous pass를 어떻게 줄이는가?
- 다음 글로 넘길 부분:
- SSE/WebSocket을 실제로 열 때 필요한 API envelope와 observability 계약.
- connection cap, heartbeat, reconnect, proxy timeout 설계.
- production streaming endpoint 검증.
게시 체크리스트 / Publish checklist
- 모든 사실 주장에 canonical 링크 있음
- 사실 vs 의견 분리 명시됨
- 금지 마케팅 표현 없음
- 코드 예제 출처 명시
- 타깃 독자 가정과 톤 일치
/lint통과- 게시 URL 기록 (게시 후):