Files
llm-wiki/vault/30-knowledge/projects/ca-tmpl/streaming-response-support.md
T

17 KiB

title, source_type, status, confidence, tags, related_projects, last_reviewed
title source_type status confidence tags related_projects last_reviewed
ca-tmpl - 이벤트 스트리밍 미지원 결정 + ArchUnit 정적 강제 project verified high
ca-skeleton
streaming
archunit
actually-implemented
ca-skeleton
ca-tmpl
2026-07-02

ca-tmpl - 이벤트 스트리밍 미지원 결정 + ArchUnit 정적 강제

Layer: wiki/projects/ — 내 프로젝트 사실. SSE / WebSocket / long-polling / chunked 의 일반 trade-off 는 wiki/concepts/streaming-response-patterns 참조.

핵심 framing: 본 문서가 actually-implemented 로 주장하는 것은 "스트리밍 지원" 이 아니라 "스트리밍 미지원을 빌드타임에 강제하는 ArchUnit 가드레일" 이다. ca-skeleton 은 이벤트/server-push 스트리밍을 지원하지 않으며, 그 미지원을 코드(ArchUnit rule)로 못박았다. 스트리밍 지원 계약 자체는 planned(보류).

프로젝트 컨텍스트

  • 프로젝트: ca-tmpl — Clean Architecture 기반 백엔드 skeleton 템플릿.
  • 결정: ca-skeleton 은 이벤트/server-push 스트리밍(SSE · WebSocket)을 default 미지원으로 확정 (D1) 하고, 그 미지원을 ArchUnit import-ban rule 3개로 정적 강제 (D3) 한다. controller/adapter 가 streaming API 를 import 하면 build 가 실패한다.
  • 왜 미지원을 결정 으로 다루는가: streaming-response 는 독립 결정이 아니라 통신/전송 프로토콜 계약(HTTP vs gRPC vs streaming)의 한 facet 이다. 전송 프로토콜은 모든 파생 프로젝트의 기본기로 박을 근거가 가장 약한, skeleton 에서 가장 마지막에 고정 해야 할 영역. 현재 sample-portfolio fixture 에 server-push use case 가 없으므로 (YAGNI / speculative generality 회피) "미지원 default + ArchUnit 차단" 을 택했다. 단순한 누락이 아니라 의도적 미지원 + 정적 강제 라는 점이 차이다.
  • 용어 주의 (핵심): 여기서 "streaming response" = 이벤트/server-push 스트리밍 (통신 모델이 request-response → server-push 로 바뀌는 것). 대용량 파일 다운로드용 StreamingResponseBody(응답 body 청크 전송, 통신 모델은 여전히 request-response)는 별개 관심사이며 차단 대상이 아니다raw/branch-notes/feature-file-resource-handling-contract D8 소유.
  • 결정 SSOT: raw/branch-notes/feature-streaming-response-contract (D1/D3 in-scope + D2 보류 + Decision Evidence Map). 본 문서는 그 중 실제 코드로 구현된 사실만 추출한다.
  • 진행 단계: 코드 구현 + 로컬 검증 완료 (ArchUnit rule 3개 + violations-as-data fixtures + over-block guard). 운영 배포 / 측정값 없음.

Ground-truth 대조 (2026-06-04, ca-tmpl @9693d72 "이벤트 스트리밍 미지원 ArchUnit 검증")

