Files
document-haness/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-analysis-finding-a05-f024.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

11 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-a05-f024 회수가 페이지 하나를 다 쓰면 남은 바이트를 들고 그대로 끝난다 multitenancy-isolation clean-architecture-backend-template 게시 전 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 case:analysis-finding-a05-f024 2026-09-04 case-analysis-finding-a05-f024.body.md
key file
analysis-finding-a05-f024 ../../../final/evidence/rendered/analysis-finding-a05-f024.svg
../../../final/evidence/raw/analysis-finding-a05-f024.txt
원본 분석 절은 analysis/05-adapter-outbound-persistence-jpa.md §24 이다.

회수가 페이지 하나를 다 쓰면 남은 바이트를 들고 그대로 끝난다

JpaQuotaReclaimGateway:53~:54 는 확정 행을 Limit.of(64) 로 한 번 조회하고 그 목록만 순회한다. 프로브에서 1 바이트 행 200 개에 200 바이트 회수를 요청했더니 차감이 64 회에 그쳤고 나머지 136 은 어디에도 보고되지 않았다.

관계

  • 쿼터 원장은 사용량을 적기만 하고 승인은 그것을 읽지 않는다 같은 원장의 다른 결함이다. 그쪽은 기록된 값을 아무도 읽지 않는 것이고, 여기는 그 값이 실제보다 커지는 것이다.
  • 빠뜨림이 통과가 되는 게이트는 게이트가 아니다 같은 계열이다. 남은 작업이 있는데 아무 신호 없이 정상 종료한다.
  • 모르는 것은 성공도 실패도 아닌 세 번째 결과여야 한다 회수 연산이 결과를 돌려주지 않으므로 다 했는지 못 했는지가 어디에도 표현되지 않는다.

문제

물리 삭제가 끝나면 정리 서비스가 회수 게이트웨이를 부른다. 그 게이트웨이가 원장에서 확정 사용량을 줄인다.

요청한 바이트가 여러 행에 걸쳐 있을 때 어떻게 되는지 확인했다.

결론

:33 이 페이지 상한을 64 로 둔다. :55~:61 이 그 한 페이지를 순회하며 차감하고, 마지막 원소를 지나면 루프가 그대로 빠져나온다. 재조회도 남은 값 반환도 로그도 없다.

저장소 클래스를 그대로 쓰고 리포지터리만 프록시로 대신해 돌렸다. 원장 행이 64 개일 때는 요청한 만큼 전부 회수된다. 65 개일 때 1 이 남고, 200 개일 때 136 이 남는다. 남는 양은 페이지를 넘는 행 수만큼 늘어난다.

클래스 자바독 :25~:27 은 나머지를 버리는 이유를 적어 두었다. 원장에 흡수할 데가 없을 때의 이야기이고, 그런 상황은 총량이 이미 잘못 적혔다는 신호라는 것이다.

프로브가 만든 나머지는 종류가 다르다. 원장에는 아직 뺄 데가 넉넉한데 한 페이지만 보고 끝냈기 때문이다. 자바독이 허용한 나머지와 상한이 만든 나머지를 코드가 구분하지 않는다.

이 메서드는 아무것도 돌려주지 않는다. 정리 서비스가 회수 결과를 물을 자리가 애초에 없다.

이 경계를 겨냥한 시험도 없다. 페이지 상한과 그 바로 위아래 값을 쓰는 시험 줄이 모두 0 개다.

검증 환경

OpenJDK : 21.0.12 Gradle : 9.0.0 확인 방식 : 게이트웨이 전문 게재와 클래스 자바독 인용, 페이지 질의와 차감 질의의 정의와 그 자바독 인용, 회수 호출자 전수, 저장소 게이트웨이를 그대로 쓰고 리포지터리만 프록시로 대신해 원장 행 수와 요청 바이트를 바꿔 가며 페이지 질의 횟수와 차감 횟수와 남은 바이트를 관측, 경계 값을 쓰는 시험 줄 계수와 이름에 회수가 든 파일 나열 소스 수정 : x

