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>
12 KiB
kind, slug, title, topic, project, status, sourceRevision, rootTreeNode, evidenceCapturedOn, body, assets, evidence, source
| kind | slug | title | topic | project | status | sourceRevision | rootTreeNode | evidenceCapturedOn | body | assets | evidence | source | |||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| CASE | analysis-finding-a06-f009 | 예산 초과 분기가 한 관측에 success 와 failure 를 차례로 부른다 | multitenancy-isolation | clean-architecture-backend-template | 게시 전 | 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 | case:analysis-finding-a06-f009 | 2026-09-04 | case-analysis-finding-a06-f009.body.md |
|
|
|
예산 초과 분기가 한 관측에 success 와 failure 를 차례로 부른다
DefaultMongoImperativeExecutor:97 이 observation.success(outcome) 를 부른 뒤 :98 이 MongoOperationRejectedException 을 던진다. 그 예외가 MongoPersistenceException 하위 타입이고 같은 try 안이라 :108 의 catch 가 잡아 :109 에서 observation.failure(...) 를 부른다.
관계
- 메시징만 성공 로그를 try 밖으로 옮기고 알림은 같은 try 에 남겨 두었다
그 사례도 성공 기록 호출이
try안에 남아 있어서 뒤따르는 예외를 같은 블록의catch가 받는다. 여기서는 로그가 아니라 관측이고,catch가 성공을 지우는 대신failure를 덧쓴다. - 데드라인을 서버로 보내는 접근자를 부르는 프로덕션 코드가 없다 같은 메서드의 같은 분기가 두 기록의 출발점이다. 그쪽은 그 검사가 왜 사후일 수밖에 없는지를, 이쪽은 그 검사가 관측에 무엇을 남기는지를 본다.
- port 계약은 동시성 요구를 적는다
그 규칙의 두 번째 항목이 현재 구현이 안전한 것과 계약이 안전을 요구하는 것을 가르라고 적는다. 여기서도 두 구현이 나중 호출로 태그를 덮어써서 결과가 하나로 남을 뿐,
MongoOperationObservation자바독에는 두 메서드를 각각 몇 번 부를 수 있는지가 없다.
문제
예산을 넘긴 연산에 대해 실행기는 거부 예외를 던진다. 그 던짐이 이미 열려 있는 관측에 무엇을 남기는지 확인했다.
결론
MongoOperationObservation:16 의 success 와 :19 의 failure 가 결과를 기록한다. 어느 쪽을 몇 번 불러야 하는지, 여러 번 불렀을 때 무엇이 남는지는 자바독에 없다.
DefaultMongoImperativeExecutor:93 이 경과 시간을 context.timeout() 과 견준다. 넘었으면 :97 이 observation.success(outcome) 를 부르고 :98 이 MongoOperationRejectedException 을 던진다.
그 예외는 MongoPersistenceException 을 상속한다. 던짐이 :87 에서 열린 try 안에 있으므로 :108 의 catch (MongoPersistenceException alreadyTranslated) 가 잡는다.
그 catch 가 실패를 기록한 뒤 예외를 다시 던진다. 그래서 예산 초과 경로에서만 한 관측 객체가 두 결과를 다 받는다.
이 저장소에 있는 구현은 둘뿐이고, 둘 다 그 두 번째 호출을 문제로 만들지 않는다. MicrometerMongoOperationObserver:76~:88 의 두 메서드가 outcomeTags 를 통째로 덮어쓰고 :96~:102 의 close() 가 stopped 플래그를 보고 타이머를 한 번만 멈추므로, 나중에 부른 failure 의 태그가 남는다. NoOpMongoOperationObserver:29~:50 은 아무것도 기록하지 않는다.
그 무해함은 두 구현이 필드를 덮어쓰기 때문이지 인터페이스가 그렇게 하라고 적어 둔 것이 아니다. 호출마다 계수기를 올리는 구현이 들어오면 이 경로의 연산 하나가 두 건이 된다.
검증 환경
OpenJDK : 21.0.12 확인 방식 : 관측 인터페이스 전문 인용, 예산 초과 분기와 그것을 감싸는 try 와 catch 인용, 거부 예외의 상속 관계 인용, 그 파일에서 두 메서드를 부르는 자리 전수와 범위별 계수, 두 구현의 메서드와 close() 인용, 인터페이스 구현체 계수와 전수 소스 수정 : x
재현 조건
- MongoOperationObservation 인터페이스를 전문으로 싣는다.
- 실행기의 try 블록을 예산 초과 분기와 catch 절까지 함께 인용한다.
- 거부 예외가 무엇을 상속하는지 인용한다.
- try 범위와 catch 범위에서 success 와 failure 를 부르는 줄을 각각 세고, 그 파일의 두 메서드 호출을 전부 나열한다.
- 두 구현의 메서드와 close() 를 인용한다.
- 그 인터페이스를 구현하는 자리를 세고 전부 나열한다.
본문
블로킹 실행기는 콜백이 반환된 뒤 경과 시간을 잰다. 예산을 넘겼으면 거부 예외를 던진다.
success 와 failure 의 자바독이 정하지 않은 것
:::evidence key="analysis-finding-a06-f009" alt="저장소 루트에서 돌린 정적 검색 출력 171줄. 먼저 MongoOperationObservation 626번 전문이 실린다. 자바독은 이것이 진행 중인 연산 하나의 관측 범위이고 AutoCloseable 이라 콜백이 플랫폼이 분류한 적 없는 것을 던지는 경로까지 모든 경로에서 닫히며, 성공에서만 닫히는 관측은 시스템이 건강하지 않을 때 정확히 건강해 보이는 지표를 만든다고 적는다. 16번이 success 로 성공적 완료와 쓰기 결과를 기록하고, 19번이 failure 로 실패 컨텍스트가 담을 수 있는 경계 있는 메타데이터만 써서 실패를 기록하며, 22번이 traceId, 25번이 close 다. 둘 중 하나만 불러야 한다거나 마지막 호출이 이긴다는 말은 없다. 이어서 DefaultMongoImperativeExecutor 85118번이 실린다. 86번이 관측을 열고 87번이 try 를 열며 88번이 콜백을 부르고 89번이 경과 시간을 재고 92번이 결과를 정한다. 93번이 경과 시간을 컨텍스트 타임아웃과 견주고, 9496번 주석이 컨텍스트가 모든 연산에 데드라인을 선언하는데 아무것도 그것과 견주지 않았으며 콜백은 드라이버 호출 중간에 끊을 수 없어 이것이 작업을 끊지 못하지만 예산을 넘긴 작업에 성공을 보고하는 것보다 위반을 보고하는 편이 낫다고 적는다. 97번이 observation.success 를 부르고 98104번이 MongoOperationRejectedException 을 던지는데 메시지에 실제 경과 밀리초와 선언된 타임아웃 밀리초가 들어간다. 106번이 정상 경로의 success 이고 107번이 결과를 돌려준다. 108번의 catch 가 MongoPersistenceException 을 잡아 109번에서 observation.failure 를 부르고 110번에서 다시 던지며, 111번과 113번의 catch 가 드라이버 예외와 스프링 예외를 번역으로 보낸다. 다음으로 MongoOperationRejectedException 124번이 실리는데 712번 자바독이 이것은 서버에 닿기 전에 플랫폼이 로컬에서 거절한 것이고 등록되지 않은 필드와 연산자, 올려 잡은 예산, 읽기 API 안의 쓰기 단계, 깊은 skip, 없는 테넌트 컨텍스트, 런타임 클라이언트의 관리 명령 같은 가드레일 예외이며 언제나 NOT_SENT 이고 재시도 불가라고 적는다. 14번이 그것이 MongoPersistenceException 을 상속한다고 보인다. 이어서 try 블록 안에서 observation.success 를 부르는 줄이 2 개, 같은 try 의 catch 가 observation.failure 를 부르는 줄이 1 개라고 나오고, 그 파일에서 두 메서드를 부르는 자리가 97번과 106번의 success, 109번과 129번의 failure 넷으로 나열된다. 다음으로 MicrometerMongoOperationObserver 70103번이 실리는데 72번이 outcomeTags 를 result unknown 으로 초기화하고, 7678번의 success 와 8188번의 failure 가 둘 다 그 필드를 통째로 덮어쓸 뿐이며, 96102번의 close 가 stopped 플래그를 보고 한 번만 sample.stop 을 부른다. 마지막으로 MongoOperationObservation 을 구현하는 자리가 2 개라고 나오고 그 둘이 NoOpMongoOperationObserver 29번의 NoOpObservation 과 MicrometerMongoOperationObserver 56번의 MicrometerObservation 이며, 대조로 센 MongoOperationObserver 구현도 2 개다." caption="관측 인터페이스 전문과 두 메서드의 계약 · 예산 초과 분기와 그것을 잡는 catch · 거부 예외가 상속하는 것 · 범위별 호출 계수와 그 파일의 네 자리 · 출하 구현의 두 메서드와 한 번만 도는 close · 구현체 둘 — 171줄 · exit 0" zoom="true"
:::
MongoOperationObservation:9~:11 은 이 타입이 AutoCloseable 인 이유를 적는다. 콜백이 플랫폼이 분류한 적 없는 것을 던지는 경로까지 포함해 모든 경로에서 범위가 닫혀야 하고, 성공에서만 닫히는 관측은 시스템이 건강하지 않을 때 정확히 건강해 보이는 지표를 만들기 때문이다.
:16 의 success 는 성공적 완료와 쓰기 결과를 기록한다. :19 의 failure 는 실패 컨텍스트가 담을 수 있는 경계 있는 메타데이터만 써서 실패를 기록한다.
둘 중 하나만 불러야 한다는 말도, 여러 번 부르면 나중 호출이 앞선 호출을 덮는다는 말도 없다.
예산 초과 분기
DefaultMongoImperativeExecutor:86 이 관측을 열고 :87 이 try 를 연다. :88 이 콜백을 부르고 :89 가 경과 시간을 잰다.
:93 이 그 경과 시간을 context.timeout() 과 견준다. 넘었으면 :97 이 observation.success(outcome) 를 부르고, :98~:104 가 실제 경과 밀리초와 선언된 타임아웃 밀리초를 담은 MongoOperationRejectedException 을 던진다.
:94~:96 주석이 이 순서의 의도를 적는다. 이 검사가 작업을 끊을 수는 없지만, 예산을 넘긴 작업에 성공을 보고하는 것보다 위반을 보고하는 편이 낫다는 것이다.
거부 예외를 같은 try 의 catch 가 잡는다
MongoOperationRejectedException:14 가 MongoPersistenceException 을 상속한다. :7~:12 자바독은 이것이 서버에 닿기 전 로컬 거절이며 언제나 NOT_SENT 이고 재시도 불가라고 적는다.
:108 의 catch (MongoPersistenceException alreadyTranslated) 가 그 예외를 잡는다. :109 가 observation.failure(...) 를 부르고 :110 이 다시 던진다.
try 범위 안에서 success 를 부르는 줄이 둘이고 그 catch 가 failure 를 부르는 줄이 하나다. 예산 초과 경로에서는 같은 관측 객체에 대해 :97 의 success 가 먼저 불리고 :109 의 failure 가 이어서 불린다.
두 구현 모두 나중 호출의 태그만 남긴다
MicrometerMongoOperationObserver:72 가 outcomeTags 를 result unknown 으로 초기화한다.
:76~:78 의 success 와 :81~:88 의 failure 가 둘 다 그 필드를 통째로 덮어쓴다. 누적하지 않는다.
:96~:102 의 close() 가 stopped 플래그를 보고 한 번만 sample.stop(...) 을 부른다. 그때 남아 있는 태그는 나중에 부른 failure 쪽이다.
NoOpMongoOperationObserver:29~:50 의 구현은 세 메서드 몸통이 전부 비어 있다. 이 저장소에서 MongoOperationObservation 을 구현하는 것은 그 둘뿐이다.
인터페이스에 호출 횟수 규칙이 없다
두 구현 모두 나중 호출이 앞선 것을 덮는 모양이라 지금은 기록되는 결과가 달라지지 않는다.
그 성질이 인터페이스에 쓰여 있지 않다. success 와 failure 를 각각 계수하는 구현을 포크가 만들면, 예산을 넘긴 연산 하나가 성공 한 번과 실패 한 번으로 세어진다.
원문이 "출하 구현" 이라 부른 것
원문이 무해하다고 적은 그 구현은 둘이다. MicrometerMongoOperationObserver:56 의 MicrometerObservation 과 NoOpMongoOperationObserver:29 의 NoOpObservation 이고, 저장소에 다른 구현은 없다.
확인하지 못한 것
타이머에 실제로 무엇이 찍히는지 연산을 돌려 재현하지 않았다.
두 호출을 각각 계수하는 구현을 포크가 만들 가능성이 얼마나 되는지는 판단하지 않았다.
위반을 성공으로 먼저 남기는 이 순서를 바꾸면 무엇이 달라지는지 따져 보지 않았다.
두 호출을 각각 계수하는 구현을 포크가 만들 가능성이 얼마나 되는지는 판단하지 않았다.