/home/donghyeon/workspace/ca-tmpl 코드를 직접 읽어 검증한 사실 (D3 구현 커밋 9693d72; 현재 checkout HEAD = db61075, 본 streaming 코드는 HEAD 에 그대로 잔존):

  • 패키지 root 는 dev.caskeleton.*.
  • 3개 D3 rule 실재src/app-bootstrap/src/test/java/dev/caskeleton/bootstrap/architecture/CleanArchitectureTest.java// ---- feature-streaming-response-contract D3 ---- 블록 (line 673~718): no_sse_emitter, no_response_body_emitter, no_websocket_handler. 셋 다 noClasses().that().resideInAPackage("dev.caskeleton..").should().dependOnClassesThat()... + .allowEmptyShould(true) 형태.
  • scan 범위 = production only — class 레벨 @AnalyzeClasses(packages = "dev.caskeleton", importOptions = ImportOption.DoNotIncludeTests.class). 테스트 fixture 는 scope 밖.
  • production 코드에 streaming import 0건grep -rln "SseEmitter|ResponseBodyEmitter|web.socket|jakarta.websocket" src/ | grep -v /test/ → 결과 없음. 즉 미지원(ban)이 실제이며 예외 production 사용처 없음.
  • violations-as-data fixtures 실재 (..architecture/violations/streaming/): SseEmitterUsingFixture, ResponseBodyEmitterUsingFixture, SpringWebSocketHandlerFixture(@EnableWebSocket), JakartaWebSocketEndpointFixture(@ServerEndpoint).
  • over-block guard fixture 실재 (..architecture/allowed/streaming/): StreamingResponseBodyAllowedFixture — 3개 rule 모두 이것을 잡지 않아야 정상(파일 다운로드 회귀 방지).
  • WebSocket fixture 격리 corpusArchitectureViolationFixtureTestSPRING_WEBSOCKET_FIXTURE_ONLY / JAKARTA_WEBSOCKET_FIXTURE_ONLY 로 spring·jakarta glob 을 각각 독립 import 해 평가 (공유 풀에서 한 glob 만 동작해도 통과하던 vacuous-pass 갭 차단).

실제 구현 내용 (actually-implemented)

ca-tmpl 코드에서 직접 확인한 산출물. 차단(ban) 가드레일 + 근거 가 구현 실체다.

D3 ArchUnit rule 3개 (app-bootstrap/.../architecture/CleanArchitectureTest.java):

rule 차단 대상 FQN / 패키지 메커니즘 근거(차단 대상 정의)
no_sse_emitter org.springframework.web.servlet.mvc.method.annotation.SseEmitter dependOnClassesThat().haveFullyQualifiedName(...) SPRING-ASYNC-C4 (SseEmitter = ResponseBodyEmitter subclass, W3C SSE 포맷)
no_response_body_emitter ...ResponseBodyEmitter 동일 (단일 FQN) SPRING-ASYNC-C3 (ResponseBodyEmitter = 객체 stream emit, SSE 의 base)
no_websocket_handler org.springframework.web.socket.. + jakarta.websocket.. (패키지 glob) dependOnClassesThat().resideInAnyPackage(...) RFC6455-C1 (full-duplex). spring-websocket handler/STOMP + Jakarta @ServerEndpoint 표면 일괄 차단
  • 셋 다 대상 = dev.caskeleton.. production code. .allowEmptyShould(true) (현재 production 에 streaming 클래스 미사용이므로 빈 결과 허용).
  • 명시적 비-차단 (의도적): StreamingResponseBody(대용량 다운로드, request-response 모델 유지) 는 차단 안 함raw/branch-notes/feature-file-resource-handling-contract D8 소유. blanket ban 시 파일 다운로드 build 가 깨지므로 의도적으로 제외. rule Javadoc 에 이 경계가 명시됨.
  • suite host = boundary branch 의 ArchUnit suite (raw/branch-notes/feature-boundary-validation-mapping-contract D5 no_problem_detail_usage 와 동일 import-ban 메커니즘 선례). archunit-junit5 1.3.0 (project §34 Stack Commitment).

테스트 fixtures (testCompileOnly 의존 + annotation-only 참조 패턴):

  • violations-as-data: SseEmitterUsingFixture, ResponseBodyEmitterUsingFixture, SpringWebSocketHandlerFixture, JakartaWebSocketEndpointFixture — 각 rule 이 위반을 실제로 잡아내는지 검증.
  • over-block guard: StreamingResponseBodyAllowedFixture — 3개 rule 이 이것을 잡지 않는지 (false positive 없음) 검증.
  • WebSocket fixture 는 @EnableWebSocket(spring) / @ServerEndpoint(jakarta) annotation-only 참조 — testCompileOnly jar 가 runtime classpath 에 없어 extendsNoClassDefFoundError 가 나던 문제를 annotation lazy-access 로 회피 (raw/errors/archunit-testcompileonly-class-loading-2026-06-02).