재현 조건

  1. 게이트웨이를 전문으로 싣고 클래스 자바독을 함께 읽는다.
  2. 페이지 질의와 차감 질의의 정의를 인용한다.
  3. 회수를 부르는 자리를 전부 찾는다.
  4. 리포지터리 인터페이스를 프록시로 바꿔 끼우고, 질의가 오면 원장에 넣어 둔 행을 상한까지 잘라 돌려주게 한다.
  5. 원장 행 수와 요청 바이트를 64·65·200 으로 바꿔 가며 게이트웨이를 부르고 페이지 질의 횟수와 차감 횟수와 남은 바이트를 적는다.
  6. 64 와 65 와 페이지 상한 상수를 쓰는 시험 줄을 센다.

본문

JpaQuotaReclaimGateway 는 물리 삭제가 끝난 뒤 원장의 확정 사용량을 줄인다. 정리 서비스가 삭제한 바이트 수를 넘겨 부른다.

게이트웨이는 확정 행을 한 번만 조회한다

:::evidence key="analysis-finding-a05-f024" alt="저장소 루트에서 돌린 정적 검색과 프로브의 출력 129줄. 먼저 JpaQuotaReclaimGateway 64줄이 전문으로 실린다. 2527번 줄 클래스 자바독은 어떤 행도 흡수하지 못하는 나머지는 이월하지 않고 버린다고 적으면서, 원장의 바닥이 0 이며 기록된 총량을 넘어서는 회수는 총량이 이미 과소 기록됐다는 뜻이라고 덧붙인다. 33번 줄이 RECLAIM_PAGE 를 Limit.of(64) 로 두고, 4463번 줄의 reclaim 이 51번에서 시각을 얻고 52번에서 outstanding 을 요청 바이트로 두며 5354번에서 확정 행을 한 번 조회해 그 결과를 순회한다. 5557번은 outstanding 이 0 이면 돌아가고, 58번이 차감량을 정하고 5961번이 갱신 건수가 1 일 때만 뺀다. 루프가 끝나면 6263번에서 메서드가 그대로 끝난다. 이어서 그 한 번의 질의가 무엇을 돌려주는지 리포지터리 106117번 줄로 나오는데, 확정 상태이고 바이트가 남은 행을 갱신 시각 내림차순으로 상한만큼 가져온다. 119139번 줄의 차감 질의는 자바독에서 그 가드가 동시 회수를 안전하게 만든다고 적는다. 회수를 부르는 자리로 정리 서비스 한 곳과 시험 몇 곳이 나온다. 그다음 프로브가 실린다. 리포지터리만 프록시로 대신하고 게이트웨이는 저장소 클래스를 그대로 쓴다. 1바이트 확정 행 64 개에 64 바이트를 요청하면 페이지 질의 1 회에 차감 64 회로 남는 바이트가 0 이다. 65 개에 65 바이트를 요청하면 페이지 질의는 그대로 1 회이고 차감이 64 회에서 멈춰 1 바이트가 남는다. 200 개에 200 바이트를 요청하면 차감이 64 회이고 136 바이트가 남는다. 마지막으로 그 경계를 짚는 시험을 세면 64 와 65 와 RECLAIM_PAGE 를 쓰는 시험 줄이 각각 0 개이고, 이름에 Reclaim 이 든 파일은 게이트웨이와 포트와 시험용 기록기 셋뿐이다." caption="게이트웨이 64줄 전문과 그 클래스 자바독 · 한 번의 페이지 질의가 돌려주는 것과 차감 질의의 가드 · 회수 호출자 · 저장소 게이트웨이를 그대로 부른 세 경우 · 그 경계를 짚는 시험의 부재 — 129줄 · exit 0" zoom="true" :::

:33RECLAIM_PAGELimit.of(64) 로 둔다.

reclaim:44 은 음수와 0 을 먼저 걸러 낸다. :52outstanding 을 요청 바이트로 두고, :53~:54 가 그 상한으로 확정 행을 조회해 향상된 for 문으로 순회한다.

