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>
118 lines
7.8 KiB
Markdown
118 lines
7.8 KiB
Markdown
---
|
|
kind: CASE
|
|
slug: a-replay-store-with-no-eviction-path
|
|
title: 재생 저장소에 제거 경로가 없고, 형제 맵의 상한은 세션 경계 하나다
|
|
topic: retention-and-unbounded-growth
|
|
project: clean-architecture-backend-template
|
|
status: 게시 전
|
|
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
|
rootTreeNode: case:a-replay-store-with-no-eviction-path
|
|
evidenceCapturedOn: 2026-09-02
|
|
assets:
|
|
- key: a-replay-store-with-no-eviction-path
|
|
file: ../../../final/evidence/rendered/a-replay-store-with-no-eviction-path.svg
|
|
evidence:
|
|
- ../../../final/evidence/raw/a-replay-store-with-no-eviction-path.txt
|
|
source:
|
|
- 분석 문서는 grpc-policy 편 §17.3 과 grpc-advanced-streaming 편 §17.1 이다. 앞의 절이 재생 저장소의 제거 경로 부재를 P2 로 판정하고, 두 모듈의 대조가 "저쪽은 풀었고 이쪽은 안 풀었다"가 아니라는 정정도 그 절에 있다. 뒤의 절이 형제 맵의 증가를 P3 으로 기록하고, 필요한 창이 좁다는 근거를 함께 적는다.
|
|
- 같은 절이 이 저장소가 커밋될 때마다 항목이 쌓인다고 적는데, 이 클래스를 잡는 프로덕션 코드는 아직 없다. 위 터미널 출력의 참조 수가 그것이다.
|
|
---
|
|
|
|
# 재생 저장소에 제거 경로가 없고, 형제 맵의 상한은 세션 경계 하나다
|
|
|
|
결과 재생 저장소가 항목을 넣기만 하고 지우지 않는다. 형제 모듈의 중복 제거기가 같은 형태이고, 그 클래스는 같은 종류의 무제한 증가를 비판하는 javadoc 을 갖고 있다.
|
|
|
|
## 관계
|
|
|
|
- **bounded 와 unbounded 오버로드를 나란히 둔 포트**
|
|
같은 Topic 의 보존 경계 문제다.
|
|
- **같은 안전 규칙은 하나의 공식과 하나의 강제 시점을 갖는다**
|
|
보존 경계를 어디서 정하는지에 대한 규칙이다.
|
|
- **runtime_memberships를 먼저 읽고 심각도를 정한다**
|
|
이 사례가 오늘의 사고가 아닌 이유를 판정하는 규칙이다.
|
|
|
|
## 문제
|
|
|
|
정확히 한 번을 보장하려면 과거를 기억해야 한다. 같은 요청이 다시 오면 저장된 결과를 돌려주고, 같은 메시지가 다시 오면 건너뛴다.
|
|
|
|
그 기억이 자료구조다. 요청 식별자나 메시지 식별자를 키로, 결과나 체크포인트를 값으로 갖는다.
|
|
|
|
## 결론
|
|
|
|
결과 재생 저장소에는 넣는 경로만 있다. 항목 하나의 크기는 제한하고 개수는 제한하지 않는다. 제거도 비우기도 축출도 만료도 없다.
|
|
|
|
형제 모듈의 중복 제거기는 같은 형태에 상한 하나를 갖는다. 세션을 끝낼 때 그 세션 키를 지운다. 그래서 이쪽의 증가는 세션 수명에 묶이고, 저장소 쪽은 프로세스 수명에 묶인다.
|
|
|
|
두 모듈의 대조는 한쪽이 풀고 한쪽이 안 푼 것이 아니다. 같은 형태의 무제한 증가를 둘 다 갖고 있고, 한쪽만 부분적 상한을 갖는다. 무거운 쪽은 저장소다.
|
|
|
|
이 종류의 자료구조에서 보존 경계는 기능이 아니라 전제다. 무엇을 언제까지 기억하는지가 정해지지 않으면 그 보장은 메모리가 버티는 동안만 성립한다.
|
|
|
|
두 모듈 모두 런타임 소속이 비어 있고, 저장소 쪽은 이 클래스를 잡는 프로덕션 코드도 없다. 지금 도는 배포에서 일어나는 문제는 아니다.
|
|
|
|
## 검증 환경
|
|
|
|
OpenJDK : 21.0.12
|
|
Gradle : 9.0.0
|
|
확인 방식 : 두 자료구조의 삽입 경로와 제거 경로 대조
|
|
소스 수정 : x
|
|
|
|
## 재현 조건
|
|
|
|
1. 재생 저장소의 맵 선언과 항목을 넣는 메서드를 읽는다.
|
|
2. 같은 파일에서 제거·비우기·축출·만료를 검색하고, 이 클래스를 잡는 프로덕션 코드를 센다.
|
|
3. 개수를 노출하는 메서드가 있는지, 그 값을 읽는 프로덕션 코드가 있는지 센다.
|
|
4. 중복 제거기의 클래스 javadoc 과 두 맵의 증가·감소 지점을 확인한다.
|
|
5. 중복 제거 키의 형식을 확인해 세션 종료가 실제로 그 세션 항목만 지우는지 본다.
|
|
6. 두 모듈의 런타임 소속을 모듈 레지스트리에서 읽는다.
|
|
|
|
## 본문
|
|
|
|
<!-- body:start -->
|
|
|
|
## 크기는 제한하고 개수는 제한하지 않는다
|
|
|
|
:::evidence key="a-replay-store-with-no-eviction-path" alt="코드베이스에서 결과 재생 저장소의 맵 선언과 항목 크기 제한, 제거·비우기·축출·만료 매치 수, 개수를 노출하는 메서드와 그 값을 읽는 코드 수, 이 클래스를 잡는 코드 수, javadoc 이 저장소를 부르는 이름을 뽑은 출력. 이어서 형제 모듈 중복 제거기의 클래스 javadoc 과 두 맵의 선언, 항목이 쌓이는 지점, 재생 판정이 보는 값, 이 파일의 제거 매치 수와 그 둘이 있는 자리, 중복 제거 키의 형식, 그리고 두 모듈의 런타임 소속이 나온다. 저장소 쪽 제거 매치가 0 이고 형제 맵 쪽은 세션 종료 하나뿐이라는 것이 그 출력에 보인다." caption="저장소: 크기 제한만 · 제거 0 · 잡는 코드 0 / 형제 맵: 메시지마다 한 항목 · 제거는 세션 종료 하나 · 키는 세션 접두 — 54줄" zoom="true"
|
|
:::
|
|
|
|
결과 재생 저장소는 결과 참조를 키로, 직렬화된 응답을 값으로 갖는 맵이다.
|
|
|
|
넣는 메서드가 크기를 검사한다. 인라인 한도를 넘는 응답은 예외로 거절하고, 객체 참조 뒤에 두라고 메시지에 적는다.
|
|
|
|
개수를 검사하는 코드는 없다. 제거도 비우기도 축출도 만료도 파일 전체에서 0 이다.
|
|
|
|
이 클래스를 잡는 프로덕션 코드도 아직 0 이다. 배선되면 멱등 키가 필요한 커밋 하나당 항목 하나가 프로세스 수명 동안 남는 형태다.
|
|
|
|
## 개수를 볼 수는 있는데 보는 곳이 없다
|
|
|
|
개수를 돌려주는 메서드가 하나 있다. 그것을 부르는 프로덕션 코드가 0 이고, 부르는 것은 테스트 하나다.
|
|
|
|
javadoc 은 이 저장소를 배포가 고를 수 있는 선택지 중 하나로 소개하면서 작은 인라인 저장소라고 부른다. 작게 유지하는 장치는 그 안에 없다.
|
|
|
|
## 형제 모듈이 같은 문제를 문장으로 적어 두었다
|
|
|
|
중복 제거기의 클래스 javadoc 이 설계 선택을 설명한다. 본 키를 모으는 집합을 거부한 이유가 셋인데, 첫 번째가 집합은 세션 수명 동안 무제한으로 증가한다는 것이다.
|
|
|
|
체크포인트 맵은 그 비판을 지킨다. 세션당 항목 하나이고 순번만 앞으로 간다.
|
|
|
|
형제 맵은 지키지 않는다. 적용된 메시지마다 결과 참조를 하나씩 넣는다.
|
|
|
|
같은 javadoc 에는 그 맵에 대해 재생 결과가 체크포인트 뒤의 좁은 창 동안만 유지된다고 한 줄 적혀 있다. 창을 닫는 코드가 없다 — 체크포인트가 그 순번을 지나가도 항목은 남는다.
|
|
|
|
필요한 창이 실제로 좁다는 것도 코드에 있다. 재생 판정은 체크포인트의 순번 비교가 내리고, 재생 응답에 쓰이는 것은 그 순번의 결과 하나다. 체크포인트보다 한참 뒤처진 순번의 결과가 필요한 구간은 재개 직후뿐이다.
|
|
|
|
## 줄어드는 지점은 세션 종료 하나다
|
|
|
|
이 파일에서 무언가를 지우는 지점은 둘이고 둘 다 같은 메서드 안에 있다. 세션을 끝내면서 체크포인트를 지우고, 재생 결과 맵에서 그 세션 접두사를 가진 키를 지운다.
|
|
|
|
중복 제거 키는 세션 값과 세대와 순번을 막대로 이어 만든다. 접두사 제거가 그 세션의 모든 세대를 함께 지운다.
|
|
|
|
그래서 이쪽의 증가는 세션 수명에 묶인다. 저장소 쪽은 프로세스 수명에 묶인다.
|
|
|
|
두 모듈 모두 모듈 레지스트리의 런타임 소속이 비어 있다. 배선 경로가 없으니 오늘 이것으로 무너지는 배포는 없다.
|
|
|
|
## 확인하지 못한 것
|
|
|
|
오래 돌려 증가를 확인해 보지는 않았다. 배선 경로가 없어 실행 대상이 없다.
|
|
|
|
<!-- body:end -->
|