docs(keycloak-session-store): import the session-storage lab as a new project
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>
This commit is contained in:
co-authored by
Claude Opus 5
parent
43bccd08a8
commit
b2963105a8
+118
@@ -0,0 +1,118 @@
|
||||
---
|
||||
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` 의 해당 키 주석이다. 분석 문서 쪽에는 이 사건을 다루는 절이 없고, 후보 원장이 지정한 `analysis/05` 의 절 번호는 그 파일에 존재하지 않는다. 원장 자신도 이 항목의 근거를 설정 파일 주석의 사후 기록으로 적고 있다.
|
||||
---
|
||||
|
||||
# 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
|
||||
|
||||
## 재현 조건
|
||||
|
||||
1. 오버라이드가 없는 프로파일에 spring.datasource.hikari.connection-timeout=5s 를 두고 부팅하면 HikariDataSource 빈 생성에서 바인딩이 실패한다.
|
||||
2. 같은 키를 저장소가 쓰는 Spring Boot·HikariCP 산출물의 바인더에 직접 넣으면 같은 예외 사슬이 나온다.
|
||||
3. 같은 출하 파일에서 server.tomcat.connection-timeout 의 형식을 확인한다.
|
||||
4. 시작 검증기가 같은 키를 어떤 파서로 읽는지, 어느 시점에 도는지 확인한다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`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` 를 결함으로 보고했다는 것이다. 실제 로더로 평탄화하도록 고친 이유가 그것이다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
실패 시점을 보려고 애플리케이션 전체를 부팅해 보지는 않았다. 바인딩 실패는 같은 산출물의 바인더에 값을 직접 넣어 확인했다.
|
||||
|
||||
<!-- body:end -->
|
||||
+167
@@ -0,0 +1,167 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: a-validator-checking-the-wrong-datasource
|
||||
title: validator가 요청을 서비스하지 않는 datasource를 검증하고 있었다
|
||||
topic: transaction-deadline-and-pool
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:a-validator-checking-the-wrong-datasource
|
||||
evidenceCapturedOn: 2026-09-03
|
||||
assets:
|
||||
- key: a-validator-checking-the-wrong-datasource
|
||||
file: ../../../final/evidence/rendered/a-validator-checking-the-wrong-datasource.svg
|
||||
- key: a-validator-checking-the-wrong-datasource-run
|
||||
file: ../../../final/evidence/rendered/a-validator-checking-the-wrong-datasource-run.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/a-validator-checking-the-wrong-datasource.txt
|
||||
- ../../../final/evidence/raw/a-validator-checking-the-wrong-datasource-run.txt
|
||||
source:
|
||||
- 이 사건의 1차 기록은 두 곳이다. 하나는 `JpaDataSourceProfileValidator` 와 `JpaResolvedDataSourceValidationTest` 두 클래스의 자바독이고, 다른 하나는 저장소 자신의 설계 문서 `docs/superpowers/specs/2026-08-15-five-adapter-runtime-remediation-review-design.md:307` 의 `JPA-INT-002` 항목이다.
|
||||
- root-tree 노드가 가리키는 `analysis/05 §14.5` 는 지금 그 파일에 없다. `analysis/05-adapter-outbound-persistence-jpa.md` 에서 `14.5` 는 0 건이고, 그 문서에 이 검증기와 병렬 네임스페이스가 나오지 않는다.
|
||||
- 원본 상류에 없는 것이 둘이다. `validateResolved` 를 부르는 프로덕션 코드가 지금은 하나 있다는 것과, 그 호출자가 발행된 빈을 쓰지 않고 자기 인스턴스를 만든다는 것이다.
|
||||
---
|
||||
|
||||
# validator가 요청을 서비스하지 않는 datasource를 검증하고 있었다
|
||||
|
||||
`JpaDataSourceProfileValidator` 의 자바독에 두 결함이 적혀 있다. 이 검증기가 `app.jpa-platform.datasource.*` 를 읽었는데 요청을 받는 풀은 `spring.datasource.hikari.*` 에서 만들어졌다는 것, 그리고 그 병렬 네임스페이스가 어떤 출하 설정에도 없어 두 필드가 늘 널이었다는 것이다. 널에서 던지는 `requirePoolBounds` 를 부르는 코드가 없어서 애플리케이션은 그대로 기동했다.
|
||||
|
||||
## 관계
|
||||
|
||||
- **connection-timeout: 5s가 모든 prod 배포를 시작 실패시켰고 local만 통과했다**
|
||||
둘 다 `spring.datasource.hikari.*` 를 두고 벌어진 일이다. 저쪽은 그 값을 바인더가 거부했고, 여기는 다른 네임스페이스를 읽느라 그 값을 보지 못했다.
|
||||
- **중복 장치를 찾으면 어느 쪽이 조립됐는지 먼저 확인한다**
|
||||
풀을 만드는 네임스페이스와 검증기가 읽는 네임스페이스가 다르면, 검증기는 배포가 쓰지 않는 값을 보고 통과한다. 어느 쪽이 풀을 만드는지 먼저 확인해야 그 통과가 무엇에 대한 것인지 알 수 있다.
|
||||
- **@Bean이 있다는 것은 조립 증거가 아니다**
|
||||
이 타입의 빈 정의가 하나 있는데 그것을 주입받는 프로덕션 코드가 없고, 실제 검사는 호출자가 직접 만든 객체가 한다.
|
||||
|
||||
## 문제
|
||||
|
||||
검증기는 배포가 실제로 향하는 데이터소스를 봐야 한다.
|
||||
|
||||
그런데 상한 값의 출처는 JpaDataSourceSettings 였다. 그것이 바인딩하는 접두는 app.jpa-platform.datasource 이고, 풀을 만드는 접두는 spring.datasource.hikari 다.
|
||||
|
||||
## 결론
|
||||
|
||||
자바독은 두 결함이 서로를 상쇄했다고 적는다.
|
||||
|
||||
첫째, JpaDataSourceProfileValidator 가 배포가 쓰지 않는 네임스페이스를 읽었다. 둘째, 그 네임스페이스는 출하 YAML 과 환경 키 레지스트리 어디에도 없어 두 필드가 늘 널이었다. requirePoolBounds 가 불렸다면 모든 배포가 기동에 실패했겠지만, 그것을 부르는 코드가 없었다.
|
||||
|
||||
지금은 JpaDataSourceSettings 클래스가 없고 설정 파일에서도 0 건이다. src 에 남은 일곱 자리 중 여섯은 자바독이고 하나는 회귀 테스트가 쓰는 검색 리터럴이다. 옛 메서드 이름 셋도 전부 자바독뿐이다.
|
||||
|
||||
다만 src 밖이 남았다. infra/jpa/postgres/README.md:18 이 운영자에게 max_connections 를 이제 없는 프로퍼티에 맞춰 잡으라고 지시한다.
|
||||
|
||||
validateResolved 는 해석된 데이터소스를 받아 널을 검사하고, 벤더 선택이 PostgreSQL 일 때만 커넥션을 열어 제품과 버전을 본다. 열지 못하면 던지므로 도달할 수 없는 데이터베이스가 첫 질의가 아니라 기동에서 실패한다.
|
||||
|
||||
여기서 원본 상류에 없는 것이 나온다.
|
||||
|
||||
지금은 그 검사를 부르는 프로덕션 코드가 있다. PersistenceJpaRootAutoConfiguration:113 한 자리이고, 그 자동설정은 ca-skeleton.persistence-jpa.enabled 뒤에 있으며 출하 기본값은 거짓이다. 그 메서드는 발행된 빈을 주입받지 않고 :109 에서 자기 인스턴스를 만드는데, 이유로 적힌 조건은 이미 제거됐다. 이 타입의 빈 정의는 JpaPlatformRuntimeAutoConfiguration:112 하나이고 그것이 JpaPlatformAutoConfiguration:49 에 위임한다. 그 빈은 어디에도 주입되지 않는다.
|
||||
|
||||
풀 상한 검사는 의도적으로 여기서 하지 않는다. 다만 HikariPoolConstraintValidator 가 읽는 다섯 키는 타임아웃과 수명과 누수 임계이고, maximum-pool-size 하한은 RuntimeNumericBoundsValidator:22 가 본다. 자바독이 함께 적은 이득 하나는 지금 성립하지 않는다 — HikariCP 는 선언만 testImplementation 이고 runtimeClasspath 에는 전이로 올라온다.
|
||||
|
||||
회귀 테스트가 그 부재를 고정한다. 훑을 목록이 비어 있지 않다는 것을 먼저 단언하고 주석을 걷어낸 뒤 검사하되, 훑는 범위는 두 모듈이다. 세 건을 돌려 전부 통과하는 것을 확인했다.
|
||||
|
||||
상류 어디에도 등급이 없다. root-tree 노드에도 candidate ledger 에도 설계 문서의 JPA-INT-002 항목에도 없어서, 이 기록도 새로 매기지 않는다. 두 결함은 닫혀 있고 남은 것은 관찰이다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12
|
||||
Spring Boot : 4.0.8
|
||||
확인 방식 : 자바독의 사후 기록 확인, app.jpa-platform.datasource 와 requirePoolBounds 의 잔존 자리 전수, validateResolved 본문과 그 프로덕션 호출자·조건 확인, 이 타입의 빈 정의와 주입처 계수, runtimeClasspath 전개, 회귀 테스트 실행
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
1. JpaDataSourceProfileValidator 의 클래스 자바독을 읽는다. 두 결함과 그 상쇄가 적혀 있다.
|
||||
2. app.jpa-platform.datasource 를 src 안팎에서 각각 찾고, 설정 파일과 바인딩 클래스 쪽을 따로 센다.
|
||||
3. 남은 히트가 주석인지 코드인지 소스 세트별로 가른다.
|
||||
4. requirePoolBounds 가 남은 자리를 센다.
|
||||
5. validateResolved 가 무엇을 읽고 어떤 조건에서 무엇을 던지는지 본문에서 읽는다.
|
||||
6. 그것을 부르는 자리를 main 과 test 로 갈라 세고, 그 자동설정이 붙는 프로퍼티와 출하 기본값을 확인한다.
|
||||
7. 그 호출자가 검증기를 주입받는지 직접 만드는지 보고, 직접 만드는 이유로 적힌 조건이 지금도 있는지 확인한다.
|
||||
8. 이 타입의 @Bean 정의를 모두 찾고 그것이 무엇에 위임하는지 확인한 뒤, 그 빈을 받는 코드를 센다.
|
||||
9. HikariPoolConstraintValidator 가 읽는 키를 나열하고, maximum-pool-size 를 검사하는 코드를 따로 찾는다.
|
||||
10. 이 모듈의 HikariCP 선언 구성과 runtimeClasspath 전개 결과를 대조한다.
|
||||
11. 회귀 테스트의 단언과 가드와 스캔 범위를 읽고, 그 테스트를 실행해 결과를 읽는다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`JpaDataSourceProfileValidator` 의 클래스 자바독에는 이 검증기가 예전에 다른 데이터소스를 검증했다는 내용이 먼저 적혀 있다.
|
||||
|
||||
## 자바독이 적은 두 결함과 그 상쇄
|
||||
|
||||
:::evidence key="a-validator-checking-the-wrong-datasource" alt="저장소 루트에서 돌린 정적 검색 출력 158줄. JpaDataSourceProfileValidator 11행부터 19행까지의 자바독이 두 결함과 상쇄를 원문 그대로 적는다. 이어서 app.jpa-platform.datasource 가 src 안에 남아 있는 일곱 자리가 main 다섯과 test 둘로 나오고, src 밖에서 그 문자열을 쓰는 파일 다섯과 줄 수, 그중 infra 의 운영자용 표에 살아 있는 줄이 이어진다. 설정 파일에서는 0 건이고 주석이 아닌 줄은 main 0 test 1 이며 JpaDataSourceSettings 클래스도 0 건이다. requirePoolBounds 가 남은 세 자리는 전부 자바독이다. 그다음 validateResolved 의 본문 50행부터 66행, 그 호출자가 붙는 프로퍼티 조건과 출하 기본값 false, 직접 생성의 이유로 적힌 조건과 그 조건이 지금 제거됐다는 사슬, 호출자가 자기 인스턴스를 만드는 106행부터 114행이 나온다. 이 타입의 빈 정의는 하나이고 JpaPlatformAutoConfiguration 의 @Bean 개수는 0 이며 그 빈을 받는 프로덕션 코드는 0 건이다. 끝으로 HikariPoolConstraintValidator 가 읽는 다섯 키와 그 빈, maximum-pool-size 를 실제로 보는 다른 검증기, HikariCP 가 선언은 testImplementation 인데 runtimeClasspath 에는 올라온다는 출력, 회귀 테스트 세 건의 이름과 그 스캔 범위와 공허한 통과를 막는 단언이 보인다." caption="자바독의 두 결함 · 네임스페이스 잔존 위치와 설정 0 · validateResolved 의 본문과 조건 · 빈 정의 하나와 주입 0 · 풀 키의 실제 소유자 · HikariCP 의 선언과 런타임 · 회귀 테스트 셋 — 158줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
첫 번째 결함으로 자바독이 적는 것은 읽는 대상의 어긋남이다. 상한 값은 `JpaDataSourceSettings` 가 `app.jpa-platform.datasource.*` 에 바인딩한 설정에서 왔고, 요청을 처리하는 풀은 `spring.datasource.hikari.*` 에서 만들어졌다. 자바독은 이것을 풀 하나에 설명이 둘인 상태로 적고, 그러면 검증기가 실제로 쓰이지 않는 쪽 설명을 보고 통과할 수 있다고 적는다.
|
||||
|
||||
두 번째가 더 나쁘다고 자바독은 적는다. 그 병렬 네임스페이스는 어떤 출하 YAML 에도, 환경 키 레지스트리의 어떤 행에도 없었다. 두 필드가 항상 널이었고 `requirePoolBounds` 는 널에서 던진다. 무언가 그것을 불렀다면 모든 배포가 기동에 실패했을 것이다.
|
||||
|
||||
아무것도 그것을 부르지 않았고, 그래서 아무것도 실패하지 않았다. 자바독은 여기까지를 두 결함이 서로를 상쇄한 것으로 적고, 애플리케이션이 시작된 이유를 두 번째 결함이 첫 번째를 가린 것으로 적는다.
|
||||
|
||||
## app.jpa-platform.datasource 가 지금 남아 있는 자리
|
||||
|
||||
`src` 안에서 이 문자열을 찾으면 일곱 자리가 나온다. 설정 파일에서는 0 건이고, 그것을 바인딩하던 `JpaDataSourceSettings` 클래스도 없다.
|
||||
|
||||
main 다섯은 전부 자바독이다. 그중 넷은 이 검증기가 아니라 다른 리프의 것으로, `NotificationSmtpProviderConfig:48` 과 `NotificationSmtpSettings:14` 와 `SmtpProviderRuntimeAssembler:40` 이 자기 결정의 이유를 대면서 이 사건을 선례로 인용하고, `KafkaMessagingAutoConfiguration:119` 도 브로커 주소를 두 번 기술하지 않는 이유로 같은 것을 든다. 나머지 하나가 검증기 자신의 `:12` 다.
|
||||
|
||||
test 둘 중 하나는 자바독이고, 나머지 하나는 `JpaResolvedDataSourceValidationTest:57` 의 `.contains("app.jpa-platform.datasource")` — 회귀 테스트가 이 문자열을 찾을 때 쓰는 검색 리터럴이다.
|
||||
|
||||
`src` 밖에도 다섯 파일이 이 문자열을 쓴다. 넷은 설계 문서와 계획 문서지만 하나는 다르다. `infra/jpa/postgres/README.md:18` 이 운영자용 표에서 `max_connections` 를 `app.jpa-platform.datasource.maximum-pool-size` 에 맞춰 잡으라고 지금도 지시한다. 그 프로퍼티는 더 이상 없다.
|
||||
|
||||
옛 상한 검사 메서드 이름도 셋 남았는데 역시 전부 자바독이다. 검증기 자신의 `:17` 과 회귀 테스트의 `:26`·`:29` 다. 부를 수 있는 메서드로는 남아 있지 않다.
|
||||
|
||||
## validateResolved 가 읽는 값과 던지는 조건
|
||||
|
||||
`validateResolved(DataSource, boolean)` 는 `DataSource` 널 검사를 먼저 하고, 벤더 선택이 PostgreSQL 이 아니면 그대로 돌아온다. 그 자리 주석이 이유를 적는다 — 로컬 개발은 설계상 H2 로 돌고 프로덕션에서 그것을 막는 것은 `PersistenceVendorProdSafetyValidator` 이며, 여기서도 PostgreSQL 을 요구하면 모든 노트북을 거절하게 된다.
|
||||
|
||||
PostgreSQL 이 선택된 배포에서만 커넥션을 열어 `versionPolicy.requireStable` 에 메타데이터를 넘긴다. 열지 못하면 `IllegalStateException` 을 던지고, 메시지는 `"A context that starts without this check reports healthy and fails on whoever sends the first request."` 다.
|
||||
|
||||
## validateResolved 의 프로덕션 호출자 하나와, 발행된 빈
|
||||
|
||||
`validateResolved` 를 부르는 프로덕션 코드는 `PersistenceJpaRootAutoConfiguration:113` 하나다. `@Bean InitializingBean jpaResolvedDataSourceCheck` 가 해석된 `DataSource` 와 `Environment` 를 받아, 벤더 프로퍼티를 읽고 그 값이 `postgresql` 인지를 두 번째 인자로 넘기는 람다를 돌려준다.
|
||||
|
||||
이 자동설정은 `:57`\~`:60` 의 `@ConditionalOnProperty(prefix = "ca-skeleton.persistence-jpa", name = "enabled", havingValue = "true")` 뒤에 있고 `application.yml:364` 의 출하 기본값은 `false` 다. JPA 마스터 스위치를 켠 배포에서만 이 검사가 돈다.
|
||||
|
||||
그 메서드는 검증기를 주입받지 않는다. `:109`\~`:110` 에서 `new JpaDataSourceProfileValidator(new PostgreSqlVersionPolicy())` 로 자기 인스턴스를 만든다. 이유도 `:95`\~`:99` 에 적혀 있다. 빈을 내놓는 클래스가 `@ConditionalOnBean(DataSource.class)` 를 달고 있어 실제 애플리케이션에서 조용히 빠지므로, 그 빈에 의존하면 이 검사도 같은 이유로 사라진다는 것이다.
|
||||
|
||||
그 이유는 지금 성립하지 않는다. `JpaPlatformRuntimeAutoConfiguration` 의 자바독 `:57`\~`:58` 은 이 클래스가 마스터 스위치를 이미 든 루트 아래에 있어 데이터소스 존재 여부가 파싱 시점에 이미 답해져 있다고 적고, 그 조건은 제거됐다.
|
||||
|
||||
이 타입을 빈으로 정의하는 자리는 하나다. `JpaPlatformRuntimeAutoConfiguration:112` 의 `@Bean` 이고, 그 메서드가 `JpaPlatformAutoConfiguration:49` 의 `dataSourceProfileValidator()` 에 위임한다. 뒤쪽 클래스에는 `@Bean` 이 하나도 없다 — 자바독 `:19` 가 `app-bootstrap` 이 조립을 소유한다는 규칙 때문에 평범한 생성으로 둔다고 적는다.
|
||||
|
||||
그 빈을 파라미터나 필드로 받는 프로덕션 코드는 0 건이다. 검사는 도는데, 도는 것은 발행된 빈이 아니라 호출자가 직접 만든 객체다.
|
||||
|
||||
## 풀 크기 상한을 읽는 것은 또 다른 검증기다
|
||||
|
||||
자바독은 풀 상한을 여기서 보지 않는 결정을 굵게 적으면서 `HikariPoolConstraintValidator` 를 지목한다. 그 검증기가 실제로 읽는 다섯 키는 `connection-timeout`, `validation-timeout`, `keepalive-time`, `max-lifetime`, `leak-detection-threshold` 이고, `RuntimeSafetyConfig:49` 의 `@Bean` 이 그것을 내놓는다.
|
||||
|
||||
옛 `requirePoolBounds` 가 널에서 던지던 `maximum-pool-size` 는 그쪽이 아니라 `RuntimeNumericBoundsValidator:22` 가 하한 1 로 본다. `minimum-idle` 은 `:26` 이다.
|
||||
|
||||
자바독은 이 클래스가 풀 타입 이름을 대지 않게 되어 HikariCP 가 `app-bootstrap` 의 프로덕션 클래스패스에서 빠진다고도 적는다. 선언은 그렇다 — `app-bootstrap/build.gradle:140` 이 `testImplementation` 하나뿐이다. 그런데 `runtimeClasspath` 를 풀어 보면 `com.zaxxer:HikariCP:7.0.2` 가 올라와 있다. `implementation project(':adapter:outbound:persistence-jpa')` 를 타고 `spring-boot-starter-data-jpa` 가 끌어온 것이라, 자바독이 적은 그 이득은 지금 성립하지 않는다.
|
||||
|
||||
## 회귀 테스트가 공허한 통과까지 막는다
|
||||
|
||||
:::evidence key="a-validator-checking-the-wrong-datasource-run" alt="gradle 실행 출력 6줄. PIPESTATUS 로 읽은 gradle 종료 코드 0 이 먼저 찍히고, JpaResolvedDataSourceValidationTest 스위트의 JUnit 결과가 tests 3, failures 0, errors 0, skipped 0 으로 나온다. 이어서 세 테스트가 각각 통과로 표시되는데 도달할 수 없는 데이터베이스가 기동에서 실패하는지, 제품 검사가 벤더 선택을 따르는지, 프로덕션 코드가 병렬 네임스페이스를 바인딩하지 않는지 셋이다." caption="회귀 테스트 세 건 실행 결과 — 6줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
`theParallelNamespaceIsGone`(\:46)이 소스를 훑어 병렬 바인딩이 없다고 단언한다. 그 앞에 단언이 하나 더 있다. 훑은 목록이 비어 있지 않다는 것(\:49\~\:51)이고, 그 단언의 문구가 이유를 적는다 — 아무 소스에도 닿지 못한 스캔은 모든 바인딩을 없다고 보고한다.
|
||||
|
||||
`:53`\~`:54` 의 주석이 왜 주석을 걷어내고 검사하는지도 적는다. 네임스페이스를 퇴역시켰다고 기록한 문장은 결함의 반대이기 때문이다.
|
||||
|
||||
다만 이 테스트가 잡을 수 있는 범위는 두 모듈이다. `productionSources()`(\:115\~\:119)가 `app-bootstrap` 과 `adapter/outbound/persistence-jpa` 만 훑는다. 위에서 센 다른 리프 넷은 애초에 스캔 대상 밖이라, 그것들이 이 테스트를 깨뜨리지 않는 이유는 주석 제거만이 아니다.
|
||||
|
||||
나머지 둘은 부재가 아니라 지금 동작을 고정한다. `anUnreachableDatabaseFailsAtStartup`(\:71)이 죽은 포트를 가리키는 `HikariDataSource` 에 대해 커넥션을 열지 못한다는 메시지를 요구하고, `theProductCheckFollowsTheVendorSelector`(\:92)가 H2 를 벤더 선택이 거짓일 때는 통과시키고 참일 때는 거절한다고 단언한다.
|
||||
|
||||
돌리면 세 건이 통과한다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
결함이 있던 시점의 코드를 직접 열어 보지 않았다. 두 결함을 적어 둔 곳은 자바독과 위 설계 문서이고, 지금의 상태만 계수와 실행으로 확인했다.
|
||||
|
||||
주입처가 0 건이라는 판정의 근거는 이름 기반 정적 검색이다. 빈 이름으로 찾아 쓰거나 리플렉션으로 꺼내는 경로까지 배제하지는 못했다.
|
||||
|
||||
결함이 있던 시점의 코드를 직접 열어 보지 않았다. 두 결함을 적어 둔 곳은 자바독과 위 설계 문서이고, 지금의 상태만 계수와 실행으로 확인했다.
|
||||
|
||||
<!-- body:end -->
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: requires-new-pins-the-outer-connection
|
||||
title: REQUIRES_NEW가 바깥 커넥션을 핀한 채 새 커넥션을 딴다
|
||||
topic: transaction-deadline-and-pool
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:requires-new-pins-the-outer-connection
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: requires-new-pins-the-outer-connection
|
||||
file: ../../../final/evidence/rendered/requires-new-pins-the-outer-connection.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/requires-new-pins-the-outer-connection.txt
|
||||
source:
|
||||
- 원본 분석 절은 final/document.md#3-2 · analysis/05 §3.1 이다.
|
||||
---
|
||||
|
||||
# REQUIRES_NEW가 바깥 커넥션을 핀한 채 새 커넥션을 딴다
|
||||
|
||||
새 트랜잭션을 여는 동안 바깥 트랜잭션의 커넥션은 반납되지 않는다. 그래서 동시 스레드 하나가 중첩 깊이만큼의 커넥션을 동시에 점유하고, 그 제약이 풀 크기 수식으로 설정 파일에 적혀 있다.
|
||||
|
||||
## 관계
|
||||
|
||||
- **REQUIRES_NEW의 커넥션 비용과 풀 사이징 제약**
|
||||
이 사례가 다루는 메커니즘이다.
|
||||
- **풀 계약 레인이 실행되지 않아 포화 동작이 확인되지 않았다**
|
||||
이 제약이 실제로 지켜지는지에 대한 미해결 질문이다.
|
||||
- **커밋 증거 단계 — NOT_STARTED에서 UNKNOWN까지**
|
||||
같은 중첩 구조가 증거 스택을 필요하게 만든 지점이다.
|
||||
|
||||
## 문제
|
||||
|
||||
새 트랜잭션을 여는 연산은 바깥 트랜잭션을 일시 중단한다. 일시 중단은 트랜잭션 경계에 대한 것이고 커넥션에 대한 것이 아니다.
|
||||
|
||||
바깥 커넥션은 반납되지 않고 유지된다. 나중에 재개해야 하기 때문이다.
|
||||
|
||||
## 결론
|
||||
|
||||
한 스레드가 동시에 여러 커넥션을 점유한다.
|
||||
|
||||
중첩 깊이 1 이면 스레드당 두 개다. 바깥 하나와 안쪽 하나다.
|
||||
|
||||
그래서 풀 크기가 동시 스레드 수보다 커야 한다. 그 제약이 설정 파일 주석에 수식으로 적혀 있다.
|
||||
|
||||
최대 풀 크기가 동시 스레드 수 곱하기 중첩 깊이에 1 을 더한 값 이상, 그리고 거기에 1 을 더한 값 이상이어야 한다는 형태다.
|
||||
|
||||
이 수식이 지켜지지 않으면 데드락이 난다. 모든 스레드가 바깥 커넥션을 잡고 안쪽 커넥션을 기다리는 상태가 되고, 아무도 반납하지 않으므로 풀 획득 타임아웃까지 전부 대기한다.
|
||||
|
||||
같은 주석 블록이 풀 사이징의 다른 근거도 함께 적는다. 작은 풀 공리와 PostgreSQL 의 시작점 수식이다. 코어 수의 두 배에 유효 스핀들 수를 더한 값에서 시작해 부하 테스트로 조정하라는 것이고, 고정 크기 풀을 권장한다.
|
||||
|
||||
수식이 주석에 있다는 것이 이 사례의 성질을 보여 준다. 제약은 알려져 있고 문서화되어 있으며, 그것을 강제하는 것은 풀 제약 검증기의 획득 하한 검사다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
Spring Boot : 4.0.8
|
||||
커넥션 풀 : HikariCP
|
||||
확인 방식 : 트랜잭션 포트 구현과 설정 파일 주석 확인
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
1. 트랜잭션 포트의 새 트랜잭션 연산을 읽는다. 모드별로 미리 만든 템플릿을 쓴다.
|
||||
2. 설정 파일의 hikari 블록에서 풀 크기 주석을 읽는다. 수식이 적혀 있다.
|
||||
3. 풀 제약 검증기가 획득 하한을 검사하는지 확인한다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`inNew`가 편해 보이지만 커넥션 비용이 곱셈이라는 것을 문서화한 제약이다.
|
||||
|
||||
## 이 제약이 문서화된 이유
|
||||
|
||||
:::evidence key="requires-new-pins-the-outer-connection" alt="분석 문서 final/document.md 에서 이 기록의 근거 절을 그대로 잘라낸 18줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md 발췌 — 18줄" zoom="true"
|
||||
:::
|
||||
|
||||
## 무엇이 금지되고 무엇으로 대신하나
|
||||
|
||||
풀 사이징 부등식이 명시돼 있고, 레코드마다 `inNew`를 도는 루프가 금지이며(풀 고갈 + 데드락), 배치로 묶거나 루프를 트랜잭션 밖으로 빼야 한다.
|
||||
|
||||
## 같은 이유의 다른 결정
|
||||
|
||||
`inRootWrite`가 `REQUIRES_NEW`로 suspend해서 "root인 척"하지 않고 fail-fast한다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
수식을 위반하는 풀 크기로 중첩 트랜잭션을 돌려 데드락을 재현하지 않았다. 풀 계약 레인이 그 동작을 검증하지만 이 사이클에서 돌리지 않았다.
|
||||
|
||||
풀 계약 레인 미실행 — 포화 시 pending count 보고와 대기 동작을 관측하지 않았다
|
||||
|
||||
<!-- body:end -->
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
---
|
||||
kind: CONCEPT
|
||||
slug: deadline-propagation
|
||||
title: 호출 예산에서 DB 로컬 타임아웃까지의 데드라인 전파
|
||||
topic: transaction-deadline-and-pool
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: concept:deadline-propagation
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: deadline-propagation
|
||||
file: ../../../final/evidence/rendered/deadline-propagation.svg
|
||||
- key: deadline-propagation-diagram
|
||||
file: ../../../final/assets/diagrams/deadline-propagation.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/deadline-propagation.txt
|
||||
source:
|
||||
- 원본 분석 절은 final/document.md#3-2 · analysis/05 §3.3 이다.
|
||||
---
|
||||
|
||||
# 호출 예산에서 DB 로컬 타임아웃까지의 데드라인 전파
|
||||
|
||||
호출자가 가진 시간 예산이 트랜잭션 타임아웃으로, 다시 데이터베이스의 로컬 타임아웃으로 좁혀진다. 각 단계가 앞 단계보다 작아야 상위 호출자가 포기한 뒤에도 하위가 계속 도는 상황이 생기지 않는다.
|
||||
|
||||
## 관계
|
||||
|
||||
- **데드라인은 호출 예산에서 시작해 세 단계로 좁힌다**
|
||||
이 개념을 규칙으로 옮긴 것이다.
|
||||
- **쓰기 트랜잭션에는 유한 타임아웃이 필수다**
|
||||
이 전파의 마지막 단계가 없을 때의 문제를 다룬 규칙이다.
|
||||
- **세션 스코프 설정은 풀로 돌아간 커넥션에 남는다**
|
||||
로컬 타임아웃을 설정할 때의 함정이다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
호출자의 남은 예산이 세 단계로 좁혀져 DB 세션 설정에 도달하는 구조의 설명이다.
|
||||
|
||||
## 마감이 좁혀지는 세 단계
|
||||
|
||||
:::evidence key="deadline-propagation-diagram" alt="호출 예산과 트랜잭션 마감과 DB 로컬 타임아웃이 위에서 아래로 쌓여 있고 오른쪽에 좁아지는 방향 화살표가 있다" caption="마감이 좁혀지는 세 단계" zoom="false"
|
||||
:::
|
||||
|
||||
## 획득 전에 요구하는 것
|
||||
|
||||
`connectionTimeout + beginBudget + minimumActionWindow + completionMargin`을 요구하고, Spring의 초 단위 타임아웃이 1초 미만이면 시작하지 않는다. begin 이후에는 statement/lock/idle 셋을 각각 유도하고 하나라도 1ms 미만이면 거부한다.
|
||||
|
||||
## 예산이 좁혀지는 경로
|
||||
|
||||
:::evidence key="deadline-propagation" alt="분석 문서 final/document.md 에서 이 기록의 근거 절을 그대로 잘라낸 18줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md 발췌 — 18줄" zoom="true"
|
||||
:::
|
||||
|
||||
## SET 이 아니라 set_config 인 이유
|
||||
|
||||
둘이다 — `SET`은 파라미터 바인딩 전에 파싱되어 syntax error가 나고, 함수 호출은 값이 statement text에서 빠진다. 세 번째 인자 `true`가 transaction-local을 뜻한다.
|
||||
|
||||
## H2가 두 가지에서 다르다
|
||||
|
||||
세션 스코프이고 idle 가드가 없다. 그것이 H2의 성질이지 선택이 아니라는 점도 함께 적혀 있다.
|
||||
|
||||
:::note
|
||||
|
||||
실제 세션에서 SHOW statement_timeout으로 적용을 확인하지 않았다
|
||||
|
||||
:::
|
||||
|
||||
## 왜 전파해야 하는가
|
||||
|
||||
상위 호출자가 30 초 예산을 갖고 있는데 데이터베이스 쿼리에 타임아웃이 없으면, 호출자가 포기한 뒤에도 쿼리는 계속 돈다. 그 커넥션은 반납되지 않고 풀에서 빠져 있다.
|
||||
|
||||
부하가 걸리면 그 상태가 누적된다. 아무도 기다리지 않는 작업이 풀을 점유한다.
|
||||
|
||||
## 계산이 자기 타입을 갖는다
|
||||
|
||||
데드라인 계산기가 별도 타입이다. 획득 봉투를 포함하는 형태와 포함하지 않는 형태를 나눠 갖는다.
|
||||
|
||||
획득 봉투는 커넥션을 얻는 데 드는 시간이다. 그것을 예산에서 빼지 않으면, 커넥션을 기다리다가 남은 시간이 없는 채로 쿼리를 시작하게 된다.
|
||||
|
||||
## 데이터베이스마다 다른 설정기
|
||||
|
||||
로컬 타임아웃을 실제로 거는 방법은 데이터베이스마다 다르다. PostgreSQL 용 설정기와 H2 용 설정기가 따로 있다.
|
||||
|
||||
이 분리가 필요한 이유는 두 가지다. 설정 문법이 다르고, 세션 스코프 설정이 커넥션에 남는 방식도 다르다.
|
||||
|
||||
## 세 단계
|
||||
|
||||
```text
|
||||
호출 예산 상위 호출자가 기다릴 수 있는 시간
|
||||
↓ 획득 봉투를 뺀다
|
||||
트랜잭션 타임아웃 스프링 트랜잭션 템플릿에 설정
|
||||
↓ 여유를 남긴다
|
||||
DB 로컬 타임아웃 데이터베이스가 스스로 끊는 시간
|
||||
```
|
||||
|
||||
각 단계가 앞 단계보다 작다. 마지막이 가장 작아야 데이터베이스가 먼저 끊고, 그래야 애플리케이션이 그 실패를 분류할 기회를 갖는다.
|
||||
|
||||
:::note
|
||||
|
||||
순서가 반대가 되면 애플리케이션이 먼저 타임아웃되고 데이터베이스는 계속 돈다. 그 쿼리는 아무도 결과를 받지 않은 채 자원을 쓴다.
|
||||
|
||||
:::
|
||||
|
||||
## 템플릿을 미리 만드는 것과의 관계
|
||||
|
||||
트랜잭션 템플릿은 모드마다 미리 만들어져 있고 전부 같은 격리 수준에 고정되어 있다. 템플릿을 호출마다 고쳐 쓰면 경합이 생기기 때문이다.
|
||||
|
||||
그래서 데드라인은 템플릿의 필드가 아니라 실행 시점에 계산되어 전달된다.
|
||||
|
||||
<!-- body:end -->
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
---
|
||||
kind: CONCEPT
|
||||
slug: requires-new-connection-cost
|
||||
title: REQUIRES_NEW의 커넥션 비용과 풀 사이징 제약
|
||||
topic: transaction-deadline-and-pool
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: concept:requires-new-connection-cost
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: requires-new-connection-cost
|
||||
file: ../../../final/evidence/rendered/requires-new-connection-cost.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/requires-new-connection-cost.txt
|
||||
source:
|
||||
- 원본 분석 절은 final/document.md#3-2 · analysis/05 §3.1, §13.2 이다.
|
||||
---
|
||||
|
||||
# REQUIRES_NEW의 커넥션 비용과 풀 사이징 제약
|
||||
|
||||
새 트랜잭션은 새 커넥션을 요구하고 바깥 커넥션은 반납되지 않는다. 그래서 풀 크기 하한이 동시 스레드 수와 중첩 깊이의 곱에 묶인다.
|
||||
|
||||
## 관계
|
||||
|
||||
- **REQUIRES_NEW가 바깥 커넥션을 핀한 채 새 커넥션을 딴다**
|
||||
이 제약이 실제로 나타나는 사례다.
|
||||
- **호출 예산에서 DB 로컬 타임아웃까지의 데드라인 전파**
|
||||
풀 획득이 예산의 일부라는 점에서 연결된다.
|
||||
- **풀 계약 레인이 실행되지 않아 포화 동작이 확인되지 않았다**
|
||||
이 제약의 검증에 대한 미해결 질문이다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`REQUIRES_NEW`는 바깥 트랜잭션의 커넥션을 **핀한 채로** 새 물리 JDBC 커넥션을 딴다. 그래서 풀 사이징 제약이 곱셈이 된다 — `maximumPoolSize >= (concurrent_threads × (1 + max_inNew_depth)) + 1`.
|
||||
|
||||
## 커넥션 비용이 곱셈이 되는 이유
|
||||
|
||||
:::evidence key="requires-new-connection-cost" alt="분석 문서 final/document.md 에서 이 기록의 근거 절을 그대로 잘라낸 18줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md 발췌 — 18줄" zoom="true"
|
||||
:::
|
||||
|
||||
## 레코드마다 inNew 를 도는 루프가 금지인 이유
|
||||
|
||||
풀 고갈과 데드락이다.
|
||||
|
||||
## 같은 곱셈 함정이 database-per-tenant에서 반복된다
|
||||
|
||||
각 tenant 풀은 개별적으로 합리적이고 그 합이 아니다 — 50 tenant × 10 = 서버 `max_connections` 100에 500 커넥션. 실패는 idle이던 것 포함 모든 tenant에 동시에 도착한다.
|
||||
|
||||
:::note
|
||||
|
||||
풀 계약 레인 미실행
|
||||
|
||||
:::
|
||||
|
||||
## 일시 중단은 커넥션을 놓지 않는다
|
||||
|
||||
새 트랜잭션 모드는 바깥 트랜잭션을 일시 중단한다. 그 일시 중단은 트랜잭션 경계에 대한 것이다.
|
||||
|
||||
바깥 커넥션은 유지된다. 나중에 재개해야 하기 때문이다.
|
||||
|
||||
## 그래서 하한이 생긴다
|
||||
|
||||
설정 파일이 그 제약을 수식으로 적는다.
|
||||
|
||||
```text
|
||||
maxPoolSize >= concurrent_threads * (1 + max_inNew_depth) + 1
|
||||
```
|
||||
|
||||
중첩 깊이 1 이면 스레드당 두 커넥션이다. 동시 스레드가 100 이면 최소 201 이 필요하다.
|
||||
|
||||
## 지켜지지 않으면 데드락이다
|
||||
|
||||
모든 스레드가 바깥 커넥션을 잡고 안쪽 커넥션을 기다린다. 아무도 반납하지 않으므로 전부 풀 획득 타임아웃까지 대기한다.
|
||||
|
||||
이 상태는 부하가 임계를 넘는 순간 한꺼번에 나타난다. 그 전까지는 아무 증상이 없다.
|
||||
|
||||
## 풀 사이징의 다른 근거와 충돌한다
|
||||
|
||||
같은 주석 블록이 반대 방향의 근거도 적는다.
|
||||
|
||||
```text
|
||||
D1 (feature-database-connection-pool-contract): small-pool axiom + PostgreSQL formula
|
||||
starting point (maximumPoolSize = cores * 2 + effective_spindle_count, adjust via load
|
||||
test). Fixed-size pool recommended (minimumIdle = maximumPoolSize).
|
||||
```
|
||||
|
||||
작은 풀이 낫다는 공리와 코어 수 기반 시작점이다. 그 값은 대개 동시 스레드 수보다 훨씬 작다.
|
||||
|
||||
:::warning
|
||||
|
||||
두 근거가 같은 손잡이를 반대 방향으로 민다. 작은 풀 공리는 값을 줄이라 하고 중첩 하한은 늘리라 한다. 해소하는 방법은 풀을 키우는 것이 아니라 중첩 깊이를 줄이는 것이다.
|
||||
|
||||
:::
|
||||
|
||||
## 고정 크기 풀
|
||||
|
||||
최소 유휴를 최대 크기와 같게 두는 것을 권장한다. 풀이 줄었다가 늘어나는 동안 중첩 하한이 일시적으로 깨지는 것을 막는다.
|
||||
|
||||
## 멀티테넌시에서의 확장
|
||||
|
||||
테넌트별 풀 예산 타입이 이 제약을 테넌트 단위로 다시 적용한다. 테넌트 하나가 풀 전체를 소진하는 것을 막으면서도 각 테넌트의 중첩 하한을 만족해야 한다.
|
||||
|
||||
<!-- body:end -->
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
---
|
||||
kind: PROJECT_DECISION
|
||||
slug: in-root-write-fails-fast
|
||||
title: inRootWrite는 suspend하지 않고 fail-fast한다
|
||||
topic: transaction-deadline-and-pool
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: decision:in-root-write-fails-fast
|
||||
decisionStatus: ADOPTED
|
||||
decidedOn: 2026-08-30
|
||||
source:
|
||||
- src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringTransactionPort.java
|
||||
- analysis/05-adapter-outbound-persistence-jpa.md
|
||||
---
|
||||
|
||||
# inRootWrite는 suspend하지 않고 fail-fast한다
|
||||
|
||||
## 결정문
|
||||
|
||||
루트 쓰기 연산은 이미 트랜잭션이 열려 있으면 바깥을 일시 중단하지 않고 예외를 던진다.
|
||||
|
||||
## 판단 이유
|
||||
|
||||
이 연산의 이름이 약속하는 것은 루트 트랜잭션이다. 호출자가 그렇게 부르는 이유는 자기 작업이 다른 트랜잭션에 참여하지 않기를 바라기 때문이다.
|
||||
|
||||
이미 트랜잭션이 열려 있는데 일시 중단하고 새로 시작하면 두 가지가 일어난다. 바깥 커넥션이 핀되고, 호출자는 자기가 루트라고 믿는데 실제로는 중첩 안에 있다.
|
||||
|
||||
첫 번째는 풀 비용이다. 두 번째는 더 나쁘다. 커밋 순서와 롤백 범위에 대한 호출자의 가정이 틀린 채로 진행된다.
|
||||
|
||||
그래서 일시 중단하지 않고 던진다. 중첩된 루트 트랜잭션이 거절되었다는 전용 예외 타입이다.
|
||||
|
||||
이 선택은 호출자에게 부담을 넘긴다. 루트를 요구하는 코드가 어디서 불리는지 알아야 하기 때문이다. 그것이 의도다. 그 사실을 아는 것이 이 연산을 쓰는 조건이다.
|
||||
|
||||
## 영향
|
||||
|
||||
감수하는 것
|
||||
|
||||
이 연산을 호출하는 경로가 트랜잭션 안에 들어가면 런타임에 실패한다. 정적으로는 막히지 않는다.
|
||||
|
||||
기존 코드에서 이 연산을 쓰려면 호출 경로를 먼저 정리해야 한다.
|
||||
|
||||
얻는 것
|
||||
|
||||
루트라고 부른 것이 실제로 루트다.
|
||||
|
||||
중첩 깊이가 예상 밖으로 늘어나 풀 하한을 깨는 경로 하나가 사라진다.
|
||||
|
||||
## 근거
|
||||
|
||||
- **REQUIRES_NEW의 커넥션 비용과 풀 사이징 제약**
|
||||
일시 중단이 커넥션을 놓지 않는다는 사실이 이 결정의 근거 중 하나다.
|
||||
- **트랜잭션 템플릿은 모드별로 미리 만들어 둔다**
|
||||
같은 포트의 다른 모드 결정이다.
|
||||
- **트랜잭션 결과 대수 — 다섯 변형이 각각 답하는 질문**
|
||||
참여와 루트를 구별하는 결과 타입이다.
|
||||
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
---
|
||||
kind: PROJECT_DECISION
|
||||
slug: templates-are-built-once-per-mode
|
||||
title: 트랜잭션 템플릿은 모드별로 미리 만들어 둔다
|
||||
topic: transaction-deadline-and-pool
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: decision:templates-are-built-once-per-mode
|
||||
decisionStatus: ADOPTED
|
||||
decidedOn: 2026-08-30
|
||||
source:
|
||||
- src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringTransactionPort.java
|
||||
- analysis/05-adapter-outbound-persistence-jpa.md
|
||||
---
|
||||
|
||||
# 트랜잭션 템플릿은 모드별로 미리 만들어 둔다
|
||||
|
||||
## 결정문
|
||||
|
||||
트랜잭션 템플릿을 모드마다 하나씩 미리 만들어 두고, 호출마다 템플릿의 속성을 바꾸지 않는다.
|
||||
|
||||
## 판단 이유
|
||||
|
||||
템플릿은 가변 객체다. 하나를 공유하면서 호출마다 전파 방식이나 타임아웃을 바꾸면 두 스레드가 서로의 설정을 본다.
|
||||
|
||||
그 경합은 재현이 어렵다. 대부분의 호출은 같은 값을 쓰므로 증상이 드물게 나타나고, 나타났을 때는 트랜잭션이 잘못된 전파 방식으로 실행된 결과만 남는다.
|
||||
|
||||
그래서 모드마다 별도 템플릿을 두고 전부 같은 격리 수준에 고정한다. 쓰기와 읽기와 새 트랜잭션 셋이다.
|
||||
|
||||
데드라인처럼 호출마다 달라지는 값은 템플릿의 필드가 아니라 실행 시점에 계산해 전달한다.
|
||||
|
||||
## 영향
|
||||
|
||||
감수하는 것
|
||||
|
||||
모드가 늘면 템플릿도 늘어난다. 조합이 많아지면 이 방식이 부담이 된다.
|
||||
|
||||
호출마다 달라져야 하는 값은 별도 경로로 전달해야 한다. 템플릿에 넣을 수 없다.
|
||||
|
||||
얻는 것
|
||||
|
||||
템플릿 경합이 구조적으로 사라진다.
|
||||
|
||||
각 모드가 무엇으로 설정되어 있는지 한 자리에서 보인다.
|
||||
|
||||
## 근거
|
||||
|
||||
- **호출 예산에서 DB 로컬 타임아웃까지의 데드라인 전파**
|
||||
호출마다 달라지는 값이 어떻게 전달되는지 설명한다.
|
||||
- **Atomic 타입의 존재는 원자성의 증거가 아니다**
|
||||
가변 공유 상태를 다루는 같은 계열의 규칙이다.
|
||||
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
---
|
||||
kind: QUESTION
|
||||
slug: pool-contract-lane-not-executed
|
||||
title: 풀 계약 레인이 실행되지 않아 포화 동작이 확인되지 않았다
|
||||
topic: transaction-deadline-and-pool
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: open-question:pool-contract-lane-not-executed
|
||||
questionStatus: OPEN
|
||||
verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다
|
||||
---
|
||||
|
||||
# 풀 계약 레인이 실행되지 않아 포화 동작이 확인되지 않았다
|
||||
|
||||
중첩 트랜잭션의 커넥션 하한과 포화 시 보고와 대기 동작을 검증하는 레인이 있다. 이 리비전에서 돌리지 않았다.
|
||||
|
||||
## 사실
|
||||
|
||||
레인은 세 가지 동작 계약을 검증한다. 중첩 깊이 1 이 동시 스레드당 커넥션 두 개를 요구한다는 것, 포화된 풀이 자기 대기 수를 보고한다는 것, 호출자가 커넥션 없이 진행하는 대신 기다린다는 것이다.
|
||||
|
||||
이 셋은 어떤 기계에서도 참이므로 플래그가 필요 없다. 그래서 성능 레인이 아니라 동작 계약 레인이다.
|
||||
|
||||
레인은 발견 실패 설정과 업투데이트 비주장 설정을 갖는다.
|
||||
|
||||
이 컨테이너에 Docker 가 있고 기본 test 레인은 통과했다.
|
||||
|
||||
## 가정
|
||||
|
||||
풀 사이징 수식이 설정 파일 주석에 적혀 있고 검증기가 획득 하한을 강제하므로 실제 동작도 그러할 것이라고 전제하고 있다. 주석과 검증기는 선언이고 레인이 동작이다.
|
||||
|
||||
## 미지수
|
||||
|
||||
중첩 트랜잭션이 실제로 스레드당 두 커넥션을 요구하는지.
|
||||
|
||||
포화 시 대기 수 보고가 정확한지.
|
||||
|
||||
호출자가 커넥션 없이 진행하는 경로가 정말 없는지.
|
||||
|
||||
## 제약
|
||||
|
||||
컨테이너가 필요하다.
|
||||
|
||||
이 레인은 전용 소스셋을 쓴다.
|
||||
|
||||
애플리케이션 소스를 수정하지 않는다.
|
||||
|
||||
## 선택지
|
||||
|
||||
레인만 개별로 돌린다
|
||||
빠르고 실패를 격리하기 좋다.
|
||||
|
||||
릴리스 게이트를 돌려 다른 레인과 함께 확인한다
|
||||
게이트 조립까지 검증되지만 시간이 더 든다.
|
||||
|
||||
## 다음 검증
|
||||
|
||||
풀 계약 레인을 실행하고 종료 코드와 실패 목록을 기록한다.
|
||||
|
||||
세 계약이 통과하면 이 질문을 닫는다.
|
||||
|
||||
실패가 나오면 어느 계약이 깨졌는지에 따라 풀 사이징 수식의 문제인지 구현의 문제인지 구분해 기록한다.
|
||||
|
||||
## 관계
|
||||
|
||||
- **컨테이너가 필요한 특수 레인의 실제 결과를 실행으로 확인하지 않았다**
|
||||
이 질문의 상위 항목이다.
|
||||
- **REQUIRES_NEW가 바깥 커넥션을 핀한 채 새 커넥션을 딴다**
|
||||
이 레인이 검증하는 제약이다.
|
||||
- **certified라 불리던 레인이 threshold를 하나도 비교하지 않고 있었다**
|
||||
이 레인이 지금 이름을 갖게 된 경위다.
|
||||
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
---
|
||||
kind: REFERENCE
|
||||
slug: deadline-narrows-in-three-stages
|
||||
title: 데드라인은 호출 예산에서 시작해 세 단계로 좁힌다
|
||||
topic: transaction-deadline-and-pool
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: reference:deadline-narrows-in-three-stages
|
||||
verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다
|
||||
---
|
||||
|
||||
# 데드라인은 호출 예산에서 시작해 세 단계로 좁힌다
|
||||
|
||||
## 목적
|
||||
|
||||
상위 호출자가 포기한 뒤에도 하위 작업이 계속 돌아 자원을 점유하는 것을 막는다.
|
||||
|
||||
## 규칙
|
||||
|
||||
1. 출발점은 호출 예산이다
|
||||
상위 호출자가 기다릴 수 있는 시간에서 시작한다. 하위가 스스로 정한 값에서 시작하지 않는다.
|
||||
|
||||
2. 획득 시간을 뺀다
|
||||
커넥션이나 슬롯을 얻는 데 든 시간은 이미 예산에서 소비된 것이다. 그것을 빼지 않으면 남은 시간이 없는 채로 작업을 시작한다.
|
||||
|
||||
3. 각 단계는 앞 단계보다 작다
|
||||
호출 예산보다 트랜잭션 타임아웃이 작고, 그보다 데이터베이스 로컬 타임아웃이 작다.
|
||||
|
||||
4. 가장 안쪽이 먼저 끊는다
|
||||
데이터베이스가 먼저 끊어야 애플리케이션이 그 실패를 분류할 기회를 갖는다. 반대면 쿼리가 결과를 받을 사람 없이 계속 돈다.
|
||||
|
||||
5. 계산을 자기 타입에 둔다
|
||||
값이 여러 곳에서 계산되면 그중 하나가 획득 시간을 빼는 것을 잊는다.
|
||||
|
||||
## 적용 조건
|
||||
|
||||
트랜잭션과 원격 호출처럼 시간이 걸리는 모든 하위 작업
|
||||
|
||||
풀에서 자원을 얻어 쓰는 경로
|
||||
|
||||
## 예외
|
||||
|
||||
배치나 백그라운드 작업처럼 상위 호출자가 없는 경우는 예산의 출발점이 다르다. 그때는 그 작업 자체의 상한이 출발점이다.
|
||||
|
||||
## 예시
|
||||
|
||||
데드라인 계산기가 획득 봉투를 포함하는 형태와 포함하지 않는 형태를 나눠 갖는다.
|
||||
|
||||
로컬 타임아웃을 거는 방법은 데이터베이스마다 달라서 설정기가 따로 있다.
|
||||
|
||||
## 관계
|
||||
|
||||
- **호출 예산에서 DB 로컬 타임아웃까지의 데드라인 전파**
|
||||
이 규칙이 나온 개념이다.
|
||||
- **쓰기 트랜잭션에는 유한 타임아웃이 필수다**
|
||||
마지막 단계가 비어 있을 때의 규칙이다.
|
||||
- **재시도 가능성은 멱등성과 실패 범주를 함께 봐야 정해진다**
|
||||
데드라인이 재시도 판정의 입력이 되는 지점이다.
|
||||
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
---
|
||||
kind: REFERENCE
|
||||
slug: session-scoped-settings-outlive-the-transaction
|
||||
title: 세션 스코프 설정은 풀로 돌아간 커넥션에 남는다
|
||||
topic: transaction-deadline-and-pool
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: reference:session-scoped-settings-outlive-the-transaction
|
||||
verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다
|
||||
---
|
||||
|
||||
# 세션 스코프 설정은 풀로 돌아간 커넥션에 남는다
|
||||
|
||||
## 목적
|
||||
|
||||
한 트랜잭션을 위해 건 세션 설정이 커넥션과 함께 풀로 돌아가, 무관한 다음 작업에 적용되는 것을 막는다.
|
||||
|
||||
## 규칙
|
||||
|
||||
1. 세션 스코프와 트랜잭션 스코프를 구별한다
|
||||
세션 스코프로 설정하면 커넥션이 살아 있는 동안 유지된다. 트랜잭션이 끝나도 사라지지 않는다.
|
||||
|
||||
2. 가능하면 로컬 스코프를 쓴다
|
||||
트랜잭션 로컬로 설정하면 커밋이나 롤백과 함께 사라진다.
|
||||
|
||||
3. 로컬이 불가능하면 반납 전에 되돌린다
|
||||
설정을 건 쪽이 그것을 지우는 책임을 갖는다. 다음 사용자가 지울 것이라고 가정하지 않는다.
|
||||
|
||||
4. 데이터베이스마다 방법이 다르다
|
||||
같은 개념의 설정이라도 문법과 스코프가 다르므로 설정기를 데이터베이스별로 둔다.
|
||||
|
||||
5. 테넌트나 사용자 컨텍스트를 세션에 남기지 않는다
|
||||
커넥션이 풀에서 재사용되면 다른 테넌트의 요청이 앞 요청의 컨텍스트를 물려받는다.
|
||||
|
||||
## 적용 조건
|
||||
|
||||
커넥션 풀을 쓰는 모든 데이터베이스 접근
|
||||
|
||||
로컬 타임아웃 검색 경로 역할 테넌트 컨텍스트 같은 세션 설정
|
||||
|
||||
## 예외
|
||||
|
||||
풀을 쓰지 않고 요청마다 새 커넥션을 여는 구성은 이 규칙의 대상이 아니다. 그 경우 다른 비용이 든다.
|
||||
|
||||
## 예시
|
||||
|
||||
로컬 타임아웃 설정기가 PostgreSQL 과 H2 로 나뉘어 있다. 설정 문법이 다르고 세션에 남는 방식도 다르기 때문이다.
|
||||
|
||||
멀티테넌시에서 테넌트 컨텍스트를 세션에 남기면 풀 재사용이 곧 테넌트 경계 위반이 된다.
|
||||
|
||||
## 관계
|
||||
|
||||
- **호출 예산에서 DB 로컬 타임아웃까지의 데드라인 전파**
|
||||
로컬 타임아웃을 거는 지점이다.
|
||||
- **connection-timeout이 5s 문자열로 출하되어 prod와 dev 배포가 전부 시작에 실패했다**
|
||||
같은 설정 계층의 다른 함정이다.
|
||||
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
---
|
||||
kind: REFERENCE
|
||||
slug: write-transactions-need-a-finite-timeout
|
||||
title: 쓰기 트랜잭션에는 유한 타임아웃이 필수다
|
||||
topic: transaction-deadline-and-pool
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: reference:write-transactions-need-a-finite-timeout
|
||||
verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다
|
||||
---
|
||||
|
||||
# 쓰기 트랜잭션에는 유한 타임아웃이 필수다
|
||||
|
||||
## 목적
|
||||
|
||||
타임아웃 없는 쓰기 트랜잭션이 잠금을 무한히 들고 있어, 다른 쓰기 전부를 막는 것을 방지한다.
|
||||
|
||||
## 규칙
|
||||
|
||||
1. 쓰기 경로에 무한 대기를 두지 않는다
|
||||
타임아웃이 없으면 잠금 대기가 끝나지 않는다.
|
||||
|
||||
2. 기본값을 무한으로 두지 않는다
|
||||
설정하지 않았을 때의 동작이 무한 대기면, 설정을 잊은 배포가 가장 위험한 배포가 된다.
|
||||
|
||||
3. 읽기와 쓰기의 상한을 따로 둔다
|
||||
읽기가 길어지는 것과 쓰기가 길어지는 것은 영향 범위가 다르다.
|
||||
|
||||
4. 타임아웃 값을 타입으로 강제한다
|
||||
설정 값이 비어 있을 수 있는 형태면 그 경로가 언젠가 무한이 된다.
|
||||
|
||||
## 적용 조건
|
||||
|
||||
잠금을 잡는 모든 쓰기 트랜잭션
|
||||
|
||||
여러 인스턴스가 같은 행을 경합하는 구조
|
||||
|
||||
## 예외
|
||||
|
||||
관리자가 명시적으로 실행하는 일회성 마이그레이션이나 백필은 상한이 다를 수 있다. 그 경우 실행 절차에 그 사실이 있어야 한다.
|
||||
|
||||
## 예시
|
||||
|
||||
트랜잭션 템플릿이 모드마다 미리 만들어져 있고 전부 같은 격리 수준에 고정되어 있다. 데드라인은 실행 시점에 계산되어 전달된다.
|
||||
|
||||
풀 커넥션 타임아웃은 기본 30 초 대신 짧은 값으로 고정한다. 풀이 고갈된 스레드를 오래 붙잡지 않고 빠르게 거절하기 위해서다.
|
||||
|
||||
## 관계
|
||||
|
||||
- **데드라인은 호출 예산에서 시작해 세 단계로 좁힌다**
|
||||
이 규칙이 속한 전파 구조다.
|
||||
- **커밋 모호성 판정은 넓혀도 좁혀도 해롭다**
|
||||
타임아웃이 만드는 실패를 어떻게 분류할지 다룬 규칙이다.
|
||||
|
||||
Reference in New Issue
Block a user