The keycloak project ended with four open questions that design could not
settle. A two-VM lab was built to answer them by measurement, and this is
that material: 26 experiments, 125 raw command outputs, 22 browser captures.
Follows the import procedure in README.md.
source/ the originating repository verbatim — 78 documents, 28 SVGs,
8 manifests, plus .source-revision recording the commit
final/ the SSOT
document.md 729 lines written from the 29 experiment documents, not
concatenated: what was predicted, what was measured, and
where the measurement itself was wrong
evidence/raw 125 outputs, flattened to <experiment>__<file> because
the originals collided (01-baseline.txt appeared three
times) and the audit only globs the top level
evidence/meta one per raw file; command and exitCode are null and the
README says why rather than inventing them
evidence/browser 22 captures
assets/ three diagrams through techviz
.techviz/ their VizSpecs
A separate project rather than an addition to keycloak: the B-layer answers
that project's four questions, but the A, C and D layers are about cluster
failure, SSO and operations, and one document.md should hold one subject.
The four question records there can point here through 관계.
Recorded rather than papered over: only three of the 28 diagrams were
remade. The repository forbids hand-drawn SVG and forbids titles inside the
canvas; all 28 originals carry both, so converting them is redrawing, not
reformatting. They stay in source/ and the gap is written into the document.
verify-pipeline.py passes. audit-records.py reports no issues.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
704 lines
31 KiB
Markdown
704 lines
31 KiB
Markdown
# adapter-outbound-support 상세 분석
|
|
|
|
|
|
## SSOT identity — 2026-08-31 재검증
|
|
|
|
- registered leaf id: `adapter-outbound-support`
|
|
- canonical state `analysisFile`: `analysis/04-adapter-outbound-support.md` (이 문서) — 이 leaf의 단일 SSOT
|
|
- source path: `src/adapter/outbound/support` · Gradle `:adapter:outbound:support`
|
|
- registry `allowed_dependencies`: `["domain-core", "application-core", "shared-contract"]`
|
|
- registry `runtime_memberships`: `["app-bootstrap"]`
|
|
- coverage ledger: `FULL_READ` **8** / `STRUCTURAL_ONLY` **0** / `EXCLUDED` **0** / `UNCLASSIFIED` **0**
|
|
- 최초 분석 revision `a24ece9c` → 재검증 revision `21234e38` · 이 리프의 변경 파일 **0**
|
|
- 재검증 증거: `EVD-333`(소스 드리프트 0), `EVD-334`(lane 재실행)
|
|
|
|
> 재검증이 확인한 것은 대상이 움직이지 않았다는 사실이지, 아래 서술이 옳다는 보증이 아니다.
|
|
> 이번 사이클에서 코드에 대고 다시 확인한 항목은 이 문서의 검증 절과 위 증거가 가리키는 범위다.
|
|
|
|
---
|
|
> 상태: COMPLETE
|
|
> 기준 revision: `a24ece9cf797f7ea647e33bf846b115208ed1ba5`
|
|
> 분석 범위: `src/adapter/outbound/support`
|
|
> Gradle path: `:adapter:outbound:support`
|
|
|
|
## 0. 커버리지와 숫자 지도
|
|
|
|
이 leaf는 크기가 작다. 그래서 대표 파일을 샘플링하지 않고 leaf-owned source/build/document를 전부 읽고, 실제 consumer와 composition-root wiring을 별도 cross-scope evidence로 추적했다.
|
|
|
|
| file group | count | disposition | reason |
|
|
|---|---:|---|---|
|
|
| production Java | 4 | FULL_READ | leaf의 전체 production source |
|
|
| test Java | 1 | FULL_READ | leaf의 전체 dedicated test source |
|
|
| `build.gradle` | 1 | FULL_READ | 실제 compile dependency와 leaf build policy |
|
|
| `README.md` | 1 | FULL_READ | 현재 코드와 대조해야 하는 설계 결정 문서 |
|
|
| `CLAUDE.md` | 1 | FULL_READ | 현재 leaf-local 분석/경계 지침 |
|
|
| resources | 0 | FULL_READ | main/test resource 없음 |
|
|
| **합계** | **8** | **FULL_READ 8 / UNCLASSIFIED 0** | bounded scope complete denominator |
|
|
|
|
추가 측정:
|
|
|
|
- production Java: 4 files / 약 97 LOC
|
|
- test Java: 1 file / 약 89 LOC
|
|
- production package directories: 2
|
|
- `dev.caskeleton.adapter.outbound`
|
|
- `dev.caskeleton.adapter.outbound.support`
|
|
- entities/tables/migrations: 없음
|
|
- configuration properties: 없음
|
|
- runtime membership registry: `app-bootstrap`
|
|
- leaf-owned Spring configuration: `OutboundSupportConfig`
|
|
|
|
Raw inventory는 `evidence/raw/015-adapter-outbound-support-inventory.txt`에 보존했다.
|
|
|
|
## 1. 모듈의 정체와 경계
|
|
|
|
`adapter-outbound-support`는 application port를 구현하는 하나의 기술 adapter라기보다 **여러 outbound adapter가 공유할 수 있는 기술적 보조 seam**이다.
|
|
|
|
현재 production surface는 사실상 세 가지다.
|
|
|
|
1. `OutboundCorrelation`
|
|
- SLF4J MDC에서 `correlation_id`를 조회한다.
|
|
- 값이 없거나 blank면 `"unknown"`을 반환한다.
|
|
|
|
2. `FailOpenDependencyLogger`
|
|
- optional/fail-open outbound 호출의 success/failure observation을 공통 포맷으로 기록한다.
|
|
- success는 DEBUG, failure는 WARN이다.
|
|
|
|
3. `OutboundSupportConfig`
|
|
- `FailOpenDependencyLogger` default bean을 제공한다.
|
|
- `@ConditionalOnMissingBean`으로 fork/application이 같은 타입을 override할 수 있게 한다.
|
|
|
|
`package-info.java`는 outbound adapter package의 개괄만 가진다.
|
|
|
|
### 1.1 허용 dependency와 실제 dependency는 다르다
|
|
|
|
`src/config/architecture/modules.json`은 support leaf가 다음 project dependency를 **허용**한다.
|
|
|
|
- `domain-core`
|
|
- `application-core`
|
|
- `shared-contract`
|
|
|
|
그러나 현재 `build.gradle`과 fresh `compileClasspath` 결과를 보면 실제 project dependency는 **0개**다. 실제 compile dependency는 다음 외부 라이브러리뿐이다.
|
|
|
|
- `spring-boot-autoconfigure` 4.0.8
|
|
- `slf4j-api` 2.0.18
|
|
|
|
즉 registry의 `allowed_dependencies`는 가능한 최대 경계를 나타내고, 현재 source graph가 그 edge를 모두 사용한다는 뜻이 아니다. support는 현 snapshot에서 domain/application/shared 타입과도 결합하지 않는다.
|
|
|
|
이 구분은 Clean Architecture 설명에서도 중요하다. “Core 쪽 dependency가 허용된다”와 “현재 adapter가 Core에 실제로 의존한다”는 별개의 사실이다.
|
|
|
|
## 2. `OutboundCorrelation`: MDC lookup을 한 곳으로 모은 작은 seam
|
|
|
|
`OutboundCorrelation.current()`의 규칙은 단순하다.
|
|
|
|
```text
|
|
MDC[correlation_id] != null && !blank
|
|
-> 해당 값
|
|
otherwise
|
|
-> "unknown"
|
|
```
|
|
|
|
`docs/registries/mdc-keys.yaml`의 `correlation_id` 계약은 다음을 선언한다.
|
|
|
|
- source: inbound filter
|
|
- type: ULID
|
|
- required in: request/dependency/application
|
|
- propagation: HTTP/async/message
|
|
- metric tag 사용 금지(high cardinality)
|
|
|
|
`OutboundCorrelation`은 이 중 **조회와 missing sentinel만 소유**한다. ULID 형식 검증이나 생성/전파를 여기서 하지 않는다. 따라서 이 utility가 임의 문자열을 그대로 반환한다고 해서 곧바로 contract 위반이라고 볼 근거는 없다. canonicalization/validation은 upstream context owner의 책임으로 보인다.
|
|
|
|
### Reachability
|
|
|
|
scope 밖 production code에서 `OutboundCorrelation`을 직접 참조하는 파일은 현재 0개다. 하지만 dead code는 아니다.
|
|
|
|
- `FailOpenDependencyLogger`가 같은 leaf 내부에서 production consumer다.
|
|
- messaging/notification tests는 MDC key를 맞추기 위해 이 상수를 직접 사용한다.
|
|
|
|
따라서 “external production reference 0”만으로 dead 판정을 하면 오탐이다. 이번에 추가된 negative-space rule이 요구하는 바로 그 사례다.
|
|
|
|
## 3. `FailOpenDependencyLogger`: 진단을 business outcome과 분리하려는 계약
|
|
|
|
### 3.1 성공과 실패 포맷
|
|
|
|
`logSuccess(...)`는 DEBUG로 다음 정보를 기록한다.
|
|
|
|
- dependency_name
|
|
- dependency_type
|
|
- operation
|
|
- outcome=`SUCCESS`
|
|
- correlation_id
|
|
|
|
`logFailure(...)`는 WARN으로 다음을 추가한다.
|
|
|
|
- outcome=`FAILURE`
|
|
- error=`<exception simple class>: <cause.getMessage()>`
|
|
|
|
README와 javadoc은 WARN을 선택한 이유를 “optional fail-open dependency가 실패해도 core use case 자체는 성공했기 때문”이라고 설명한다.
|
|
|
|
이 logger 자체는 retry, recovery, fallback을 수행하지 않는다. **실패 정책을 결정하는 주체가 아니라 이미 결정된 fail-open outcome을 관측하는 기술 seam**이다.
|
|
|
|
### 3.2 실제 production consumer
|
|
|
|
repository-wide production reference scan에서 support package를 직접 import하는 current production files는 네 개뿐이었다.
|
|
|
|
Messaging:
|
|
|
|
- `MessagingConfig`
|
|
- `OutboundMessagePublisher`
|
|
|
|
Notification:
|
|
|
|
- `NotificationConfig`
|
|
- `FailOpenNotificationProvider`
|
|
|
|
반대로 support README가 “공유 consumer”로 설명하는 `cache-redis`, `httpclient`는 Gradle dependency는 유지하지만 support production type을 직접 참조하지 않는다. 이 차이는 §8에서 별도로 다룬다.
|
|
|
|
## 4. Confirmed P1 — `cause.getMessage()` 때문에 PII-safe logging 계약이 성립하지 않는다
|
|
|
|
### 4.1 문서와 테스트가 주장하는 계약
|
|
|
|
support source와 README는 다음 취지의 강한 주장을 한다.
|
|
|
|
> logger method가 body/recipient/payload를 받지 않기 때문에 PII가 log에 닿지 않는다.
|
|
|
|
`FailOpenDependencyLoggerTest`도 실패 로그에 email/body marker가 없음을 검사한다.
|
|
|
|
하지만 테스트 fixture의 exception은 단순히 `"connection refused"`다. 즉 PII marker는 logger에 들어가는 어떤 argument에도 존재하지 않는다. 이 테스트는 **payload object가 직접 argument로 전달되지 않는다는 것**만 확인할 뿐, exception message를 통한 leakage를 검사하지 않는다.
|
|
|
|
### 4.2 실제 logger input은 payload-free가 아니다
|
|
|
|
`logFailure`는 다음 값을 그대로 formatted message에 넣는다.
|
|
|
|
```java
|
|
cause.getClass().getSimpleName() + ": " + cause.getMessage()
|
|
```
|
|
|
|
그리고 consumer SPI들은 exception message의 내용을 제한하지 않는다.
|
|
|
|
- `NotificationProvider.send(Notification)` → arbitrary `Exception`
|
|
- `GoogleEmailClient.send(Notification)` → arbitrary `Exception`
|
|
- `SlackClient.send(Notification)` → arbitrary `Exception`
|
|
- `MessageBroker.send(OutboundMessage)` → arbitrary `Exception`
|
|
|
|
특히 `Notification` contract는 recipient/body가 PII이며 logger에 전달하면 안 된다고 명시한다. 하지만 provider SDK/fork implementation이 recipient나 response/body 일부를 exception message에 넣는 것을 이 interface가 통제할 수 없다.
|
|
|
|
### 4.3 실행 재현
|
|
|
|
현재 compiled `FailOpenDependencyLogger`에 다음 exception을 전달하는 focused probe를 실행했다.
|
|
|
|
```text
|
|
RuntimeException(
|
|
"provider rejected recipient secret@gmail.com body=secret-body-content")
|
|
```
|
|
|
|
실제 formatted WARN에는 다음 문자열이 그대로 남았다.
|
|
|
|
```text
|
|
error="RuntimeException: provider rejected recipient secret@gmail.com body=secret-body-content"
|
|
```
|
|
|
|
probe source와 output은 각각:
|
|
|
|
- `evidence/raw/021a-support-logger-pii-probe.java`
|
|
- `evidence/raw/021-support-logger-pii-probe.txt`
|
|
|
|
에 보존했다.
|
|
|
|
### 4.4 global masking도 이 보장을 복구하지 않는다
|
|
|
|
`app-bootstrap`의 `LogMaskingPatterns`는 방어 심층화로 다음과 같은 secret 형태를 mask한다.
|
|
|
|
- password/secret/token/api-key 계열 key=value
|
|
- Authorization credentials
|
|
- standalone Bearer token
|
|
|
|
그러나 arbitrary email address나 free-form body PII를 일반적으로 제거하는 규칙은 없다. app-bootstrap README 자체도 regex masking을 **보증이 아니라 defence-in-depth**라고 설명한다.
|
|
|
|
따라서 현재 “logger signature 때문에 PII가 들어올 수 없다”는 1차 방어선 설명은 사실과 맞지 않는다.
|
|
|
|
### 4.5 영향과 수정 후보
|
|
|
|
우선순위: **P1 (security/privacy contract)**
|
|
|
|
가능한 방향은 두 가지다.
|
|
|
|
1. 공통 logger가 raw `cause.getMessage()`를 기록하지 않고 exception type + bounded/stable error classification만 기록한다.
|
|
2. raw cause message가 정말 필요한 일부 dependency만 별도의 sanitizer/classifier를 거쳐 명시적으로 허용한다.
|
|
|
|
어느 쪽이든 현재의 “arbitrary exception message를 공통 logger가 그대로 출력”하는 방식은 PII-safe라는 강한 계약과 양립하지 않는다.
|
|
|
|
Regression test는 exception message 자체에 email/body/token marker를 넣어 formatted log에 남지 않는지 검증해야 한다. 현재 테스트처럼 payload object만 logger argument에서 제외하는 것으로는 부족하다.
|
|
|
|
## 5. Confirmed P1 — notification consumer는 diagnostic failure를 authoritative failure로 바꿀 수 있다
|
|
|
|
이 finding은 support logger의 consumer semantics를 추적하면서 발견했다.
|
|
|
|
### 5.1 messaging은 이미 이 문제를 구분한다
|
|
|
|
`OutboundMessagePublisher`는 broker call과 observation을 분리한다.
|
|
|
|
```text
|
|
broker.send
|
|
-> success/failure fact 결정
|
|
-> observeQuietly(logger...)
|
|
```
|
|
|
|
`observeQuietly`는 logger가 RuntimeException을 던져도 caller-visible broker outcome을 바꾸지 않는다.
|
|
|
|
source comment에는 과거 버그도 직접 기록돼 있다.
|
|
|
|
- send와 success log가 같은 try block에 있었음
|
|
- broker는 이미 메시지를 받았음
|
|
- success logger가 실패함
|
|
- 같은 catch가 이를 publish failure로 오인했음
|
|
|
|
현재 `OutboundMessagePublisherTest.aLoggerFailureAfterAConfirmedSendIsNotAPublishFailure`는 logger가 DEBUG에서 실제로 예외를 던지도록 만들고도 publish가 예외 없이 끝나며 broker send가 1회 완료됐음을 검증한다.
|
|
|
|
fresh focused test도 통과했다.
|
|
|
|
### 5.2 notification은 같은 shared logger를 다른 방식으로 사용한다
|
|
|
|
현재 `FailOpenNotificationProvider`는 다음 구조다.
|
|
|
|
```text
|
|
try {
|
|
delegate.send()
|
|
logSuccess()
|
|
} catch (Exception ex) {
|
|
logFailure(ex)
|
|
}
|
|
```
|
|
|
|
여기서는 provider outcome과 diagnostic outcome이 분리되지 않는다.
|
|
|
|
#### Case A — provider 성공 후 success logger 실패
|
|
|
|
`delegate.send()`가 성공한 뒤 `logSuccess()`가 RuntimeException을 던지면 같은 catch가 잡는다. 그 결과 이미 성공한 provider send에 대해 `logFailure()`까지 호출된다.
|
|
|
|
focused probe 결과:
|
|
|
|
```text
|
|
SUCCESS_PATH sends=1 debugCalls=1 warnCalls=1
|
|
```
|
|
|
|
실제 send는 1회 성공했지만 success observation failure 때문에 WARN failure observation이 추가 호출됐다.
|
|
|
|
#### Case B — provider 실패 후 failure logger도 실패
|
|
|
|
provider failure가 catch된 뒤 `logFailure()`가 RuntimeException을 던지면 이를 흡수하는 바깥 경계가 없다.
|
|
|
|
probe 결과:
|
|
|
|
```text
|
|
FAILURE_PATH propagated=IllegalStateException:logger-warn-failed warnCalls=1
|
|
```
|
|
|
|
즉 클래스가 “provider failure를 swallow해 core use case를 실패시키지 않는다”고 선언해도 diagnostics failure가 caller까지 전파될 수 있다.
|
|
|
|
probe source/output:
|
|
|
|
- `evidence/raw/022a-notification-logger-failure-probe.java`
|
|
- `evidence/raw/022-notification-logger-failure-probe.txt`
|
|
|
|
### 5.3 현재 notification test가 green인 이유
|
|
|
|
`NotificationAdapterTest`는 ordinary `ListAppender`를 사용한다. provider failure와 PII object가 log line에 직접 들어가지 않는 것은 검증하지만 logger/appender 자체가 실패하는 fixture는 없다.
|
|
|
|
fresh `NotificationAdapterTest`는 정상 통과했다. 따라서 이 finding은 “기존 테스트 실패”가 아니라 **green test가 다루지 않는 failure seam**이다.
|
|
|
|
우선순위: **P1 (reliability / outcome correctness)**
|
|
|
|
수정 후보:
|
|
|
|
- messaging과 동일하게 provider call과 observation을 분리하고 observation failure를 non-authoritative로 흡수한다.
|
|
- 또는 `FailOpenDependencyLogger` 자체를 no-throw contract로 바꿔 모든 consumer를 보호한다.
|
|
|
|
후자는 shared behavior를 바꾸므로 messaging/notification뿐 아니라 future consumer까지 contract review가 필요하다. 어느 owner가 isolation을 가져갈지는 후속 Decision 후보로 남긴다.
|
|
|
|
## 6. `OutboundSupportConfig`: unconditional shared bean seam과 실제 runtime wiring
|
|
|
|
`OutboundSupportConfig`는 `@Configuration`이며 `FailOpenDependencyLogger` bean 하나만 제공한다.
|
|
|
|
```text
|
|
@ConditionalOnMissingBean
|
|
FailOpenDependencyLogger failOpenDependencyLogger()
|
|
```
|
|
|
|
별도 master property condition은 없다. 이는 support 자체를 optional capability로 취급하지 않고, 실제 provider/client capability의 on/off를 sibling adapter가 소유하게 하려는 구조다.
|
|
|
|
### 6.1 direct production reference 0이지만 unwired가 아니다
|
|
|
|
`OutboundSupportConfig`를 support 밖 production Java에서 명시적으로 참조하는 파일은 0개다. 그러나 실제 composition root `CaSkeletonApplication`은 다음 broad package를 component scan한다.
|
|
|
|
```text
|
|
dev.caskeleton.adapter
|
|
```
|
|
|
|
`AUTO_CONFIGURED_PACKAGES` exclusion에는 messaging/notification/persistence 등은 들어가지만 support package는 포함되지 않는다. 따라서 support config는 broad component scan으로 도달한다.
|
|
|
|
registry도 support runtime membership을 `app-bootstrap`으로 선언하고 `app-bootstrap/build.gradle`이 support project를 직접 `implementation`한다.
|
|
|
|
따라서 이 configuration은 현재 **active scanned path**다.
|
|
|
|
### 6.2 conditional sibling comparison
|
|
|
|
support config 자체에는 `@ConditionalOnProperty`가 없고 `@ConditionalOnMissingBean`만 있다. 이것은 같은 optional adapter들의 master switch 누락으로 판정하지 않았다.
|
|
|
|
이유:
|
|
|
|
- support는 provider/client를 생성하지 않는다.
|
|
- logger bean 하나만 default로 제공한다.
|
|
- actual messaging/notification/httpclient 등은 자기 capability root에서 activation을 소유한다.
|
|
- support README와 config javadoc 모두 이 비대칭을 의도적으로 설명한다.
|
|
|
|
`OptionalAdapterBeanGatingTest`도 support config를 함께 넣은 상태에서 optional adapters가 기본 disabled여도 context가 성공함을 검증한다. 다만 이 test는 `OutboundSupportConfig`를 `.withUserConfiguration(...)`으로 직접 공급하므로 full-app component-scan evidence 자체는 아니다. full app wiring은 `CaSkeletonApplication` source와 registry/build edge가 별도 근거다.
|
|
|
|
## 7. Build / ArchUnit enforcement
|
|
|
|
### 7.1 registry
|
|
|
|
`modules.json`에서 support는 독립 leaf이며 runtime membership은 app-bootstrap이다.
|
|
|
|
### 7.2 Gradle dependency validation
|
|
|
|
fresh `verifyCleanArchitectureDependencies`가 통과했다. 이 task는 registry의 allowed dependency와 실제 Gradle project edge를 비교한다.
|
|
|
|
중요한 한계는 이 검증이 **edge가 허용되는지**를 판단한다는 점이다. 사용되지 않는 allowed edge까지 제거해야 한다고 판단하지는 않는다.
|
|
|
|
### 7.3 outbound peer isolation
|
|
|
|
`CleanArchitectureTest.OUTBOUND_ADAPTERS_ARE_PEERS_SHARING_ONLY_SUPPORT`는 outbound adapter family를 slice로 나누고 서로 직접 의존하지 못하게 한다.
|
|
|
|
유일한 shared-code 예외는 target package가:
|
|
|
|
```text
|
|
..adapter.outbound.support..
|
|
```
|
|
|
|
인 dependency다.
|
|
|
|
따라서 messaging → notification 같은 peer coupling은 금지하지만 messaging → support는 허용한다.
|
|
|
|
fresh `CleanArchitectureTest --rerun-tasks`도 통과했다.
|
|
|
|
이 구조는 support 모듈이 단순 편의 library가 아니라 **outbound family에서 sanctioned shared dependency point**라는 점을 build-time fitness function으로 고정한다.
|
|
|
|
## 8. Negative-space probes
|
|
|
|
강화된 분석 규칙에 따라 네 가지 부재/중복/drift probe를 별도로 수행했다.
|
|
|
|
### 8.1 Public surface reachability
|
|
|
|
Raw evidence: `016-adapter-outbound-support-public-reachability.txt`
|
|
|
|
| public type | support 밖 current reference | 판정 |
|
|
|---|---|---|
|
|
| `FailOpenDependencyLogger` | messaging/notification production + tests | active shared surface |
|
|
| `OutboundCorrelation` | external production 0, downstream tests 존재 | leaf-internal production utility, dead 아님 |
|
|
| `OutboundSupportConfig` | external production direct ref 0, app-bootstrap test ref 존재 | component-scan active path |
|
|
|
|
결론적으로 현재 세 타입 중 confirmed dead public type은 없다.
|
|
|
|
### 8.2 Conditional sibling comparison
|
|
|
|
Raw evidence: `017-adapter-outbound-support-conditional-wiring.txt`
|
|
|
|
- support config: unconditional configuration + missing-bean override seam
|
|
- app composition root: support package는 broad component scan에 포함
|
|
- optional provider adapter package들은 별도 conditional/auto-config ownership
|
|
|
|
support의 unconditional nature는 현재 역할과 일치하며 conditional mismatch defect로 판정하지 않았다.
|
|
|
|
### 8.3 Duplicate / competing mechanism sweep
|
|
|
|
Raw evidence: `018-adapter-outbound-support-duplicate-mechanisms.txt`
|
|
|
|
확인한 주요 후보:
|
|
|
|
- `FailOpenDependencyLogger`: current fail-open shared logger
|
|
- `Slf4jOutboxRelayFailureReportAdapter`: durable/outbox failure reporter
|
|
- cache Redis 내부의 자체 logger들
|
|
- historical `OutboundHttpDependencyLogger`
|
|
|
|
현재 evidence로는 이들을 같은 runtime responsibility의 confirmed duplicate라고 볼 수 없다.
|
|
|
|
- outbox reporter는 fail-closed durable relay의 typed report를 기록한다.
|
|
- Redis logger는 lifecycle/config/SDK observability 역할이다.
|
|
- HTTP dependency logger는 현재 source에서 제거됐다.
|
|
|
|
따라서 **현재 중복 fail-open dependency logger 구현 defect는 확인되지 않았다.**
|
|
|
|
다만 messaging과 notification이 동일 logger를 사용하면서 diagnostics-failure semantics가 다르다는 consumer-level inconsistency는 §5의 confirmed finding이다.
|
|
|
|
### 8.4 Documentation / measured-claim drift
|
|
|
|
Raw evidence: `019-adapter-outbound-support-document-drift.txt`
|
|
|
|
여기서는 명확한 drift가 확인됐다.
|
|
|
|
#### Drift 1 — dependency SSOT 위치
|
|
|
|
README:
|
|
|
|
```text
|
|
src/build.gradle 의 allowedProjectDependencies[...]가 SSOT
|
|
```
|
|
|
|
현재:
|
|
|
|
- root `AGENTS.md`: `src/config/architecture/modules.json`가 SSOT
|
|
- support `CLAUDE.md`: 동일
|
|
- `src/build.gradle`은 registry를 읽고 `allowedProjectDependencies` map을 **파생 생성**함
|
|
|
|
즉 variable 자체는 아직 존재하지만 source-of-truth 위치 설명은 outdated다.
|
|
|
|
#### Drift 2 — CLAUDE.md 부재 주장
|
|
|
|
README:
|
|
|
|
```text
|
|
이 모듈은 아직 별도 CLAUDE.md를 두지 않았다
|
|
```
|
|
|
|
현재:
|
|
|
|
```text
|
|
src/adapter/outbound/support/CLAUDE.md
|
|
```
|
|
|
|
가 실제 존재한다.
|
|
|
|
#### Drift 3 — 존재하지 않는 현재 비교 대상
|
|
|
|
README는 fail-open WARN logger와 `OutboundHttpDependencyLogger`를 현재 대비되는 구현처럼 설명한다.
|
|
|
|
current repository exact search에서는 이 symbol이 support README 한 줄 외에 존재하지 않는다.
|
|
|
|
Git history를 보면 해당 class는 초기 repository에 존재했으나 commit `5f10b791...`에서 httpclient 관련 old classes/tests와 함께 삭제됐다. support README는 initial commit 이후 이 변화에 맞춰 갱신되지 않았다.
|
|
|
|
우선순위: **P3 documentation maintenance**
|
|
|
|
## 9. Candidate unnecessary Gradle edges — cache/httpclient → support
|
|
|
|
Raw evidence: `020-adapter-outbound-support-project-edge-usage.txt`
|
|
|
|
다음 네 leaf는 모두 support를 `implementation project(':adapter:outbound:support')`로 선언한다.
|
|
|
|
- cache-redis
|
|
- httpclient
|
|
- messaging
|
|
- notification
|
|
|
|
하지만 current production Java reference는:
|
|
|
|
- messaging: 있음
|
|
- notification: 있음
|
|
- cache-redis: 0
|
|
- httpclient: 0
|
|
|
|
support leaf에는 resource도 없다. 따라서 cache/httpclient의 edge는 **현재 source에서 직접 필요성을 찾지 못한 candidate stale dependency**다.
|
|
|
|
다만 static textual reference만으로 Gradle edge가 100% 불필요하다고 단정하지 않는다. compile/runtime classpath presence 자체를 의도적으로 이용하는 plugin/reflection mechanism이 있는지 downstream leaf 전체 분석에서 다시 확인해야 한다.
|
|
|
|
우선순위: **P3 cleanup candidate**
|
|
|
|
검증 기준:
|
|
|
|
1. 해당 leaf에서 support dependency 제거
|
|
2. compile/test/runtime classpath 및 focused tests 실행
|
|
3. app-bootstrap shipped composition/architecture tests 실행
|
|
4. runtime bean graph 차이가 없는지 확인
|
|
|
|
현재 분석에서는 source를 수정하지 않았다.
|
|
|
|
## 10. 테스트 레인과 실제 증명 범위
|
|
|
|
### 10.1 support dedicated test
|
|
|
|
Fresh command:
|
|
|
|
```text
|
|
./gradlew :adapter:outbound:support:test --rerun-tasks --console=plain
|
|
```
|
|
|
|
결과: BUILD SUCCESSFUL.
|
|
|
|
이 test가 실제로 증명하는 것:
|
|
|
|
- normal Logback path에서 failure correlation id가 기록됨
|
|
- MDC 없을 때 unknown sentinel
|
|
- ordinary exception fixture에서 payload marker가 log에 없음
|
|
- success DEBUG logging
|
|
|
|
증명하지 않는 것:
|
|
|
|
- exception message에 PII가 있을 때의 안전성
|
|
- logger/appender 자체 실패 시 consumer behavior
|
|
- full app component scan
|
|
- downstream provider semantics
|
|
|
|
### 10.2 messaging consumer test
|
|
|
|
Fresh `OutboundMessagePublisherTest` 통과.
|
|
|
|
이 class에는 logger가 success observation에서 실제 RuntimeException을 던지는 regression test가 있고, send outcome이 logger failure와 분리됨을 증명한다.
|
|
|
|
### 10.3 notification consumer test
|
|
|
|
Fresh `NotificationAdapterTest` 통과.
|
|
|
|
normal logger에서 provider failure를 swallow하고 direct Notification PII가 log line에 없음을 증명한다. throwing-logger case는 없다.
|
|
|
|
### 10.4 optional adapter gating
|
|
|
|
Fresh `OptionalAdapterBeanGatingTest` 통과.
|
|
|
|
support config와 여러 optional adapter configs를 ApplicationContextRunner에 함께 넣었을 때 disabled defaults가 실제 provider bean을 만들지 않는다는 것을 검증한다. full `CaSkeletonApplication` scan과 동일한 boot path는 아니다.
|
|
|
|
### 10.5 architecture suite / dependency registry
|
|
|
|
- `CleanArchitectureTest --rerun-tasks`: BUILD SUCCESSFUL
|
|
- `verifyCleanArchitectureDependencies`: BUILD SUCCESSFUL
|
|
|
|
이 둘은 source/package/project dependency constraint를 증명하며 diagnostics runtime failure나 PII behavior를 증명하지 않는다.
|
|
|
|
## 11. 역사적 형태
|
|
|
|
support README와 source는 initial repository부터 존재한다. 이후 architecture가 크게 성장하는 동안 support의 역할 설명 일부가 current implementation과 어긋났다.
|
|
|
|
특히 `OutboundHttpDependencyLogger`는 실제 historical class였다. 따라서 README의 해당 문장이 처음부터 허구였던 것은 아니다. 문제는 **class가 삭제된 이후 문서가 함께 이동하지 않았다는 것**이다.
|
|
|
|
또 messaging current source에는 logger failure를 business/publish failure와 분리하기 위해 `observeQuietly`가 도입된 과거 regression 설명이 남아 있다. 이 history는 notification consumer의 현재 shape와 비교할 때 중요한 evidence가 된다. 두 consumer가 같은 shared logger를 사용하지만 하나만 diagnostics를 non-authoritative로 격리한다.
|
|
|
|
## 12. Findings / improvement backlog
|
|
|
|
### P1 — arbitrary exception message가 PII-safe logging boundary를 우회한다
|
|
|
|
- **Observed fact:** `FailOpenDependencyLogger.logFailure`는 raw `cause.getMessage()`를 formatted WARN에 포함한다.
|
|
- **Runtime evidence:** explicit email/body marker가 포함된 exception message가 실제 formatted log에 그대로 출력됐다.
|
|
- **Contract conflict:** README/source/test는 payload/PII가 logger에 닿지 않는다고 주장한다.
|
|
- **Why it matters:** fork/provider SDK exception message는 application이 통제하지 못하며 recipient/body/remote response를 포함할 수 있다.
|
|
- **Verification:** `021a` probe와 raw output, 향후 dedicated regression test.
|
|
- **Candidate:** raw cause message 제거 또는 explicit sanitizer/classifier.
|
|
- **Tech-Log:** CASE + DECISION 후보.
|
|
|
|
### P1 — notification fail-open consumer가 logger failure를 격리하지 않는다
|
|
|
|
- **Observed fact:** `FailOpenNotificationProvider`는 send와 logSuccess를 동일 try에 두고 catch 안의 logFailure를 보호하지 않는다.
|
|
- **Runtime evidence:** successful send 뒤 debug logger failure가 warn failure observation을 만들었고, provider failure 뒤 warn logger failure는 caller까지 전파됐다.
|
|
- **Comparison:** messaging은 같은 shared logger를 `observeQuietly`로 이미 격리하고 regression test를 갖는다.
|
|
- **Why it matters:** diagnostics가 business/provider outcome을 바꿔서는 안 된다는 non-authoritative observation 원칙이 consumer마다 달라진다.
|
|
- **Verification:** `022a` focused probe; notification에 throwing-logger regression 추가.
|
|
- **Candidate:** notification에서 observation isolation 또는 shared logger no-throw contract.
|
|
- **Tech-Log:** CASE + DECISION 후보.
|
|
|
|
### P3 — support README가 current architecture registry/history와 drift
|
|
|
|
- **Observed fact:** SSOT 위치, CLAUDE.md 존재 여부, HTTP logger 존재 여부가 current source와 불일치.
|
|
- **Why it matters:** support module의 dependency policy와 비교 설계를 읽는 사람이 현재 architecture를 잘못 이해한다.
|
|
- **Verification:** `019` raw search/history.
|
|
- **Candidate:** README를 `modules.json`/current consumer topology에 맞춰 갱신.
|
|
- **Tech-Log:** 보통 refactor/doc maintenance; 독립 CASE 우선순위는 낮음.
|
|
|
|
### P3 — cache-redis/httpclient의 support project dependency 필요성 재검증
|
|
|
|
- **Observed fact:** 두 leaf 모두 Gradle support dependency는 있지만 current production Java support reference는 0이고 support resource도 없다.
|
|
- **Why it matters:** 불필요 edge는 classpath와 architecture narrative를 실제 필요보다 넓힌다.
|
|
- **Verification:** dependency 제거 후 각 leaf focused test + app composition test.
|
|
- **Status:** candidate only; downstream leaf exhaustive analysis 전에는 confirmed dead edge로 단정하지 않음.
|
|
- **Tech-Log:** OPEN QUESTION / refactor candidate.
|
|
|
|
## 13. 확인한 것 / 확인하지 못한 것
|
|
|
|
### 확인한 것
|
|
|
|
- leaf-owned source/test/build/README/CLAUDE 8개 전부 FULL_READ
|
|
- current compile dependency graph
|
|
- registry membership와 allowed project edges
|
|
- app-bootstrap component-scan wiring
|
|
- support public surface reachability
|
|
- optional sibling activation shape
|
|
- duplicate logger/correlation mechanism search
|
|
- README named-claim drift와 relevant Git history
|
|
- messaging/notification direct consumers
|
|
- PII exception-message runtime probe
|
|
- diagnostics-failure runtime probe
|
|
- focused support/messaging/notification/gating tests
|
|
- architecture/dependency verification
|
|
|
|
### 이 scope에서 exhaustive하지 않은 것
|
|
|
|
- messaging 전체 module
|
|
- notification 전체 adapter module
|
|
- cache-redis 전체 module
|
|
- httpclient 전체 module
|
|
- production Logback deployment/backend 장애 행동
|
|
- 실제 외부 provider SDK의 구체 exception message corpus
|
|
|
|
따라서 §4의 핵심은 “특정 SDK가 지금 반드시 PII를 exception에 넣는다”가 아니다. **공통 logger contract가 arbitrary exception message를 허용하고 실제로 그대로 출력하므로 PII-safe를 보장할 수 없다는 것**이다.
|
|
|
|
§5 역시 실제 production disk-full 사고를 주장하지 않는다. 현재 compiled consumer에 throwing logger를 주었을 때 outcome semantics가 깨지는 코드 경로를 재현한 것이다.
|
|
|
|
## 14. 완료 판정
|
|
|
|
`adapter-outbound-support`는 다음 gate를 충족했다.
|
|
|
|
- bounded denominator: 8 / unclassified 0
|
|
- production/test source 전부 FULL_READ
|
|
- build dependency와 runtime membership 확인
|
|
- composition-root wiring 확인
|
|
- public contract와 logging semantics 추출
|
|
- success/failure consumer path 추적
|
|
- dedicated tests와 downstream focused tests 실행
|
|
- ArchUnit/project dependency enforcement 확인
|
|
- public reachability probe 수행
|
|
- conditional sibling probe 수행
|
|
- duplicate mechanism probe 수행
|
|
- documentation drift probe 수행
|
|
- raw evidence에 exact command/cwd/time/revision/exit/output 보존
|
|
- P1/P3 backlog와 limitations 분리
|
|
|
|
따라서 이 bounded scope를 `COMPLETE`로 판정할 수 있다. 프로젝트 전체는 아직 후속 outbound/persistence/messaging/inbound/bootstrap scopes와 마지막 `analysis/99-cross-scope.md`가 남아 있으므로 계속 `IN_PROGRESS`다.
|
|
|
|
## Source anchors
|
|
|
|
이 문서가 backtick으로 인용한 타입·경로를 저장소 트리에 대고 해석한 결과다. 해석된 것만 싣는다 — 총 **12개** (main 4 · test 1 · 기타 7).
|
|
|
|
```
|
|
src/adapter/outbound/support/build.gradle
|
|
src/config/architecture/modules.json (adapter-outbound-support 항목)
|
|
|
|
main:
|
|
src/main/java/dev/caskeleton/adapter/outbound/package-info.java
|
|
src/main/java/dev/caskeleton/adapter/outbound/support/FailOpenDependencyLogger.java
|
|
src/main/java/dev/caskeleton/adapter/outbound/support/OutboundCorrelation.java
|
|
src/main/java/dev/caskeleton/adapter/outbound/support/OutboundSupportConfig.java
|
|
|
|
test:
|
|
src/test/java/dev/caskeleton/adapter/outbound/support/FailOpenDependencyLoggerTest.java
|
|
|
|
기타:
|
|
AGENTS.md
|
|
CLAUDE.md
|
|
README.md
|
|
docs/registries/mdc-keys.yaml
|
|
src/app-bootstrap/build.gradle
|
|
src/build.gradle
|
|
src/config/architecture/modules.json
|
|
|
|
해석되지 않은 인용 (12종) — 외부 타입·문서상 약칭 등:
|
|
modules.json
|
|
evidence/raw/015-adapter-outbound-support-inventory.txt
|
|
evidence/raw/021a-support-logger-pii-probe.java
|
|
evidence/raw/021-support-logger-pii-probe.txt
|
|
evidence/raw/022a-notification-logger-failure-probe.java
|
|
evidence/raw/022-notification-logger-failure-probe.txt
|
|
016-adapter-outbound-support-public-reachability.txt
|
|
017-adapter-outbound-support-conditional-wiring.txt
|
|
018-adapter-outbound-support-duplicate-mechanisms.txt
|
|
019-adapter-outbound-support-document-drift.txt
|
|
020-adapter-outbound-support-project-edge-usage.txt
|
|
analysis/99-cross-scope.md
|
|
|
|
```
|