49 KiB
title: branch / feature-log-management-contract
source_type: branch-note
status: raw
id: BR-CA-SKELETON-OPERATIONAL-CONTRACT-003
kind: project-work-item
project: ca-skeleton-operational-contract
work_item: WI-CA-SKELETON-OPERATIONAL-CONTRACT-003
inherits: [DEC-CA-SKELETON-OPERATIONAL-CONTRACT-ERROR-ENVELOPE-001@1]
refines: []
overrides: []
depends_on: []
contract_packet: 1
branch: feature-log-management-contract
parent_branch:
related_projects: [ca-skeleton]
governing_docs: [wiki/projects/ca-tmpl/observability-log-metric-trace-runbook]
tags: [branch, ca-skeleton, logging, observability]
created: 2026-05-21
target_merge:
status_label: implemented
last_pass: 2026-06-14 (Phase C2 전면 구현 완료 — DRIFT-1~6 + sampling 전부 actually-implemented, locally-verified. 사용자 결정 2건: Q1=dependency level 어댑터종류 분리(optional fail-open WARN / core HTTP ERROR), Q2=full HMAC pseudonymization. 구현: (1)DRIFT-2 Layer1 masking = LogMaskingPatterns(SSOT 정규식 catalog) → SecretMaskingJsonGeneratorDecorator(JSON encoder, MaskingJsonGeneratorDecorator — %replace는 LogstashEncoder가 PatternLayout 우회하므로 부적합) + SecretMaskingMessageConverter(%maskedMsg, local pattern). (2)DRIFT-1/D10 = logback-spring.xml <springProfile name="local,dev"> PatternLayout vs !local & !dev JSON. (3)DRIFT-3 = uri_template. (4)DRIFT-4 = OutboundDependencyLogger snake_case + dependency_type + WARN(Q1) + 5 callers. (5)DRIFT-5 = MetricsAsyncAppender(AsyncAppender 서브클래스, Metrics.globalRegistry로 log.appender.dropped.total 발행; discardingThreshold>queueSize 트릭으로 결정론적 테스트). (6)DRIFT-6 = UserPrincipalPseudonymizer(application-core 포트) + HmacUserPrincipalPseudonymizer(adapter-identifier, HMAC-SHA-256 hex) + PseudonymizationConfig/PrivacySettings(app-bootstrap, salt=APP_PRIVACY_PSEUDONYMIZATION_SALT) + RequestLoggingFilter 배선. sampling = SamplingTurboFilter(≤INFO rate, WARN/ERROR 100%). 리뷰 체인 3단계 ALL PASS(architect/spec/quality ready). 가드레일 green(verifyCleanArchitectureDependencies/verifyPublicPathSnapshot/touched-module tests). 잔존: (a)D9 전용 audit appender는 의도적 보류(생산자 부재 + retention은 data-retention 소유 + §Decisionized Work Items 비포함). (b)./gradlew :app-bootstrap:test에 pre-existing 실패 1건 — outbound_adapter_method_returns_only_domain_or_primitives(OutboundHttpSettings.circuitBreaker()/retry(), commit d702572 도입, clean tree에서도 실패 — 본 작업과 무관). 사용자가 직접 커밋.)
contract_packet_sha256: 214a476351c279e55c3afdd170db0165aa04000a706ff51a758f96a11dcc791c
branch: feature-log-management-contract
Layer:
raw/branch-notes/— structured log schema와 로그 금지 정책을 정의합니다.
부모 (필수)
raw/project-notes/ca-skeleton-operational-contract
브랜치 계약 패킷
- 생성 시 프로젝트 개정:
1 - 패킷 스키마:
contract_packet: 1 - 완료 조건: log field·masking contract와 verification test가 통과한다
상속한 프로젝트 결정
| Decision Ref | Project Summary | Branch Application | Source |
|---|---|---|---|
DEC-CA-SKELETON-OPERATIONAL-CONTRACT-ERROR-ENVELOPE-001@1 |
foundation이 envelope schema와 error.category enum의 단일 owner다 | structured log field와 error category correlation 및 masking contract에 적용한다 | raw/project-notes/ca-skeleton-operational-contract |
브랜치 지역 결정
기존 branch-local 결정은 아래
## Decision Evidence Map의 D-row가 소유하며 이 packet에서 복제하지 않는다.
| Decision ID | Decision | Relation | Supporting Claims | Status |
|---|
선언한 예외
| Override ID | Overrides | Reason | Approval | Status |
|---|
없음.
목표
운영자는 exception class보다 어떤 operation, dependency, retryable 여부, trace/correlation 정보가 필요한지 봅니다. 이 branch는 skeleton의 log contract를 확정합니다.
- 이슈:
- PR:
범위
포함 범위
- JSON log schema.
- request/application/dependency/security/audit log 구분.
- PII/secrets/token/password/body 로그 금지.
- level 기준.
- sampling 정책.
- async appender overflow 기준.
- stdout/file logging 기준.
- profile별 console encoder 포맷 — local/dev=human-readable pattern, staging/prod=JSON (D10).
제외 범위
- 실제 로그 수집 플랫폼 구축.
- Grafana/ELK 대시보드 구현.
- business metric 정의.
근거 (필수, 최소 1개+)
본 branch의 결정 근거. 상세 비교는 §외부 근거 / 대안 조사 (있다면) 참조.
| Source | 정당화하는 결정 |
|---|---|
| raw/official-docs/log-logback-mask-pattern-converter-official.md | Logback PatternLayout / custom converter spec |
| raw/official-docs/log-ecs-schema-elastic-official.md | 자체 schema와 ECS 매핑 가능성 평가 |
| raw/official-docs/log-otel-log-data-model-spec.md | trace 자동 correlation 장점이나 OTLP collector 추가 deploy 필요 |
| raw/official-docs/container-stdout-logging-12factor-official.md | D4: production logging = stdout JSON default; file logging = local/dev only — Twelve-Factor App Factor XI ("Logs") 직접 근거 (LOG-12F-C1 ~ C4) |
| raw/official-docs/ecs-awslogs-stdout-cloudwatch-aws-official.md | D4 — AWS ECS 환경 구체화: awslogs 드라이버가 컨테이너 stdout/stderr 를 Docker 경유로 CloudWatch Logs 전달 — 앱 내 별도 shipper 불필요 (LOG-ECS-AWSLOGS-C1) |
| raw/official-docs/k8s-logging-architecture-kubernetes-official | D4 — Kubernetes 공식 문서: stdout/stderr 직접 출력이 가장 권장(C1), streaming sidecar 는 stdout 불가 앱 폴백(C2), file→stdout 이중 경로 디스크 2배 경고(C3), 단일 파일 앱 /dev/stdout 권장(C4), kubelet 기본 rotation 10Mi/5files + 장기 retention 불가(C5) |
외부 근거 / 대안 조사 (2026-05-22 — Group G-A: Log management)
채택 결정 + 뒷받침
- 결정: structured JSON log + Logback masking converter (Layer 1 SSOT) + prod 10% INFO sampling.
- 뒷받침 source:
- raw/official-docs/log-logback-mask-pattern-converter-official.md — Logback PatternLayout / custom converter spec. Layer 1 (final encoder 직전) 위치가 MDC/message/stack trace 전부 통과시키는 가장 넓은 catch-net임을 spec으로 확인.
- raw/official-docs/log-ecs-schema-elastic-official.md — 자체 schema와 ECS 매핑 가능성 평가. ca-tmpl 필수 8-10 field가 ECS와 1:1 매핑 가능 (예:
traceId↔trace.id).
검토 대안 + source
- 대안 1 — ECS schema 직접 채택: raw/official-docs/log-ecs-schema-elastic-official.md. 업계 표준이나 field 폭주(수백 개) 위험.
- 대안 2 — OpenTelemetry log signal: raw/official-docs/log-otel-log-data-model-spec.md. trace 자동 correlation 장점이나 OTLP collector 추가 deploy 필요. 2024 GA로 ecosystem maturity 낮음.
비교 핵심 1줄
자체 schema는 minimal core 강제 + JVM 친화 + stdout 단순화가 강점, ECS는 vendor 호환성, OTel log signal은 미래 통합. skeleton 단계에서는 자체 schema + Logback 채택이 운영 비용 최소.
TODO
TODO drained 2026-05-22 — 결정은 아래 "결정 사항" / "Sampling Policy (final)" / "Redaction Layer SSOT" / "Audit Log Retention / Compliance" / "Log Type별 필수 필드" / "Decisionized Work Items" 참조. JSON log 필수 필드/log type 구분/redaction(PII/secrets/token/body)/level/sampling/async overflow/stdout-vs-file 모두 표 또는 결정 라인으로 반영됨. structured log field 존재 테스트는
feature-contract-verification-test-suite로 위임. 잔존 TODO 없음.
진행 중 메모
- log body는 기본 금지. allowlist redaction 없이 켜지지 않아야 합니다.
결정 사항 (decisions)
- 2026-05-21: JSON log를 기본 운영 포맷으로 둠.
- 2026-05-22: MDC/log key naming의 SSOT는
feature-operational-error-observability-foundation. 이 branch는 log type, level, sink, overflow policy를 소유. - 2026-05-22: domain layer logger는 금지. domain invariant violation의 reason code는 application layer에서 client-safe diagnostic log로 변환.
- 2026-05-22: production logging은 stdout JSON default, file logging은 local/dev only.
- 2026-05-22: log sampling(prod 10%) > trace sampling(prod 1%)는 의도된 분리. log는 운영 진단에 trace보다 자주 필요(특히 trace_id 없는 단순 query). log-only correlation은 request_id로 추적. distributed-tracing branch와 정합.
결정-근거 매핑
각 결정이 어떤 raw source claim 으로 뒷받침되는지 명시한다.
Decision ID는 이 branch-note 안에서 안정적으로 유지한다.Supporting Claims는raw/<category>/<slug>.md#<CLAIM-ID>형식으로 연결한다.
| Decision ID | Decision | Supporting Claims | Evidence Strength | Open Risk |
|---|---|---|---|---|
| D1 | JSON log를 기본 운영 포맷으로 둠 (structured JSON + Logback masking converter Layer 1 SSOT) | raw/official-docs/log-logback-mask-pattern-converter-official.md#LOG-LBK-C1, raw/official-docs/log-logback-mask-pattern-converter-official.md#LOG-LBK-C3, raw/official-docs/log-logback-mask-pattern-converter-official.md#LOG-LBK-C5 |
official-vendor-doc (Logback PatternLayout / custom converter / %replace 정규식 치환 spec) |
Logback 의 built-in PII masking converter 부재는 본 페이지 인용으로 직접 증명되지 않음 (LOG-LBK-C5 Usage Boundaries). regex false negative 가능 (Base64 token 등) — 정규식 catalog 별도 운영 검증 필요. 구현 현황: JSON encoder(LogstashEncoder)는 actually-implemented, masking converter(Layer 1)는 미구현 → DRIFT-2 |
| D2 | MDC key naming 의 SSOT 는 feature-operational-error-observability-foundation (consume only) |
raw/official-docs/log-logback-mask-pattern-converter-official.md#LOG-LBK-C4 (%mdc{key} converter spec — MDC 가 thread-local 임을 확인) |
official-vendor-doc |
MDC 가 async/reactive thread 전환 시 자동 전파된다는 뜻 아님 (LOG-LBK-C4 Does not prove). Reactive boundary 별도 propagation 필요 |
| D3 | domain layer logger 금지 — application layer 에서 client-safe diagnostic log 로 변환 | UNSUPPORTED_DECISION (외부 official-standard / official-vendor-doc 직접 근거 없음 — clean architecture / DDD 일반 원칙에 가까운 ca-tmpl 내부 정책) | N/A | 외부 raw source 로 직접 뒷받침되지 않으므로 면접/외부 공개 시 "내부 정책" 으로만 표현. wiki 추출 시 wiki/concepts/clean-architecture-* 류 별도 근거 필요. ArchUnit domain-core logger import 금지 rule 로 강제 가능 (정적 검증) |
| D4 | production logging stdout JSON default, file logging local/dev only | raw/official-docs/container-stdout-logging-12factor-official.md#LOG-12F-C1 (앱은 로그 라우팅·저장 직접 관리 금지 + logfile 쓰기·관리 시도 금지), raw/official-docs/container-stdout-logging-12factor-official.md#LOG-12F-C2 (각 프로세스는 이벤트 스트림을 unbuffered stdout 에 기록), raw/official-docs/container-stdout-logging-12factor-official.md#LOG-12F-C3 (staging/prod 에서 실행 환경이 스트림 캡처·라우팅 담당), raw/official-docs/ecs-awslogs-stdout-cloudwatch-aws-official.md#LOG-ECS-AWSLOGS-C1 (AWS ECS + awslogs 드라이버가 컨테이너 stdout/stderr 를 Docker 경유 CloudWatch Logs 로 전달 — 앱 내 별도 shipper 불필요), raw/official-docs/ecs-awslogs-stdout-cloudwatch-aws-official.md#LOG-ECS-AWSLOGS-C2 (awslogs 캡처 대상 = stdout/stderr — 파일 로그는 별도 처리 필요) |
official-standard (Twelve-Factor App Factor XI) + official-vendor-doc (AWS ECS awslogs) |
stdout 로그 포맷(JSON vs plain-text)은 Factor XI 가 규정하지 않음 — D1(JSON 기본 포맷)은 별도 log-logback-mask-pattern-converter-official / log-ecs-schema-elastic-official 근거. FILE_ENABLED=true 가 local/dev 에만 적용되는지 환경 게이트 검증 필요. AWS ECS 이외 환경(K8s, on-prem)에서 stdout 수집 체인은 별도 검증 필요 — LOG-ECS-AWSLOGS-C1 은 AWS-vendor 특화 근거. 구현 현황: FILE_ENABLED property(default false) toggle 로 actually-implemented |
| D5 | log sampling (prod 10%) > trace sampling (prod 1%) 분리 — log 가 운영 진단에 trace 보다 자주 필요 | UNSUPPORTED_DECISION (sampling 비율 분리는 운영 trade-off; 인용된 official-docs 3개 어디에도 정량 권장값 없음) | N/A | log/trace sampling 비율 의 정량 권장 표준 부재. 본 비율은 운영 가정 — 운영 후 재조정 필요. distributed-tracing D6 (trace 1%) 와 상호 확인된 짝. 구현 현황: sampling 로직 미구현 → DRIFT(planned) |
| D6 | ECS schema 직접 채택 거부 (자체 schema 유지, ECS 와 매핑은 보존) | raw/official-docs/log-ecs-schema-elastic-official.md#LOG-ECS-C1 (ECS 의 정의 + Usage Boundaries: ECS 가 비-Elastic sink 의 공식 표준이라는 뜻은 아님) |
official-vendor-doc |
ca-tmpl 자체 schema 가 ECS 보다 우월하다는 결론 아님. 단지 minimal core 강제 + 자체 운영 비용 최소화의 trade-off |
| D7 | OpenTelemetry Log signal 직접 emit 거부 (ecosystem maturity / collector deploy 비용) | raw/official-docs/log-otel-log-data-model-spec.md#LOG-OTEL-C1, raw/official-docs/log-otel-log-data-model-spec.md#LOG-OTEL-C5 (OTel log spec + W3C trace context 정합) |
official-standard |
본 spec 자체는 stdout JSON 보다 열등하다고 말하지 않음 (LOG-OTEL-C1 Does not prove). 운영 비용 평가는 ca-tmpl 내부 판단 |
| D8 | INFO 이하만 sampling 대상, WARN/ERROR 100% 보장 | UNSUPPORTED_DECISION (인용된 official-docs 에 sampling 정책 직접 근거 없음 — 운영 best practice 일반론) | N/A | WARN/ERROR 100% 보장은 운영 관행이나 표준 spec 인용 없음. ca-tmpl 내부 정책으로만 표현. 구현 현황: AsyncAppender discardingThreshold(≤INFO drop, WARN/ERROR 보존)는 actually-implemented; 비율 sampling 은 미구현 |
| D9 | audit log = append-only file appender + remote forwarding + immutable | UNSUPPORTED_DECISION (audit log retention SSOT 는 data-retention-privacy-contract consume — 본 branch 인용 자료에 직접 근거 없음) |
N/A | 외부 audit log immutability 표준 (예: SOX / PCI DSS) 별도 raw source 필요. retention 수치는 raw/branch-notes/feature-data-retention-privacy-contract SSOT (audit 365일). 본 branch 는 형식 만 owns. 구현 현황: 전용 audit appender 미구현 → planned |
| D10 | local/dev profile 콘솔(stdout) 출력 = human-readable PatternLayout encoder, JSON(logstash-logback-encoder)은 staging/prod 전용 — logback-spring.xml 의 <springProfile> 분기로 encoder 선택. D1(JSON=기본 운영 포맷)을 profile 축으로 정밀화 (운영=staging/prod 은 JSON 유지, local console 만 가독성 예외). D4(sink routing)와 보완 관계 — D4=어디로 보낼지, D10=local console 을 어떤 포맷으로 찍을지 |
raw/official-docs/log-logback-mask-pattern-converter-official.md#LOG-LBK-C1 (PatternLayout 은 logging event 를 printf 류 human-readable String 으로 출력하며 JSON encoder 와 별개 메커니즘임을 spec 으로 확인 — profile별 encoder 전환의 직접 근거) |
official-vendor-doc (mechanism only) |
<springProfile> 분기 메커니즘 자체 + "local=human-readable 이 DX 에 유리" rationale 은 운영 trade-off (D4/D5 동류; <springProfile> element 의 raw source 부재 → UNSUPPORTED operational 가정, 면접/외부 공개 시 "내부 DX 정책"으로 표현). Redaction 영향 없음: local PatternLayout 에도 동일 %replace 마스킹 converter(LOG-LBK-C5) 적용 가능 → 가독성 전환이 secret 노출로 이어지지 않음. 구현 현황: 실제 logback-spring.xml 은 전 profile JSON LogstashEncoder 사용, <springProfile> 분기·PatternLayout 부재 → D10 은 planned, DRIFT-1 |
MDC Key Consumption
이 branch는 MDC/log key 이름표를 다시 작성하지 않습니다. feature-operational-error-observability-foundation의 "MDC Key Standard (final)" 표를 그대로 consume 합니다. 키 추가/변경이 필요하면 foundation branch의 registry를 먼저 갱신해야 합니다.
- Core 6 키 (foundation owner):
request_id,trace_id,span_id,correlation_id,tenant_id(tenant-context-policy owner),user_principal—mdc-keys.yaml에 등록, snake_case 강제. - 본 branch owner log-field 키 (13개,
mdc-keys.yamlowner_branch: feature-log-management-contract):operation,method,status,duration_ms,dependency_name,dependency_type,outcome,error_code,event_type,source_ip_anon,actor,action,target. 각 row 의required_test: contract-verification:log-fields. - 실제 코드 노출 키(
logback-spring.xmlincludeMdcKeyName):trace_id,span_id,request_id,correlation_id,user_principal(actually-implemented). 본 branch owner 13 키는 registry 등록되었으나 encoder 자동노출 목록엔 미포함 — log type 별 structured argument 로 주입 (planned 정합).
Sampling Policy (final)
INFO 이하만 sampling 대상. WARN/ERROR는 항상 100% 보장.
| profile | high-traffic endpoint | normal endpoint |
|---|---|---|
| prod | 10% | 100% |
| staging | 100% | 100% |
| dev/local | 100% | 100% |
async appender overflow default: drop oldest INFO/DEBUG with counter metric (log.appender.dropped.total). WARN/ERROR drop 금지.
⚠️ 구현 정합 메모(2026-06-13): 실제
AsyncAppender(queueSize=512, discardingThreshold=20, neverBlock=false)는 유입(newest) ≤INFO 이벤트를 drop 하며(잔여 capacity ≤ threshold 시), 완전 포화 시 caller thread 가 block(neverBlock=false). "drop oldest" 표현은 Logback 동작과 불일치 — Audit DRIFT-5. 비율 기반 prod 10% sampling 은 TurboFilter 미구현(planned).log.appender.dropped.total은 metrics.yaml 등록되었으나 AsyncAppender 가 Micrometer counter 미발행 → wiring planned.
Redaction Layer SSOT
- Layer 1 (primary): Logback masking converter (PatternLayout 단계). 모든 ERROR/WARN 진입 시 token/password/auth header pattern을 ****로 치환.
- Layer 2 (secondary): Jackson
@JsonSerialize(using=MaskingSerializer.class)for known PII fields in DTO. - Layer 3 (defensive): request body capture filter — allowlist 없이는 capture 자체 금지.
- Layer 1이 SSOT. Layer 2/3는 보완. allowlist 미정 시 capture forbidden(default deny).
⚠️ 구현 정합 메모(2026-06-13): Layer 1~3 모두 미구현(
documented-only/planned) —logback-spring.xml에 masking converter(%replace) 없음. 현재 secret-누출 방지는 by construction (logger 가 body/payload 인자를 받지 않음 —OutboundDependencyLogger). governing docobservability-log-metric-trace-runbookL108 도 "masking 정책은 문서에만 존재" 로documented-only명시. Audit DRIFT-2.user_principalpseudonymization 알고리즘 SSOT 는 raw/branch-notes/feature-data-retention-privacy-contract 로 확정 (pseudonymization key = HMAC-SHA-256 + 90d salt rotation 결정 + pseudonymized id ↔ original id 변환표 owns). foundation §2 Q12 가 본 branch/security-operational-baseline 도 후보로 열거했으나 알고리즘 결정은 data-retention 보유 — 본 branch 는 security/audit log 에 pseudonymized 형태로만 기록 계약만 owns. DRIFT-6 (코드는 rawidpUserId()기록).
Audit Log Retention / Compliance
- audit log retention 정책은 data-retention-privacy-contract SSOT consume. 본 branch는 audit log의 형식(append-only file appender + remote forwarding)만 결정.
- append-only file appender + remote forwarding (Loki/CloudWatch).
- audit log는 변경/삭제 금지(immutable).
Log Type별 필수 필드
| log type | 필수 필드 |
|---|---|
| request | request_id, trace_id, method, uri_template, status, duration_ms |
| dependency | dependency_name, dependency_type, duration_ms, outcome, error_code (실패 시) |
| security | event_type, user_principal (pseudonymized), source_ip (anonymized — last octet zeroed) |
| audit | actor, action, target, before_hash, after_hash, occurred_at |
| application | 자유 형식, 단 mandatory MDC keys (foundation 표) 유지 |
Work Item Contract
각 TODO는 아래 판정 단위로 재작성되어야 canonical 승급 가능합니다. TODO가 단순히 기준 작성으로 남아 있으면 이 branch는 완료로 보지 않습니다.
| field | required | rule |
|---|---|---|
| Decision | yes | 구현자가 선택해야 하는 기본값 |
| Allowed | yes | 허용되는 예외와 조건 |
| Forbidden | yes | 절대 금지되는 구현/문서 상태 |
| Required registry update | conditional | error/env/header/log/metric/capability 변경 시 필수 |
| Required contract test | yes | 계약 위반 시 실패해야 하는 테스트 |
| Failure condition | yes | review/build에서 실패로 판정할 상태 |
| Canonical extraction target | yes | wiki/projects 승급 위치 |
Decisionized Work Items
| item | Decision | Allowed | Forbidden | Required test |
|---|---|---|---|---|
| key names | foundation registry consumes | external ECS mapping table | branch-local MDC names | log field test |
| domain diagnostics | application logs translated reason code | domain exception has safe enum | domain logger | forbidden import test |
| sink | prod stdout JSON | local file logging | prod file-only logs | profile log test |
| overflow | bounded async appender + drop/blocks documented | sync logging for small apps | unbounded queue | overflow policy test |
| console format | local/dev=human-readable pattern, staging/prod=JSON encoder (D10) | <springProfile> 분기 in logback-spring.xml + local pattern 에 %replace 마스킹 유지 |
local 에서 JSON 강제 / prod·staging 에서 pattern 강제 / 마스킹 없는 local pattern | profile별 console encoder 형식 test |
구현 가이드
결정 (Decisions) 이 "무엇 을 할 것인가" 라면, 본 §는 "어디에 어떻게 구현될 것인가" 의 사전 명세. ca-tmpl
src/코드를 anchor 로 쓰되, 코드로 확인 안 된 것은planned/documented-only로 표기. 모든 sub-section 은 본 branch 의 Decision ID + Supporting Claim ID 를 reference (R1). 근거 없는 detail 은UNSUPPORTED_IMPL_DECISION라벨 (R2).
1. Logback appender topology
Trace: D1(JSON 기본 운영 포맷,
LOG-LBK-C1) + D4(stdout default / file local-dev). 실제 구현:src/app-bootstrap/src/main/resources/logback-spring.xml.
- UNSUPPORTED_IMPL_DECISION:
queueSize=512/discardingThreshold=20의 수치 는 운영 가정 (Logback default queueSize=256 에서 상향) — raw source 없음, 운영 후 재조정.
| 요소 | 구현 (코드 확인) | 등급 | Trace |
|---|---|---|---|
| JSON_CONSOLE | ch.qos.logback.core.ConsoleAppender + net.logstash.logback.encoder.LogstashEncoder (app-bootstrap/build.gradle:76 logstash-logback-encoder:8.0) |
actually-implemented | D1 / LOG-LBK-C1 |
| JSON_FILE | RollingFileAppender + SizeAndTimeBasedRollingPolicy(maxSize/maxHistory/totalSizeCap), conditional FILE_ENABLED(default false) |
actually-implemented | D4 |
| async wrap | ch.qos.logback.classic.AsyncAppender (ASYNC_CONSOLE/ASYNC_FILE) queueSize=512, discardingThreshold=20, neverBlock=false |
actually-implemented | Sampling Policy overflow / D8 |
| MDC 노출 | encoder includeMdcKeyName: trace_id, span_id, request_id, correlation_id, user_principal |
actually-implemented | D2 (consume foundation) |
| 설정 바인딩 | <springProperty> ← ca-skeleton.logging.* → LoggingSettings.java (@ConfigurationProperties, warn-and-default: bad value → warn 로그 + default, startup 실패 안 함) |
actually-implemented | D4 |
2. Console encoder per profile (D10) — 미구현
Trace: D10 (local/dev=human-readable PatternLayout, staging/prod=JSON via
<springProfile>),LOG-LBK-C1.
- UNSUPPORTED_IMPL_DECISION + DRIFT-1: 실제
logback-spring.xml은 전 profileLogstashEncoder(JSON) 사용 —<springProfile>분기 / PatternLayout 부재(src/grep 0건). → D10 은planned. 구현 시<springProfile name="local,dev">안 PatternLayout<encoder>,<springProfile name="staging,prod">안 LogstashEncoder 로 분기 + local PatternLayout 에도%replace마스킹 동일 적용 (가독성 전환이 secret 노출로 이어지지 않게).
3. Request log type (adapter-web)
Trace: Log Type별 필수 필드(request) + D2 consume. 구현:
src/adapter-web/.../filter/RequestLoggingFilter.java(actually-implemented;RequestLoggingFilterTestlocally-verified per governing doc L42).
- UNSUPPORTED_IMPL_DECISION + DRIFT-3: 필수필드 표는
uri_template(low-cardinality route) 요구하나 코드는req.getRequestURI()(raw path, high-cardinality) 기록 (RequestLoggingFilter.java:64). 정합하려면RateLimitKeyResolver.java:48와 동일하게HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE사용 →planned.
- 현재 동작(2026-06-14 actually-implemented):
log.info("http_request method={} uri_template={} status={} duration_ms={}")at INFO (OncePerRequestFilter).HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE로 route template 추출(fallback:getRequestURI()for 404s).UserPrincipalPseudonymizer생성자 주입 —user_principalMDC 에는pseudonymizer.pseudonymize(user.idpUserId())결과만 기록(null 반환 시 미기록). 기존path=필드명은uri_template=로 변경(DRIFT-3 해소). 2개 신규 테스트 추가(logs_uri_template_not_raw_path_when_handler_mapping_attribute_set,puts_pseudonymized_user_principal_on_mdc_not_raw_id) — locally-verified via./gradlew :adapter-web:test(BUILD SUCCESSFUL). - MDC 주입:
request_id(inboundX-Request-Idsanitize 또는 UUID),correlation_id(X-Correlation-Id),trace_id(= request_id mirror, Micrometer Tracing 공급 전까지 — foundation/tracing D7),user_principal(pseudonymized viaUserPrincipalPseudonymizer— DRIFT-6 해소, rawidpUserId()미기록). - inbound id 보안:
HeaderSanitizer.sanitize(.., MAX_ID_LENGTH=200)— CR/LF·제어문자 strip + length cap (CWE-117, foundation D14). finally 에서 MDC 전부 remove.
4. Dependency log type (adapter-outbound)
Trace: Log Type별 필수 필드(dependency) + 테스트 계약. 구현:
src/adapter-outbound/.../support/OutboundDependencyLogger.java(actually-implemented).
- UNSUPPORTED_IMPL_DECISION + DRIFT-4: (a) 필수필드 표(
dependency_name/dependency_type/duration_ms/error_code) vs 코드 필드(dependency/operation/outcome/correlationId/error) 불일치; (b) 테스트 계약 "5xx와 dependency failure가 ERROR 이하면 실패" vs 코드logFailure= WARN(optional fail-open adapter) — core dependency 실패 vs optional fail-open 실패의 level 정책 분리가 노트에 미명시; (c) message 안correlationId=(camelCase) vs MDC snake_case SSOT 불일치. → 모두planned정합 (또는 결정 명시).
- 현재 동작:
logSuccess= DEBUG (dependency operation outcome correlationId),logFailure= WARN (dependency operation outcome correlationId error="class: message"). payload/recipient/PII 인자 자체를 받지 않음 (by construction PII 안전 — Redaction Layer 보완).
5. Redaction layers — Layer 1 미구현
Trace: Redaction Layer SSOT (Layer 1/2/3) + D1 (
LOG-LBK-C5).
- UNSUPPORTED_IMPL_DECISION + DRIFT-2/DRIFT-6: Layer 1 masking converter(
%replace) 미구현 (src/grep 0건;AuthErrorResponseWriter.java:25-30주석 "a full log-masking filter is delegated tofeature-secrets-config-source-contract/ log-management"). 현재 안전성 = by construction. → Layer 1~3planned.user_principal은 rawidpUserId()기록(RequestLoggingFilter.java:81) — pseudonymization 미적용(DRIFT-6); 알고리즘 SSOT 는 raw/branch-notes/feature-data-retention-privacy-contract (HMAC-SHA-256 + 90d salt rotation), 본 branch 는 pseudonymized 형태로만 기록 계약만 owns.
6. Sink routing & sampling
Trace: D4 (sink) + D5/D8 + Sampling Policy(final).
- UNSUPPORTED_IMPL_DECISION: sink toggle
FILE_ENABLED(default false=stdout only)는 actually-implemented. 비율 sampling(prod 10% INFO, WARN/ERROR 100%)은 logback 에TurboFilter/sampler 부재(grep) →planned. 구현 시 level<WARN + high-traffic logger 대상ch.qos.logback.classic.turbo.TurboFilter또는 marker 기반 sampler +log.appender.dropped.totalMicrometer wiring (현재 미배선, DRIFT-5).
엣지·실패·의존
R4(깊이 게이트) 캡처용. 정상 경로 외에 구현 중 부딪힐 실패/엣지/다른 계약 의존을 미리 열거.
- 실패·엣지 경로:
- logging config 오설정 → silent fallback:
LoggingSettingswarn-and-default — 잘못된 값(blank path, timezone, queueSize≤0)은 warn 로그 후 default 로 진행, startup 실패 안 함. profile 오설정이 prod 에서 잘못된 encoder/sink 를 silently 선택해도 부팅됨 → Claims To Verify 의 profile별 첫 로그 라인 assert 로 검출. - async queue 포화: 잔여 capacity ≤ discardingThreshold(20) → 유입 ≤INFO drop, WARN/ERROR 보존; 완전 포화 + neverBlock=false → caller thread block(latency spike 위험). (노트 "drop oldest" 표현 부정확 — DRIFT-5).
- regex masking false negative: Base64/URL-encoded token 등은 정규식 우회 가능 — Layer 1 구현 후 정규식 catalog 운영 검증 (Claims To Verify).
- MDC async/reactive 경계 미전파:
LOG-LBK-C4(thread-local) →@Async/reactive 에서request_id/trace_id유실. TaskDecorator 또는 Micrometer Observation propagation 필요 (Claims To Verify). - file appender prod 오활성화:
FILE_ENABLED=true가 prod 에 새면 stdout+file 이중 sink + 디스크 fill (D4 위반) — env 검증 또는 prod profile 강제 off 필요.
- logging config 오설정 → silent fallback:
- 다른 계약 의존:
- raw/branch-notes/feature-operational-error-observability-foundation D11/D19 (MdcKeys snake_case SSOT) consume — foundation 이 키 rename/추가 시 본 branch
MdcKeys.java+ encoderincludeMdcKeyName+ Log Type 필드표 동시 갱신 필요. foundation §Coverage 가 "structured JSON Logback + masking/redaction + log sampling | delegated → 본 branch" 로 위임 명시. - raw/branch-notes/feature-data-retention-privacy-contract (retention 수치 + PII field allowlist) consume — 본 branch 는 형식 만 owns(D9). retention 수치(application 30일 / security 180일 / audit 365일)는 그 branch Retention by Profile 표가 SSOT.
- raw/branch-notes/feature-distributed-tracing-contract D6 (trace sampling prod 1%) — D5 log sampling 10% 의 의도된 짝(양 노트 L74 상호 확인).
trace_id는 tracing/foundation owner, 본 branch 는 Micrometer 공급 전까지request_idmirror. - raw/branch-notes/feature-data-retention-privacy-contract —
user_principalpseudonymization 알고리즘 SSOT (HMAC-SHA-256 + 90d salt rotation, pseudonymized id ↔ original id 변환표 owns). 본 branch 는 security/audit log 에 pseudonymized 형태로만 기록 계약만 owns. foundation §2 Q12 가 본 branch/raw/branch-notes/feature-security-operational-baseline 도 후보로 열거했으나 알고리즘 결정은 data-retention 보유로 확정. - test 위임: structured log field 존재/계약 위반 테스트는 raw/branch-notes/feature-contract-verification-test-suite (
contract-verification:log-fields/log-mdc-keys).
- raw/branch-notes/feature-operational-error-observability-foundation D11/D19 (MdcKeys snake_case SSOT) consume — foundation 이 키 rename/추가 시 본 branch
Audit & Findings (ground-truth drift — 2026-06-13 /branch-spec)
ca-tmpl
src/코드 대조로 발견한 노트↔구현 drift. 사용자 작성 결정 영역 이므로 자동 rewrite 안 함 — 정합 권고만. 정합 시점에 본 § + 해당 결정/spec § 갱신.
| ID | Finding | 근거 (코드 grep) | 권고 |
|---|---|---|---|
| DRIFT-1 | <springProfile> human-readable encoder 미구현logback-spring.xml 에 <springProfile name="local,dev">=PatternLayout(%maskedMsg 포함) / <springProfile name="!local & !dev">=LogstashEncoder(JSON) 분기. CONSOLE 단일 appender명으로 async wrap 공유 |
logback-spring.xml(재작성) |
완료 |
| DRIFT-2 | LogMaskingPatterns(정규식 SSOT: token/password/secret/api-key/authorization/bearer→****, capture-group replacement) → JSON은 SecretMaskingJsonGeneratorDecorator(logstash MaskingJsonGeneratorDecorator — %replace는 JSON encoder가 PatternLayout 우회하여 부적합, Claims To Verify L291 해소), pattern은 SecretMaskingMessageConverter(%maskedMsg). 단일 catalog로 profile 전환 시 마스킹 일관성 보장 |
LogMaskingPatterns.java / SecretMaskingJsonGeneratorDecorator.java / SecretMaskingMessageConverter.java / logback-spring.xml + LogMaskingPatternsTest(token/password/bearer 마스킹 검증) |
완료 (Layer 2/3는 by-construction 보완 유지) |
| DRIFT-3 | uri_template 아닌 raw getRequestURI()RequestLoggingFilter 가 HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE 로 route template 추출, path= → uri_template= 필드명 변경, 신규 테스트(logs_uri_template_not_raw_path_when_handler_mapping_attribute_set) 추가 — actually-implemented, locally-verified |
RequestLoggingFilter.java (수정됨) / RequestLoggingFilterTest.java (신규 테스트 추가) |
완료 |
| DRIFT-4 | OutboundDependencyLogger 필드 snake_case 정합(dependency_name/dependency_type/operation/outcome/correlation_id/error) + outcome 대문자(SUCCESS/FAILURE) + dependency_type 파라미터 추가 + 5 callers(kafka/outbox/slack/google-email/cache) 타입 전달. 레벨 정책: optional fail-open=WARN 유지(유스케이스 성공 → 관측만, ERROR 미승격), core HTTP path(OutboundHttpDependencyLogger)=ERROR. duration_ms/error_code는 core HTTP path 전용으로 문서화 |
OutboundDependencyLogger.java + 5 callers + 5 tests |
완료 |
| DRIFT-5 | log.appender.dropped.total metric 미배선 + "drop oldest" 표현 부정확MetricsAsyncAppender extends AsyncAppender 가 append()에서 isQueueBelowDiscardingThreshold() && isDiscardable() 시 Metrics.counter("log.appender.dropped.total","appender",name,"level",INFO/DEBUG) 발행(Metrics.globalRegistry 경유 — Spring Boot가 앱 registry를 글로벌 composite에 추가). logback-spring.xml ASYNC_CONSOLE/ASYNC_FILE class 교체. 표현은 Sampling Policy 메모에서 정정 완료 |
MetricsAsyncAppender.java + MetricsAsyncAppenderTest(discardingThreshold>queueSize 결정론 트릭) + logback-spring.xml |
완료 |
| DRIFT-6 | user_principal raw 기록 (pseudonymization 미적용)RequestLoggingFilter 에 UserPrincipalPseudonymizer 생성자 주입, MDC.put(USER_PRINCIPAL, pseudonymizer.pseudonymize(user.idpUserId())) 로 교체(null 반환 시 미기록), 신규 테스트(puts_pseudonymized_user_principal_on_mdc_not_raw_id) 추가 — actually-implemented, locally-verified |
RequestLoggingFilter.java:81→95 (수정됨) / RequestLoggingFilterTest.java (신규 테스트 추가). 포트: UserPrincipalPseudonymizer.java(application-core); 구현체: HmacUserPrincipalPseudonymizer.java(adapter-identifier). app-bootstrap 빈 배선 완료(2026-06-14): PseudonymizationConfig(@Bean UserPrincipalPseudonymizer ← HmacUserPrincipalPseudonymizer(salt)) + PrivacySettings(@ConfigurationProperties ca-skeleton.privacy.pseudonymization-salt, warn-and-default) + APP_PRIVACY_PSEUDONYMIZATION_SALT(.env/env-keys.yaml/application.yml) |
완료 (전 경로 배선 + PseudonymizationConfigTest/PrivacySettingsTest green) |
| DRIFT-7 | logback-spring.xml Janino <if condition> 토글이 logback 1.5.x status 경고 2종 유발: IfNestedWithinSecondPhaseElementSC(<if>-in-<root>) + IfModelHandler(condition 속성 deprecated, 2027 제거예정)<if condition='property(K).equals(V)'> 6곳 → 내장 <condition class="ch.qos.logback.core.boolex.PropertyEqualityCondition"><key>K</key><value>V</value> (조회 경로 OptionHelper.propertyLookup(local,context) 동일 → context-scoped <springProperty> 값 보존, 동작 무변경). <root> 내 <if> 3개는 logback 권고대로 <root> 를 top-level <condition>+<if> 로 감싸 un-nest(ASYNC×FILE 2×2). janino dep(build.gradle) 제거 |
logback-core 1.5.34(dependencyInsight), 소스 condition= 0건, bootRun(local): |-WARN/|-ERROR 0건 + 정상 기동(Tomcat:8080) + local PatternLayout 분기 정상 |
완료 (토글 origin=base-template f9ad280; 파일 최신 owner-touch d10a751=본 branch) |
테스트 계약
- dependency failure log에는 dependency name/type/duration/error code가 있어야 함.
- token/password/body가 log에 나오면 실패.
- 5xx와 dependency failure가 ERROR 이하로 기록되면 실패.
- requestId/traceId/correlationId 없는 request log는 실패.
- domain package가 logger를 직접 사용하면 실패.
위 계약은
feature-contract-verification-test-suite가 실행 (contract-verification:log-fields/log-mdc-keys). 현 구현과의 gap 은 Audit & Findings(DRIFT-3/4/6) 참조 — 계약이 요구하는uri_template/dependency_type/pseudonymizeduser_principal이 코드에 아직 없으므로, 테스트 활성화 시 정합 작업이 선행되어야 함.
검증해야 할 주장
공식 문서나 사례는 근거지만, 내 프로젝트에서의 동작을 자동으로 보장하지 않는다. 구현 전/중/후에 실제로 검증해야 하는 주장을 분리한다.
| Claim | Why uncertain | How to verify | Status |
|---|---|---|---|
Logback %replace(p){r, t} 가 ca-tmpl logback.xml 의 token/password/auth header 정규식을 실제로 마스킹 |
regex 기반 → false negative 가능 (Base64 token 등), encoder 적용 순서 검증 필요. 현재 Layer 1 미구현(DRIFT-2) | unit test: log line 에 token=abc123 / Authorization: Bearer xxx 주입 후 final encoder output 에서 **** 확인 |
planned |
structured JSON encoder (logstash-logback-encoder) 와 %replace converter 의 적용 순서 — encoder 가 PatternLayout 우회 시 masking 누락 |
Logback 공식 페이지에 encoder vs converter 순서 명시 없음. 실제 구현은 LogstashEncoder(PatternLayout 우회) → converter 적용점 별도 설계 필요 |
integration test: JSON encoder 채택 후 masking pattern 통과 여부 확인 | planned |
| MDC 가 async/reactive thread 전환 시 자동 전파되지 않음 — TaskDecorator 또는 Micrometer Observation propagation 필요 | LOG-LBK-C4 Does not prove: MDC 는 thread-local |
async test: @Async 메서드 호출 후 MDC 의 request_id 가 유지되는지 확인 |
planned |
stdout JSON 의 ECS field naming 변환 (traceId → trace.id) 시 기존 alert/dashboard 영향 |
LOG-ECS-C2/C3 Does not prove: 자동 변환 보장 없음 |
grep 으로 alert/dashboard config 에서 traceId 사용 위치 확인 후 일괄 변환 plan |
planned |
| OTel log signal 채택 시 SeverityNumber 변환 (SLF4J INFO → OTel 9–12) 자동성 | LOG-OTEL-C4 Does not prove: SLF4J ↔ OTel 1:1 매핑 보장 없음 |
OpenTelemetryAppender 추가 후 LogRecord severity 검증 (별도 spike) |
needs-confirmation |
| log sampling 10% / trace sampling 1% 비율이 운영 진단에 충분 — drop 된 INFO 가 incident 시 사후 부족 발생 여부 | 운영 가정, 실제 traffic + incident frequency 데이터 부재. 비율 sampling 자체 미구현(planned) | prod 도입 후 1 분기 incident 회고 — log 부족으로 root cause 미해결 case 카운트 | needs-confirmation |
| audit log immutability 의 file system / object storage level 보장 (append-only 강제) | 본 branch 의 "append-only file appender" 결정은 application level 만 — OS / S3 versioning 별도 필요 | filesystem permission test + S3 object lock 정책 review | planned |
local/dev 에서 human-readable pattern, staging/prod 에서 JSON encoder 가 <springProfile> 분기로 실제 적용되는지 (D10) |
springProfile config 오류 시 silent fallback 가능 — 잘못된 profile 이 prod 에서 pattern 을 쓰거나 local 이 JSON 으로 떨어질 수 있음. 현재 분기 미구현(DRIFT-1) | profile별 boot 후 첫 로그 라인 형식 assert (local=non-JSON pattern / prod=valid JSON) + local pattern 에서도 %replace 마스킹 동작 확인 |
planned |
request log 가 uri_template(low-cardinality) 로 기록되는지 — getRequestURI() (DRIFT-3) |
high-cardinality path 가 log/metric tag 폭주 유발; BEST_MATCHING_PATTERN_ATTRIBUTE 사용으로 정합 |
logs_uri_template_not_raw_path_when_handler_mapping_attribute_set 테스트 locally-verified (BUILD SUCCESSFUL) |
actually-implemented |
관심사 커버리지 (coverage-auditor 자동 생성)
/coverage가 생성하는 생성물 — 손유지 금지. 기준:rules/coverage-gate.md. governing_docs:observability-log-metric-trace-runbook. 마지막 감사: 2026-06-13 → Covered (Blocking 0 / Should-fix 2 / Advisory 1). Should-fix 2건은 타 branch(data-retention·distributed-tracing)의## Coverage섹션 부재(UNLINKED_DELEGATION) — 본 branch 결정 범위 밖, follow-up 으로 이관.
| 관심사 | 상태 | owner | 심각도 | 근거 |
|---|---|---|---|---|
| Structured JSON Logback 스키마 (필수 필드, ECS 호환 매핑) | covered-here | — | — | D1 (LOG-LBK-C1/C3/C5) + D6 (ECS 거부); Log Type별 필수 필드 표. logback-spring.xml JSON_CONSOLE + LogstashEncoder actually-implemented |
| Log type 분류 (request/dependency/security/audit/application) | covered-here | — | — | Log Type별 필수 필드 표; mdc-keys.yaml owner_branch: feature-log-management-contract 13 key (코드 ground-truth) |
| Level 정책 (INFO 이하 sampling, WARN/ERROR 100%) | covered-here | — | — | D8; Sampling Policy (final); logback-spring.xml discardingThreshold=20 neverBlock=false actually-implemented |
| Sink 라우팅 (stdout JSON default, file local/dev only) | covered-here | — | — | D4 (LOG-12F-C1/C2/C3, LOG-ECS-AWSLOGS-C1/C2, K8s LOG-K8S-C1~C4); FILE_ENABLED toggle actually-implemented |
| Async overflow 정책 (queueSize/discardingThreshold/neverBlock) | covered-here | — | — | D8; Sampling Policy overflow 메모; AsyncAppender queueSize=512 actually-implemented |
| Sampling 정책 (prod 10% INFO / WARN·ERROR 100% / profile 표) | covered-here | — | — | D5/D8; Sampling Policy (final). 비율 TurboFilter 는 planned(DRIFT) 이나 정책 결정 은 covered |
| Masking/Redaction SSOT (Logback converter Layer 1 + Layer 2/3) | covered-here | — | — | D1; Redaction Layer SSOT §. Layer 1 planned(DRIFT-2)이나 governing doc 도 documented-only — SSOT 결정 자체는 covered |
| Alternatives evaluation (ECS vs OTel log signal vs 자체 schema) | covered-here | — | — | D6 (ECS 거부) + D7 (OTel 거부); 외부 근거 / 대안 조사 §. governing doc 이 본 branch 를 대안 검토 owner 로 명시 |
| Per-profile console encoder (D10) | covered-here | — | — | D10; Decisionized Work Items console format 행. 구현 planned(DRIFT-1)이나 결정 은 covered |
| MDC key naming SSOT | delegated | raw/branch-notes/feature-operational-error-observability-foundation | OK | D2 + §MDC Key Consumption. foundation §Coverage 가 "structured JSON Logback + masking/redaction + log sampling → 본 branch" 위임 명시 (양방향 링크 확인) |
| Retention 수치 + PII field allowlist | delegated | raw/branch-notes/feature-data-retention-privacy-contract | Should-fix | D9 + §Audit. owner 노트에 ## Coverage 섹션 부재 + 역링크 평문(UNLINKED_DELEGATION-1) — follow-up: 해당 branch 에 Coverage 추가 + wikilink 정식화 |
| Trace sampling + traceparent 전파 | delegated | raw/branch-notes/feature-distributed-tracing-contract | Should-fix | D5 + §엣지(상호 확인된 짝). owner 노트에 ## Coverage 섹션 부재(UNLINKED_DELEGATION-2) — follow-up |
user_principal pseudonymization 알고리즘 |
delegated | raw/branch-notes/feature-data-retention-privacy-contract | Advisory | 알고리즘 SSOT = data-retention 의 HMAC-SHA-256 + 90d salt rotation 결정(coverage-auditor 확인). 본 branch 는 pseudonymized 형태로만 기록 계약만 owns. 코드 raw idpUserId() 기록은 DRIFT-6 으로 capture — 정합 시 owner 알고리즘 적용 |
마주친 문제
- 아직 없음.
묶음
- raw/official-docs/container-stdout-logging-12factor-official
- raw/official-docs/ecs-awslogs-stdout-cloudwatch-aws-official
- raw/official-docs/k8s-logging-architecture-kubernetes-official
- raw/official-docs/log-ecs-schema-elastic-official
- raw/official-docs/log-logback-mask-pattern-converter-official
- raw/official-docs/log-otel-log-data-model-spec
Phase C2 실 구현 시작(2026-06-14): DRIFT-6 포트 인터페이스 생성. 이후 errors / interview prep 누적 시 추가.
오류 기록 (본 feature 작업 중 발생)
@ComponentFilter 에 생성자 의존성 추가 →@WebMvcTest슬라이스 컨텍스트 로드 실패 (OperationalContractRuntimeTest).addFilters=false여도@WebMvcTest는Filter빈을 인스턴스화 하므로RequestLoggingFilter(UserPrincipalPseudonymizer)가 빈 부재로NoSuchBeanDefinitionException. 광역 스캔(@WebMvcTest(CaSkeletonApplication))만 영향, 패키지-국한 슬라이스(sample-portfolio)는 무영향. 해결=@Import(PseudonymizationConfig.class). → raw/errors/webmvctest-component-filter-constructor-dep-breaks-slice-2026-06-14- Pre-existing(본 작업 무관) ArchUnit 실패:
outbound_adapter_method_returns_only_domain_or_primitivesonOutboundHttpSettings.circuitBreaker()/retry()(@ConfigurationProperties record accessor가 nested config record 반환). commit d702572 도입, stash한 clean tree에서도 실패로 확인. B7 규칙이 @ConfigurationProperties accessor를 false-positive로 잡는 rule-precision 이슈 — feature-boundary-validation-mapping-contract / outbound-http-client-baseline 소유. 본 브랜치 scope 외, 미수정.
면접 준비 (이 작업에서 나올 수 있는 면접 질문)
- HMAC-SHA-256 을 pseudonymizer 로 선택한 이유 / 단방향성 증명 방법 / salt rotation 90일 근거 —
feature-data-retention-privacy-contractSSOT consume. javax.crypto.Mac이 thread-safe 하지 않은 이유 + singleton bean 에서 thread safety 확보 방법 (per-call 인스턴스 생성 vs ThreadLocal vs instance pool).HexFormat.of().formatHex(byte[])— Java 17 도입 API, 기존String.format("%02x")루프 대비 장점.- 구조화 JSON 로그 마스킹:
%replace가LogstashEncoder(JSON)에 안 걸리는 이유 +MaskingJsonGeneratorDecorator대안. AsyncAppender 드롭 메트릭 결정론 테스트. → raw/interviews/deterministic-logback-asyncappender-drop-metric-test-2026-06-14
Blog topics
- "Spring-free 모듈에서 crypto adapter 구현하기 —
adapter-identifier설계 결정" (HMAC-SHA-256 구현,Macthread safety,implementationvsapiGradle 선택, forbidden Spring 어노테이션) - "Logback Layer 1 secret masking:
%replace로는 JSON을 못 가린다 — 단일 정규식 SSOT로 encoder/pattern 양 경로 일관 마스킹" → raw/blog-topics/logback-layer1-secret-masking-json-vs-pattern-2026-06-14
관련 일일 노트
이 브랜치를 작업한 날짜들. 양방향 nav 유지.
- raw/daily-notes/2026-06-14 (DRIFT-6 UserPrincipalPseudonymizer 포트 인터페이스 생성 + HmacUserPrincipalPseudonymizer 구현체 생성 + DRIFT-3/DRIFT-6 RequestLoggingFilter 연결 — uri_template 로그 + pseudonymized user_principal MDC)
완료 후 정리
- PR 링크:
- 리뷰 메모:
- 머지 결과 / 배포 환경:
- wiki 추출 대상 (verified만,
wiki/projects/로만 추출):actually-implemented항목:locally-verified항목:prod-verified항목:
- 추출하지 않을 항목 (planned / documented-only / abandoned):