유닛 의존 · 접점 · 파일 겹침
unit-of-work.md 가 유닛 하나씩을 적었다면 이 문서는 둘 사이를 적는다.
셋을 담는다 — 무엇이 무엇을 딛는가, 같은 파일을 누가 만지는가, 접점의 모양은
무엇인가.
1. 착수 순서
U-바닥 -> U-관측API -> U-대기열 -> U-drain -> U-제어판
CP0 CP1 CP2 CP3 CP4
└──────> U-MCP 장면 밖 · 병렬
CP5
U-MCP 는 이 사슬에 안 들어간다 (2026-09-05 에 범위로 들어왔다 —
requirements/decisions.md 5절). U-관측API 의 라우트 둘만 딛으므로 CP1 이
초록인 순간부터 언제든 착수 가능하고, 파일 겹침이 0 이라 U-drain ·
U-제어판과 같은 시간에 돈다. 착수 순서에 자리를 안 잡는 유일한 유닛이다.
병합 순서가 곧 착수 순서다 (START.md §5). 배치 폭이 1 이라 직렬이고,
그래서 「두 유닛이 같은 파일을 동시에 만진다」가 이 실행에서는 **「먼저 들어간
쪽 위에 뒤가 얹힌다」**로 읽힌다. 병렬로 돌릴 때의 병목 목록이 아니라 얹는
순서의 목록이다.
U-바닥이 늦으면 전원이 선다. 다른 넷이 전부 이것을 딛는다. CP0 가
초록이어야 다음이 시작된다.
1.1 상자로 본 것
같은 것을 상자로 그린 것이다. 화살표는 「딛는다」 방향으로 아래에서 위를
가리킨다 — 먼저 서야 하는 것이 아래에 있다.
┌──────────────────────────────────┐ ┌──────────────────────────────────┐
│ U-MCP CP5 ○ │ │ U-제어판 CP4 ○ │
│ runctl mcp · stdio 하나 │ │ enodectl serve · 프로세스 제어 │
│ 도구 일곱 · 새 의존 0 │ │ + 호스트 화면 S3 · S5 │
└──────────────────────────────────┘ └──────────────────────────────────┘
▲ ▲ ▲
│ │ │
├────────────────────────────────┘ │
│ │
┌──────────────────────────────────┐ ┌──────────────────────────────────┐
│ U-관측API CP1 ● │ │ U-drain CP3 ○ │
│ GET /v1/nodes · /v1/runs │ │ 정책 파일 · 광고 왕복 │
│ + 중앙 화면 다섯 │ │ 두 모드 · 해제 │
└──────────────────────────────────┘ └──────────────────────────────────┘
▲
│
┌──────────────────────────────────┐
│ U-대기열 CP2 ● │
│ QUEUED · 202 · FIFO │
│ 승격 · 깨우기 다섯 자리 │
└──────────────────────────────────┘
┌────────────────────────────────────────────────────────────────────────────┐
│ U-바닥 CP0 ● 스키마 · 상태 어휘 · 조회 메서드 · 정책 자리 │
│ │
│ 위의 다섯이 전부 이것을 딛는다. 화살표 다섯을 안 그리는 이유다 │
└────────────────────────────────────────────────────────────────────────────┘
● 초록 — 코드까지 끝나고 게이트가 통과했다 CP0 · CP1 · CP2
○ 아직 안 돌았다 CP3 · CP4 · CP5
읽는 법 셋.
① 바닥을 밑에 깐 것은 그림 사정이 아니다. 나머지 다섯이 전부 그것을
딛어서 화살표를 그리면 다섯 줄이 지나간다. 1절의 「U-바닥이 늦으면 전원이
선다」가 그림에서는 이 모양이다.
② 왼쪽 기둥이 비어 있다. U-관측API -> U-MCP 간선 하나뿐이고 그 위가
끝이다. CP1 이 초록이므로 U-MCP 는 지금 착수 가능하고 위로 아무도 안
매달려 있다 — 늦어도 아무것도 안 막는다.
③ 오른쪽 기둥이 직렬이다. U-대기열 -> U-drain -> U-제어판 이 한 줄로
서 있고 거기에 U-관측API -> U-제어판 이 가로질러 들어온다. 남은 병목은
그 세로줄 하나이고, U-제어판 이 기다리는 것은 U-drain 의
policy.Read · policy.Write 다.
두 기둥이 만지는 파일이 하나도 안 겹친다(3절의 행렬). 그래서 같은 시간에
돌아도 서로 안 부딪친다.
2. 의존 행렬
세로가 딛는 쪽, 가로가 딛히는 쪽이다. X 는 실제로 코드가 걸리는 자리다.
바닥 관측API 대기열 drain 제어판 MCP
U-바닥 —
U-관측API X —
U-대기열 X —
U-drain X X —
U-제어판 X X X —
U-MCP X —
의존이 걸리는 자리 여섯. 각각 무엇을 받는지 적는다 — 이름만 적으면
받는 쪽이 모양을 짐작한다.
U-MCP 의 행에 X 가 하나뿐이고, 열은 통째로 비어 있다. 아무도 그것을
안 딛는다는 뜻이고, 그래서 이 유닛은 뒤에 오는 것을 막지 않는다.
| 딛는 쪽 | 딛히는 쪽 | 받는 것 |
|---|---|---|
| U-관측API | U-바닥 | store.ListNodes · store.ListRuns · StepView.Chosen · contract.Policy · match.Reject 의 json 태그 |
| U-대기열 | U-바닥 | store.QUEUED 상수 · store.ListQueued · store.DrainingNodes · queue.go 파일 자체 |
| U-drain | U-바닥 | contract.Policy{Drain} · nodes 정책 열 · UpsertAdvert 의 이전 정책 반환 |
| U-drain | U-대기열 | store.WakeQueued — 해제를 감지한 광고 처리가 부른다 (깨우기 여섯째) |
| U-제어판 | U-관측API | GET /v1/nodes · GET /v1/runs/{id} — 현재 작업과 마지막 관측을 조립한다 |
| U-제어판 | U-drain | policy.Read · policy.Write — 자기 파서를 안 만든다 |
| U-MCP | U-관측API | GET /v1/nodes · GET /v1/runs — fleet.list · runs.list 가 감싼다. HTTP 의존이고 패키지 임포트가 아니다 |
U-제어판 -> U-관측API 는 HTTP 의존이다. 패키지 임포트가 아니라 라우트를
부르는 것이고, 그것이 NFR-P1 · NFR-P2 가 요구하는 모양이다.
3. 파일 겹침 행렬
o 는 고친다, c 는 부르기만 한다, NEW 는 신설이다. 한 행에 o 가 둘
이상이면 그 파일이 접점이고 4절에 처리가 있다.
파일 바닥 관측 대기열 drain 제어판 MCP
───────────────────────────────────────────── ───── ───── ────── ────── ────── ─────
internal/store/schema.sql o
internal/store/store.go o o
internal/store/observe.go o
internal/store/list.go NEW o c
internal/store/queue.go NEW o o
internal/store/release.go o
internal/store/reap.go o c
internal/contract/advert.go o
internal/match/match.go o
internal/boundary/imports_test.go NEW o
internal/api/api.go o o o
internal/api/list.go NEW o
internal/api/queue.go NEW o
internal/api/ui/ NEW o
cmd/mediator/main.go o
internal/enode/policy.go NEW o c
internal/enode/advertise.go o
internal/enode/claim.go o
internal/panel/ NEW o
internal/panel/ui/ NEW o
cmd/enodectl/serve.go NEW o
cmd/enodectl/main.go o
cmd/enodectl/proc_unix.go o
cmd/enodectl/proc_windows.go o
internal/mcp/ NEW o
internal/runctl/client.go o
cmd/runctl/main.go o
마지막 셋이 U-MCP 열이다. 셋 다 o 가 하나뿐이라 4절에 처리가 없다 —
겹치는 자리가 없다는 것이 그 뜻이다.
internal/match/match.go 는 unit-boundaries.md §2 가 「아무 유닛도 안
만진다」로 적은 파일이다. requirements.md §8.4 ①-b 의 결정으로 U-바닥이
한 번 만진다 — Reject 에 json 태그 셋을 다는 것이고 Match 의 시그니처와
로직은 그대로다. NFR-I4 는 지켜진다. 그 문장을 여기서 정정한다.
internal/panel/ui/ 는 행렬에 새로 선 줄이다. U-화면을 U-제어판에 합쳤으므로
파일 경계는 유지하고 계획 단위만 합쳤다는 것을 행렬이 그대로 보인다
(aidlc-state.md 실행 계획 4절).
4. 겹치는 자리와 처리
행렬에서 o 가 둘 이상인 행은 넷이다.
internal/api/api.go — 셋이 만진다
가장 뜨거운 자리다. 기존 라우트 15개가 전부 이 파일을 지나고, 새 유닛
셋이 각각 다른 이유로 들어온다.
U-관측API 등록 줄 셋 — /v1/nodes · /v1/runs · /ui/
핸들러 본체는 list.go 와 ui/ 에 있다. 이 파일에는 줄만 는다
U-대기열 submit 의 QUEUED 분기 (매칭 거절 처리)
submit 이 BusyNodes 에 DrainingNodes 를 합쳐 넘기는 자리
postResult · postCancel · postNodes 재기동 감지 뒤의 WakeQueued
등록 줄 없음 — 새 라우트를 안 만든다
U-drain 광고 응답에 policy 를 더하는 줄 하나
postResult 끝의 at-boundary 종료 블록 (store.Cancel)
해제를 감지한 광고 처리의 WakeQueued
처리 — 병합 순서대로 직렬로 받는다. 관측이 먼저 등록 줄을 넣고, 대기열이
submit 과 종료 지점들을 고치고, drain 이 광고 응답과 postResult 끝에
얹는다. 셋이 같은 함수를 만지는 자리는 postResult 하나뿐이고 거기서도
대기열은 끝의 깨우기 · drain 은 끝의 종료 블록이라 줄이 겹치지 않는다.
위험 판정 — submit 과 postResult 는 모든 제출과 모든 단계 보고가
지나는 뜨거운 경로다. 되돌리기는 이 파일의 등록 줄과 두 블록만 되돌리면
된다 (aidlc-state.md 실행 계획 2절).
internal/store/store.go — 둘이 만진다
U-바닥 UpsertAdvert 가 정책을 받는 시그니처까지 만들고 이전 정책을 돌려준다
U-drain 그 자리에 값을 채우기만 한다
처리 — 순서가 곧 처리다. 바닥이 모양을 먼저 세우므로 drain 은 시그니처를
안 바꾼다.
internal/store/queue.go — 둘이 만진다
U-바닥 파일을 만들고 QUEUED 상수 · DrainingNodes · ListQueued 를 넣는다
U-대기열 같은 파일에 WakeQueued 를 더한다
처리 — 같은 파일이지만 함수가 다르다. 겹치는 줄이 없다.
internal/store/reap.go — 하나가 고치고 하나가 부른다
U-대기열 Reap 뒤의 WakeQueued 호출을 더한다. 파일을 만진다
U-drain at-boundary 종료 경로로 Cancel 을 호출만 하고 안 고친다
처리 — c 는 겹침이 아니다. Cancel 이 이미 verdict 를 쓰고
sealRecord 까지 부르므로 drain 이 고칠 것이 없다 (requirements.md §2.1).
겹치지 않는데 적어 두는 자리 하나
internal/match/match.go 는 U-바닥 하나만 만진다. 그런데 match.Match 를
아무도 안 만진다는 것이 NFR-I4 이자 ADR-014 결정 3 이라 여기 적는다 —
태그를 다는 것과 매처를 고치는 것을 뒤에 오는 사람이 헷갈리지 않게.
5. 접점 표
requirements.md §4.1 의 확정된 계약을 유닛 쌍에 붙인 것이다. §4.1 이
값의 정본이고 여기는 소유자를 적는다. Functional Design 이 내부를 다듬을
수 있으나 아래 모양은 못 바꾼다 — 유닛 둘 이상이 읽기 때문이다.
| # | 접점 | 내는 유닛 | 쓰는 유닛 | 모양 |
|---|---|---|---|---|
| 1 | contract.Policy{Drain} |
U-바닥 | U-drain · U-관측API · U-제어판 | "" | "graceful" | "at-boundary". 문자열 그대로 |
| 2 | store.DrainingNodes(ctx) |
U-바닥 | U-대기열 · U-drain | map[string]bool. match.Match 의 busy 와 같은 모양 |
| 3 | store.ListNodes · store.ListRuns |
U-바닥 | U-관측API | decisions.md 2절의 응답 모양 |
| 4 | store.UpsertAdvert 의 반환 |
U-바닥 | U-drain | 이전 정책을 돌려준다. 해제 감지가 이 값을 읽는다 |
| 5 | match.Reject 의 json 태그 |
U-바닥 | U-관측API | {code, as, reason}. 두 라우트가 같은 글자를 쓴다 |
| 6 | store.WakeQueued(ctx) error |
U-대기열 | U-drain · 기동 | 도착 순서로 전부. 재진입 · 동시 호출 안전 |
| 7 | GET /v1/nodes 노드 항목 |
U-관측API | U-제어판 · 화면 | ADR-065 모양 + draining |
| 8 | GET /v1/runs 의 행 |
U-관측API | 화면 | decisions.md 2절 + current[] + reject |
| 9 | current[] 의 항목 |
U-관측API | 화면 (S1 카드 · Run 행) | {node, step, attempt, started_at}. CLAIMED 마다 하나 |
| 10 | 광고 응답의 policy |
U-drain | 데몬 (Worker) |
advertResponse 에 policy. GET /v1/nodes 의 납작한 draining 과 이름이 다르다 (§4.2) |
| 11 | policy.Read · policy.Write |
U-drain | U-제어판 | 정본은 internal/enode. 제어판은 파서를 안 만든다 |
| 12 | enodectl serve 의 라우트 |
U-제어판 | 화면 (S3 · S5) | §4.3. 전부 ?node=<name> 을 받는다 |
| 13 | GET /api/status 의 observed · stale |
U-제어판 | 화면 (S5) | 캐시가 비면 observed 없음 · stale true |
주인 없는 접점 0. 13 개가 전부 내는 유닛 하나를 갖는다.
접점 하나에 대한 주의 — 10 번
광고 응답의 policy 와 GET /v1/nodes 의 draining 은 같은 값을 다른
이름으로 낸다. 헷갈리는 자리라 근거를 다시 적는다.
광고 응답 policy{drain} 요청이 올려보낸 것과 같은 객체의 왕복이다
GET /v1/nodes draining 관측이고 ADR-065 가 납작한 필드로 짰다
데몬은 policy.drain 만 읽는다. 왕복이 필요한 이유는 Worker 가 Mediator
가 확인한 값으로 움직여야 하기 때문이다 — 그렇지 않으면 노드는 임대를
놓았는데 중앙은 아직 후보로 아는 창이 생긴다.
6. 유닛 사이를 안 건드리는 것
의존이 없다는 것도 값이다. 뒤에 오는 사람이 새로 잇지 않게 적는다.
U-관측API -> U-대기열 없다. QUEUED 는 그냥 또 하나의 state 문자열이다
U-관측API -> U-drain 없다. draining 은 nodes 열의 투영이다
U-대기열 -> U-drain 없다. 거꾸로만 있다 (drain 해제가 WakeQueued 를 부른다)
U-제어판 -> U-대기열 없다. 제어판은 대기열을 안 본다
internal/panel -> internal/store 금지. 경계 검사가 센다
internal/panel -> internal/api 금지
internal/api/ui -> internal/store 금지
internal/enode -> internal/panel 금지. 데몬은 제어판을 모른다
아래 넷은 U-바닥이 내는 경계 검사 테스트가 go/parser 로 기계로 센다
(NFR-P5 · unit-boundaries.md §5). 패키지 이름이 바뀌면 이 표도 같이 바꾼다 —
검사기는 표를 읽는다.