Files
document-haness/docs/clean-architecture-backend-template/tech-log-studio/declaration-and-document-drift/case/case-a06-f016-flamingock.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

12 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 a06-f016-flamingock javadoc은 재개 가능한 것만 거부한다고 적는데 검사는 마이그레이션을 보지 않는다 declaration-and-document-drift clean-architecture-backend-template 게시 전 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 case:a06-f016-flamingock 2026-09-02
key file
a06-f016-flamingock ../../../final/evidence/rendered/a06-f016-flamingock.svg
key file
a06-f016-flamingock-probe ../../../final/evidence/rendered/a06-f016-flamingock-probe.svg
../../../final/evidence/raw/a06-f016-flamingock.txt
../../../final/evidence/raw/a06-f016-flamingock-probe.txt
원본 분석 절은 analysis/06-adapter-outbound-persistence-mongo.md#L942 이다. 등급은 P3 이다. 그 절이 담은 것은 넷이다. 펜스 반환값과 그 근거, javadoc 마지막 문장의 범위, 82행 검사가 적용 스트림보다 앞이라는 위치, 그리고 시험이 이 조합을 다루지 않는다는 것.
빈 목록도 같은 문장으로 거절된다는 것, 같은 마이그레이션이 펜스가 있는 리스에서는 완료된다는 것, 154행이 재개 가능한 마이그레이션에만 걸리는 펜스 조건이라는 것, 그리고 검사를 옮겨도 157행에서 다시 막힌다는 것은 이 기록에서 확인했다.

javadoc은 재개 가능한 것만 거부한다고 적는데 검사는 마이그레이션을 보지 않는다

잠금 어댑터는 펜스로 미지정 값을 돌려주고 그 근거를 javadoc 에 적는다. 이어지는 한 문장이 실행자의 거부 범위를 재개 가능한 마이그레이션으로 한정한다. 실제 조건은 리스의 펜스 값 하나여서 마이그레이션이 하나도 없어도 거부한다.

관계

  • fenced lease — 만료 시각만으로는 부족한 이유 펜싱이 무엇을 보장하는지 다룬 개념이다.
  • recordApplied가 펜스를 비교하지 않아 밀려난 실행자가 원장을 차지한다 같은 원장의 펜스 처리 사례다.
  • 문서가 UUIDv7이라 말하고 생성되는 것은 v4다 두 사례 모두 javadoc 이 적은 것과 코드가 만드는 값이 다르다.

문제

펜스 메서드가 미지정 값을 돌려주는 것과 그 근거는 옳다. 로컬 카운터로 펜싱을 흉내내면 펜싱처럼 보이면서 아무것도 보호하지 않고, 프로세스마다 따로 세는 수는 다른 프로세스가 무엇을 획득했는지에 대해 아무 말도 하지 않는다.

어긋난 것은 같은 javadoc 의 마지막 한 문장이다. 실행자가 바로 이 이유로 재개 가능한 마이그레이션을 거부한다고 적는다.

결론

그 문장이 가리키는 장치는 실재한다. 재개 가능한 마이그레이션만 체크포인트를 남기고, 그 저장에 펜스가 넘어가며, 원장은 미지정 펜스로 들어온 체크포인트를 거절한다. 그 자리를 지키는 조건은 재개 가능성 하나만 본다.

닿지 못할 뿐이다. 진입점의 세 관문 중 세 번째가 리스의 펜스 값만 보고 던지고, 그 자리는 적용 스트림보다 앞이다. 조건에 마이그레이션이 들어가지 않으므로 목록이 비어 있어도 같은 문장이 나온다.

탐침으로 확인했다. 빈 목록을 이 리스로 적용해도 거절된다. 목록에 아무것도 없어 재개 가능한지 여부가 존재하지 않는데도 같다. 체크포인트를 만들지 않는 마이그레이션 한 건도 마찬가지이고, 그 한 건은 펜스가 있는 리스에서는 완료로 끝난다.

거절 문장 자체는 정확하다. 펜스 없는 리스는 멈춘 실행자를 배제할 수 없다고 말하고 재개 가능성을 언급하지 않는다. 그리고 거절 자체가 안전한 선택이다. 문제는 javadoc 을 읽고 이 어댑터를 고른 쪽이 재개 불가능한 마이그레이션은 돈다고 읽는다는 것이다.

