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 |
|
|
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 격리 corpus —
ArchitectureViolationFixtureTest가SPRING_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 참조 —testCompileOnlyjar 가 runtime classpath 에 없어extends시NoClassDefFoundError가 나던 문제를 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_emitter가SseEmitter·ResponseBodyEmitterimport fixture 를 실제로 위반으로 잡음.no_websocket_handler의org.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_usageimport-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 차단).
testCompileOnlyfixture 에서NoClassDefFoundError를 어떻게 피했는가 —extends TextWebSocketHandler대신@EnableWebSocketannotation-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:
testCompileOnlyWebSocket/Jakarta fixture가 JUnit discovery에서 class loading failure를 내는 문제를 annotation-only 참조로 피한 글감. annotation-only가 모든 fixture 참조를 안전하게 만든다고 쓰지 않는다.
관련 개념
- wiki/concepts/streaming-response-patterns — SSE vs WebSocket vs long-polling vs chunked transfer 의 일반 trade-off, sync-baseline rationale, 언제 스트리밍이 가치 있고 언제 아닌가.
Sources
- raw/branch-notes/feature-streaming-response-contract — D1(미지원 확정) / D3(ArchUnit 강제) / D2(지원 계약 보류) + Decision Evidence Map + 구현 가이드(2026-06-02). 본 문서의 결정 SSOT.
- raw/blog-topics/streaming-response-not-supported-archunit-ban-2026-07-02 — streaming 미지원 + ArchUnit ban 블로그 글감 raw seed. canonical 반영 범위: verified import-ban rule + skeleton scope decision + 과장 금지 항목.
- raw/blog-topics/archunit-testcompileonly-fixture-annotation-pattern-2026-06-02 — ArchUnit
testCompileOnlyfixture annotation-only 패턴 블로그 글감 raw seed. - raw/project-notes/ca-skeleton-operational-contract — §3 Structured API Response Contract, §13 API Contract Surface, §34 Stack Commitment (archunit-junit5 1.3.0).
- raw/branch-notes/feature-boundary-validation-mapping-contract — D5
no_problem_detail_usage(import-ban 메커니즘 선례 + ArchUnit suite host). - raw/branch-notes/feature-file-resource-handling-contract — D8 (
StreamingResponseBody소유, 차단 제외 경계). - raw/errors/archunit-testcompileonly-class-loading-2026-06-02 —
testCompileOnlyfixtureNoClassDefFoundError+ annotation-only 해결 패턴. - ca-tmpl @9693d72 코드 (ground-truth):
src/app-bootstrap/src/test/java/dev/caskeleton/bootstrap/architecture/CleanArchitectureTest.java(line 673~718, D3 rule 3개),.../architecture/violations/streaming/{SseEmitterUsingFixture,ResponseBodyEmitterUsingFixture,SpringWebSocketHandlerFixture,JakartaWebSocketEndpointFixture}.java,.../architecture/allowed/streaming/StreamingResponseBodyAllowedFixture.java,.../architecture/ArchitectureViolationFixtureTest.java.
Claim ID / Decision Evidence Map / UNSUPPORTED_DECISION: handled per branch-note Decision Evidence Map.