Files
document-haness/docs/clean-architecture-backend-template/tech-log-studio/retention-and-unbounded-growth/case/case-a-replay-store-with-no-eviction-path.md
T
DongHyeonkaandClaude Opus 5 b2963105a8 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>
2026-09-04 22:51:59 +09:00

7.8 KiB

kind, slug, title, topic, project, status, sourceRevision, rootTreeNode, evidenceCapturedOn, assets, evidence, source
kind slug title topic project status sourceRevision rootTreeNode evidenceCapturedOn assets evidence source
CASE a-replay-store-with-no-eviction-path 재생 저장소에 제거 경로가 없고, 형제 맵의 상한은 세션 경계 하나다 retention-and-unbounded-growth clean-architecture-backend-template 게시 전 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 case:a-replay-store-with-no-eviction-path 2026-09-02
key file
a-replay-store-with-no-eviction-path ../../../final/evidence/rendered/a-replay-store-with-no-eviction-path.svg
../../../final/evidence/raw/a-replay-store-with-no-eviction-path.txt
분석 문서는 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. 두 모듈의 런타임 소속을 모듈 레지스트리에서 읽는다.

본문

크기는 제한하고 개수는 제한하지 않는다

:::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 에는 그 맵에 대해 재생 결과가 체크포인트 뒤의 좁은 창 동안만 유지된다고 한 줄 적혀 있다. 창을 닫는 코드가 없다 — 체크포인트가 그 순번을 지나가도 항목은 남는다.

필요한 창이 실제로 좁다는 것도 코드에 있다. 재생 판정은 체크포인트의 순번 비교가 내리고, 재생 응답에 쓰이는 것은 그 순번의 결과 하나다. 체크포인트보다 한참 뒤처진 순번의 결과가 필요한 구간은 재개 직후뿐이다.

줄어드는 지점은 세션 종료 하나다

이 파일에서 무언가를 지우는 지점은 둘이고 둘 다 같은 메서드 안에 있다. 세션을 끝내면서 체크포인트를 지우고, 재생 결과 맵에서 그 세션 접두사를 가진 키를 지운다.

중복 제거 키는 세션 값과 세대와 순번을 막대로 이어 만든다. 접두사 제거가 그 세션의 모든 세대를 함께 지운다.

그래서 이쪽의 증가는 세션 수명에 묶인다. 저장소 쪽은 프로세스 수명에 묶인다.

두 모듈 모두 모듈 레지스트리의 런타임 소속이 비어 있다. 배선 경로가 없으니 오늘 이것으로 무너지는 배포는 없다.

확인하지 못한 것

오래 돌려 증가를 확인해 보지는 않았다. 배선 경로가 없어 실행 대상이 없다.