수정은 사실상 하나다. javadoc 을 실제 범위로 고치는 것이다. 검사를 스트림 안으로 옮기는 쪽은 둘에 막힌다. 재개 가능성은 실행이 체크포인트를 돌려준 뒤에야 드러나므로 실행 전에 판별할 수 없고, 옮기더라도 적용이 끝난 뒤 같은 미지정 값이 원장 기록으로 넘어가 거기서 거절된다. 거절 시점이 실행 전에서 실행 후로 밀릴 뿐이다.

지금 배포를 멈추는 결함은 아니다. 실행자와 원장과 잠금을 프로덕션에서 참조하는 곳이 없고, 이 어댑터를 만드는 곳은 시험 두 곳이며 그 시험들은 적용을 부르지 않는다. 이 리프를 가져다 조립하는 쪽이 처음 실행할 때 드러난다.

검증 환경

OpenJDK : 21.0.12 확인 방식 : 밀폐된 탐침으로 실행자 진입점 호출 소스 수정 : x

재현 조건

  1. 잠금 어댑터의 펜스 메서드가 돌려주는 값과 그 javadoc 을 읽는다.
  2. 진입점이 지나는 세 관문과 적용 스트림의 줄 번호를 확인한다.
  3. 그 파일에서 리스의 펜스를 읽는 곳을 전수로 세고, 각각이 그 값을 어디로 넘기는지 확인한다.
  4. 원장이 미지정 펜스를 어떻게 다루는지 읽는다.
  5. 마이그레이션 인터페이스에 재개 가능성을 선언하는 멤버가 있는지 확인한다.
  6. 어댑터로 빈 목록을 적용한다.
  7. 체크포인트를 만들지 않는 마이그레이션 한 건을 같은 리스로, 그리고 펜스가 있는 리스로 각각 적용한다.
  8. 어댑터가 나오는 곳과 프로덕션 참조를 전수로 센다.

본문

잠금 어댑터의 펜스 메서드는 미지정 값을 돌려주고, javadoc 이 그 선택의 근거를 적는다.

근거 문단과 그 뒤의 한 문장

:::evidence key="a06-f016-flamingock" alt="잠금 어댑터의 펜스 메서드와 그 javadoc, 실행자 진입점이 지나는 세 관문, 그 파일에서 리스의 펜스를 읽는 곳 전수와 각각의 줄 번호, 원장이 미지정 펜스를 거절하는 자리, 마이그레이션 인터페이스의 멤버 목록, 이 어댑터가 나오는 곳 전수, 실행자와 원장과 잠금의 프로덕션 참조, 그리고 마이그레이션을 적용하는 진입점 전수를 출력한 터미널 기록." caption="펜스는 UNFENCED 를 반환하고 javadoc 은 재개 가능한 것을 거부한다고 적음 · 관문은 보유 검사 74행, 목록 검증 80행, 펜스 검사 82행이고 적용 스트림은 93행 · 펜스를 읽는 곳은 82·154·157행이고 뒤 둘은 원장으로 넘어가 미지정이면 거절 · 인터페이스에 재개 가능성 멤버 없음 · 프로덕션 참조 0 — 73줄 · exit 0" zoom="true" :::

/**
 * Unfenced: the engine's lock exposes no monotonic token.
 *
 * <p>Reported honestly rather than synthesised from a local counter, which would look like
 * fencing and protect nothing — a per-process counter says nothing about what another process
 * acquired. The runner refuses resumable migrations under an unfenced lease for exactly this
 * reason.
 */

앞 두 문장은 왜 흉내내지 않았는지를 적는다. 문제는 마지막 문장이 거절 범위를 재개 가능한 마이그레이션으로 한정한 것이다.

그 문장이 가리키는 장치는 실재한다

실행자가 리스의 펜스를 읽는 곳은 셋이다.

 82:    if (lock.fence() == MongoMigrationLock.UNFENCED) {
154:    result.resumePoint().ifPresent(checkpoint -> ledger.saveCheckpoint(checkpoint, lock.fence()));
157:          migration.id(), migration.checksum(), context.operator(), clock.instant(), lock.fence());

154행은 재개 지점을 남긴 마이그레이션에만 걸린다. 그 값을 받은 원장은 미지정이면 거절한다.

