글감 1,001개 중 제1부(§3~§11) 앵커를 하나라도 가진 것은 112개뿐이었다. 나머지 889개는
제2부 모듈 분석 65편의 절 제목에서 나온 것이고, 그것이 재판정이 필요했던 이유다.
주제 44 → 16 (43개가 독자 질문 없이 있었다. 지금은 전부 있다)
글감 1,001 → 123 (제1부 앵커 112 + 제1부가 채택했는데 비어 있던 자리 11)
후보 965 → 1,088 · PENDING 905 → 0
error 3,042 → 0
내려온 889개는 후보 대장에 KEEP_IN_SSOT 로 남는다 — 버린 것이 아니라 분석에 남기고 독립
기록으로 만들지 않기로 한 것이다. 그 글감을 받치던 기록 파일 828개는 지웠다. 계약이 정본이고,
파일이 남아 있다는 이유로 계약에서 뺀 주제가 되살아나면 안 된다. 이력에는 그대로 있다 —
git checkout a0ca2bb -- <경로>.
제1부가 채택했는데 글감이 없던 자리 열하나를 채웠다: mongo high-water mark 가 재전달 이벤트를
삼킨 P1, admin plane 이 가드만 켜고 서비스는 켜지 않은 것과 그 짝인 결정, 실패 어휘 세 층과
SQLState 매트릭스 병합 규칙, 부하 아래에서만 새는 admission 경계, 발행 증거와 완료 판정의
분리, keyset·JSONB 결정 둘.
Concept 17개에 basis-version 을 채우고, 계약 제목과 기록 제목이 갈라져 있던 23건을 기록 쪽에
맞췄다. candidateScope 에 excludedAnchorPattern 을 적어 제2부 앵커만 가진 글감이 다시 올라올
수 없게 한다.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
8.2 KiB
kind: CASE slug: a-five-second-string-that-broke-every-prod-deploy title: connection-timeout: 5s가 모든 prod 배포를 시작 실패시켰고 local만 통과했다 topic: transaction-deadline-and-pool project: clean-architecture-backend-template status: 게시 전 sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 rootTreeNode: case:a-five-second-string-that-broke-every-prod-deploy evidenceCapturedOn: 2026-09-02 body: case-a-five-second-string-that-broke-every-prod-deploy.body.md assets:
- key: a-five-second-string-that-broke-every-prod-deploy-bind file: ../../../final/evidence/rendered/a-five-second-string-that-broke-every-prod-deploy-bind.svg
- key: a-five-second-string-that-broke-every-prod-deploy file: ../../../final/evidence/rendered/a-five-second-string-that-broke-every-prod-deploy.svg evidence:
- ../../../final/evidence/raw/a-five-second-string-that-broke-every-prod-deploy-bind.txt
- ../../../final/evidence/raw/a-five-second-string-that-broke-every-prod-deploy.txt source:
- 이 사건의 1차 기록은
app-bootstrap/src/main/resources/application.yml의 해당 키 주석이다. 분석 문서 쪽에는 이 사건을 다루는 절이 없고, 후보 원장이 지정한final/document.md#a05의 절 번호는 그 파일에 존재하지 않는다. 원장 자신도 이 항목의 근거를 설정 파일 주석의 사후 기록으로 적고 있다.
connection-timeout: 5s가 모든 prod 배포를 시작 실패시켰고 local만 통과했다
풀 연결 타임아웃 기본값이 5s 로 출하됐다. 이 키는 long 밀리초에 바인딩되므로 문자열은 변환 단계에서 거절된다. 로컬 프로파일이 값을 덮어쓰고 있어서 로컬로 띄우는 실행은 통과했다.
관계
- 로컬이 다른 DB면 로컬 테스트는 다른 시스템에 대한 진술이다 통과한 레인이 다른 값을 보고 있었다.
- validator가 요청을 서비스하지 않는 datasource를 검증하고 있었다 같은 app-bootstrap 모듈에서 시작 검증기가 다른 것을 보고 있던 사례다.
문제
그 속성의 바인딩 대상은 HikariConfig 의 setConnectionTimeout(long) 세터다. 밀리초 정수만 받는다.
같은 출하 파일의 server.tomcat.connection-timeout 은 지속 시간에 바인딩되고 20s 로 적혀 있다. 마지막 단어가 같고 형식이 다르다.
결론
프로파일 오버라이드가 형식 결함을 가렸다. 로컬에는 오버라이드가 있었고 나머지에는 없었다.
풀 제약 검증기는 같은 키를 DurationStyle 로 읽어서 5s 를 5000 밀리초로 받는다. 다만 이 검증기는 모든 싱글턴이 만들어진 뒤에 도는 자리에 있어서, 이 값으로는 그 전에 빈 생성이 실패한다.
지금은 출하 기본값이 정수로 고쳐져 있고, 밀리초 키 일곱 개의 출하 기본값이 정수인지 확인하는 정적 가드가 있다.
검증 환경
Spring Boot : 4.0.8 HikariCP : 7.0.2 OpenJDK : 21.0.12 확인 방식 : 출하 설정과 바인딩 대상 확인, 실제 바인더로 두 형식 주입 소스 수정 : x
재현 조건
- 오버라이드가 없는 프로파일에 spring.datasource.hikari.connection-timeout=5s 를 두고 부팅하면 HikariDataSource 빈 생성에서 바인딩이 실패한다.
- 같은 키를 저장소가 쓰는 Spring Boot·HikariCP 산출물의 바인더에 직접 넣으면 같은 예외 사슬이 나온다.
- 같은 출하 파일에서 server.tomcat.connection-timeout 의 형식을 확인한다.
- 시작 검증기가 같은 키를 어떤 파서로 읽는지, 어느 시점에 도는지 확인한다.
본문
spring.datasource.hikari.connection-timeout 은 HikariConfig#setConnectionTimeout(long) 에 바인딩된다. 밀리초 정수만 받는 자리다.
기본값이 5s 로 출하됐다. 지금은 ${APP_DATASOURCE_CONNECTION_TIMEOUT:5000} 로 고쳐져 있다.
통과한 레인은 그 값을 보고 있지 않았다
설정 파일의 주석이 사건을 기록한다. prod 와 dev 배포가 전부 시작에 실패했고, 로컬은 application-local.yml 이 값을 덮어쓰고 있어서 통과했다는 것이다. 그래서 결함을 만날 수 있는 곳이 prod 레인뿐이었다.
테스트 레인도 이 기본값을 보지 않는다. application-test.yml 이 자기 몫으로 connection-timeout: 30000 을 들고 있다.
바인더에 직접 넣으면 어디서 끊기는지 보인다
:::evidence key="a-five-second-string-that-broke-every-prod-deploy-bind" alt="저장소가 쓰는 Spring Boot 와 HikariCP 산출물로 같은 속성 키에 두 형식을 넣어 실제로 바인딩한 실행 결과. 지속 시간 문자열은 예외 사슬 세 단계로 거절되고 정수는 값이 그대로 들어간다." caption="Spring Boot 4.0.8 · HikariCP 7.0.2 — 같은 키, 두 형식, 실제 바인딩" zoom="true" :::
예외가 세 단계로 내려간다. 바인딩 실패, 문자열에서 long 으로의 변환 실패, 그리고 "5s" 에 대한 숫자 형식 예외다. 같은 자리에 5000 을 넣으면 값이 그대로 들어간다.
같은 파일에 같은 이름이 두 형식으로 있다
:::evidence key="a-five-second-string-that-broke-every-prod-deploy" alt="코드베이스에서 출하 설정 파일의 두 네임스페이스에 같은 이름의 키가 다른 형식으로 있는 것과, 앞쪽이 바인딩되는 세터의 long 시그니처, 같은 키를 읽는 시작 검증기의 인터페이스와 파싱 함수, 그리고 그 형식을 막는 정적 가드의 키 목록과 그 가드가 한 번 헛짚었던 자리를 뽑은 출력. 한 파일 안에서 같은 마지막 단어가 정수와 지속 시간 두 형식으로 쓰인다는 것이 그 출력에 그대로 보인다." caption="같은 파일의 두 네임스페이스 · long 세터 · 검증기의 다른 파서 · 정적 가드의 키 목록" zoom="true" :::
hikari 아래의 키는 정수를 받고, tomcat 아래의 키는 20s 로 적혀 있다. 전체 경로는 다르고 마지막 단어만 같다.
검증기는 이 값을 다르게 읽고, 이 값에는 도달하지 못한다
풀 제약 검증기가 같은 키를 읽는다. 파서는 DurationStyle.detectAndParse 다. 그 규칙에서 5s 는 5000 밀리초이고 하한을 넘으므로 유효한 값이다.
다만 이 검증기는 SmartInitializingSingleton 이라 모든 싱글턴이 만들어진 뒤에 돈다. 5s 로는 그 전에 HikariDataSource 빈 생성에서 바인딩이 죽는다. 검증기가 잘못된 값을 통과시킨 것이 아니라, 이 값에 대해 발언할 수 있는 자리에 있지 않았다.
두 문서가 같은 관용을 반대로 부른다
설정 파일의 주석은 그 관용을 결함으로 부른다. 잘못된 기본값이 지원되는 것처럼 보이게 만든 것이 그 관용이고, 여기서 조용히 고치는 대신 열린 항목으로 기록해 둔다고 적는다.
파서 쪽 javadoc 에는 Spring Boot 가 지속 시간 속성에 받아 주는 밀리초 정수와 단순 지속 시간과 ISO-8601 을 똑같이 받는다고 계약으로 적혀 있다. 그리고 테스트 하나가 connection-timeout=5s 와 validation-timeout=PT3S 로 컨텍스트가 실패하지 않음을 고정한다.
한쪽은 열린 결함이라 적고 다른 쪽은 고정된 계약이라 적는다. 두 기술은 서로를 참조하지 않는다.
같은 함정을 이름으로 막는다
정적 가드가 있다. 출하 설정 파일 넷을 Spring 의 yaml 로더로 직접 읽어 평탄화한 뒤, 밀리초 정수에 바인딩되는 키 일곱 개의 출하 기본값이 정수인지 확인한다.
부팅이 아니라 스캔이라 타입 목록을 손으로 들고 있다. 밀리초 키가 새로 생기면 그 목록에 줄을 더하는 것이 유일한 편입 경로다.
가드 자신의 주석이 초기 버전의 실패도 적어 둔다. 키의 마지막 단어를 정규식으로 맞춰서, 진짜 지속 시간인 server.tomcat.connection-timeout: 20s 를 결함으로 보고했다는 것이다. 실제 로더로 평탄화하도록 고친 이유가 그것이다.
확인하지 못한 것
실패 시점을 보려고 애플리케이션 전체를 부팅해 보지는 않았다. 바인딩 실패는 같은 산출물의 바인더에 값을 직접 넣어 확인했다.