--- kind: CASE slug: a06-f016-flamingock title: javadoc은 재개 가능한 것만 거부한다고 적는데 검사는 마이그레이션을 보지 않는다 topic: declaration-and-document-drift project: clean-architecture-backend-template status: 게시 전 sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 rootTreeNode: case:a06-f016-flamingock evidenceCapturedOn: 2026-09-02 assets: - key: a06-f016-flamingock file: ../../../final/evidence/rendered/a06-f016-flamingock.svg - key: a06-f016-flamingock-probe file: ../../../final/evidence/rendered/a06-f016-flamingock-probe.svg evidence: - ../../../final/evidence/raw/a06-f016-flamingock.txt - ../../../final/evidence/raw/a06-f016-flamingock-probe.txt source: - 원본 분석 절은 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" ::: ```java /** * Unfenced: the engine's lock exposes no monotonic token. * *

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. */ ``` 앞 두 문장은 왜 흉내내지 않았는지를 적는다. 문제는 마지막 문장이 거절 범위를 재개 가능한 마이그레이션으로 한정한 것이다. ## 그 문장이 가리키는 장치는 실재한다 실행자가 리스의 펜스를 읽는 곳은 셋이다. ```text 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행은 재개 지점을 남긴 마이그레이션에만 걸린다. 그 값을 받은 원장은 미지정이면 거절한다. ```java private static void requireCurrentFence(long fence, String what) { if (fence == MongoMigrationLock.UNFENCED) { ``` 재개 가능성에만 반응하는 펜스 조건이 그 자리에 있다. javadoc 의 마지막 문장은 근거 없는 착오가 아니다. ## 세 번째 관문이 먼저 던진다 ```text 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행이 그보다 앞이다. 그 자리에서 나오는 문장도 재개 가능성을 말하지 않는다. ```java "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" ::: 이 어댑터로 리스를 만들고 아무것도 없는 목록을 적용했다. ```text [검사가 마이그레이션을 보기 전에 걸린다] 마이그레이션 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; ... ``` 목록이 비어 있으면 재개 가능한지 여부가 존재하지 않는데 같은 문장이 나온다. 한 건을 넣어도 결과는 같다. 그 한 건은 한 번의 호출로 완료를 돌려주므로 체크포인트를 남기지 않는다. 같은 마이그레이션을 펜스가 있는 리스로 적용하면 이렇게 된다. ```text [같은 마이그레이션, 펜스가 있는 리스] 재개 불가 1건 -> 실행, 결과 1건 COMPLETED ``` 거절의 대상은 마이그레이션이 아니라 리스다. ## 검사를 옮기는 수정은 두 곳에 막힌다 마이그레이션 인터페이스에는 재개 가능성을 선언하는 멤버가 없다. ```text 17: MongoMigrationId id(); 20: MongoMigrationChecksum checksum(); 23: MongoMigrationPrecondition precondition(); 26: MongoMigrationResult execute(MongoMigrationContext context); 29: MongoMigrationPostcondition postcondition(); ``` 재개 가능한지는 `execute` 가 체크포인트를 돌려준 뒤에야 드러난다. 그리고 82행을 스트림 안으로 옮겨도 157행이 같은 미지정 값을 원장 기록으로 넘긴다. 원장은 그것을 거절하므로, 데이터베이스는 바뀌고 원장에는 아무것도 남지 않는 상태가 된다. ## 이 어댑터가 나오는 곳 어댑터를 만드는 곳은 시험 두 곳이다. 하나는 획득 실패를, 다른 하나는 해제와 해제 뒤 갱신 거절을 확인한다. 적용을 부르는 시험은 없어서 javadoc 과 82행의 어긋남이 시험에도 걸리지 않는다. 조립 쪽도 같다. 실행자와 원장과 잠금을 프로덕션에서 참조하는 곳이 없고, 자동 구성이 이 패키지에서 만드는 빈도 없다. 지금 도는 배포를 멈추는 결함이 아니라, 이 리프를 가져다 조립하는 쪽이 처음 실행하는 순간에 드러날 결함이다. ## 확인하지 못한 것 이 어댑터를 실제 배포에서 골랐을 때의 운영 경험은 다루지 않았다. 확인한 것은 세 번째 관문의 조건과 그 조건이 만드는 거절의 범위다.