Files
llm-wiki/raw/branch-notes/feature-startup-failure-log-suppression.md

292 lines
20 KiB
Markdown

---
title: branch / feature-startup-failure-log-suppression
source_type: branch-note
status: raw
branch: feature-startup-failure-log-suppression
parent_branch:
related_projects: [ca-tmpl]
tags: [branch, ca-tmpl, runtime, spring-boot, error-handling, flyway, log-routing]
created: 2026-07-03
target_merge:
status_label: in-progress
id: BR-CA-SKELETON-OPERATIONAL-CONTRACT-058
kind: project-work-item
project: ca-skeleton-operational-contract
work_item: WI-CA-SKELETON-OPERATIONAL-CONTRACT-058
inherits: [DEC-CA-SKELETON-OPERATIONAL-CONTRACT-BOOTSTRAP-001@1, DEC-CA-SKELETON-OPERATIONAL-CONTRACT-ERROR-ENVELOPE-001@1]
refines: []
overrides: []
depends_on: [WI-CA-SKELETON-OPERATIONAL-CONTRACT-001, WI-CA-SKELETON-OPERATIONAL-CONTRACT-017]
contract_packet: 1
contract_packet_sha256: e7c1afc830ee67bc838ff152355faa14fe6c26669d8914655bda307ea186532b
---
# branch: feature-startup-failure-log-suppression
> Layer: `raw/branch-notes/` — ca-tmpl startup failure logging 작업 기록.
> 실제 git branch: `develop`.
<!-- section-id: branch-parent -->
## 부모 (필수)
- [[raw/project-notes/ca-skeleton-operational-contract]]
<!-- GENERATED: branch-contract:start -->
<!-- section-id: branch-contract-packet -->
## 브랜치 계약 패킷
- **생성 시 프로젝트 개정**: `1`
- **패킷 스키마**: `contract_packet: 1`
- **완료 조건**: suppressible startup failure 조건과 retained actionable error test가 명시된다
<!-- section-id: inherited-project-decisions -->
### 상속한 프로젝트 결정
| Decision Ref | Project Summary | Branch Application | Source |
|---|---|---|---|
| `DEC-CA-SKELETON-OPERATIONAL-CONTRACT-BOOTSTRAP-001@1` | 신규 환경의 default 진입 명령은 ./gradlew bootstrap이다 | Work Item 완료 조건에 적용 | [[raw/project-notes/ca-skeleton-operational-contract]] |
| `DEC-CA-SKELETON-OPERATIONAL-CONTRACT-ERROR-ENVELOPE-001@1` | foundation이 envelope schema와 error.category enum의 단일 owner다 | 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 -->
## 목표
- Spring Boot startup failure에서 ca-tmpl의 구조화 `MIGRATION_FAILED` 로그와 Spring Boot 기본
`Application run failed` stacktrace가 함께 출력되는 문제를 줄인다.
- 목표 정책: startup failure는 `startup.phase`, `error.code`, `error.category`, root-cause 요약만
남기고 framework/driver stacktrace는 application log에 출력하지 않는다.
<!-- section-id: branch-scope -->
## 범위
### 포함 범위
- `app-bootstrap` startup failure logging 계약 변경.
- `StartupFailures` canonical log에서 SLF4J throwable 인자 제거.
- `SpringBootExceptionReporter`로 typed startup failure의 Spring Boot 기본 실패 report 억제.
- Logback `TurboFilter`로 startup failure 이후의 SpringApplication 중복 close/report message 억제.
- settings/validator startup failure를 `StartupFailures.envValidation(...)`로 통일.
- Spring Boot context refresh cancellation / failure analysis residual log 억제.
- focused tests, `:app-bootstrap:test`, `verifyCleanArchitectureDependencies`, `check` 검증.
### 제외 범위
- runtime HTTP exception response shape 변경.
- persistence `DB_*` SQLState matrix 변경.
- production 환경 로그 검증.
## 근거
| Source | 정당화하는 결정 |
|---|---|
| Local code evidence: `src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/runtime/startup/StartupFailures.java` | 기존 canonical startup failure log가 throwable cause를 SLF4J에 넘겨 stacktrace를 출력하던 사실 확인. |
| Local dependency evidence: `javap org.springframework.boot.SpringApplication` | `SpringBootExceptionReporter#reportException``true`를 반환하면 Spring Boot가 failure를 logged exception으로 등록하고 generic report path를 종료하는 흐름 확인. |
| Local test evidence: `./gradlew check` | 전체 Gradle guard 통과로 구현·검증 결과 확인. |
## TODO
- [x] Startup failure canonical log에서 throwable proxy 제거 — 등급: `actually-implemented`
- [x] root-cause class/message 구조화 필드 추가 — 등급: `actually-implemented`
- [x] startup failure 전용 `SpringBootExceptionReporter` 등록 — 등급: `actually-implemented`
- [x] SpringApplication 중복 close/report log filter 추가 — 등급: `actually-implemented`
- [x] app-bootstrap settings/validator plain startup exception을 `StartupValidationException`으로 번역 — 등급: `actually-implemented`
- [x] Spring Boot context cancellation / failure analysis residual log filter 확장 — 등급: `actually-implemented`
- [x] DB down / invalid tracing sample rate `bootRun` 재현 검증 — 등급: `locally-verified`
- [x] focused tests, `:app-bootstrap:test`, `verifyCleanArchitectureDependencies` 실행 — 등급: `locally-verified`
## 진행 중 메모
- TDD RED로 `StartupFailuresTest`가 기존 throwable proxy 때문에 실패하는 것을 먼저 확인했다.
- `SpringBootExceptionReporter`만으로는 `Unable to close ApplicationContext` WARN을 제어하지 못하므로,
canonical startup failure가 이미 기록된 뒤 SpringApplication의 exact duplicate message만 차단하는
Logback filter를 추가했다.
- `./gradlew check` 첫 실행은 Spotless formatting 위반으로 실패했고, `:app-bootstrap:spotlessApply`
적용 후 재실행에서 통과했다.
- 2026-07-03 후속 hardening: `ConfigurationProperties` record와 runtime startup validator가 plain
`IllegalStateException`/`IllegalArgumentException`을 던지던 사각을 `StartupFailures.envValidation(...)`
으로 통일했다.
- invalid tracing sample rate 재현에서 기존 `BindException`/`NumberFormatException`/FailureAnalysis 출력은
compact `STARTUP_VALIDATION_FAILED` 로그 1줄로 축소되었다.
- DB down migration 재현에서 기존 context refresh cancellation `BeanCreationException` WARN은 더 이상
grep 결과에 나타나지 않았다. 남은 Spring `BeanPostProcessorChecker`/Micrometer WARN은 exception report가
아니라 별도 framework lifecycle/noise 축이다.
## 결정 사항
- 2026-07-03: startup failure log는 stacktrace 대신 root-cause summary field만 남긴다 / 이유:
운영자가 분류할 수 있는 정보는 유지하면서 driver/framework stacktrace 노출과 중복을 줄이기 위해 /
검토한 대안: 중복 제거만, profile별 stacktrace 분기 / 근거: local code + test evidence.
- 2026-07-03: Spring Boot generic `Application run failed``SpringBootExceptionReporter`로 typed
startup failure에 한해 억제한다 / 이유: unknown startup failure의 Boot 기본 진단은 유지하기 위해 /
검토한 대안: `org.springframework.boot.SpringApplication` logger 전체 off / 근거: local dependency
evidence.
- 2026-07-03: context close 중복 WARN은 marker 기반 Logback filter로 exact message만 차단한다 / 이유:
reporter 이후 context close 단계에서 발생하는 별도 SpringApplication WARN을 좁은 범위로 억제하기 위해 /
검토한 대안: logger 전체 off, 방치 / 근거: attached runtime log + local test evidence.
- 2026-07-03: startup validation/settings failure는 plain Java exception 대신 `StartupFailures.envValidation(...)`
으로 번역한다 / 이유: Boot binding/context failure path에 들어가더라도 cause chain에
`StartupFailureException`이 포함돼 compact reporter/filter 정책이 적용되게 하기 위해 / 검토한 대안:
reporter가 모든 `IllegalStateException`을 잡도록 확장, Spring Boot failure analyzer logger만 억제 / 근거:
local bootRun 재현 + focused tests.
- 2026-07-03: `LoggingFailureAnalysisReporter`와 Spring context refresh cancellation log는 startup failure
marker가 켜진 뒤에만 filter에서 억제한다 / 이유: unknown boot failure 진단은 보존하고, 이미 compact
startup failure가 기록된 중복 exception detail만 제거하기 위해 / 검토한 대안: Spring logger level 조정,
failure analysis reporter 전체 비활성 / 근거: local bootRun 재현 + filter tests.
## 결정-근거 매핑
| Decision ID | Decision | Supporting Claims | Evidence Strength | Open Risk |
|---|---|---|---|---|
| D1 | `StartupFailures`는 cause를 예외에는 보존하되 SLF4J throwable 인자로 넘기지 않고 root-cause class/message만 로그 구조화 필드로 남긴다. | UNSUPPORTED_DECISION — local code inspection and user-approved policy; trade-off: stacktrace triage detail is removed from startup logs. | actually-implemented + locally-verified | 운영자가 전체 stacktrace를 로그에서 바로 보지 못하므로 재현 환경에서 cause chain 확인이 필요할 수 있다. |
| D2 | `StartupFailureExceptionReporter`는 cause chain에 `StartupFailureException`이 있을 때만 `true`를 반환해 Boot generic failure report를 억제한다. | UNSUPPORTED_DECISION — local `javap` inspection of Spring Boot failure reporting path; trade-off: official doc raw source was not captured in this task. | actually-implemented + locally-verified | Spring Boot internal flow가 major upgrade에서 바뀌면 reporter 효과를 재검증해야 한다. |
| D3 | `StartupFailureSpringBootLogFilter`는 canonical startup failure 이후 SpringApplication의 `Application run failed``Unable to close ApplicationContext` exact message만 차단한다. | UNSUPPORTED_DECISION — attached log symptom + local filter tests; trade-off: process-local marker assumes startup failure is fatal. | actually-implemented + locally-verified | 동일 JVM에서 startup failure 후 테스트가 계속되는 특수 상황은 marker reset test helper에 의존한다. |
| D4 | Wiki branch-note slug는 logical work unit `feature-startup-failure-log-suppression`을 사용하고, 실제 git branch `develop`은 본문에 기록한다. | UNSUPPORTED_DECISION — LLM Wiki naming lint rejects `develop.md`; trade-off: ca-tmpl branch-name capture와 wiki naming gate 사이의 충돌을 wiki document shape 우선으로 해결. | documented-only | git branch 기준 검색 시 logical note slug를 한 번 더 확인해야 한다. |
| D5 | app-bootstrap startup settings/validators는 invalid config를 plain Java exception이 아니라 `StartupFailures.envValidation(...)`으로 던진다. | UNSUPPORTED_DECISION — local bug reproduction and user-approved policy; trade-off: direct constructor tests now observe `StartupValidationException` instead of `IllegalArgumentException`. | actually-implemented + locally-verified | `LoggingSettings`처럼 startup validation owner가 아닌 warn-and-default bootstrap helper는 별도 정책 예외로 남는다. |
| D6 | startup failure marker 이후 `LoggingFailureAnalysisReporter`와 Spring context refresh cancellation log를 filter에서 억제한다. | UNSUPPORTED_DECISION — local bootRun symptom and filter tests; trade-off: fatal startup failure window에서 Spring Boot failure-analysis banner를 숨긴다. | actually-implemented + locally-verified | Spring Boot logger/message 이름이 major upgrade에서 바뀌면 bootRun 재현 테스트로 재확인 필요. |
## 구현 가이드
### 1. Canonical startup log
> **Trace**: D1
>
> - **UNSUPPORTED_IMPL_DECISION**: structured field 이름은 `error.root_cause.class`와
> `error.root_cause.message`를 사용했다. 기존 `error.code`/`error.category` dotted naming과 맞춘
> local convention이다.
| File | 구현 |
|---|---|
| `StartupFailures.java` | cause가 있을 때 `rootCause(cause)`를 찾아 class/message를 structured argument로 기록하고, throwable 인자는 넘기지 않는다. |
| `StartupFailuresTest.java` | migration failure log event의 `ThrowableProxy`가 null이고 root-cause summary field가 있는지 검증한다. |
### 2. Spring Boot duplicate report suppression
> **Trace**: D2, D3
>
> - **UNSUPPORTED_IMPL_DECISION**: `SpringBootExceptionReporter`와 Logback `TurboFilter`를 함께 사용했다.
> reporter는 generic report path만 막고, filter는 reporter 이후 context close duplicate WARN만 좁게 막는다.
| File | 구현 |
|---|---|
| `StartupFailureExceptionReporter.java` | cause chain에 `StartupFailureException`이 있으면 `true`, 아니면 `false`. |
| `META-INF/spring.factories` | `org.springframework.boot.SpringBootExceptionReporter` key로 reporter 등록. |
| `StartupFailureLogState.java` | canonical startup failure가 기록됐는지 process-local marker 제공. |
| `StartupFailureSpringBootLogFilter.java` | marker가 켜진 뒤 `org.springframework.boot.SpringApplication`의 exact duplicate messages만 `DENY`. |
| `logback-spring.xml` | startup failure duplicate filter를 turbo filter로 등록. |
### 3. Startup validation translation hardening
> **Trace**: D5, D6
>
> - **UNSUPPORTED_IMPL_DECISION**: settings record compact constructor도 `StartupFailures.envValidation(...)`
> 을 직접 호출한다. `app-bootstrap`의 운영 설정 검증이며 비즈니스 규칙이 아니므로 composition-root
> 책임 안에 둔다.
| File | 구현 |
|---|---|
| `AsyncExecutorSettings.java`, `IdempotencySettings.java`, `OutboxSettings.java`, `TracingSettings.java` | invalid runtime setting을 `StartupFailures.envValidation(...)`으로 변환한다. |
| `RuntimeNumericBoundsValidator.java`, `HikariPoolConstraintValidator.java`, `OpenInViewSafetyValidator.java`, `SecretSourceValidator.java` | startup safety guard의 plain exception을 `StartupFailures.envValidation(...)`으로 변환한다. |
| `StartupFailureSpringBootLogFilter.java` | marker 이후 `LoggingFailureAnalysisReporter` 전체와 Spring context refresh cancellation prefix를 `DENY`한다. |
| focused settings/validator/filter tests | invalid path가 `StartupValidationException`으로 번역되고 residual Boot logs가 filter에서 차단되는지 검증한다. |
## 엣지·실패·의존
- **실패·엣지 경로**: cause chain self-reference는 reporter와 root cause walker가 무한 루프를 피해야 한다.
- **실패·엣지 경로**: non-startup exception은 Spring Boot 기본 failure report를 유지해야 한다.
- **다른 계약 의존**: `app-bootstrap` logging bootstrap과 Spring Boot `spring.factories` loading path에 의존한다.
## 검증해야 할 주장
| Claim | Why uncertain | How to verify | Status |
|---|---|---|---|
| 실제 bootRun에서 DB down 시 `Application run failed`, context refresh cancellation, `BeanCreationException` stacktrace가 출력되지 않는다. | 2026-07-03 후속 hardening에서 실제 재현 수행. | `APP_DATASOURCE_URL='jdbc:postgresql://127.0.0.1:1/ca_skeleton' timeout 60s ./gradlew :app-bootstrap:bootRun --quiet 2>&1 \| rg -n "startup failure\|Application run failed\|Exception encountered during context initialization\|LoggingFailureAnalysisReporter\|BeanCreationException\|ConnectException\|Caused by:" -C 2` | locally-verified |
| 실제 bootRun에서 invalid tracing sample rate가 `BindException`/`NumberFormatException` stacktrace 대신 compact startup failure로 출력된다. | 2026-07-03 후속 hardening에서 실제 재현 수행. | `APP_MIGRATION_ON_STARTUP=false APP_TRACING_SAMPLE_RATE='not-a-number' timeout 60s ./gradlew :app-bootstrap:bootRun --quiet 2>&1 \| rg -n "startup failure\|Application run failed\|Exception encountered during context initialization\|LoggingFailureAnalysisReporter\|BindException\|NumberFormatException\|Caused by:" -C 2` | locally-verified |
| Spring Boot major upgrade 후에도 `SpringBootExceptionReporter`의 true-return behavior가 동일하다. | local dependency bytecode 확인에 기반한 결정이다. | Spring Boot upgrade branch에서 reporter focused test와 실제 startup failure 로그 재현. | needs-confirmation |
## 관심사 커버리지
| 관심사 | 상태 | owner | 심각도 | 근거 |
|--------|------|-------|--------|------|
| startup failure canonical log | covered-here | — | — | D1 |
| Spring Boot duplicate failure report | covered-here | — | — | D2, D3, D6 |
| app-bootstrap startup validation/settings plain exception | covered-here | — | — | D5 |
| runtime HTTP exception response | delegated | existing adapter-web error contract | OK | Out of scope |
| runtime background `log(..., ex)` stacktrace | delegated | future runtime logging hardening | OK | Out of scope |
## 마주친 문제
- Spotless formatting failure
- 원인: 새 Java 파일의 line wrapping이 Spotless 규칙과 달랐다.
- 시도: `./gradlew check` 실행.
- 해결: `./gradlew :app-bootstrap:spotlessApply``./gradlew check` 재실행.
- 별도 에러 노트로 분리됨: [[raw/errors/startup-log-suppression-spotless-format-2026-07-03]]
- 2026-07-03 후속 hardening 중 TDD RED failures
- 원인: 의도적으로 settings/validator tests를 `StartupValidationException` 기대치로 먼저 바꿔 기존 plain exception 사각을 재현.
- 해결: `StartupFailures.envValidation(...)` 전환 후 focused suite green.
- 별도 에러 노트: 없음. 의도된 RED 단계로 별도 트러블슈팅 문서화 대상 아님.
- 2026-07-03 전체 `check` 실패
- 원인: `:sample-portfolio:test` web context startup 중 Tomcat `PortInUseException`/`BindException`.
- 시도: import order Spotless failure 수정 후 `./gradlew check` 재실행.
- 해결: 이번 변경 범위 밖의 sample-portfolio test/runtime port collision으로 분리 기록. focused startup/logging suite, `:app-bootstrap:test`, `verifyCleanArchitectureDependencies`, bootRun 재현은 통과/확인.
- 별도 에러 노트로 분리됨: [[raw/errors/sample-portfolio-tomcat-port-in-use-check-2026-07-03]]
## 묶음 (이 branch에서 파생된 자료)
<!-- GENERATED: errors:start -->
- [[raw/errors/sample-portfolio-tomcat-port-in-use-check-2026-07-03]]
- [[raw/errors/startup-log-suppression-spotless-format-2026-07-03]]
<!-- GENERATED: errors:end -->
### Sub-branches (세부 작업)
- 없음.
### 오류 기록 (이 branch 작업 중 발생)
- [[raw/errors/startup-log-suppression-spotless-format-2026-07-03]] — `check` 중 Spotless formatting failure.
- [[raw/errors/sample-portfolio-tomcat-port-in-use-check-2026-07-03]] — 전체 `check` 중 sample-portfolio Tomcat port collision.
- 2026-07-03 후속 hardening: TDD RED와 경로 오입력은 작업 중 검증/도구 사용 이슈로 branch-note에만 기록.
### 면접 준비 (이 작업에서 나올 수 있는 면접 질문)
- 추출할 별도 면접 질문 없음.
### 강의 (이 작업을 위해 학습한 강의)
- 없음.
### job-posting tie-ins (이 작업에서 파생된 글감)
- 후보: startup failure compact logging hardening은 블로그 글감으로 확장 가능하나, 이번 캡처에서는 별도 raw/blog-topic으로 분리하지 않음.
## 관련 일일 노트
- 없음.
## 완료 후 정리
- PR 링크:
- 리뷰 메모:
- 머지 결과 / 배포 환경: local verification only.
- **wiki 추출 대상**:
- `actually-implemented` 항목: startup failure stacktrace suppression implementation; startup settings/validator exception translation hardening; residual Spring Boot failure-analysis/context-cancellation log filter.
- `locally-verified` 항목: focused tests, `:app-bootstrap:test`, `verifyCleanArchitectureDependencies`, DB down bootRun reproduction, invalid tracing sample rate bootRun reproduction.
- `prod-verified` 항목: 없음.
- **추출하지 않을 항목**:
- 전체 `./gradlew check`: `:sample-portfolio:test``PortInUseException`으로 실패. 변경 범위와 분리해 raw error note에 기록.
- runtime background `log(..., ex)` stacktrace cleanup은 별도 future work.