
소프트웨어 팩토리 시리즈 첫 글에서 Cortex는 소프트웨어 팩토리를 이해하고 조직이 책임 있게 도입하는 방법을 설명했다. 이번 글은 오픈소스 소프트웨어 팩토리 engrams가 기존 소프트웨어 개발 생명주기(SDLC)의 단계를 가능한 범위에서 자동화하는 방식을 소개한다.
워크플로를 오래 이어 가는 방법
가장 작은 사례로 Jira에 등록된 버그를 고쳐 운영 환경에 배포하는 과정을 생각해 보자. 사람이 처리한다면 티켓을 받고, 코드를 수정해 풀 리퀘스트(PR)를 올리고, 동료의 리뷰를 요청한다. 이후 댓글에 답하고 지속적 통합(CI) 오류를 고치며 브랜치를 재정리하고, 필요하면 다시 리뷰를 요청한다. 변경 사항이 병합되면 배포하고 티켓을 닫는다. 이는 단계와 인계·트리거가 정해져 있지만 각 단계에 걸리는 시간은 일정하지 않은 워크플로다.
기업은 헨리 포드의 모델 T 조립 라인 시절부터 업무 절차를 체계화해 왔고, 이런 흐름은 2000년대 초 비즈니스 프로세스 관리(BPM) 분야로 발전했다. 달라진 점은 이제 인공지능(AI)이 각 단계에 참여하거나 단계를 전부 맡을 수 있다는 것이다.
이런 업무는 일반적인 코드 실행 방식으로 처리하기 어렵다. 요청 처리기는 리뷰를 기다리느라 하루 동안 연결이나 스레드, 가상 머신(VM)을 열어 둘 수 없다. 작업 큐는 중단된 위치를 기억하지 않아 재시도할 때 처음부터 시작하고, 같은 프롬프트를 보내거나 댓글을 다시 달고 PR을 다시 열 수 있다. 크론 작업과 상태 열만으로 구현하면 단계와 전환, 경합 상황을 직접 관리하는 상태 기계가 된다. 수일 동안 실행되는 작업이 있으면 배포가 진행 중인 작업 수백 건의 한가운데서 이뤄질 수도 있다. Slack과 GitHub의 웹훅도 2xx 응답을 받을 때까지 재시도하므로 같은 입력이 여러 번 도착할 수 있다.
내구성 있는 워크플로(durable workflow)는 이런 문제를 해결하기 위한 실행 방식이다. 최신 워크플로 엔진은 BPM 실행 엔진과 달리 일반 코드처럼 프로세스를 모델링할 수 있다. 런타임은 각 단계가 끝날 때 결과를 데이터베이스에 체크포인트로 기록한다. 프로세스가 중단되거나 배포로 재시작되면 마지막으로 끝난 단계부터 이어서 실행하고, 완료된 단계는 다시 실행하는 대신 저장된 결과를 돌려준다. 기다리기도 기본 기능이다. 메시지가 올 때까지 워크플로를 멈춰 두어도 연결이나 VM을 계속 점유하지 않는다.
Cortex는 이 방식으로 각 실행이 이전에 멈춘 위치에서 재개되고 부수 효과가 한 번만 발생하도록 한다고 설명한다.
`engrams`의 자동화 구성
engrams는 사용자가 지정한 클라우드에서 Firecracker 마이크로VM을 실행해 Claude Code, Codex 또는 자체 하네스를 구동하는 소프트웨어 팩토리다. 대기 중인 에이전트는 블롭 저장소에 스냅샷으로 저장되며, Cortex는 대기 비용이 거의 들지 않고 인증 정보가 VM 안으로 들어가지 않는다고 설명한다. 이 플랫폼 위에서 에이전트 세션을 사용자가 설계한 워크플로의 한 단계처럼 다룰 수 있도록 제품을 만들었다.
Cortex는 처음부터 빈 워크플로 편집기를 제공하는 대신 일상적으로 쓸 수 있는 두 기능으로 시작했다.
- PR 리뷰에서는 탐색 에이전트가 변경 사항을 읽고 문제를 제안하며, 검증 에이전트가 각 항목을 확인해 검증하지 못한 것은 제외한다. 결과는 인라인 댓글이 포함된 GitHub 리뷰 하나로 게시된다. 새 푸시가 들어오면 진행 중인 리뷰를 대체한다.
- Slack 스레드에서는 채널에서 봇을 호출하면 사용자의 인증 정보를 이용하는 클라우드 에이전트 세션이 열린다. 세션은 스레드에 연결되고, 답글은 같은 세션에 추가된다. 에이전트가 질문하면 Slack 양식으로 전달된다.
PR 리뷰의 자동화는 GitHub 웹훅을 받고, 리뷰 프롬프트를 사용하는 Claude Code VM을 실행해 결과를 파일로 작성하고, 그 파일을 내려받은 뒤, 별도의 새 VM에서 결과를 독립적으로 검증하고, 마지막으로 GitHub 리뷰를 게시하는 단계로 구성된다. engrams는 여기에 크론·수동·웹훅 트리거와 필터·분기·반복 같은 제어 흐름 기능을 더했다. 더 세밀한 제어 흐름을 위해 QuickJS 기반의 WebAssembly(WASM) 샌드박스 JavaScript 블록도 제공한다. Slack에서 @engrams 봇을 호출할 때 스마트 프로필 선택 기능을 구동하는 의사결정 모델 Jev를 사용하는 블록도 있다. Jev 소개에서 Cortex는 Jev를 유행 중인 저렴한 의사결정 모델이라고 설명한다.
DBOS로 실행과 재개를 관리한다
Cortex는 자체 클라우드에서 실행하기 쉽고 단순하며 가벼운 내구성 워크플로 엔진이라는 이유로 DBOS를 선택했다. engrams의 배포를 최대한 단순하게 만들고 싶었으며, 오픈소스 경쟁 제품에서 겪는 운영 부담 없이 Postgres만 있으면 된다는 점이 매력적이었다고 밝혔다. 새로 출시한 OpEx Review 제품을 위해 DBOS를 시험해 본 조직 내 경험도 있었다.
DBOS는 Bun 기반 TypeScript 제품 영역에 내장돼 실행된다. 시스템 테이블은 Drizzle 테이블과 함께 dbos 스키마에 저장되고, HTTP 서버가 요청을 받기 전에 엔진이 시작된다.
DBOS.setConfig({
name: "engrams-orchestrator",
systemDatabaseUrl: config.databaseUrl,
systemDatabaseSchemaName: "dbos",
executorID: dbosExecutorId(), // the pod name
runAdminServer: false,
logger: dbosLogger,
});
await DBOS.launch();
Cortex는 손으로 작성한 두 워크플로를 AutomationRunWorkflow라는 하나의 등록된 워크플로로 바꿨다. 이 워크플로는 데이터로 저장된 블록 그래프를 해석하며, 기본 제공 자동화와 사용자 지정 자동화 모두 이를 거쳐 실행된다.
export async function automationRunWorkflowImpl(input, deps = {}) {
// Bump on ANY change to step naming, step order, recv semantics,
// or finalize position. Lives in the registered body so a bump
// rotates the DBOS application version.
const ENGINE_STEP_CONTRACT = 4;
const engine = deps.engine ?? productionEngineDeps(); // DBOS.runStep, DBOS.recv, ...
return interpretAutomation({ ...input, contract: ENGINE_STEP_CONTRACT }, engine);
}
export const automationRunWorkflow = DBOS.registerWorkflow(
automationRunWorkflowImpl, { name: "AutomationRunWorkflow" });
등록된 워크플로 함수의 소스 코드가 DBOS 애플리케이션 버전을 결정하므로 본문은 의도적으로 작게 유지한다. 자동화 그래프는 편집 때마다 바뀔 수 있지만 등록된 본문은 거의 바뀌지 않는다. 재생이 같은 결과를 내도록 몇 가지 규칙을 뒀다. 실행 시작 단계에서 해당 실행에 고정된 자동화 버전을 불러와 체크포인트로 저장하므로, 실행 중 관리자가 자동화를 수정해도 재생은 같은 그래프를 따른다. 각 단계에는 그래프 위치를 나타내는 step:<framePath>:<attempt> 형식의 이름을 붙인다. 반복 회차와 재시도마다 별도의 체크포인트와 실행 기록이 생긴다. 분기 조건과 반복 종료 조건, 필터는 단계 안에서 계산해 재생 시 저장된 불리언 값을 사용한다. 제어 흐름에서 Date.now() 같은 비결정적 함수를 호출하지 않으며, 재시도는 엔진이 관리한다. 템플릿 오류나 제공자 측 4xx 오류처럼 영구 오류로 표시된 경우는 다시 시도하지 않는다.
선형·분기·반복·재시도·기한·중지·대체 그래프에서 단계 이름과 순서를 확인하는 스냅샷 테스트도 둔다. 단계 순서가 의도치 않게 바뀌면 CI가 실패하고, 의도적으로 변경할 때는 ENGINE_STEP_CONTRACT를 올린다.
대기 중에는 자원을 점유하지 않는다
자동화 실행마다 메일박스 하나를 두며, 실행 ID는 DBOS 워크플로 ID와 같다. 형식은 autorun:<automationId>:<deliveryKey>다. 세션이 유휴 상태가 되거나 Slack 답글, 에이전트 신호를 기다릴 때 인터프리터는 DBOS.recv에서 멈춘다. 이때 소켓이나 잠금, VM을 열어 두지 않는다.
engrams 배포에서 컴퓨팅 노드가 가장 큰 비용을 차지하기 때문에 Cortex는 유휴 세션을 효율적으로 종료하고 다시 불러올 수 있도록 제어 영역을 만들었다. 멈춰 있는 워크플로는 DBOS 시스템 테이블의 행으로, 대기 중인 VM도 파일과 함께 Postgres의 행으로 저장된다. 메시지가 도착하면 엔진이 실행을 깨우고 마지막 완료 단계부터 재개한다.
실행을 깨우는 모든 입력은 해당 워크플로 ID를 대상으로 한 DBOS.send다. 세션 리스너는 에이전트가 작업을 마치거나 종료될 때 session_idle과 session_ended를 보낸다. 에이전트는 도구를 통해 신호를 보내고, 디스패처는 실행에 합류하는 새 이벤트와 대체 신호를 전달한다. 운영자는 중지 신호를 보낼 수 있다.
부수 효과를 중복 실행하지 않도록 설계한다
Cortex는 초기에 에이전트의 마이크로VM 안에서 실행되는 명령을 멱등하게 처리하기가 가장 어렵다는 점을 발견했다. git clone, 빌드, Claude Code에 메시지 보내기 같은 작업은 몇 분이 걸릴 수 있고, VM이 작업 중 스냅샷 저장·축출·복원되면 결과 처리에 문제가 생길 수 있다.
PR 리뷰 중 DBOS 단계에서 git clone을 실행하던 때에는 복제 도중 정기 VM 스냅샷이 게스트 연결을 초기화했다. 명령은 끝났지만 결과를 전달할 곳이 없었다. 이를 해결하기 위해 VM 작업도 DBOS 단계처럼 다뤘다. 각 실행 명령에 결정적인 ID를 붙여 재생된 단계가 같은 실행을 요청하게 했다. 게스트는 출력과 종료 상태를 VM 내부에 기록해 연결이 끊기거나 스냅샷을 저장하거나 다른 호스트로 옮겨도 결과를 보존한다. 같은 ID로 다시 호출하면 새 명령을 시작하는 대신 실행 중이거나 완료된 명령에 연결한다.
따라서 명령 실행 단계가 재시작 뒤 통째로 다시 실행돼도 기존 명령에 연결해 원래 결과를 받을 수 있다. Cortex는 DBOS의 최소 한 번 전달(at-least-once delivery)에 의존하면서 모든 작업에 멱등성을 적용해 자동화 전반에서 사실상 한 번만 전달되도록 했다고 설명한다.
| 부수 효과 | 결정적 ID | 중복 제거 방식 |
|---|---|---|
| VM에서 명령 실행 | 결정적 실행 ID | VM의 실행 기록: 연결 또는 시작 |
| 에이전트에 프롬프트 전송 | slack:<event_id> 같은 트리거 이벤트 ID | VM의 하네스가 재시작과 스냅샷을 거쳐도 프롬프트 ID를 기억 |
| 웹훅으로 실행 시작 | 워크플로 ID autorun:<automationId>:<provider>:<deliveryId> | DBOS |
| 크론 실행 시점 | 실행 ID에 포함된 해당 시점의 에포크 | DBOS |
Cortex는 DBOS의 내구성 워크플로 모델 덕분에 engrams가 작동한다고 설명한다. 편집기에서 그린 블록 그래프도 직접 작성한 코드와 같은 방식으로 재시작과 배포를 견디고, 며칠 동안 답변을 기다리며, VM 안의 명령까지 포함한 부수 효과를 한 번만 수행한다는 것이다. engrams는 각 설치 환경에 이미 있는 Postgres에서 실행된다.
몇 시간이나 며칠 동안 작업하는 에이전트를 만들고 있다면 engrams를 시도하거나 자체 Postgres에서 DBOS로 시작할 수 있다.
댓글 (0)
로그인하면 이 기사에 내 생각을 남길 수 있어요