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

14 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-f014 인덱스 diff 는 열네 요소 중 keySignature 와 unique 만 비교한다 multitenancy-isolation clean-architecture-backend-template 게시 전 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 case:analysis-finding-a06-f014 2026-09-04 case-analysis-finding-a06-f014.body.md
key file
analysis-finding-a06-f014 ../../../final/evidence/rendered/analysis-finding-a06-f014.svg
../../../final/evidence/raw/analysis-finding-a06-f014.txt
원본 분석 절은 analysis/06-adapter-outbound-persistence-mongo.md §57 이다.

인덱스 diff 는 열네 요소 중 keySignature 와 unique 만 비교한다

MongoIndexManifest:22:35 가 열네 요소를 선언하는데 MongoIndexDescriptorView:15:20 은 여섯 컴포넌트만 갖는다. MongoIndexDiffEngine.compare 가 두 값을 함께 읽는 곳은 셋인데, :49~:50 은 양방향이고 :55hidden 은 한 방향만 본다.

관계

  • 빠뜨림이 통과가 되는 게이트는 게이트가 아니다 이 사례가 그 규칙의 형태다. 견주지 않은 요소가 달라져도 MongoIndexDiff 의 네 목록이 전부 빈다. MongoIndexDiff:9~:11 은 그 결과를 CI 산출물로 설계했다고 적는데, 지금 그것을 만드는 프로덕션 코드는 없다.
  • 문서와 상수가 서로 일치하는 것으로는 아무것도 증명되지 않는다 그 규칙이 요구하는 검사 경계 명시가 여기에 없다. 그 규칙 3 이 요구하는 검사 경계 명시가 여기에 없다. MongoIndexDescriptorView:9 가 축소 사실은 밝히지만, 그 결과 비교되지 않는 아홉 요소를 어디에도 적지 않는다.
  • TTL 규칙 셋을 가진 타입들을 부르는 프로덕션 코드가 없다 그 기록의 expireAfter 가 이 diff 에서 견주어지지 않는 열 요소 중 하나다. TTL 보존 기간을 바꿔도 빈 보고서가 나온다.

문제

인덱스 드리프트 보고서는 선언한 인덱스와 서버에 있는 인덱스를 견주어 만든다. 매니페스트는 인덱스 하나에 대해 열네 가지를 선언한다.

그 열넷 중 무엇이 실제로 견주어지는지 확인했다.

결론

MongoIndexManifest:22~:35 의 요소는 열넷이고 MongoIndexDescriptorView:15~:20 은 여섯이다. 매니페스트 요소 가운데 뷰에 같은 이름이 있는 것은 name 과 unique 와 hidden 과 metadataOwnership 넷뿐이다.

MongoIndexDiffEngine.compare:41~:58 이 매니페스트와 뷰를 대조한다. 두 값을 한 줄에서 함께 읽는 곳은 셋인데, :49~:50 이 keySignature 와 unique 로 change 를 정하고 :55 가 declared.hidden() && !actual.hidden() 로 hide 를 정한다.

그 반대는 검사하지 않는다. 서버가 숨긴 인덱스를 매니페스트가 보인다고 선언한 경우인데, MongoIndexDiff:13~:14 가 적은 폐기·숨김·관측·승인 순서에서 숨김까지 간 인덱스를 다시 쓰기로 바꾸면 그 조합이 남는다. 그때 계획기는 그 인덱스를 쓰지 않는다.

뷰에 대응 필드가 없는 열 요소는 어느 방향으로도 비교되지 않는다. 그 열에서 keys 하나만 keySignature 로 요약돼 들어가고, 남는 아홉에 expireAfter 가 있다.

MongoIndexDiff:9~:11 은 이 결과가 CI 산출물이며 실행마다 순서가 바뀌면 이전 실행과 대조할 수 없어서 모든 목록을 정렬한다고 적는다. 네 목록이 모두 비면 render() 가 빈 문자열을 낸다.

다만 그 산출물을 지금 만드는 프로덕션 코드는 없다. 엔진 이름이 main 에 나오는 줄이 자기 클래스 선언 하나뿐이고, 부르는 것은 시험 넷이다.