루프 안에서 :55outstanding 이 0 이면 돌아가고, :58 이 차감량을 Math.min 으로 정하고, :59 가 갱신 건수가 1 일 때만 :60 에서 뺀다.

목록이 끝나면 :62~:63 에서 메서드가 끝난다. outstanding 이 얼마든 상관없다.

그 한 번의 질의가 무엇을 돌려주는가

FileserverQuotaRepository:107~:117 은 확정 상태이고 확정 바이트가 남은 행을 갱신 시각 내림차순으로 상한만큼 가져온다.

:126~:139 의 차감 질의는 조건부 갱신이다. :122~:124 자바독은 그 가드가 동시 회수를 안전하게 만든다고 적는다 — 다른 회수가 이미 낮춰 놓은 행은 0 행을 갱신하고 호출자가 다음 행으로 넘어간다는 것이다.

그 설계는 한 페이지 안에서만 성립한다. 다음 행이 페이지 밖에 있으면 넘어갈 곳이 없다.

저장소 게이트웨이를 그대로 부른 결과

리포지터리 인터페이스만 프록시로 대신했다. 프록시는 요청한 만큼의 1 바이트 확정 행을 만들어 페이지 상한까지 돌려주고 차감 호출을 센다. 게이트웨이는 저장소 클래스 그대로다.

1 바이트 행 64 개에 64 바이트를 요청하면 페이지 질의 1 회에 차감 64 회이고 남는 바이트가 0 이다.

65 개에 65 바이트를 요청하면 페이지 질의는 여전히 1 회이고 차감이 64 회에서 멈춘다. 1 바이트가 남는다.

200 개에 200 바이트를 요청하면 차감이 64 회이고 136 바이트가 남는다. 남는 양은 원장에 쌓인 행 수에 따라 늘어난다.

자바독이 정당화한 나머지는 다른 나머지다

:25~:27 은 나머지를 이월하지 않는 근거를 적는다. 어떤 행도 흡수하지 못하는 나머지라면 원장의 총량이 이미 과소 기록됐다는 뜻이고, 음수 잔액이 그것을 고쳐 주지는 않는다는 것이다.

프로브가 만든 나머지는 그 경우가 아니다. 흡수할 행이 원장에 그대로 남아 있는데 페이지 상한이 그것을 보지 못하게 했다.

코드는 두 나머지를 구분하지 않는다. 둘 다 같은 자리에서 조용히 끝난다.

남았다는 사실을 담을 자리가 없다

reclaim 의 반환형은 void 다. 호출자는 요청한 바이트가 전부 회수됐는지 알 방법이 없다.

64 와 65 와 RECLAIM_PAGE 를 쓰는 시험 줄은 각각 0 개다. 이름에 회수가 든 파일은 게이트웨이와 포트와 시험용 기록기 셋이다.

원문과 갈리는 자리

원문은 65 개 행에 65 바이트를 요청하면 1 이 남는다고 적었고 프로브가 그대로 재현한다. 원문은 실제 PostgreSQL 로 확인했고 이 기록은 리포지터리를 프록시로 대신했다.

원문이 적지 않은 것이 둘이다. 남는 양이 원장 행 수에 비례해 커진다는 것과, 클래스 자바독이 정당화한 나머지가 페이지 상한이 만드는 나머지와 다른 것이라는 점이다.

확인하지 못한 것

데이터베이스를 띄워 확인하지는 않았다. 프록시가 돌려준 목록으로 루프의 종료 조건만 봤다.

운영 중인 원장에서 한 범위가 64 행을 넘기는 일이 있는지 세지 않았다.

차감 질의가 0 행을 갱신해 건너뛰는 경우와 페이지 상한이 겹치면 남는 바이트가 더 늘어나는지 시험하지 않았다.

실제 PostgreSQL 로 돌리지 않았다. 리포지터리를 프록시로 대신해 게이트웨이의 루프만 관측했다.

차감 질의가 0 행을 갱신해 건너뛰는 경우와 페이지 상한이 겹치면 남는 바이트가 더 늘어나는지 시험하지 않았다.