로컬/dev 검증 (locally-verified)

  • CleanArchitectureTest (D3 3개 rule 포함) + ArchitectureViolationFixtureTest (위 fixtures) GREEN — 2026-06-02 기준 ArchUnit 33 rules / FixtureTest 24 tests 모두 통과로 branch-note 에 기록.
  • 검증한 사실:
    • no_sse_emitter / no_response_body_emitterSseEmitter·ResponseBodyEmitter import fixture 를 실제로 위반으로 잡음.
    • no_websocket_handlerorg.springframework.web.socket.. + jakarta.websocket.. glob 이 spring·jakarta fixture 를 각각 격리 corpus 에서 잡음 (over-block 없음).
    • StreamingResponseBodyAllowedFixture 가 3개 rule 어디에도 안 걸림 (file-resource D8 다운로드 회귀 방지).
  • 검증 범위는 JVM 정적 분석(ArchUnit bytecode) + 단위 테스트까지. 실제 SSE/WebSocket 연결을 띄워 동작/부하를 본 것이 아니다 (애초에 미지원이므로 그런 통합 테스트 없음).

운영 검증 (prod-verified)

없음. 운영 환경에 배포된 적이 없다. connection 수 / event throughput / 인시던트 / 릴리즈 노트 어느 것도 없다 (스트리밍 자체가 미지원이므로 운영 streaming 지표도 존재하지 않는다).

문서/계획만 존재 (documented-only / planned)

다음은 설계/문서/보류 상태이며 면접에서 "구현했다 / 지원한다"고 말하면 안 된다.

  • 이벤트 스트리밍 지원 계약 전체 (D2): planned / not-adopted. 만약 지원하기로 하면 필요한 ① 매커니즘 선택(SSE vs WebSocket — 재개 시 SSE 우선) ② event envelope shape(envelope {success,data,meta} 적용 여부 vs SSE 고유 event:/data: 포맷) ③ per-event trace context 전파 ④ timeout/heartbeat/reconnect/connection cap ⑤ reverse proxy 설정 의무 — 전부 보류. 근거 raw 6개는 branch-note §Sources 에 보존.
  • 재개 트리거: (a) 실제 server→client push use case 등장 (실시간 알림 / LLM token streaming / 대용량 export 진행률) 또는 (b) 통신/전송 프로토콜 계약 branch 착수. 재개 시 D3 SSE 차단 rule 을 명시적으로 해제해야 함.
  • per-event trace span 정책 (OPEN): tracing branch (raw/branch-notes/feature-distributed-tracing-contract D5/D7)는 traceparent 를 request 단위 로만 전파 — "한 long-lived connection 의 N개 event 에 traceId 를 어떻게" 는 기존 Decision 으로 닫히지 않는 진짜 OPEN 갭. D2 재개 시 동시 결정 필요.
  • 미지원 시 비동기 우회 경로: server-push 가 필요하면 LRO polling(raw/branch-notes/feature-api-contract-baseline D17: 202 + Location + polling + Retry-After) 또는 webhook outbound(raw/branch-notes/feature-webhook-outbound-contract, 미결정). 본 branch 가 작성한 코드 아님 — 형제 branch 결정 재사용.

면접에서 말할 수 있는 범위

자신 있게 답할 수 있는 질문

  • ca-skeleton 이 이벤트 스트리밍을 왜 미지원으로 결정 했는가 — 전송 프로토콜은 가장 마지막에 고정할 facet + 현재 fixture 에 server-push use case 부재(YAGNI) + api-contract-baseline 의 "request-response only" 선언과의 일관성.
  • 그 미지원을 어떻게 강제 했는가 — 단순 누락이 아니라 ArchUnit import-ban rule 3개(no_sse_emitter / no_response_body_emitter / no_websocket_handler)로 production 코드가 streaming API 를 import 하면 build 실패. boundary branch 의 no_problem_detail_usage import-ban 선례를 차용.
  • StreamingResponseBody 를 왜 차단 했는가 — 그것은 server-push 가 아니라 대용량 다운로드(request-response 모델 유지)이고 file-resource D8 소유. blanket ban 했으면 다운로드 build 가 깨졌을 것. 무엇을 차단하고 무엇을 제외했는지의 경계 를 설명할 수 있음.
  • rule 동작을 어떻게 보증했는가 — violations-as-data fixtures 로 "위반을 실제로 잡는지" + over-block guard fixture 로 "허용 케이스를 안 잡는지" 양방향 검증. WebSocket spring/jakarta glob 은 격리 import corpus 로 각각 독립 검증(vacuous-pass 차단).
  • testCompileOnly fixture 에서 NoClassDefFoundError 를 어떻게 피했는가 — extends TextWebSocketHandler 대신 @EnableWebSocket annotation-only 참조 (annotation 은 JVM lazy access 라 class load 시 불필요, ArchUnit bytecode 분석은 정상).