MongoIndexDescriptorView:9~:12 는 그 축소 자체는 밝힌다. 무엇이 감지 불가가 되는지만 적지 않는다.

검증 환경

OpenJDK : 21.0.12 확인 방식 : 매니페스트와 뷰의 요소 전수 인용, 두 목록의 요소 계수와 자기검증, 요소마다 뷰에 있는지 이름으로 대조, 비교 엔진 본문 인용과 두 값을 함께 읽는 줄 계수와 전수, diff 레코드 전문 인용, 엔진의 main 등장 계수와 시험 호출 대조 소스 수정 : x

재현 조건

  1. MongoIndexManifest 와 MongoIndexDescriptorView 의 레코드 헤더를 전문으로 싣는다.
  2. 두 목록의 요소를 세고, 열넷이 아니면 스크립트가 실패하게 한다.
  3. 매니페스트 요소마다 같은 이름이 뷰에 있는지 대조해 전부 나열한다.
  4. MongoIndexDiffEngine.compare 를 인용하고 그 안에서 declared 와 actual 을 한 줄에서 함께 읽는 줄을 세고 전부 뽑는다.
  5. MongoIndexDiff 의 클래스 자바독을 인용한다.
  6. MongoIndexDiffEngine 이 main 에 나오는 줄을 세고, 그 엔진의 compare 를 부르는 시험 줄과 대조한다.

본문

MongoIndexDiffEngine 이 매니페스트와 서버에서 읽은 뷰를 대조해 드리프트 보고서를 만든다.

매니페스트가 선언하는 열넷

:::evidence key="analysis-finding-a06-f014" alt="저장소 루트에서 돌린 정적 검색 출력 192줄. 먼저 MongoIndexManifest 1145번이 실린다. 1219번 자바독은 이것이 diff 나 검토에 필요한 모든 것을 담은 선언된 인덱스 하나이고, expectedUsage 가 이 인덱스가 존재하는 연산을 이름 지어 이 인덱스가 아직 필요한지를 프로덕션 통계로 추측하지 않고 답할 수 있게 하며, metadataOwnership 이 누가 만들었는지를 적어 드리프트 정리가 선언하지 않은 암호화나 검색 인덱스를 지우자고 제안하지 못하게 한다고 적고, 키 순서가 표현상의 세부가 아니라 인덱스 정체성의 일부라 보존된다고 적는다. 2135번의 record 헤더가 name 과 keys 와 unique 와 sparse 와 hidden 과 deprecated 와 partialFilterExpression 과 collationProfile 과 expireAfter 와 wildcardProjection 과 shardKeySupport 와 expectedUsage 와 owner 와 metadataOwnership 열넷을 선언한다. 이어서 MongoIndexDescriptorView 620번이 실린다. 712번 자바독은 이것이 서버에 실제로 있는 인덱스이며 D4 관리 클라이언트로 읽어 diff 가 견줄 수 있는 필드로 줄인 것이고, 소유권은 매니페스트를 믿는 대신 여기서 추론하는데 diff 의 목적 자체가 매니페스트가 모르는 인덱스를 찾는 것이며 그중 일부는 암호화나 검색에 속해 절대 삭제 후보가 되면 안 되기 때문이라고 적는다. 1420번이 collection 과 name 과 keySignature 와 unique 와 hidden 과 metadataOwnership 여섯을 선언한다. 다음으로 MongoIndexDiffEngine 2570번이 실린다. 4158번 반복문이 선언된 인덱스마다 같은 이름의 뷰를 찾는데, 4348번이 뷰가 없고 폐기 표시도 없으면 create 에 넣고, 4950번이 keySignature 가 다르거나 unique 가 다르면 change 에 넣으며 5152번 주석이 같은 이름에 다른 정의는 MongoDB 가 조용히 다시 만들지 않으므로 질의가 스캔을 시작할 때 발견되는 대신 재빌드가 계획돼야 한다고 적고, 5557번이 선언은 숨김인데 서버는 아닐 때만 hide 에 넣는다. 6067번은 매니페스트에 없는 서버 인덱스를 dropCandidates 에 넣는데 애플리케이션 드리프트로 삭제 가능한 소유권만 본다. 그 아래 한 줄에서 declared 값과 actual 값을 견주는 줄이 3 개라고 나오고 그 셋이 49번과 50번과 55번으로 실리며, 참고로 그 반복문에서 declared 나 actual 이 나오는 줄 전부도 함께 나온다. 이어서 매니페스트 요소가 14 개이고 뷰 요소가 6 개라고 나온 뒤, 매니페스트 요소마다 뷰에 있는지가 전부 나열되는데 name 과 unique 와 hidden 과 metadataOwnership 넷만 뷰에 있고 나머지 열은 뷰에 없다. 요소 수가 14 가 아니면 실패하는 자기검증도 함께 찍힌다. 마지막으로 MongoIndexDiff 149번이 실리는데 615번 자바독이 모든 목록이 정렬돼 있어 같은 입력에 대해 diff 가 바이트 단위로 같으며 실행마다 순서가 바뀌는 CI 산출물은 이전 실행과 견줄 수 없고 그것이 인덱스 드리프트 보고서의 목적 대부분이라고 적고, dropCandidates 는 이름 그대로 폐기와 숨김과 관측과 승인을 통과해야 무엇이든 삭제되는 제안일 뿐이라고 적으며, 31번의 empty 와 3638번의 isClean 과 41~49번의 render 가 이어진다. 그 아래 MongoIndexDiffEngine 이 main 에 나오는 줄이 1 개인데 그것이 23번의 클래스 선언이고, 대조로 센 시험 쪽 engine.compare 호출은 MongoIndexDiffEngineTest 30·42·49·61번 네 줄이다." caption="매니페스트가 선언하는 열넷과 그 자바독 · 비교용 뷰가 나르는 여섯과 축소를 밝힌 자바독 · 비교 엔진 본문과 실제로 견주는 두 줄 · 요소마다 뷰에 있는지 대조한 전수와 자기검증 · CI 산출물이라 적은 diff 자바독과 호출 계수 — 192줄 · exit 0" zoom="true" :::

