오케스트레이터-워커 패턴
라우팅이 미리 정해진 경로 중 하나를 고르는 구조라면, 오케스트레이터-워커는 작업을 실행 시점에 동적으로 분해하는 구조다. 중앙의 오케스트레이터 호출이 들어온 작업을 여러 개의 하위 작업으로 쪼개고, 각 하위 작업을 워커에게 펼쳐(fan-out) 맡긴 뒤, 워커들의 결과를 다시 합성(synthesize)한다. 결정적 차이는 하위 작업의 개수와 내용을 사전에 알 수 없다는 점이다. "이 코드베이스에서 영향받는 파일을 모두 수정하라"처럼 몇 개의 파일이 나올지 입력에 따라 달라지는 작업이 전형이다.
패턴의 골격은 분해, 팬아웃, 합성의 세 단계다. 워커의 수가 고정되지 않고 오케스트레이터가 런타임에 정한다는 점이 핵심이다.
라우팅과의 경계
시험은 이 두 패턴을 거의 항상 짝지어 묻는다. 구분 기준은 단순하다. 분기가 미리 정해진 고정된 카테고리 집합에서 하나로 좁혀지면 라우팅이다. 하위 작업의 수가 입력에 따라 가변적이고 오케스트레이터가 그것을 런타임에 결정해 여러 갈래로 펼치면 오케스트레이터-워커다. 라우팅은 "선택", 오케스트레이터는 "분해 후 합성"이다.
코드 주도 오케스트레이션
이 패턴은 두 실행 표면 모두에서 구현된다. 워크플로우로 직접 구현하면 오케스트레이션 로직(분해·디스패치·합성)을 내가 코드로 작성하고 워커는 각각 모델 호출이다. 아래는 Claude API와 도구 사용을 직접 조합한 워크플로우 버전이다.
# orchestrator/run.pyimport anthropic, json, concurrent.futures as cfclient = anthropic.Anthropic()def llm(prompt: str, max_tokens: int = 16000) -> str: resp = client.messages.create( model="claude-opus-4-8", max_tokens=max_tokens, thinking={"type": "adaptive"}, messages=[{"role": "user", "content": prompt}], ) return next(b.text for b in resp.content if b.type == "text")# 1) 오케스트레이터: 작업을 가변 개수의 하위 작업으로 분해plan = client.messages.create( model="claude-opus-4-8", max_tokens=4096, messages=[{"role": "user", "content": f"작업을 하위 작업 목록으로 분해: {task}"}], output_config={"format": {"type": "json_schema", "schema": { "type": "object", "properties": {"subtasks": {"type": "array", "items": {"type": "string"}}}, "required": ["subtasks"], "additionalProperties": False}}})subtasks = json.loads(next(b.text for b in plan.content if b.type == "text"))["subtasks"]# 2) 워커 팬아웃: 하위 작업을 병렬 처리with cf.ThreadPoolExecutor() as ex: results = list(ex.map(lambda s: llm(f"이 하위 작업을 수행: {s}"), subtasks))# 3) 합성: 워커 결과를 하나로 통합final = llm("다음 결과들을 일관된 산출물로 통합:\n" + "\n---\n".join(results))
하위 작업 개수가 plan의 출력에 따라 달라지는 점이 라우팅과의 결정적 차이다. 라우팅이라면 분기 수가 코드에 고정되어 있었을 것이다.
캐싱과 모델 선택에 관한 함정
오케스트레이터는 한 세션을 길게 끌고 가는 경우가 많다. 이때 시험이 노리는 포인트가 프롬프트 캐시 무효화다. 세션 도중에 모델을 바꾸면 캐시가 통째로 무효화된다. 캐시는 모델 단위로 스코프되기 때문이다. 따라서 비용을 줄이려고 일부 하위 작업을 더 싼 모델로 처리하고 싶다면, 메인 루프의 모델을 바꾸지 말고 더 싼 모델로 도는 서브에이전트를 별도로 띄우는 것이 정석이다. 마찬가지로 세션 중간에 도구 집합을 추가·제거·재정렬해도 캐시가 무효화된다. 도구는 프롬프트 맨 앞(위치 0)에 렌더링되기 때문이며, 동적 도구 발견이 필요하면 스키마를 교체하지 않고 덧붙이는 도구 검색(tool search)을 쓴다.
시험에서 어떻게 나오는가
- 함정 1. 라우팅 대 오케스트레이터-워커 구분. 하위 작업 수가 가변적이고 런타임에 결정되면 오케스트레이터다.
- 함정 2. "비용 절감을 위해 세션 중간에 더 싼 모델로 전환"이라는 보기는 캐시 무효화를 유발하므로 오답이다. 정답은 싼 모델로 도는 서브에이전트를 분리하는 것이다.
- 핵심 포인트. 오케스트레이터는 분해·디스패치·합성의 세 단계로 구성되며, 합성 단계를 빠뜨린 보기는 불완전한 패턴이다.
정리
- 오케스트레이터-워커는 작업을 런타임에 동적으로 분해해 여러 워커로 팬아웃한 뒤 결과를 합성하는 구조다.
- 하위 작업의 개수가 사전에 정해지지 않는다는 점이 라우팅(고정된 분기에서 하나 선택)과의 결정적 차이다.
- 분해, 디스패치, 합성의 세 단계가 모두 있어야 완전한 패턴이며 합성 누락은 오답 신호다.
- 세션 중간 모델 전환과 도구 집합 변경은 프롬프트 캐시를 무효화하므로, 싼 모델은 별도 서브에이전트로, 도구 확장은 도구 검색으로 처리한다.
- 모든 호출은
claude-opus-4-8와 adaptive thinking을 쓰고 구조화 출력은output_config.format으로 한다.