10장. 병렬 오케스트레이션
다음은 여러 현장에서 반복된 패턴을 합친 합성 사례로, 등장하는 숫자는 설명을 위한 예시 수치다.
에이전트 수를 2개에서 20개로 늘렸더니 완료 속도가 느려졌다. 준비되지 않은 작업이 시작됐고, CI는 긴 대기열이 되었으며, 리뷰어는 동시에 도착한 변경을 감당하지 못했다. 일부 작업은 같은 실패를 반복했다. 실행 용량은 늘었지만 검증과 통합의 용량, 작업 간 의존성, 사람의 주의는 그대로였다.
오케스트레이터의 역할은 최대한 많이 시작하는 것이 아니다. 준비된 작업을 적절한 실행자에게 보내고, 전체 공정이 감당할 만큼만 진행시키며, 실패와 취소를 일관된 상태로 관리하는 것이다.
이번 장의 약속
- 작업 DAG와 용량을 함께 고려하는 스케줄러를 설계한다.
- 감독자·작업자·리뷰어 역할을 분리한다.
- 역압, 공정성, 취소, 재시도를 상태 기계로 다룬다.
- 병렬성의 이득을 실측하고 적정 동시성을 찾는다.
오케스트레이터의 다섯 책임
- 준비 판정: 의존성, 명세, 승인, 소유권이 충족된 작업만 선택한다.
- 배치: 위험, 도구, 작업 공간, 예상 비용에 맞는 실행자에 할당한다.
- 수명 주기: 시작, 심박, 중단, 재시도, 완료 상태를 관리한다.
- 역압: 검증·리뷰·통합 대기열이 넘치기 전에 새 시작을 줄인다.
- 기록: 왜 그 작업이 그 시각에 실행됐고 어떤 정책 결정이 있었는지 남긴다.
모델이 작업 계획을 제안할 수 있지만 큐 상태와 권한을 직접 바꾸게 하지 않는다. 제어면의 상태 전이는 결정론적 코드와 정책이 소유한다.
작업과 실행의 상태를 분리한다
하나의 작업은 여러 실행을 가질 수 있다.
Task ORDER-12
run-1 FAILED (TOOL_TRANSIENT)
run-2 BLOCKED (SPEC_CHANGED)
run-3 SUCCEEDED
작업 상태 예시:
DRAFT → READY → RUNNING → VERIFYING → REVIEWING → INTEGRATING → DONE
│ │ │ │
├─> BLOCKED ├─> CHANGES_REQUESTED
└─> CANCELED └─> FAILED
실행이 실패해도 작업은 재시도 정책에 따라 READY로 돌아갈 수 있다. 반대로 작업이 취소되면 실행을 종료하고 결과를 자동 통합하지 않는다.
준비 큐는 DAG의 현재 절단면이다
모든 미완료 작업을 큐에 넣지 않는다. 현재 시점에 모든 선행 조건을 만족한 노드만 준비 큐에 들어간다.
의사코드 — 원본: 이 장의 현장 스케줄러 설명; 명령: 없음.
function readyTasks(graph, state) {
return graph.tasks.filter((task) =>
state[task.id] === "PENDING" &&
task.dependsOn.every((id) => state[id] === "DONE") &&
approvalsSatisfied(task) &&
ownershipAvailable(task)
);
}
실제 구현은 명세 해시, 선행 산출물, 위험 승인까지 확인한다. 상태 갱신과 작업 배치는 원자적으로 처리해 두 스케줄러가 같은 작업을 가져가지 않게 한다. 작은 로컬 공장에서는 단일 프로세스 큐로 시작하고, 분산 제어면이 필요해질 때 임대와 멱등성을 강화한다.
감독자, 작업자, 리뷰어
감독자
작업 그래프와 정책을 읽고 배치·중단·승인을 조정한다. 제품 코드를 직접 고치지 않는다.
작업자
한 작업 계약과 격리 공간 안에서 변경과 자기 검사를 수행한다. 다른 작업 상태나 전역 정책을 바꾸지 않는다.
리뷰어
명세, diff, 게이트 증거를 독립적으로 평가한다. 작업자의 자기 설명을 참고하되 그대로 신뢰하지 않는다. 가능하면 작업자의 긴 대화와 숨은 가설보다 공식 입력과 결과 증거를 먼저 본다.
역할을 꼭 서로 다른 모델이 맡을 필요는 없다. 중요한 것은 신뢰 경계와 입력을 분리하는 것이다. 같은 모델을 새 실행에서 리뷰어로 사용해도 작업자의 자기합리화 문맥을 물려주지 않으면 가치가 있다. 고위험 변경에는 사람 전문 리뷰가 남는다.
용량은 단계마다 다르다
실행 슬롯: 8
통합 테스트 슬롯: 2
보안 스캔 슬롯: 1
사람 리뷰 슬롯: 3
통합 슬롯: 1
작업자 8개가 계속 결과를 내면 검증과 리뷰 큐가 쌓인다. 단계별 WIP 한도를 둔다.
RUNNING 최대 4
VERIFYING 최대 2
REVIEWING 최대 3
INTEGRATING 최대 1
그림 10-1. 실행 슬롯이 비어 있어도 검증·리뷰·통합이 포화되면 시작을 멈춘다. 이 WIP 제어는 운영 설계 목표이며 교육용 로컬 하니스가 단계별 용량 제한을 모두 구현했다는 뜻은 아니다.
후단이 한도에 도달하면 새 작업을 시작하지 않는다. 실행자가 놀더라도 전체 리드 타임과 재작업을 줄이는 편이 낫다.
역압 신호
다음 중 하나가 임계치를 넘으면 동시성을 줄이거나 시작을 멈춘다.
- 검증·리뷰 대기 시간
- 준비 완료 변경의 수와 총 diff 크기
- 통합 충돌률
- CI 상위 90% 지연
- 사람 리뷰 시간과 미처리 개수
- 같은 실패 코드의 연속 발생
- 비용/시간 예산 소진 속도
임계치는 기준선을 보고 정한다. 예를 들어 리뷰 대기열이 3개라는 숫자보다 리뷰어 수와 평균 diff 크기가 함께 중요하다.
공정성과 우선순위
단순 FIFO는 오래된 큰 작업 하나가 작은 긴급 수정을 막거나, 반대로 작은 작업이 계속 들어와 큰 작업이 굶을 수 있다. 우선순위에 다음을 고려한다.
- 제품/장애 긴급도
- 기다린 시간(aging)
- 선행 작업을 많이 해제하는 정도
- 예상 실행·검증 비용
- 위험 승인 가능 여부
- 소유 경계의 현재 충돌
우선순위 수식을 복잡하게 만들기 전에 정책을 사람이 설명할 수 있어야 한다. 긴급 플래그에는 소유자, 만료 시각, 사유를 요구해 모든 작업이 긴급해지는 현상을 막는다.
재시도는 새 실행이다
운영 시스템에서는 재시도 attempt마다 추적 가능한 실행 정체성과 새 작업 공간을 둡니다. 같은 작업 공간을 재사용하면 첫 시도의 부작용이 섞입니다.
taskId: ORDER-12
attempt: 2
runId: run-ORDER-12-002
parentRunId: run-ORDER-12-001
retryReason: TOOL_TRANSIENT
재시도 전 결정:
- 오류가 재시도 가능한가?
- 입력 명세와 기준 커밋이 그대로인가?
- 첫 실행의 부분 외부 효과가 있는가?
- 새 작업 공간이 필요한가?
- 백오프와 최대 시도가 얼마인가?
제품 결함, 정책 위반, 명세 누락은 같은 입력으로 자동 재시도하지 않는다. 일시적인 인프라 오류만 제한적으로 재시도한다.
교육용 하니스는 공장 전체에 하나의 runId를 쓰고 작업별 attempt와 전용 작업 공간으로 시도를 구분합니다. 아래 parentRunId 모델과 외부 효과 확인은 운영 확장 설계이며 현재 예제의 산출물이라고 오해하지 않습니다.
취소와 늦게 도착한 결과
명세가 바뀌거나 상위 작업이 취소되면 실행에 취소 신호를 보낸다. 도구가 즉시 멈추지 않아 결과가 늦게 도착할 수 있다. 제어면은 다음을 확인한다.
- 실행 임대가 아직 유효한가?
- 작업 세대(generation)와 입력 해시가 현재와 같은가?
- 작업 상태가 결과를 받을 수 있는가?
취소된 세대의 성공 결과는 자동 통합하지 않고 STALE_RESULT로 보존한다. 유용한 변경이라면 새 입력에서 다시 검증한다.
현재 로컬 예제는 취소 신호, 임대 만료, STALE_RESULT를 구현하지 않습니다. 이 절은 실제 에이전트와 분산 작업자를 연결하기 전에 추가할 제어 계약입니다.
실패 폭발을 막는 회로 차단기
같은 도구나 명세 문제로 여러 작업이 실패할 때 개별 재시도가 폭풍을 만든다.
최근 5분 동안 TOOL_REGISTRY_UNAVAILABLE 5회
→ 해당 도구를 사용하는 새 작업 일시 중단
→ 한 개의 탐침 실행만 허용
→ 회복 확인 후 점진적으로 재개
명세 템플릿 오류가 공통 원인이면 관련 작업군 전체를 차단하고 사람에게 하나의 사건으로 알린다.
회로 차단기도 현재 예제의 구현 기능이 아니라 현장 확장 항목입니다. 캡스톤은 DAG, 동시 worker limit, 충돌 재시도까지만 실행으로 검증합니다.
실습: 동시성 1, 2, 4를 비교한다
같은 캡스톤 DAG를 worker 동시성 1, 2, 4로 세 번 실행합니다. 첫 배치의 domain-model과 reader-docs만 실제 독립 후보이며 나머지는 의존 순서로 실행됩니다. 세 명령은 모두 고정 run ID order-capstone과 --clean을 사용하므로 다음 실행이 이전 실행 폴더를 지웁니다. 따라서 아래처럼 한 번 실행할 때마다 trace를 읽고 관찰표에 기록한 뒤 다음 동시성으로 넘어갑니다.
npm run demo:parallel -- --concurrency 1
npm run report -- --latest --trace
# 아래 관찰표를 기록한 뒤 다음 두 줄을 실행한다.
npm run demo:parallel -- --concurrency 2
npm run report -- --latest --trace
# 다시 기록한 뒤 마지막 두 줄을 실행한다.
npm run demo:parallel -- --concurrency 4
npm run report -- --latest --trace
터미널 요약만으로 실제 동시 작업 수를 판정하지 않습니다. 매 실행 직후 .factory/runs/order-capstone/events.jsonl 또는 위 trace에서 agent.invoked 순서를 읽고 직접 잰 wall time을 함께 기록합니다. 원본 근거 파일 세 벌을 보존하려면 다음 실행 전에 실행 폴더를 별도 위치에 복사합니다.
전체 완료 시간
domain-model과 reader-docs의 agent.invoked 위치
두 번째 agent.invoked 전에 review.completed가 있었는지
전체 배치 수
작업/시도/재시도 수
wall time(환경과 함께 기록)
동시성 1에서는 reader-docs agent.invoked가 domain-model review.completed 뒤에 옵니다. 동시성 2와 4에서는 두 agent.invoked가 먼저 기록됩니다. 이는 호출 순서의 증거이지 두 작업의 겹친 실행 시간을 직접 측정한 active-worker 메트릭은 아닙니다. 현재 fixture는 제한된 검증 슬롯, 사람 리뷰 대기, 충돌률 메트릭을 모델링하지 않습니다. 따라서 이 세 실행의 시간 차이를 현장 확장성 수치로 사용하지 않습니다. demo:conflict에서 파일 충돌 비용을 별도로 관찰하고, 실제 팀에서는 단계별 WIP와 리뷰 대기를 추가로 계측합니다.
실습: 작업자 인계의 최소 단면을 확인한다
npm run demo:worker-loss
이 명령은 demo:handoff의 별칭입니다. 첫 attempt가 명시적으로 체크포인트를 반환하고 두 번째 attempt가 이어받습니다.
attempt 1 workspace created
checkpoint saved
task returned to pending
attempt 2 workspace created
resumedFromHandoff=true
factory GREEN
현재 fixture는 심박 손실, 프로세스 사망, 임대 만료, 늦은 결과를 실제로 만들지 않습니다. 그런 운영 복구를 구현할 때 위 개념 절의 leaseId, 만료 시각, 작업 세대, STALE_RESULT 테스트를 추가해야 합니다.
여러 에이전트가 항상 낫지 않은 이유
에이전트를 추가하면 서로 다른 관점과 병렬 탐색을 얻을 수 있다. 동시에 조정 문맥, 중복 작업, 의견 충돌, 통합 비용이 늘어난다. 다음에는 단일 작업자가 적합하다.
- 작업이 작고 순차적이다.
- 하나의 모듈과 일관된 문맥이 중요하다.
- 자동 판정이 강하고 탐색 공간이 좁다.
다음에는 여러 역할이 가치가 있다.
- 독립적인 작업 조각이 실제로 존재한다.
- 구현과 공격적 리뷰를 분리해야 한다.
- 여러 설계 후보를 제한된 예산 안에서 비교한다.
- 긴 작업에서 전문 도구·도메인이 뚜렷이 나뉜다.
에이전트 수가 아니라 작업 구조와 검증 용량에서 시작한다.
왜 실패하는가
빈 실행 슬롯을 낭비로 본다
후단이 포화인데 새 작업을 시작하면 전체 대기와 재작업이 증가한다. 단계별 WIP를 최적화한다.
모든 실패를 작업자에게 돌린다
공통 도구·정책·명세 오류는 제어면 사건이다. 실패 코드를 집계해 작업군을 차단한다.
리뷰어가 작업자의 결론만 읽는다
같은 잘못된 가정을 이어받는다. 승인 명세, diff, 독립 게이트 결과를 기준으로 평가한다.
취소를 프로세스 종료로만 본다
늦게 도착한 결과와 부분 외부 효과가 남는다. 세대, 입력 해시, 임대 유효성을 판정한다.
운영 판단: 동시성을 늘릴 조건
- 준비 큐에 실제 독립 작업이 지속적으로 존재한다.
- 검증·리뷰·통합의 상위 90% 대기가 안정적이다.
- 충돌률과 재작업률이 허용 범위 안이다.
- 실행별 격리와 임대 만료가 검증됐다.
- 공통 실패 회로 차단기가 동작한다.
- 사람의 개입 시간이 동시성 증가와 함께 폭증하지 않는다.
한 단계씩 늘리고 같은 업무군에서 다시 측정한다.
연습문제
- 실행 슬롯 10, 테스트 슬롯 2, 리뷰어 1명인 공장의 WIP 한도를 설계하라.
- 일시 오류와 결정적 오류를 각각 세 개 분류하고 재시도 정책을 적어라.
- 명세 변경 뒤 늦게 도착한 성공 결과를 통합하지 않는 판정 필드를 설계하라.
- 팀의 작업 하나가 단일 에이전트와 다중 역할 중 어느 쪽에 맞는지 조정 비용을 포함해 논증하라.
체크포인트
- DAG에서 현재 준비된 작업만 큐에 들어간다.
- 단계별 용량과 WIP 한도가 있으며 후단 포화가 새 시작을 줄인다.
- 재시도는 새 실행·새 임대·명시적 부모 관계를 가진다.
- 취소·작업자 손실·공통 실패를 결정론적으로 복구한다.
다음 장에서는 각 변경을 빠르고 설명 가능한 품질 게이트에 통과시킨다. 좋은 오케스트레이션도 잘못된 완료 판정을 빠르게 반복하면 소용없다.