MongoIndexManifest:22~:35 가 인덱스 하나에 대해 열네 요소를 선언한다. name, keys, unique, sparse, hidden, deprecated, partialFilterExpression, collationProfile, expireAfter, wildcardProjection, shardKeySupport, expectedUsage, owner, metadataOwnership 이다.

:14~:17 자바독은 그 열넷 가운데 둘을 따로 설명한다. expectedUsage 는 이 인덱스가 존재하는 연산을 이름 지어 "아직 필요한가" 를 프로덕션 통계로 추측하지 않고 답할 수 있게 하고, metadataOwnership 은 누가 만들었는지를 적어 드리프트 정리가 선언하지 않은 암호화나 검색 인덱스를 지우자고 제안하지 못하게 한다.

비교용 뷰가 선언하는 여섯 필드

MongoIndexDescriptorView:15~:20 이 여섯 컴포넌트를 선언한다. collection, name, keySignature, unique, hidden, metadataOwnership 이다.

:9~:12 자바독이 그 축소를 밝힌다. D4 관리 클라이언트로 읽어 diff 가 비교할 수 있는 필드로 줄였다는 것이다. 소유권을 매니페스트에서 받지 않고 여기서 추론하는 이유도 적는다 — diff 의 목적이 매니페스트가 모르는 인덱스를 찾는 것이고, 그중 일부는 암호화나 검색에 속해 삭제 후보가 되면 안 되기 때문이다.

그 축소로 어떤 요소가 비교 대상에서 빠지는지는 어디에도 적혀 있지 않다.

compare 가 declared 와 actual 을 함께 읽는 세 줄

MongoIndexDiffEngine.compare:41 이 선언된 인덱스마다 같은 이름의 뷰를 찾는다.

:43~:48 이 뷰가 없고 폐기 표시도 없으면 create 에 넣는다.

