--- 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`. ## 부모 (필수) - [[raw/project-notes/ca-skeleton-operational-contract]] ## 브랜치 계약 패킷 - **생성 시 프로젝트 개정**: `1` - **패킷 스키마**: `contract_packet: 1` - **완료 조건**: suppressible startup failure 조건과 retained actionable error test가 명시된다 ### 상속한 프로젝트 결정 | 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]] | ### 브랜치 지역 결정 > 기존 branch-local 결정은 아래 `## Decision Evidence Map / 결정-근거 매핑`의 D-row가 소유하며 이 packet에서 복제하지 않는다. | Decision ID | Decision | Relation | Supporting Claims | Status | |---|---|---|---|---| ### 선언한 예외 | Override ID | Overrides | Reason | Approval | Status | |---|---|---|---|---| ## 목표 - 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에 출력하지 않는다. ## 범위 ### 포함 범위 - `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에서 파생된 자료) - [[raw/errors/sample-portfolio-tomcat-port-in-use-check-2026-07-03]] - [[raw/errors/startup-log-suppression-spotless-format-2026-07-03]] ### 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.