fix: 하네스 제거 및 keycloak 문서 보강

This commit is contained in:
DongHyeonka
2026-07-25 12:53:13 +09:00
parent 6c53ded9cb
commit d71669eb59
2329 changed files with 138239 additions and 172816 deletions
@@ -1 +0,0 @@
../../../vault/30-knowledge/projects/ca-tmpl/streaming-response-support.md
@@ -0,0 +1,142 @@
---
title: ca-tmpl - 이벤트 스트리밍 미지원 결정 + ArchUnit 정적 강제
source_type: project
status: verified
confidence: high
tags: [ca-skeleton, streaming, archunit, actually-implemented]
related_projects: [ca-skeleton, ca-tmpl]
last_reviewed: 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 참조 — `testCompileOnly` jar 가 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`·`ResponseBodyEmitter` import 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_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 참조를 안전하게 만든다고 쓰지 않는다.
## 관련 개념
- [[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 `testCompileOnly` fixture 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]] — `testCompileOnly` fixture `NoClassDefFoundError` + 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.
## Cluster / 묶음
<!-- GENERATED: derived-blogs:start -->
- [[wiki/blog/ca-tmpl-streaming-response-support-2026-07-02]]
<!-- GENERATED: derived-blogs:end -->