:49~:50keySignature 가 다르거나 unique 가 다르면 change 에 넣는다. :51~:52 주석은 같은 이름에 다른 정의를 MongoDB 가 조용히 다시 만들지 않으므로, 질의가 스캔을 시작할 때 발견되는 대신 재빌드가 계획돼야 한다고 적는다.

:55~:57declared.hidden() && !actual.hidden() 일 때 hide 에 넣는다.

한 줄에서 선언 값과 서버 값을 함께 읽는 줄은 셋이다. :49:50:55 다. 앞의 둘이 change 를 결정하고 :55hide 를 결정한다.

한 방향만 보는 검사

:55 의 조건은 선언이 숨김이고 서버가 아닌 경우다.

반대에 대한 분기가 없다. 서버에서는 숨겨져 있는데 매니페스트가 보인다고 선언한 상태다.

이 상태에서는 매니페스트가 hidden 을 거짓으로 선언한 인덱스를 서버가 숨겨 두고 있고, 질의 계획기는 숨겨진 인덱스를 쓰지 않는다. 보고서는 그것을 어느 목록에도 넣지 않는다.

뷰에 없는 열 요소

매니페스트 요소마다 뷰에 같은 이름이 있는지 대조했다. nameuniquehiddenmetadataOwnership 넷만 있다. 뷰 요소가 여섯이 아니면 스크립트가 실패한다.

나머지 열은 없다. keys, sparse, deprecated, partialFilterExpression, collationProfile, expireAfter, wildcardProjection, shardKeySupport, expectedUsage, owner 다.

keyskeySignature 라는 다른 이름으로 요약돼 들어간다. 나머지 아홉에는 뷰에 대응하는 컴포넌트가 없다.

그 아홉에 expireAfter 가 있다. 30일을 1일로 바꾸는 것은 대량 삭제인데, 이 diff 는 그것을 차이로 보고하지 않는다.

네 목록이 모두 비었을 때의 산출물

MongoIndexDiff:9~:11 은 모든 목록을 정렬해 같은 입력에 대해 바이트 단위로 같은 결과를 낸다고 적는다. 실행마다 순서가 바뀌는 CI 산출물은 이전 실행과 견줄 수 없고 그것이 드리프트 보고서의 목적 대부분이기 때문이다.

:13~:14dropCandidates 가 이름 그대로 제안일 뿐이며 폐기와 숨김과 관측과 승인을 통과해야 무엇이든 삭제된다고 적는다.

:41~:49render() 가 네 목록을 이어 붙인다. 모두 비면 결과가 빈 문자열이다. 비교하지 않는 아홉 요소가 달라져도 같은 결과다.

다만 지금 그 보고서를 만드는 프로덕션 코드는 없다. MongoIndexDiffEngine 이 main 에 나오는 줄이 :23 의 클래스 선언 하나뿐이고, 그 엔진의 compare 를 부르는 것은 MongoIndexDiffEngineTest:30·:42·:49·:61 네 줄이다.

원문과 갈리는 자리

원문은 비교되는 것이 keySignatureunique 둘이고 hidden 이 한 방향이라고 적는다. 그 판정은 그대로다. 다만 한 줄에서 두 값을 함께 읽는 줄로 세면 :55 를 포함해 셋이다.

여기에 더한 것은 둘이다. 하나는 뷰에 무엇이 없는지를 이름으로 전부 맞춰 본 결과다 — 매니페스트 열넷 가운데 뷰에 이름이 있는 것은 넷이고 keyskeySignature 로 형태를 바꿔 들어가므로 감지 불가가 되는 것은 아홉이다. 다른 하나는 그 엔진이 아직 프로덕션에서 불리지 않는다는 것이다.

확인하지 못한 것

선언과 서버 상태를 실제로 넣어 빈 보고서가 나오는 것을 프로브로 보이지 않았다.

이 보고서가 어느 CI 단계의 산출물이 되는지 워크플로 파일을 훑지 않았다.

keySignature 의 생성 규칙을 열어 보지 않았다. 키 순서가 그 서명에 담기는지 확인하지 않았다.