적당히 답할 수 있는 질문

  • SSE vs WebSocket vs long-polling vs chunked 의 일반 trade-off (단방향 vs 양방향, HTTP 인프라 재사용, proxy 부담). (개념 수준 — wiki/concepts/streaming-response-patterns.)
  • 재개 시 왜 SSE 를 우선 후보로 두는가 — 단방향 push 에 적합 + 기존 HTTP 인프라 재사용 + WebSocket 대비 proxy 부담 낮음 (WHATWG-SSE-C / SPRING-ASYNC-C4 근거).
  • SSE/WebSocket 운영 부담의 일반적 성격 (thundering herd, fan-out, 이벤트 유실) — 우아한형제들 사례를 참고 로 인용하되 공식 best practice 로 말하지 않음.

답하면 안 되는 질문 (모른다고 해야 함)

  • "ca-skeleton 에서 SSE/WebSocket 을 구현/지원하는가?" → 미지원. 오히려 ArchUnit 으로 차단했다.
  • "스트리밍 응답을 운영에서 돌려봤는가 / connection 부하를 측정했는가?" → 미지원이므로 그런 운영 지표 없음.
  • "per-event trace span / reconnect / connection cap 정책을 설계했는가?" → D2 보류. 설계 안 함.

과장 금지 지점

  • "스트리밍을 지원/구현했다" → 절대 금지. 구현한 것은 미지원을 강제하는 차단 rule 이지 스트리밍 기능이 아니다.
  • "운영에서 검증했다 / prod 에서 돌고 있다" → 금지. 정적 분석(ArchUnit) + 단위 테스트까지가 검증 범위.
  • 우아한형제들 SSE/WebSocket 사례를 "공식 best practice" 로 인용 → 금지. company-case-study 이며 ca-skeleton 규모에 그대로 일반화 불가.
  • "미지원이 정답이다" → 단정 금지. real-time 요구가 있는 도메인이면 결정이 달라진다 — skeleton 의 minimalist default 일 뿐, 도입 가능성은 열어둠(D2).
  • StreamingResponseBody 도 차단했다고 말하기 → 금지. 명시적으로 제외 했다 (file-resource D8 경계).

Blog-topic ingest: streaming-response-not-supported-archunit-ban (2026-07-02)

raw/blog-topics/streaming-response-not-supported-archunit-ban-2026-07-02 는 SSE/WebSocket을 지금 지원하지 않는다는 결정을 문서 선언이 아니라 ArchUnit import-ban으로 고정한 이유를 블로그로 풀기 위한 raw seed다.

  • locally-verified 로 말할 수 있는 부분: SseEmitter, ResponseBodyEmitter, Spring/Jakarta WebSocket import-ban rule과 fixture 검증.
  • project-local policy 로 말할 부분: ca-tmpl skeleton의 sync baseline/minimal default에서는 streaming을 기본 surface로 열지 않는다.
  • 블로그 전 과장 방지: streaming 기술 자체가 나쁘다는 결론으로 쓰지 않고, StreamingResponseBody 제외 경계와 D2 보류 범위를 보존한다.
  • raw/blog-topics/archunit-testcompileonly-fixture-annotation-pattern-2026-06-02: testCompileOnly WebSocket/Jakarta fixture가 JUnit discovery에서 class loading failure를 내는 문제를 annotation-only 참조로 피한 글감. annotation-only가 모든 fixture 참조를 안전하게 만든다고 쓰지 않는다.

관련 개념

Sources

Claim ID / Decision Evidence Map / UNSUPPORTED_DECISION: handled per branch-note Decision Evidence Map.

Cluster / 묶음