- analysis/·source-index·state.json 을 final/document.md 제2부·제3부로 접었다. SSOT 는 하나다 - 파일럿 — commit-ambiguity-as-a-result 를 새 기준으로 재선별. 후보 14 → 글감 5 (PROMOTE 5 · MERGE_INTO 3 · KEEP_IN_SSOT 4 · 보류 2). 기록 5건을 다시 썼고 그림 1개를 techviz 로 만들었다 - 재선별이 잡은 것: 제1부 §6.2·§11.1 이 자기 §13.2 와 어긋나 있었다(레인을 안 돌렸다 vs 돌렸다) — 정정. 이미 답이 나와 있던 Question 을 HEAD 재실행 질문으로 다시 세웠다. Concept 이 인용한 코드가 SSOT 에 없어 뺐다 - candidateScope·sourceRepository 기록. 나머지 43개 주제는 재선별 대기(PENDING 905) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
171 lines
13 KiB
Markdown
171 lines
13 KiB
Markdown
---
|
|
kind: CASE
|
|
slug: a05-f033-jpa-flyway-migration
|
|
title: 선택된 카드의 생산자 태스크가 지금 빌드를 실패시킨다
|
|
topic: what-a-gate-does-not-prove
|
|
project: clean-architecture-backend-template
|
|
status: 게시 전
|
|
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
|
rootTreeNode: case:a05-f033-jpa-flyway-migration
|
|
evidenceCapturedOn: 2026-09-02
|
|
body: case-a05-f033-jpa-flyway-migration.body.md
|
|
assets:
|
|
- key: a05-f033-jpa-flyway-migration
|
|
file: ../../../final/evidence/rendered/a05-f033-jpa-flyway-migration.svg
|
|
- key: a05-f033-jpa-flyway-migration-run
|
|
file: ../../../final/evidence/rendered/a05-f033-jpa-flyway-migration-run.svg
|
|
- key: a05-f033-jpa-flyway-migration-gate
|
|
file: ../../../final/evidence/rendered/a05-f033-jpa-flyway-migration-gate.svg
|
|
evidence:
|
|
- ../../../final/evidence/raw/a05-f033-jpa-flyway-migration.txt
|
|
- ../../../final/evidence/raw/a05-f033-jpa-flyway-migration-run.txt
|
|
- ../../../final/evidence/raw/a05-f033-jpa-flyway-migration-gate.txt
|
|
source:
|
|
- 원본 분석 절은 `final/document.md#a05` §132 다. 등급은 P1 이다. 생산자가 현재 리비전에서 실패한다는 판정, 두 실패의 원인, 두 스트림의 파일 수, 단언이 마지막으로 갱신된 시점과 그 뒤 들어온 마이그레이션 다섯의 날짜, 그리고 이 레인을 도는 자동 경로가 그 절에 있다.
|
|
- 이 카드를 선행 조건으로 두는 카드 전수와 집계 게이트가 카드 id 를 하드코딩하는 자리, 게이트를 부르는 잡의 방아쇠, 그리고 47행 실패에 가려진 62행 단언은 이 기록에서 확인했다.
|
|
---
|
|
|
|
# 선택된 카드의 생산자 태스크가 지금 빌드를 실패시킨다
|
|
|
|
준비도 카드 하나가 선택 상태다. 그 카드의 생산자 태스크를 돌리면 네 시험 중 둘이 47행과 89행에서 멈추고 빌드가 실패한다. 두 실패 모두 스트림에 마이그레이션이 들어왔는데 그 스트림의 적용 집합을 목록 리터럴로 고정한 단언이 그대로인 것이다.
|
|
|
|
## 관계
|
|
|
|
- **아무도 돌리지 않는 레인의 게이트는 마지막으로 돌린 사람이 본 것을 보고한다**
|
|
이 사례가 그 규칙의 형태다.
|
|
- **마이그레이션 스트림은 자기 history 테이블을 갖는다**
|
|
스트림별 버전 목록을 한 곳에 고정하는 구조다.
|
|
- **컨테이너가 필요한 레인의 실제 결과를 실행으로 확인하지 않았다**
|
|
이 레인이 그 질문의 대상 중 하나다.
|
|
|
|
## 문제
|
|
|
|
카드마다 증거를 만들 태스크가 이름으로 묶여 있다. 이 카드를 선행 조건으로 두는 카드가 열하나이고 그중 셋이 선택 상태이며, 셋 중 하나가 R2 집계 게이트다.
|
|
|
|
## 결론
|
|
|
|
두 실패는 같은 기제의 두 사례다. 단언이 스트림의 적용 집합을 목록 리터럴로 고정하는데, 그 스트림에 마이그레이션이 들어오면 리터럴이 낡는다. 시나리오는 다르다. 하나는 기존 history 채택이고 다른 하나는 새 코어 스트림 초기화다. 그 차이는 실패에 관여하지 않는다.
|
|
|
|
기반 스트림은 적용 집합을 1, 3, 4, 5, 6 으로 고정했는데 실제로는 9, 10, 11, 12 가 더 있다. 코어 스트림은 1 로 고정했는데 실제로는 2 가 더 있다.
|
|
|
|
단언은 7월 31일 이후로 바뀌지 않았다. 그 뒤 두 스트림에 마이그레이션 다섯이 들어왔고, 지금 실패하는 두 단언을 함께 낡게 만든 것은 8월 15일의 커밋 하나다. 그 커밋이 기반 스트림의 V9 와 코어 스트림의 V2 를 동시에 넣었다.
|
|
|
|
고칠 자리는 셋이다. 47행과 89행은 지금 실패하고, 62행은 47행 실패에 가려 실행되지 않는다. 47행을 고치면 62행이 곧바로 같은 이유로 실패한다.
|
|
|
|
같은 파일의 네 번째 고정 단언은 통과한다. 그 스트림은 픽스처라 자라지 않았다. 깨지는 것은 스트림이 자란 자리뿐이다.
|
|
|
|
재발 방지는 같은 소스 세트가 이미 쓰는 방식을 기반 스트림에도 적용하는 것이다. 그 방식은 스트림마다 표 이름과 버전 목록을 한 자리에 두고, 그 목록에 붙은 주석이 목록을 스트림의 계약이라고 못박는다.
|
|
|
|
## 검증 환경
|
|
|
|
OpenJDK : 21.0.12
|
|
Gradle : 9.0.0
|
|
데이터베이스 : PostgreSQL, Testcontainers
|
|
확인 방식 : 깨끗한 작업 트리에서 재실행을 강제해 생산자 태스크 실행, 게이트의 태스크 그래프 조회
|
|
소스 수정 : x
|
|
|
|
## 재현 조건
|
|
|
|
1. 준비도 카드 정의에서 이 카드의 상태와 생산자 태스크를 확인한다.
|
|
2. 이 카드를 선행 조건으로 두는 카드를 전수로 열거하고 각각의 상태를 읽는다.
|
|
3. 그 태스크가 고정한 적용 집합 넷을 읽는다.
|
|
4. 두 스트림의 실제 마이그레이션 파일을 나열하고, 단언 파일과 각 마이그레이션의 최종 커밋 날짜를 비교한다.
|
|
5. 깨끗한 작업 트리에서 재실행을 강제해 그 태스크를 돌린다.
|
|
6. 풀 리퀘스트 워크플로가 부르는 게이트의 태스크 그래프에 이 태스크가 있는지 조회한다.
|
|
|
|
## 본문
|
|
|
|
<!-- body:start -->
|
|
|
|
카드 정의는 카드마다 증거를 만드는 태스크를 이름으로 지정한다. `jpa-flyway-migration` 의 그 태스크가 지금 무엇을 하는지가 이 기록의 전부다.
|
|
|
|
## 카드 하나에 걸린 것
|
|
|
|
:::evidence key="a05-f033-jpa-flyway-migration" alt="준비도 카드 정의에서 이 카드의 상태와 생산자 태스크, 이 카드를 선행 조건으로 두는 카드 전수와 각각의 상태, R2 집계 게이트의 선행 조건 목록과 게이트가 카드 id 를 하드코딩한 자리, 그 태스크가 고정한 적용 집합 네 개, 두 스트림의 실제 마이그레이션 파일, 단언 파일과 각 마이그레이션의 최종 커밋 날짜, 그리고 같은 소스 세트가 이 실패 유형을 적어 둔 주석을 출력한 터미널 기록." caption="카드는 selected, 생산자는 postgresqlMigrationIntegrationTest · 이 카드를 선행 조건으로 두는 카드 열하나, 그중 selected 셋 · 고정 단언 넷은 47·62·89·123행 · 실제 스트림은 아홉 개와 둘 · 단언은 7월 31일 이후 미갱신 — 56줄 · exit 0" zoom="true"
|
|
:::
|
|
|
|
카드 정의가 스스로 적는 선행 조건은 둘뿐이다. 반대 방향이 크다. 이 카드를 선행 조건으로 두는 카드가 열하나이고, 그중 `jpa-primary-foundation` 은 R2 집계 게이트다.
|
|
|
|
## 네 시험 중 둘이 47행과 89행에서 멈춘다
|
|
|
|
:::evidence key="a05-f033-jpa-flyway-migration-run" alt="작업 트리가 깨끗한 것을 확인하고 카드의 생산자 태스크를 재실행 강제로 돌린 결과와 실패한 두 시험의 이름과 줄 번호, 통과·실패 수, 실패한 태스크 이름, 빌드 결과, 그리고 두 실패의 단언 메시지 전문을 출력한 터미널 기록. 채집 스크립트가 파이프와 || true 로 실패를 삼켜 스크립트 자신은 0 으로 끝난다. 빌드는 실패했다." caption="작업 트리 변경 0줄 · 재실행 강제 · 네 시험 중 둘 실패 · BUILD FAILED, 태스크 종료 1 · 기반 스트림은 9·10·11·12 가, 코어 스트림은 2 가 예상 밖 — 27줄 · 채집 스크립트 exit 0" zoom="true"
|
|
:::
|
|
|
|
```text
|
|
PostgreSqlMigrationIntegrationTest > adoptsImmutableLegacyHistoryThenRunsTheIndependentCoreStream() FAILED
|
|
org.opentest4j.AssertionFailedError at PostgreSqlMigrationIntegrationTest.java:47
|
|
PostgreSqlMigrationIntegrationTest > freshCoreStreamInitializesWithoutLegacyHistory() FAILED
|
|
org.opentest4j.AssertionFailedError at PostgreSqlMigrationIntegrationTest.java:89
|
|
4 tests completed, 2 failed
|
|
> Task :adapter:outbound:persistence-jpa:postgresqlMigrationIntegrationTest FAILED
|
|
BUILD FAILED in 31s
|
|
```
|
|
|
|
늘어난 버전이 단언 메시지에 그대로 찍힌다.
|
|
|
|
```text
|
|
Expecting actual:
|
|
["1", "3", "4", "5", "6", "9", "10", "11", "12"]
|
|
to contain exactly (and in same order):
|
|
["1", "3", "4", "5", "6"]
|
|
but some elements were not expected:
|
|
["9", "10", "11", "12"]
|
|
```
|
|
|
|
단언 파일과 마이그레이션 파일의 최종 커밋 날짜를 나란히 놓으면 언제부터인지가 나온다.
|
|
|
|
```text
|
|
2026-07-31 PostgreSqlMigrationIntegrationTest.java
|
|
2026-08-15 jpa/core/V2, postgresql/V9 (같은 커밋)
|
|
2026-08-18 postgresql/V10
|
|
2026-08-28 postgresql/V11, postgresql/V12
|
|
```
|
|
|
|
## 넷 중 셋이 낡았다
|
|
|
|
같은 파일에 구조가 똑같은 고정 단언이 넷 있다.
|
|
|
|
```java
|
|
47: .containsExactly("1", "3", "4", "5", "6");
|
|
62: assertThat(appliedVersions(postgres, "flyway_jpa_core_history")).containsExactly("0", "1");
|
|
89: assertThat(appliedVersions(fresh, "flyway_jpa_core_history")).containsExactly("1");
|
|
123: assertThat(appliedVersions(interrupted, "flyway_readiness_interrupted")).containsExactly("1");
|
|
```
|
|
|
|
62행은 47행과 같은 시험 안에 있어서, 47행이 죽으면 실행되지 않는다. 그 시험은 코어 스트림을 `0` 으로 baseline 한 뒤 migrate 하고 조회 도우미는 baseline 행까지 읽는다. 코어 스트림에는 V1 과 V2 가 있으므로 62행이 받을 값은 `0, 1, 2` 다.
|
|
|
|
123행은 통과한다. `flyway_readiness_interrupted` 는 픽스처 스트림이라 마이그레이션이 늘지 않았다.
|
|
|
|
## 같은 소스 세트가 이 실패를 이미 적어 두었다
|
|
|
|
```java
|
|
// This list is the stream's contract, not a note about its length: the lifecycle below
|
|
// disables, re-enables and interrupts the stream and asserts the applied set is unchanged
|
|
// each time. So a migration added to the stream belongs here, and the version that landed
|
|
// without being added is why the lane failed the first time anybody ran it.
|
|
List.of("0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10"),
|
|
```
|
|
|
|
`PostgreSqlOptionalStreamLifecycle` 은 같은 소스 세트에 있고 스트림마다 표 이름과 버전 목록을 한 자리에 둔다. 마이그레이션을 더할 때 고칠 자리가 그 목록 하나다. 기반 스트림 쪽은 그 자리가 시험 본문 세 곳에 흩어져 있다.
|
|
|
|
## 이름으로 부르는 워크플로는 없지만 그래프에는 있다
|
|
|
|
:::evidence key="a05-f033-jpa-flyway-migration-gate" alt="이 태스크를 실행하는 워크플로 둘과 각각의 방아쇠, 각 워크플로가 부르는 게이트 태스크, 매니페스트 태스크가 활성 카드의 준비도 태스크에 의존하는 코드, 풀 리퀘스트 게이트의 실제 태스크 그래프에 이 준비도 태스크가 들어 있음을 보이는 dry-run 출력, 그리고 R2 집계 게이트가 이름으로 요구하는 카드 일곱을 출력한 터미널 기록." caption="ci-quality-gates 는 pull_request 와 main push 에서 돌고 jpa-r2-evidence 는 수동 실행뿐 · 매니페스트 태스크가 활성 카드의 준비도 태스크에 dependsOn · dry-run 그래프에 postgresqlMigrationIntegrationTest 존재 — 37줄 · exit 0" zoom="true"
|
|
:::
|
|
|
|
이 태스크를 이름으로 부르는 워크플로는 없다. 태그 레인 다섯 어디에도 속하지 않고 릴리스 게이트에도 없다. 대신 매니페스트를 만드는 태스크가 활성 카드마다 그 카드의 준비도 태스크에 의존하고, 이 태스크가 그중 하나다.
|
|
|
|
그래서 `verifyJpaCandidateEvidence` 의 그래프에 이 태스크가 들어온다. 그 게이트를 부르는 잡은 풀 리퀘스트마다, 그리고 main 으로 push 할 때마다 돈다.
|
|
|
|
R2 집계 게이트 쪽은 수동 실행뿐이고, 그 게이트는 카드 일곱의 매니페스트가 전부 R2 여야 통과한다. 그 일곱에 이 카드가 이름으로 들어 있다. 다만 게이트는 그 검사에 닿기 전에 멈춘다. 매니페스트를 만드는 태스크가 먼저 실패하기 때문이다.
|
|
|
|
## 확인하지 못한 것
|
|
|
|
47행을 고친 뒤 62행이 실패하는 것을 실행으로 보지는 않았다. 소스를 고치지 않는 조건이라, 62행이 받을 값은 같은 스트림을 baseline 없이 도는 다른 시험이 실제로 낸 `1, 2` 와 조회 도우미가 baseline 행까지 읽는다는 것에서 도출했다.
|
|
|
|
같은 준비도 묶음의 다른 레인 하나는 이 컨테이너의 네트워크 구성 때문에 실패한다. 인증서가 로컬 이름으로 발급되어 있고 검증 모드가 전체 검증이므로, 컨테이너의 매핑 포트가 시험 JVM 의 루프백에서 열려 있어야 한다. 이 분석은 Docker 소켓을 공유하는 형제 컨테이너에서 실행돼 매핑 포트가 브리지 주소에만 열렸고 실패는 연결 예외다.
|
|
|
|
그 레인은 건너뛰지 않고 실패하도록 설계되어 있으므로 동작 자체는 의도대로다. 다만 건너뛰지 않는다는 선택의 대가로 Docker 호스트와 시험 JVM 이 루프백을 공유하는 환경이 그 레인의 암묵적 전제가 된다.
|
|
|
|
<!-- body:end -->
|