private static void requireCurrentFence(long fence, String what) {
  if (fence == MongoMigrationLock.UNFENCED) {

재개 가능성에만 반응하는 펜스 조건이 그 자리에 있다. javadoc 의 마지막 문장은 근거 없는 착오가 아니다.

세 번째 관문이 먼저 던진다

74:    if (!lock.held()) {
80:    migrations.forEach(this::validate);
82:    if (lock.fence() == MongoMigrationLock.UNFENCED) {
93:    return migrations.stream()

82행의 조건은 리스의 펜스 값 하나다. 80행은 목록을 순회하지만 원장과 체크섬을 볼 뿐이고, 82행은 그 결과도 마이그레이션도 참조하지 않는다. 154행에 닿으려면 93행의 스트림에 들어가야 하는데 82행이 그보다 앞이다.

그 자리에서 나오는 문장도 재개 가능성을 말하지 않는다.

"this migration lease exposes no fencing token, so a stalled runner cannot be excluded;"
    + " use a fenced lease or run the migration inside a maintenance window with the"
    + " application stopped"

빈 목록도 거절된다

:::evidence key="a06-f016-flamingock-probe" alt="잠금 어댑터가 보고하는 보유 여부와 펜스 값, 빈 마이그레이션 목록과 체크포인트를 만들지 않는 마이그레이션 한 건을 그 리스로 적용한 결과와 거절 문장, 그리고 같은 마이그레이션을 펜스가 있는 리스로 적용한 결과를 출력한 터미널 기록." caption="펜스는 -1 · 빈 목록도 거절, 재개 불가 한 건도 같은 문장으로 거절 · 같은 마이그레이션이 펜스가 있는 리스에서는 COMPLETED — 11줄 · exit 0" zoom="true" :::

이 어댑터로 리스를 만들고 아무것도 없는 목록을 적용했다.

[검사가 마이그레이션을 보기 전에 걸린다]
  마이그레이션 0건 -> 거절, MongoOperationRejectedException
      this migration lease exposes no fencing token, so a stalled runner cannot be excluded; ...
  재개 불가 1건 -> 거절, MongoOperationRejectedException
      this migration lease exposes no fencing token, so a stalled runner cannot be excluded; ...

목록이 비어 있으면 재개 가능한지 여부가 존재하지 않는데 같은 문장이 나온다. 한 건을 넣어도 결과는 같다. 그 한 건은 한 번의 호출로 완료를 돌려주므로 체크포인트를 남기지 않는다.

같은 마이그레이션을 펜스가 있는 리스로 적용하면 이렇게 된다.

[같은 마이그레이션, 펜스가 있는 리스]
  재개 불가 1건 -> 실행, 결과 1건 COMPLETED

거절의 대상은 마이그레이션이 아니라 리스다.

검사를 옮기는 수정은 두 곳에 막힌다

마이그레이션 인터페이스에는 재개 가능성을 선언하는 멤버가 없다.

17:  MongoMigrationId id();
20:  MongoMigrationChecksum checksum();
23:  MongoMigrationPrecondition precondition();
26:  MongoMigrationResult execute(MongoMigrationContext context);
29:  MongoMigrationPostcondition postcondition();

재개 가능한지는 execute 가 체크포인트를 돌려준 뒤에야 드러난다. 그리고 82행을 스트림 안으로 옮겨도 157행이 같은 미지정 값을 원장 기록으로 넘긴다. 원장은 그것을 거절하므로, 데이터베이스는 바뀌고 원장에는 아무것도 남지 않는 상태가 된다.

이 어댑터가 나오는 곳

어댑터를 만드는 곳은 시험 두 곳이다. 하나는 획득 실패를, 다른 하나는 해제와 해제 뒤 갱신 거절을 확인한다. 적용을 부르는 시험은 없어서 javadoc 과 82행의 어긋남이 시험에도 걸리지 않는다.

조립 쪽도 같다. 실행자와 원장과 잠금을 프로덕션에서 참조하는 곳이 없고, 자동 구성이 이 패키지에서 만드는 빈도 없다. 지금 도는 배포를 멈추는 결함이 아니라, 이 리프를 가져다 조립하는 쪽이 처음 실행하는 순간에 드러날 결함이다.

확인하지 못한 것

이 어댑터를 실제 배포에서 골랐을 때의 운영 경험은 다루지 않았다. 확인한 것은 세 번째 관문의 조건과 그 조건이 만드는 거절의 범위다.