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>
134 lines
9.7 KiB
Markdown
134 lines
9.7 KiB
Markdown
---
|
|
kind: CASE
|
|
slug: a-transaction-capability-true-and-its-validator-never-run
|
|
title: 브로커 트랜잭션을 무조건 참으로 선언하고, 그 조건을 검사하는 검증기는 기동 시 돌지 않는다
|
|
topic: capability-declaration-vs-proof
|
|
project: clean-architecture-backend-template
|
|
status: 게시 전
|
|
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
|
rootTreeNode: case:a-transaction-capability-true-and-its-validator-never-run
|
|
evidenceCapturedOn: 2026-09-02
|
|
assets:
|
|
- key: a-transaction-capability-true-and-its-validator-never-run
|
|
file: ../../../final/evidence/rendered/a-transaction-capability-true-and-its-validator-never-run.svg
|
|
evidence:
|
|
- ../../../final/evidence/raw/a-transaction-capability-true-and-its-validator-never-run.txt
|
|
source:
|
|
- 분석 문서는 메시징 플랫폼 편 §3.5 다. 그 절이 프로파일 검증기 여덟 개의 도달성을 세고, 조립에서 실행되는 셋을 적는다. 실행되지 않는 다섯 중 넷은 빌드 전용 모듈에 있어 조립 지점이 없는 것이 등급과 일치한다. 출하되는 모듈에서 조립되지 않은 것은 이 트랜잭션 검증기 하나뿐이고, 그래서 이 항목이 P2 다.
|
|
- 능력 상수의 아홉째가 프로파일과 무관한 상수라는 것은 Kafka 어댑터 편이고, 아홉째와 열째의 독자 수 대비는 같은 플랫폼 편 §3.4 의 표에 있다.
|
|
---
|
|
|
|
# 브로커 트랜잭션을 무조건 참으로 선언하고, 그 조건을 검사하는 검증기는 기동 시 돌지 않는다
|
|
|
|
Kafka 어댑터의 능력 상수가 브로커 트랜잭션을 프로파일과 무관하게 참으로 답한다. 그 조건을 검사하는 검증기는 스타터가 빈으로 만들지만 기동 검증에 감싸지 않아 실행되지 않는다.
|
|
|
|
## 관계
|
|
|
|
- **능력 선언의 세 출처와 그것이 파생되지 않을 때**
|
|
이 사례가 속한 구조다.
|
|
- **검증기는 발행이 아니라 주입이 강제다**
|
|
이 사례의 두 번째 절반에 해당하는 규칙이다.
|
|
- **능력 선언은 프로파일에서 파생되어야 하고 상수는 그것을 할 수 없다**
|
|
첫 번째 절반에 해당하는 규칙이다.
|
|
|
|
## 문제
|
|
|
|
Kafka 트랜잭션은 조건 다섯이 모두 맞아야 활성화된다.
|
|
|
|
그 다섯을 전부 보는 클래스가 이 저장소에 있다.
|
|
|
|
## 결론
|
|
|
|
능력 상수가 프로파일을 보지 않는다. 열두 성분 중 아홉째 자리가 고정으로 참이고 그 선언에 프로파일 참조가 없다.
|
|
|
|
그 플래그를 읽는 프로덕션 코드는 0 이다. 바로 옆 열째 플래그는 발행 경로가 읽는데, 그 플래그는 일부러 거짓으로 내려져 있다. 그 자리 javadoc 이 이유를 적는다 — 참으로 선언하면 호출자가 브로커가 중복을 제거한다고 믿고 자기 멱등성을 만들지 않는다는 것이다.
|
|
|
|
아홉째의 과대 선언이 오늘 낳는 결과는 조회 경로의 피해와 다르다.
|
|
|
|
두 번째 절반이 검증 경로다. 스타터가 트랜잭션 검증기를 빈으로 발행하지만 기동 검증에 감싸지 않는다. 자동설정 바깥에서 이것을 아는 코드는 하나도 없다. 다섯 규칙이 어디에서도 실행되지 않는다.
|
|
|
|
감쌀 수 없는 이유는 인자 수가 아니다. 래퍼는 소비자 함수를 받으므로 나머지를 캡처하는 람다면 들어간다. 두 번째 인자가 그것을 막는다. 트랜잭션 식별자 접두는 이 저장소의 main 에서 이 검증기의 파라미터 이름으로만 존재한다.
|
|
|
|
## 검증 환경
|
|
|
|
OpenJDK : 21.0.12
|
|
Gradle : 9.0.0
|
|
확인 방식 : 능력 상수의 성분 위치와 독자 계수, 검증기의 규칙과 언급 계수, 래퍼 시그니처와 인자 출처 확인
|
|
소스 수정 : x
|
|
|
|
## 재현 조건
|
|
|
|
1. 능력 record 에서 브로커 트랜잭션이 몇 번째인지 확인하고, Kafka 가 그 자리에 넘기는 값과 그 선언의 프로파일 참조를 확인한다.
|
|
2. 그 플래그를 읽는 프로덕션 코드를 세고, 옆 열째 플래그와 대조한다.
|
|
3. 트랜잭션 검증기가 요구하는 조건을 전부 나열한다.
|
|
4. 기동 검증으로 감싸이는 검증기와 직접 불리는 검증기를 확인한다.
|
|
5. 트랜잭션 검증기를 언급하는 프로덕션 코드와 테스트를 센다.
|
|
6. 래퍼의 시그니처와 두 번째 인자의 출처를 확인한다.
|
|
|
|
## 본문
|
|
|
|
<!-- body:start -->
|
|
|
|
Kafka 트랜잭션은 생산자 설정 넷과 목적지 선언 하나가 동시에 맞아야 성립한다. 그중 어느 하나라도 어긋나면 커밋 경계가 갈라진다.
|
|
|
|
이 저장소에는 그 다섯을 전부 검사하는 클래스가 있다. 스타터가 그것을 빈으로 만든다. 그리고 아무도 그것을 부르지 않는다.
|
|
|
|
## 아홉째 자리가 프로파일을 보지 않는다
|
|
|
|
:::evidence key="a-transaction-capability-true-and-its-validator-never-run" alt="코드베이스에서 능력 record 의 아홉째 성분과 Kafka 가 그 자리에 넘기는 값과 그 상수의 프로파일 참조 수, 그 플래그를 읽는 코드 수와 바로 옆 열째 플래그를 읽는 코드와 그 열째가 거짓인 이유를 적은 javadoc, 트랜잭션 검증기가 요구하는 다섯 조건, 기동 검증으로 감싸이는 검증기와 직접 불리는 검증기와 감싸이지 않은 채 빈으로만 발행되는 트랜잭션 검증기와 그것을 언급하는 코드 수, 그리고 감쌀 수 없는 진짜 이유인 래퍼 시그니처와 두 번째 인자의 출처를 뽑은 출력 54줄. 아홉째 플래그를 읽는 코드가 0 이고 열째는 읽히면서 일부러 거짓이라는 대비가 그 출력에 보인다." caption="아홉째 성분과 그 값 · 읽는 코드 0 · 옆 열째는 읽히고 거짓 · 검증기의 다섯 조건 · 감싸인 것과 아닌 것 · 두 번째 인자의 출처 없음 — 54줄" zoom="true"
|
|
:::
|
|
|
|
능력 record 는 열두 개의 불리언을 위치로 받고, 아홉째가 브로커 트랜잭션이다. Kafka 전송이 그 자리에 참을 넘기고, 그 선언에 프로파일 참조는 0 이다.
|
|
|
|
트랜잭션 식별자 없이 구성된 배포도 같은 답을 받는다.
|
|
|
|
## 옆자리가 이 플래그의 무게를 보여 준다
|
|
|
|
이 아홉째 플래그를 읽는 프로덕션 코드가 0 이다.
|
|
|
|
바로 옆 열째는 다르다. 발행 경로가 그 값을 읽어 판단한다. 그리고 Kafka 는 그 자리에 거짓을 넘긴다.
|
|
|
|
그 자리 javadoc 이 왜 거짓인지 적는다. 참으로 선언하면 중복 제거 요청이 받아들여진 뒤 조용히 아무 일도 하지 않고, 호출자는 브로커가 중복을 제거한다고 믿어 원래 만들었을 멱등성을 건너뛴다. 거짓으로 두면 그 요청이 기동 실패가 되는데, 그것이 이 플래그가 존재하는 이유라는 것이다.
|
|
|
|
같은 종류의 과대 선언이 하나는 실제 피해를 만들고 하나는 만들지 않는다. 차이는 읽는 코드가 있느냐다.
|
|
|
|
그러므로 아홉째의 과대 선언이 오늘 만드는 것은 조회 경로의 피해가 아니다. 남는 것은 검증 경로다.
|
|
|
|
## 검증기가 요구하는 다섯
|
|
|
|
트랜잭션 식별자 접두가 비어 있지 않을 것, 생산자가 멱등일 것, 응답 확인이 전부일 것, 오프셋 커밋이 수동일 것. 그리고 다섯째로 목적지가 인박스 트랜잭션을 선언하지 않을 것이다.
|
|
|
|
다섯째가 중요하다고 javadoc 이 직접 말한다. 목적지가 인박스 트랜잭션을 선언한다는 것은 부작용이 데이터베이스에 있다는 뜻이고, Kafka 트랜잭션은 거기까지 걸칠 수 없다. 둘을 함께 설정할 수 있게 두면 팀이 "트랜잭션"이라는 단어를 두 번 읽고 경로 전체가 원자적이라고 결론짓게 된다는 것이다.
|
|
|
|
## 그 검증기는 어디에서도 실행되지 않는다
|
|
|
|
스타터에는 기동 시 프로파일마다 검증기를 돌리는 래퍼가 있다. 그 래퍼로 감싸인 검증기가 셋이다. 목적지 프로파일 검증기는 래퍼 대신 직접 호출로 돈다.
|
|
|
|
트랜잭션 검증기는 그냥 빈이다. 자동설정 밖에서 그것을 언급하는 프로덕션 코드가 0 이고, 그것을 만드는 테스트도 0 이다.
|
|
|
|
다섯 규칙은 main 에서도 test 에서도 한 번도 실행되지 않는다.
|
|
|
|
## 같은 결함이 이 스타터에서 한 번 고쳐졌다
|
|
|
|
래퍼 클래스의 javadoc 이 왜 만들어졌는지 적는다.
|
|
|
|
Kafka 와 Rabbit 과 보안 검증기가 전부 빈이었고 어디에도 주입되지 않았다. 컨텍스트는 브로커마다 검증기를 발행했고 아무것도 검증하지 않았다.
|
|
|
|
그다음 문장이 결과를 적는다. 브로커가 줄 수 없는 보증을 약속하는 프로파일이 — 비트랜잭션 생산자 위의 정확히 한 번 주장, 복제본 하나짜리의 정족수 확인, 프로덕션 리스너의 평문 자격증명이 — 깨끗하게 부팅한 뒤 그것에 의존하는 첫 메시지에서 실패한다. 그것을 알게 되는 자리로는 틀린 곳이다.
|
|
|
|
그 수정이 그 세 검증기에 적용됐다. 트랜잭션 검증기가 남았다.
|
|
|
|
## 감쌀 수 없는 이유는 인자 수가 아니다
|
|
|
|
래퍼는 프로파일 공급자와 소비자 함수를 받는다. 인자 수 자체는 장애가 아니다 — 나머지 둘을 캡처하는 람다면 타입이 맞는다.
|
|
|
|
걸리는 것은 두 번째 인자다. 트랜잭션 식별자 접두는 이 저장소의 main 에서 이 검증기의 파라미터 이름과 그 javadoc 과 그것을 검사하는 조건문, 셋으로만 존재한다. 브로커 프로파일에도 설정 키에도 그 값이 없다.
|
|
|
|
감쌀 자리보다 공급할 값이 먼저 없다.
|
|
|
|
## 확인하지 못한 것
|
|
|
|
검증기가 실제로 건너뛰는지 컨텍스트를 세워 보지는 않았다. 판정 근거가 감싸기 목록과 언급 계수의 대조라, 리플렉션으로 부르는 경로까지는 배제하지 못했다.
|
|
|
|
<!-- body:end -->
|