fix: 하네스 제거 및 keycloak 문서 보강
This commit is contained in:
@@ -1 +0,0 @@
|
||||
../../vault/10-projects/ca-skeleton-operational-contract/branch-notes/feature-streaming-response-contract.md
|
||||
@@ -0,0 +1,312 @@
|
||||
---
|
||||
title: branch / feature-streaming-response-contract
|
||||
source_type: branch-note
|
||||
status: verified
|
||||
branch: feature-streaming-response-contract
|
||||
parent_branch:
|
||||
related_projects: [ca-skeleton]
|
||||
tags: [branch, ca-skeleton, streaming, sse, websocket, async]
|
||||
created: 2026-05-31
|
||||
last_reviewed: 2026-06-04
|
||||
target_merge:
|
||||
status_label: in-progress
|
||||
id: BR-CA-SKELETON-OPERATIONAL-CONTRACT-045
|
||||
kind: project-work-item
|
||||
project: ca-skeleton-operational-contract
|
||||
work_item: WI-CA-SKELETON-OPERATIONAL-CONTRACT-045
|
||||
inherits: [DEC-CA-SKELETON-OPERATIONAL-CONTRACT-API-VERSIONING-001@1, DEC-CA-SKELETON-OPERATIONAL-CONTRACT-RESILIENCE-001@1]
|
||||
refines: []
|
||||
overrides: []
|
||||
depends_on: []
|
||||
contract_packet: 1
|
||||
contract_packet_sha256: 3a771345ceaa06edbf916bcf7a52f6ade375f5415fe2ed9b6e0b914fab6c41a4
|
||||
---
|
||||
|
||||
# branch: feature-streaming-response-contract
|
||||
|
||||
> Layer: `raw/branch-notes/` — Server-Sent Events (SSE) / WebSocket / long-polling / chunked streaming 응답의 *지원 여부* + 도입 시 계약을 정의합니다. 완료 후 `/ingest` 로 `wiki/projects/` 에만 추출합니다.
|
||||
> `status_label`: `in-progress` | `review` | `merged` | `abandoned`
|
||||
|
||||
> 🟢🟡 **D3 구현 완료 — 2026-06-02.** 본 branch 는 두 갈래로 분리됩니다 (결정 §결정 사항):
|
||||
> - **🟢 IN-SCOPE (구현 완료)**: ca-skeleton 은 **이벤트/server-push 스트리밍(SSE·WebSocket)을 미지원으로 확정** (D1) 하고, 이를 **ArchUnit rule 로 정적 차단** (D3). `SseEmitter` / `ResponseBodyEmitter` / WebSocket 계열 import 차단. — `locally-verified` (2026-06-02).
|
||||
> - **🟡 DEFERRED (보류)**: 이벤트 스트리밍을 *지원하기로 했을 때* 의 매커니즘·envelope·per-event span 계약 (D2). 재개 트리거 = 실제 server→client push use case (실시간 알림 / LLM token streaming) 또는 통신/전송 프로토콜 계약 branch 착수. 6개 근거 raw 는 §Sources 에 보존.
|
||||
>
|
||||
> **용어 주의 (핵심)**: 본 branch 의 "streaming response" = **이벤트/server-push 스트리밍** (통신 모델이 request-response → server-push 로 바뀌는 것). `StreamingResponseBody` (대용량 파일 다운로드용 *응답 body 청크 전송* — 통신 모델은 여전히 request-response) 는 **별개 관심사이며 본 branch 범위 밖** — [[raw/branch-notes/feature-file-resource-handling-contract]] D8 이 소유. 차단 대상 아님 (R3 OUT_OF_BRANCH_SCOPE).
|
||||
|
||||
<!-- section-id: branch-parent -->
|
||||
## 부모 (필수)
|
||||
|
||||
> 이 branch 가 어느 작업 묶음에 속하는지. 본 branch 는 project-note 의 직접 자식 (`parent_branch:` 비어 있음).
|
||||
|
||||
- **Parent project (canonical SSOT)**: [[raw/project-notes/ca-skeleton-operational-contract]]
|
||||
|
||||
> ca-skeleton 은 별도 root branch 없이 project-note 가 SSOT 역할. 본 feature branch 는 project-note 의 §3 Structured API Response Contract (envelope 정책) + §13 API Contract Surface + §8 Distributed Tracing Contract 의 운영 계약 중 *streaming response* 영역을 정제한다.
|
||||
|
||||
### 형제 branch (cross-cite 후보)
|
||||
|
||||
- [[raw/branch-notes/feature-api-contract-baseline]] — 동기 request-response 표면 SSOT. streaming 은 그 *예외* 표면 — envelope 정책 우회 여부 결정 필요.
|
||||
- [[raw/branch-notes/feature-webhook-outbound-contract]] — async server-push 의 *대안* 매커니즘. 결정 시점에 webhook vs streaming trade-off 비교.
|
||||
- [[raw/branch-notes/feature-operational-error-observability-foundation]] — envelope `success/error` 정책. streaming 은 한 connection 안에 multiple event 가 흐르므로 envelope shape 적용 모호.
|
||||
- [[raw/branch-notes/feature-distributed-tracing-contract]] — streaming connection 의 trace context 전파 (HTTP request 단위 traceId 가 multiple event 에 어떻게 적용?).
|
||||
- [[raw/branch-notes/feature-rate-limit-idempotency-contract]] — streaming connection 의 rate limit 정책 (connection-per-user limit?).
|
||||
|
||||
<!-- GENERATED: branch-contract:start -->
|
||||
<!-- section-id: branch-contract-packet -->
|
||||
## 브랜치 계약 패킷
|
||||
|
||||
- **생성 시 프로젝트 개정**: `1`
|
||||
- **패킷 스키마**: `contract_packet: 1`
|
||||
- **완료 조건**: 지원 protocol과 timeout·failure contract test가 고정된다
|
||||
|
||||
<!-- section-id: inherited-project-decisions -->
|
||||
### 상속한 프로젝트 결정
|
||||
|
||||
| Decision Ref | Project Summary | Branch Application | Source |
|
||||
|---|---|---|---|
|
||||
| `DEC-CA-SKELETON-OPERATIONAL-CONTRACT-API-VERSIONING-001@1` | URI prefix /v1이 default이며 X-Api-Version은 compatibility 실험용 보조 header다 | Work Item 완료 조건에 적용 | [[raw/project-notes/ca-skeleton-operational-contract]] |
|
||||
| `DEC-CA-SKELETON-OPERATIONAL-CONTRACT-RESILIENCE-001@1` | Resilience4j가 default이며 Spring Retry는 simple blocking retry에만 허용한다 | Work Item 완료 조건에 적용 | [[raw/project-notes/ca-skeleton-operational-contract]] |
|
||||
|
||||
<!-- section-id: branch-local-decisions -->
|
||||
### 브랜치 지역 결정
|
||||
|
||||
> 기존 branch-local 결정은 아래 `## Decision Evidence Map / 결정-근거 매핑`의 D-row가 소유하며 이 packet에서 복제하지 않는다.
|
||||
|
||||
| Decision ID | Decision | Relation | Supporting Claims | Status |
|
||||
|---|---|---|---|---|
|
||||
|
||||
<!-- section-id: declared-overrides -->
|
||||
### 선언한 예외
|
||||
|
||||
| Override ID | Overrides | Reason | Approval | Status |
|
||||
|---|---|---|---|---|
|
||||
<!-- GENERATED: branch-contract:end -->
|
||||
|
||||
<!-- section-id: branch-goal -->
|
||||
## 목표
|
||||
|
||||
ca-skeleton 의 현재 default 는 *request-response 동기 응답* 만 지원합니다. SSE / WebSocket / long-polling / chunked streaming 은 envelope 정책 적용 모호 + 운영 부담 (connection 수, timeout, load balancer 설정 차이) 이 큰 영역.
|
||||
|
||||
본 branch 의 **1차 결정**: ca-skeleton 이 streaming 을 *지원* 할지 *명시적으로 미지원* 할지.
|
||||
|
||||
만약 *지원* 결정이면:
|
||||
1. 어떤 streaming 매커니즘 (SSE / WebSocket / long-poll / chunked-transfer-encoding)
|
||||
2. event envelope shape (envelope.success/error 적용 여부, event header)
|
||||
3. trace context 전파 (한 connection 에 multiple event 의 traceId 정책)
|
||||
4. timeout / heartbeat / reconnect 정책
|
||||
5. observability (connection metric / event throughput / error rate)
|
||||
6. load balancer / reverse proxy 설정 의무 (sticky session? keep-alive 시간?)
|
||||
|
||||
만약 *미지원* 결정이면:
|
||||
1. 정확한 *out of scope* 라벨링
|
||||
2. 대안 매커니즘 안내 (webhook outbound, polling endpoint)
|
||||
3. controller 에서 streaming API 사용 금지 ArchUnit rule (`StreamingResponseBody`, `SseEmitter`, `@WebSocket` 등 import 차단)
|
||||
|
||||
- 이슈:
|
||||
- PR:
|
||||
|
||||
<!-- section-id: branch-scope -->
|
||||
## 범위
|
||||
|
||||
### 포함 범위 (🟢 결정 완료 — 착수 가능)
|
||||
|
||||
- 이벤트/server-push 스트리밍 지원 여부 결정 → **D1: 미지원 확정**
|
||||
- 미지원의 ArchUnit 정적 강제 → **D3: `no_sse_emitter` / `no_response_body_emitter` / `no_websocket_handler`** (§구현 가이드)
|
||||
|
||||
### Deferred (🟡 D2 — 지원 시 계약, 보류)
|
||||
|
||||
- 지원 결정 시 매커니즘 선택 (SSE / WebSocket / long-poll / chunked) — 재개 시 SSE 우선 (D2)
|
||||
- 지원 결정 시 event envelope shape + per-event trace context 전파 (tracing branch 와 OPEN)
|
||||
- 지원 결정 시 timeout / heartbeat / reconnect / connection cap
|
||||
- 지원 결정 시 observability metric + reverse proxy 설정 가이드
|
||||
|
||||
### 제외 범위
|
||||
|
||||
- GraphQL subscription — 별도 query layer (ca-skeleton 은 REST default)
|
||||
- gRPC streaming — ca-skeleton 은 HTTP/REST default, gRPC 도입은 완전 별도 branch
|
||||
- WebRTC — 미디어 streaming 은 ca-skeleton 범위 밖
|
||||
- async webhook 발송 — [[raw/branch-notes/feature-webhook-outbound-contract]] SSOT
|
||||
- async polling endpoint (LRO) — [[raw/branch-notes/feature-api-contract-baseline]] D17 SSOT
|
||||
|
||||
## 근거 (필수, 최소 1개+)
|
||||
|
||||
> 본 branch 의 결정 근거 (D1/D3 in-scope + D2 보류분).
|
||||
|
||||
> 2026-06-02: `wiki-decision-researcher` 가 streaming 매커니즘 비교를 위해 아래 6개 raw 를 아카이빙 완료 (각 verbatim quote + self-grep 검증 + 본 branch 로 Parent upward link). D2(지원 계약) 재개 시 재조사 없이 재사용.
|
||||
|
||||
| Source | 정당화할 결정 영역 | Claim ID | 상태 |
|
||||
|---|---|---|---|
|
||||
| [[raw/official-docs/whatwg-html-server-sent-events]] | WHATWG HTML SSE spec (EventSource, retry, last-event-id, text/event-stream wire format) | `WHATWG-SSE-C1~C6` | ✅ 아카이빙 (official-standard) |
|
||||
| [[raw/official-docs/rfc6455-websocket]] | IETF RFC 6455 WebSocket protocol (full-duplex, HTTP Upgrade handshake, masking) | `RFC6455-C1~C6` | ✅ 아카이빙 (official-standard) |
|
||||
| [[raw/official-docs/spring-mvc-async-streaming]] | Spring MVC `SseEmitter` / `ResponseBodyEmitter` / `StreamingResponseBody` vendor doc | `SPRING-ASYNC-C1~C7` | ✅ 아카이빙 (official-vendor-doc) |
|
||||
| [[raw/official-docs/rfc9112-http-1-1-chunked-transfer]] | HTTP/1.1 chunked transfer encoding (§7.1 framing, HTTP/2 금지 경계) | `RFC9112-CHUNK-C1~C5` | ✅ 아카이빙 (official-standard) |
|
||||
| [[raw/official-docs/rfc9110-http-semantics]] (기존) | HTTP semantics — 단 C1~C22 는 모두 다른 branch 귀속, streaming connection 의미론 claim 없음 (재개 시 §7.1 chunked 로 대체) | — | 기존 raw (streaming traceability 단절) |
|
||||
| [[raw/company-tech-blogs/sse-realtime-notification-woowahan]] | 한국 사례 — SSE 운영 부담 (thundering herd, Pub/Sub fan-out, 4천만/일) | `WOOWA-SSE-C1~C6` | ✅ 아카이빙 (company-case-study — 공식 best practice 아님) |
|
||||
| [[raw/company-tech-blogs/realtime-service-experience-woowahan-websocket]] | 한국 사례 — WebSocket 운영 문제 (이벤트 유실, 모바일 네트워크, 클러스터링) | `WOOWA-WS-C1~C5` | ✅ 아카이빙 (company-case-study — 공식 best practice 아님) |
|
||||
|
||||
> 미발견: kakao 공식 기술블로그 SSE/WebSocket 실운영 글 (JS-rendered 페이지 본문 추출 실패). 재개 시 접근 가능하면 보강.
|
||||
|
||||
## TODO
|
||||
|
||||
각 항목 옆에 증거 등급 표기: `actually-implemented` | `locally-verified` | `prod-verified` | `documented-only` | `planned` | `needs-confirmation`
|
||||
|
||||
- [x] **(P0)** ca-skeleton 의 이벤트/server-push 스트리밍 지원 여부 결정 → **D1: 미지원 확정** (2026-06-02)
|
||||
- [x] **(D3)** ArchUnit rule 구현: `no_sse_emitter`, `no_response_body_emitter`, `no_websocket_handler` — `CleanArchitectureTest` 에 등록. violations-as-data fixtures (streaming/), over-block guard (allowed/streaming/), testCompileOnly 3종 추가. ArchUnit 33 rules, FixtureTest 24 tests — 모두 GREEN. — 등급: `locally-verified` (2026-06-02, 커밋 미확정 — 사용자가 직접 커밋 예정)
|
||||
- **품질 리뷰 후속 (2026-06-02)**: WebSocket fixture 테스트가 공유 `VIOLATION_CLASSES` 풀에서 평가되어 spring/jakarta 두 glob 중 하나만 동작해도 vacuous-pass 가능하던 갭 → spring·jakarta fixture 를 각각 `ClassFileImporter.importClasses(...)` 격리 corpus(`SPRING_WEBSOCKET_FIXTURE_ONLY` / `JAKARTA_WEBSOCKET_FIXTURE_ONLY`)로 평가하도록 수정. 각 glob 독립 검증. annotation-only fixture 라 격리 import 시 link-time 클래스 로딩 안전(NoClassDefFoundError 없음, 24 tests GREEN 재확인).
|
||||
- [ ] ~~지원 결정 시 매커니즘 trade-off~~ → **D2 보류**. 재개 시 §Sources 6개 raw 로 비교 (SSE 우선). — 등급: `planned`
|
||||
- [ ] 지원 결정 시 event envelope shape (envelope `{success, data, meta}` 적용? 별도 SSE event format `event:...\ndata:...\n\n`?) — 등급: `planned`
|
||||
- [ ] 지원 결정 시 trace context 전파 (W3C `traceparent` 가 한 connection 의 multiple event 에 어떻게 적용? per-event 새 span?) — 등급: `planned`
|
||||
- [ ] 지원 결정 시 timeout / heartbeat / reconnect (Last-Event-Id 활용 / connection idle timeout / heartbeat ping 간격) — 등급: `planned`
|
||||
- [ ] 지원 결정 시 reverse proxy 설정 의무 (Nginx `proxy_buffering off`, `proxy_read_timeout`, keep-alive) — 등급: `planned`
|
||||
- [ ] 지원 결정 시 connection 수 cap (per-user / per-IP / per-tenant) — DoS 방어 — 등급: `planned`
|
||||
|
||||
## 진행 중 메모
|
||||
|
||||
- 본 branch 의 *1차 결정* 은 사실상 "ca-skeleton minimalist 정신" vs "도입 필요성" 의 trade-off. 현재 sample-portfolio fixture 가 streaming 시나리오 없으므로 *미지원 default + ArchUnit 차단* 이 가장 자연스러운 default 일 가능성 — 단 실제 사용자 도메인이 추가될 때 도입 가능성 열어둠.
|
||||
- 미지원 결정의 핵심 cost: streaming 이 필요한 use case (real-time notification, large file streaming, server-push) 가 등장하면 webhook outbound 또는 polling 으로 우회 — [[raw/branch-notes/feature-webhook-outbound-contract]] 가 webhook 대안 SSOT.
|
||||
|
||||
## 결정 사항
|
||||
|
||||
- **D1 (2026-06-02): 이벤트/server-push 스트리밍 미지원 확정.** ca-skeleton 은 **SSE / WebSocket 등 server-push 이벤트 스트리밍을 default 로 지원하지 않는다.** (통신 모델을 request-response → server-push 로 바꾸는 영역)
|
||||
- **사유 ①** streaming-response 는 독립 결정이 아니라 **통신/전송 프로토콜 계약(HTTP vs gRPC vs streaming)의 한 facet**. 전송 프로토콜은 skeleton 에서 *가장 마지막에 고정* 해야 할 영역 (가장 덜 보편적, 모든 파생 프로젝트의 기본기로 박을 근거 약함).
|
||||
- **사유 ②** 현 sample-portfolio fixture 에 server-push use case 부재 (YAGNI / speculative generality 회피).
|
||||
- **사유 ③** sibling [[raw/branch-notes/feature-api-contract-baseline]] 가 이미 verified scope 에 "ca-skeleton 은 request-response 만 지원, SSE/WS 도입은 별도 branch" 선언 — 일관성.
|
||||
- **범위 명확화 (R3)**: 미지원 대상은 **이벤트 스트리밍(server-push)** 이지, `StreamingResponseBody` 기반 *대용량 다운로드(응답 body 청크 전송)* 가 아니다. 후자는 request-response 모델 내 다운로드 최적화이며 [[raw/branch-notes/feature-file-resource-handling-contract]] D8 이 소유. 본 branch 차단 대상에서 제외.
|
||||
|
||||
- **D3 (2026-06-02): 이벤트 스트리밍 미지원을 ArchUnit rule 로 정적 강제 (IN-SCOPE, 착수 가능).** D1 을 코드 단계에서 강제 — controller/adapter 가 이벤트 스트리밍 API 를 import 하면 build 실패.
|
||||
- rule: `no_sse_emitter`, `no_response_body_emitter`, `no_websocket_handler` (상세 명세는 §구현 가이드).
|
||||
- **메커니즘 선례**: [[raw/branch-notes/feature-boundary-validation-mapping-contract]] D5 (`no_problem_detail_usage` = import-level 차단), 동 branch 가 ArchUnit suite **host**, archunit-junit5 1.3.0 (project §34 Stack Commitment).
|
||||
- 근거: `SPRING-ASYNC-C4` (`SseEmitter` = `ResponseBodyEmitter` subclass, W3C SSE 포맷), `SPRING-ASYNC-C3` (`ResponseBodyEmitter` = 객체 stream emit) — 차단 대상 클래스의 vendor 정의.
|
||||
|
||||
- **D2 (2026-06-02): 이벤트 스트리밍 *지원* 계약 = 보류 (Deferred).** *만약* 지원하기로 하면 필요한 매커니즘 선택(SSE vs WebSocket) / event envelope shape / per-event trace span / timeout·heartbeat·reconnect / connection cap / reverse proxy 의무 — 모두 보류.
|
||||
- **재개 트리거**: (a) 실제 server→client push use case 등장 (실시간 알림 / **LLM token streaming** / 대용량 export 진행률), 또는 (b) 통신/전송 프로토콜 계약 branch 착수 (예정 — 생성 시 본 branch D2 를 forward-ref). 재개 시 §Sources 6개 raw 로 되묻지 않고 진행 가능.
|
||||
- **재개 시 우선 매커니즘**: SSE (`SseEmitter`) — 단방향 push 에 적합, 기존 HTTP 인프라 재사용, WebSocket 대비 proxy 부담 낮음. 근거: `WHATWG-SSE-C1~C3` (official-standard) + `SPRING-ASYNC-C4` (official-vendor-doc). 단 재개 시 D3 ArchUnit rule 의 SSE 차단을 명시적으로 해제해야 함.
|
||||
|
||||
## 결정-근거 매핑
|
||||
|
||||
| Decision ID | Decision | Supporting Claims | Evidence Strength | Open Risk |
|
||||
|---|---|---|---|---|
|
||||
| D1 | 이벤트/server-push 스트리밍(SSE·WebSocket) 미지원 확정 | `SPRING-ASYNC-C4` (SseEmitter=W3C SSE), `WHATWG-SSE-C5` (단방향 push), `RFC6455-C1` (full-duplex) — *무엇을 미지원하는지* 의 클래스 정의 + [[raw/branch-notes/feature-api-contract-baseline]] scope 선언 (request-response only) = `UNSUPPORTED_DECISION` (project-internal consistency 논거 — api-baseline Out of scope 선언과의 정합, 보조 근거) | `official-standard` + `official-vendor-doc` + `cross-branch-consistency` | 실제 push use case 등장 시 D2 로 재개. `StreamingResponseBody`(다운로드)와 혼동 금지 — 별 관심사 |
|
||||
| D3 | D1 을 ArchUnit import-ban 으로 정적 강제 (IN-SCOPE): `no_sse_emitter` / `no_response_body_emitter` / `no_websocket_handler` | `SPRING-ASYNC-C4` (`SseEmitter` FQN + 역할), `SPRING-ASYNC-C3` (`ResponseBodyEmitter` = 객체 stream emit) + 선례 [[raw/branch-notes/feature-boundary-validation-mapping-contract]] D5 (import-level 차단 메커니즘) + `project-ssot` ([[raw/project-notes/ca-skeleton-operational-contract]] §34 Stack Commitment — archunit-junit5 1.3.0 lock) | `official-vendor-doc` (차단 대상 클래스) + `cross-branch-SSOT` (ArchUnit suite host = boundary branch) + `project-ssot` | rule 명명 + 차단 메커니즘(import vs reference) = `UNSUPPORTED_IMPL_DECISION` (§구현 가이드). WebFlux 타입은 stack(MVC) 밖이라 미포함 |
|
||||
| D2 | 이벤트 스트리밍 *지원* 계약(매커니즘/envelope/per-event span/timeout/cap/proxy) = 보류 (Deferred) | `WHATWG-SSE-C1~C6`, `RFC6455-C1~C6`, `RFC9112-CHUNK-C1~C5`, `SPRING-ASYNC-C1~C7`, `WOOWA-SSE-C1~C6`, `WOOWA-WS-C1~C5` (재개 시 재조사 불필요하게 보존) | `official-standard` + `official-vendor-doc` + `company-case-study` | 재개 트리거 전까지 dormant. 아래 OPEN 갭(per-event span) 동반 |
|
||||
|
||||
> **교차 계약 의존 정리**:
|
||||
>
|
||||
> - **(D3 의존, 활성)** ArchUnit suite **host = [[raw/branch-notes/feature-boundary-validation-mapping-contract]]** (D5 import-ban 선례). 본 branch 의 3개 rule 은 그 suite 에 등록. archunit-junit5 1.3.0 (project §34). suite 구조가 바뀌면 본 branch rule 영향.
|
||||
> - **(폴링 대안, 닫힘)** server-push 미지원 시 비동기 응답 우회 = [[raw/branch-notes/feature-api-contract-baseline]] **D17** (verified LRO: 202 + `Location` + polling endpoint + `Retry-After`). webhook 우회는 [[raw/branch-notes/feature-webhook-outbound-contract]] 가 미결정(forward-ref) + 동 branch 가 SSE 대체를 *본 branch* 로 역참조 → 상호 보류.
|
||||
> - **(D3 경계, OUT_OF_BRANCH_SCOPE)** `StreamingResponseBody` 는 [[raw/branch-notes/feature-file-resource-handling-contract]] D8 (verified, 다운로드 메커니즘) 소유 → 차단 안 함. blanket ban 시 파일 다운로드 build 실패하므로 명시적 제외.
|
||||
> - **(D2 동반 OPEN, 미해결)** per-event trace span: [[raw/branch-notes/feature-distributed-tracing-contract]] D5/D7 은 traceparent 를 *request 단위* 로 전파하나 propagation surface(outbound HTTP/Kafka/Rabbit/scheduler)에 **long-lived streaming connection 없음**. "한 connection 의 N개 event 에 traceId 를 어떻게" 는 기존 Decision ID 로 닫히지 않는 진짜 OPEN 갭 — D2 재개 시 tracing branch 와 동시 결정 필요.
|
||||
|
||||
## 구현 가이드
|
||||
|
||||
> 본 §는 **D3 (이벤트 스트리밍 미지원의 ArchUnit 정적 강제)** 만 명세한다. D2 (지원 계약) 는 보류이므로 구현 명세 없음 — 재개 시 작성.
|
||||
|
||||
### 1. 이벤트 스트리밍 차단 ArchUnit rules (D3)
|
||||
|
||||
> **Trace**: D1 (이벤트/server-push 스트리밍 미지원) → D3 (ArchUnit 정적 강제). 차단 대상 클래스 정의 = `SPRING-ASYNC-C4` (`SseEmitter` = `ResponseBodyEmitter` subclass, W3C SSE 포맷), `SPRING-ASYNC-C3` (`ResponseBodyEmitter` = 객체 stream emit). 메커니즘 선례 = [[raw/branch-notes/feature-boundary-validation-mapping-contract]] D5 (`no_problem_detail_usage` import-level 차단). suite host = boundary branch ArchUnit suite, archunit-junit5 1.3.0 (project §34).
|
||||
>
|
||||
> - **UNSUPPORTED_IMPL_DECISION**:
|
||||
> - ① rule 이름 `no_sse_emitter` / `no_response_body_emitter` / `no_websocket_handler` — 임의 명명 (boundary D5 명명 컨벤션 차용, 근거 raw 가 이름 권고 안 함).
|
||||
> - ② **import-level `dependOnClassesThat()` vs reference-level 차단** 의 메커니즘 선택 — boundary D5 와 동일 trade-off (false positive 회피 vs 회귀 차단). import-level 채택은 사용자 결정.
|
||||
> - ③ **WebSocket 차단 FQN 범위** — RFC 6455 (`RFC6455-C*`) 는 *프로토콜* 만 다루고 Spring/Jakarta API 클래스를 열거하지 않음. 아래 FQN 목록(spring-websocket 패키지 + STOMP + Jakarta) 은 사용자가 선정한 차단 표면.
|
||||
> - ④ **`ResponseBodyEmitter` 포함 여부** — D1 은 "이벤트 스트리밍" 미지원. `ResponseBodyEmitter` 는 SSE 의 base 이자 incremental 객체-emit 메커니즘(`SPRING-ASYNC-C3`)이므로 차단에 포함. 단 비-SSE JSON object-stream 용도까지 막는 것은 사용자 판단 (server-push 성격으로 간주).
|
||||
> - ⑤ **차단 scope = production code only** (`ImportOption.DoNotIncludeTests`) — 테스트에서 차단 위반 재현용 fixture 작성 가능하도록.
|
||||
|
||||
| rule 이름 | 차단 대상 FQN | 메커니즘 | 비고 |
|
||||
|---|---|---|---|
|
||||
| `no_sse_emitter` | `org.springframework.web.servlet.mvc.method.annotation.SseEmitter` | `noClasses().should().dependOnClassesThat().haveFullyQualifiedName(...)` | SSE server-push 의 직접 표면 (`SPRING-ASYNC-C4`) |
|
||||
| `no_response_body_emitter` | `org.springframework.web.servlet.mvc.method.annotation.ResponseBodyEmitter` | 동일 | SSE 의 base + incremental 객체 emit (`SPRING-ASYNC-C3`). `SseEmitter` 가 이를 상속하므로 함께 차단. **비-SSE JSON object-stream 용도까지 server-push 모델로 간주해 차단** (`UNSUPPORTED_IMPL_DECISION④`) |
|
||||
| `no_websocket_handler` | `org.springframework.web.socket..` (패키지 전체) + `jakarta.websocket..` (패키지 전체) | `dependOnClassesThat().resideInAnyPackage(...)` | spring-websocket(`WebSocketHandler`, `@EnableWebSocket`, `@EnableWebSocketMessageBroker`/STOMP) + Jakarta `@ServerEndpoint` 계열 일괄 차단 (`RFC6455-C1` full-duplex 모델). **WebSocket 표면은 단일 FQN 이 없어 `haveFullyQualifiedName` 대신 `resideInAnyPackage` glob 불가피** (`UNSUPPORTED_IMPL_DECISION③`) |
|
||||
|
||||
**명시적 비-차단 (R3 OUT_OF_BRANCH_SCOPE)**:
|
||||
- `org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody` — **차단하지 않음.** 대용량 다운로드용 응답 body 청크 전송 (`SPRING-ASYNC-C2`: "for example, for a file download"), 통신 모델은 request-response 유지. [[raw/branch-notes/feature-file-resource-handling-contract]] D8 (verified) 소유. blanket ban 시 다운로드 build 실패.
|
||||
- WebFlux reactive 타입(`Flux<ServerSentEvent>` 등) — ca-skeleton stack 은 spring-webmvc(`SPRING-ASYNC-C1`/`C5` 의 servlet 전제)이므로 해당 클래스가 classpath 에 없음 → rule 불필요 (재개 시 WebFlux 전환하면 별도 검토).
|
||||
|
||||
### 2. 미지원 시 대안 경로 (문서화만, 코드 없음)
|
||||
|
||||
> **Trace**: D1 미지원 결정의 운영 cost 흡수 경로. 코드는 본 branch 가 작성하지 않음 — 기존 형제 branch 결정 재사용.
|
||||
|
||||
- 비동기 server→client 결과 전달이 필요하면: [[raw/branch-notes/feature-api-contract-baseline]] **D17** LRO 패턴 (202 + `Location` + `GET /operations/{id}` polling + `Retry-After`) 사용.
|
||||
- 외부 시스템 push 가 필요하면: [[raw/branch-notes/feature-webhook-outbound-contract]] (미결정, forward-ref).
|
||||
|
||||
## 엣지·실패·의존
|
||||
|
||||
> R4 캡처. D3 (ArchUnit 차단) 구현 중 부딪힐 실패/엣지/계약 의존. D2 (지원 계약) 는 보류이므로 그쪽 엣지(connection 끊김/reconnect/cap 초과)는 재개 시 작성 — 여기서는 "OPEN" 으로만 표시.
|
||||
|
||||
- **실패·엣지 경로 (D3 차단 rule)**:
|
||||
- **false positive — `StreamingResponseBody` 오차단**: rule glob 이 너무 넓으면 file-resource D8 다운로드가 깨짐. 기대 동작 = `StreamingResponseBody` 는 rule 대상 FQN 목록에 *포함하지 않음* (§구현 가이드 명시적 비-차단). 차단되면 안 됨.
|
||||
- **transitive dependency 차단**: `dependOnClassesThat()` 는 transitive import 도 잡으므로, 제3 라이브러리가 내부적으로 `SseEmitter` 를 참조하면 false positive 가능. 기대 동작 = production source 의 *직접* 의존만 위반으로 본다 (필요 시 `ImportOption` 으로 vendor 패키지 제외 — UNSUPPORTED_IMPL_DECISION). 해소 선례: [[raw/branch-notes/feature-boundary-validation-mapping-contract]] §구현 가이드 §8 `no_problem_detail_usage` 의 import-level 차단 trade-off 참조.
|
||||
- **WebSocket 패키지 glob 과다**: `org.springframework.web.socket..` 전체 차단이 의도치 않은 하위 유틸까지 막을 수 있음. 기대 동작 = WebSocket 미사용이 default 이므로 전체 차단이 안전, 도입 시 rule 해제.
|
||||
- **테스트 코드**: 차단 위반 재현 fixture 는 test 에 있어야 하므로 rule scope = `DoNotIncludeTests` (production만).
|
||||
- **다른 계약 의존**:
|
||||
- [[raw/branch-notes/feature-boundary-validation-mapping-contract]] **D5 / ArchUnit suite host** 에 의존 — 본 branch 3개 rule 을 그 suite 에 등록. suite 구조·archunit-junit5 버전(project §34, 1.3.0)이 바뀌면 본 branch 영향.
|
||||
- [[raw/branch-notes/feature-api-contract-baseline]] **D17** consume (미지원 시 비동기 우회 = LRO polling). D17 계약이 바뀌면 본 branch §구현 가이드 §2 대안 경로 갱신 필요.
|
||||
- [[raw/branch-notes/feature-file-resource-handling-contract]] **D8** 과 경계 공유 — `StreamingResponseBody` 소유권. D8 이 다른 streaming 클래스를 쓰기 시작하면 본 branch 비-차단 목록 재검토.
|
||||
- **D2 보류분 OPEN 의존 (재개 시)**: [[raw/branch-notes/feature-distributed-tracing-contract]] D5/D7 — long-lived connection 의 per-event trace span 정책 미존재. 재개 시 동시 결정 필요.
|
||||
|
||||
## 검증해야 할 주장
|
||||
|
||||
| Claim | Why uncertain | How to verify | Status |
|
||||
|---|---|---|---|
|
||||
| `no_sse_emitter` / `no_response_body_emitter` 가 production 코드의 `SseEmitter`·`ResponseBodyEmitter` import 를 build 단계에서 실제 차단 | ArchUnit rule 작성 후 실측 전까지 동작 미보장 | violations-as-data fixtures (`SseEmitterUsingFixture`, `ResponseBodyEmitterUsingFixture`) → FixtureTest GREEN | `locally-verified` (2026-06-02) |
|
||||
| `no_websocket_handler` 의 `org.springframework.web.socket..` + `jakarta.websocket..` glob 이 정확히 WebSocket 표면만 차단 (over-block 없음) | 패키지 glob 범위가 vendor 패키지 구조에 의존 | `SpringWebSocketHandlerFixture` (`@EnableWebSocket`), `JakartaWebSocketEndpointFixture` (`@ServerEndpoint`) fixtures → FixtureTest GREEN | `locally-verified` (2026-06-02) |
|
||||
| `StreamingResponseBody` 가 rule 에 걸리지 않아 file-resource D8 다운로드가 정상 build | blanket ban 시 false positive 위험 (핵심 회귀) | `StreamingResponseBodyAllowedFixture` over-block guard → 3개 D3 rule 모두 `hasViolation() == false` — GREEN | `locally-verified` (2026-06-02) |
|
||||
| ArchUnit suite host(boundary branch) 에 3개 rule 등록이 기존 suite 와 충돌 없음 | suite 구조·rule 명명 충돌 가능 | `CleanArchitectureTest` 33 tests (기존 30 + D3 3) — 0 failures | `locally-verified` (2026-06-02) |
|
||||
|
||||
## 마주친 문제
|
||||
|
||||
- **testCompileOnly + class loading**: `SpringWebSocketHandlerFixture` 최초 버전은 `TextWebSocketHandler` 를 extends — JUnit 이 fixture 클래스를 로드할 때 `NoClassDefFoundError` 발생 (`testCompileOnly` jar 는 runtime classpath 에 없으므로). 해결: `@EnableWebSocket` annotation 참조만으로 교체. Annotation 은 JVM 에서 lazy access (class load 시 필요 없음) → ArchUnit bytecode 분석은 정상 동작. 패턴: annotation-only reference 는 `testCompileOnly` fixture 에서 runtime-safe.
|
||||
- **jakarta.websocket-api 2.1.1 는 server-only**: `jakarta.websocket-api` 2.1.1 jar 는 `jakarta.websocket.server.*` 만 포함 (`Session`, `OnMessage` 등 base 패키지 없음). `jakarta.websocket-all` 또는 client jar 가 필요. `@ServerEndpoint` (server 패키지) 만으로 fixture 재작성.
|
||||
|
||||
## 묶음 (이 branch에서 파생된 자료)
|
||||
|
||||
<!-- GENERATED: sources:start -->
|
||||
- [[raw/company-tech-blogs/realtime-service-experience-woowahan-websocket]]
|
||||
- [[raw/company-tech-blogs/sse-realtime-notification-woowahan]]
|
||||
- [[raw/official-docs/rfc6455-websocket]]
|
||||
- [[raw/official-docs/rfc9112-http-1-1-chunked-transfer]]
|
||||
- [[raw/official-docs/spring-mvc-async-streaming]]
|
||||
- [[raw/official-docs/spring-streaming-response-body]]
|
||||
- [[raw/official-docs/whatwg-html-server-sent-events]]
|
||||
<!-- GENERATED: sources:end -->
|
||||
|
||||
<!-- GENERATED: blog-topics:start -->
|
||||
- [[raw/blog-topics/archunit-testcompileonly-fixture-annotation-pattern-2026-06-02]]
|
||||
- [[raw/blog-topics/streaming-response-not-supported-archunit-ban-2026-07-02]]
|
||||
<!-- GENERATED: blog-topics:end -->
|
||||
|
||||
### Sub-branches (세부 작업)
|
||||
|
||||
- (없음)
|
||||
|
||||
### 오류 기록 (이 branch 작업 중 발생)
|
||||
|
||||
- [[raw/errors/archunit-testcompileonly-class-loading-2026-06-02]] — `testCompileOnly` 로 선언된 타입을 extends 하는 fixture 가 JUnit 실행 시 `NoClassDefFoundError` 를 유발하는 문제 + 해결 패턴
|
||||
|
||||
### 면접 준비 (이 작업에서 나올 수 있는 면접 질문)
|
||||
|
||||
- [[raw/interviews/archunit-violations-as-data-pattern-2026-06-02]] 참조 (ArchUnit violations-as-data 패턴, testCompileOnly fixture 설계)
|
||||
|
||||
### 강의 (이 작업을 위해 학습한 강의)
|
||||
|
||||
- (없음)
|
||||
|
||||
### job-posting tie-ins (이 작업에서 파생된 글감)
|
||||
|
||||
- [[raw/blog-topics/archunit-testcompileonly-fixture-annotation-pattern-2026-06-02]] — testCompileOnly 타입을 ArchUnit fixture 에서 안전하게 참조하는 annotation-only 패턴
|
||||
|
||||
## 관련 일일 노트
|
||||
|
||||
- (없음 — scaffolding 단계)
|
||||
|
||||
## 완료 후 정리
|
||||
|
||||
- PR 링크:
|
||||
- 리뷰 메모:
|
||||
- 머지 결과 / 배포 환경:
|
||||
- **wiki 추출 대상** (verified만, `wiki/projects/`로만 추출):
|
||||
- `actually-implemented` 항목: D3 ArchUnit 차단 rule 3개(`no_sse_emitter` / `no_response_body_emitter` / `no_websocket_handler`) + violations-as-data fixtures + over-block guard + `StreamingResponseBody` 비-차단 경계 → [[wiki/projects/ca-tmpl/streaming-response-support]]
|
||||
- `locally-verified` 항목: `CleanArchitectureTest` + `ArchitectureViolationFixtureTest` GREEN (rule catch + over-block guard + spring/jakarta 격리 corpus) → 동 문서 §로컬/dev 검증
|
||||
- `prod-verified` 항목: 없음 (스트리밍 미지원이므로 운영 streaming 지표 부재)
|
||||
- **추출하지 않을 항목** (planned / documented-only / abandoned): D2 스트리밍 지원 계약 전체(매커니즘/envelope/per-event span/timeout/cap/proxy) = `planned` 보류. 일반 개념(SSE/WebSocket/long-poll/chunked trade-off)은 canonical [[wiki/concepts/streaming-response-patterns]] 로 분리.
|
||||
|
||||
> **/ingest 처리 (2026-06-04, ca-tmpl @9693d72 ground-truth 대조)**: 본 branch 의 D3(미지원 ArchUnit 강제)를 `wiki/projects/ca-tmpl/streaming-response-support.md` 로 추출(CREATE), 일반 개념을 `wiki/concepts/streaming-response-patterns.md` 로 추출(CREATE). production 코드에 streaming import 0건 확인 — 미지원(ban)이 실제. honest framing: 구현된 것은 *차단 가드레일* 이지 스트리밍 지원 아님. status → verified.
|
||||
Reference in New Issue
Block a user