본문으로 건너뛰기

© 2026 Molayo

Zenn헤드라인2026. 05. 25. 10:52

Cursor Composer 2.5를 Codens의 executor lane에 추가한 이야기 (Opus의 1/10 비용 +)

요약

Cursor Composer 2.5의 압도적인 비용 효율성에 주목하여 Codens 시스템에 새로운 실행 경로(executor lane)를 추가한 사례를 다룹니다. Anthropic Opus 대비 약 1/10 수준의 비용으로 유사한 성능을 구현하기 위해 섀도우 런을 통해 운영 신뢰성을 검증하는 과정을 설명합니다.

핵심 포인트

  • Composer 2.5는 Opus 대비 약 1/10의 시도당 비용을 제공함
  • Codens 시스템에 runner_cursor.py라는 제3의 실행 경로를 추가함
  • 기존 경로의 리스크를 방지하기 위해 섀도우 런 방식으로 검증 진행
  • 비용 절감을 위해 재시도 횟수와 시도당 단가의 상관관계를 고려함

Cursor가 Composer 2.5를 출시했을 때 가장 먼저 눈에 띈 것은 benchmark의 절대값이 아니라, per-attempt cost(시도당 비용)의 자릿수가 하나 다르다는 사실이었습니다. SWE-Bench Multilingual에서 Anthropic Opus와 동등한 수준임을 주장하면서도, 1회 생성에 드는 cost(비용)가 개략적으로 1/10 수준이었습니다. Codens의 fix_verify loop는 task 하나에 대해 여러 번 retry(재시도)를 수행하는 구조이므로, attempt(시도) 단가가 자릿수 단위로 다르면 retry cap(재시도 제한) × per-attempt cost를 적분했을 때의 총비용이 10배 차이 나게 됩니다.

benchmark 숫자만 보고 움직이는 것은 성급하다 생각하면서도, "일단 구현과 canary(카나리)를 실행하여 production reliability(운영 신뢰성)를 직접 눈으로 확인하는 수밖에 없다"는 마음으로 착수한 이야기입니다. 결과부터 말씀드리면, runner_cursor.py라는 제3의 lane(경로)을 Codens Purple에 추가하여 사내 프로젝트 1개에 대해 flip(전환)하여 shadow run(섀도우 런) 중입니다. v4-v17 기준으로 16 failed / 9 done으로 production ready(운영 준비 완료) 상태와는 거리가 멀지만, bridge 주변의 failure mode(실패 모드)와 operator UX(운영자 경험)는 대략적으로 정리했으므로 기록으로 남겨둡니다.

Composer 2.5를 도입한 경제성

Codens는 원래 Anthropic API(per-token billing 방식으로 raw API를 직접 호출하는 구성, subscription 방식 아님)와 self-hosted Qwen의 2개 lane으로 동작하고 있었습니다. 각 task는 PurpleTask.execute_model에서 model을 가지며, 프로젝트 단위의 default(기본값)는 PurpleProject.default_model로 결정됩니다.

도입 동기는 commit의 short message에 적힌 "Composer 2.5 is ~10x cheaper than Opus at comparable SWE-Bench scores"였습니다. 발표된 수치를 전부 그대로 믿을지는 별개지만, 동등한 수준을 주장하는 케이스를 cost 1/10로 가져올 수 있다면 무시할 수 없습니다.

retry cap × per-attempt cost에 관한 이야기는 별도 기사(CDTSK-1362)에서 쓴 대로, Codens는 model마다 cap을 다르게 설정하고 있으며 현재 Claude는 3 attempts, Qwen은 6 attempts입니다. Composer 2.5를 Opus와 동일한 3 attempts로 운용하더라도 per-attempt cost가 1/10이라면 per-task 총비용이 1/10로 떨어지는 계산이 나옵니다. 다만 SWE-Bench는 public dataset이며 vendor(공급업체)는 당연히 그 부분을 최적화할 것이고, customer task는 SWE-Bench와는 다른 code base 분포를 가지므로, "Opus와 동등"하다는 것은 가설로 취급하고 production reliability는 별도로 측정한다는 스탠스로 설계를 시작했습니다.

executor lane 추가 = 어떤 작업을 하는가

Codens Purple는 이미 execute_model에서 runner를 분기하는 구조를 가지고 있었기 때문에, 새로운 lane을 추가하는 것은 엄밀히 말해 runner_cursor.py를 만들고 enum을 하나 늘리는 작업일 뿐입니다. 기존 lane에 변경을 가하면 production 트래픽의 95%가 흐르는 기존 경로에 regression(회귀 오류)을 일으킬 리스크가 있으므로 건드리지 않습니다.

class ExecuteModel(str, Enum):
CLAUDE_OPUS_4 = "claude-opus-4"
QWEN_3_CODER = "qwen-3-coder"
...

새로운 lane은 opt-in 전용이며, 전환은 SQL 한 줄로 가능하고, canary는 사내 1개 프로젝트로 제한했습니다. 구현은 2단계(2 phase)로 나누었습니다. 처음에는 1단계(1 phase)에서 full SDK wire까지 한꺼번에 처리하려고 했으나, production 배포 시의 rollback(롤백) 비용을 고려하여 나누었습니다. Phase 1에서는 skeleton(골격)과 DB migration(마이그레이션)을, Phase 2에서는 실제 Cursor SDK를 호출하도록 했습니다. 이렇게 하면 Phase 1 롤백은 alembic downgrade 한 번으로 끝나고, Phase 2 롤백은 runner_cursor 본체를 no-op으로 되돌리기만 하면 됩니다. '1 PR 1 기능'의 입도(granularity)보다 '1 PR 1 rollback unit'의 입도로 생각하는 것이 나중에 더 편하다는 것이 배운 점입니다.

Phase 1 (commit 5a575031)에서 수행한 작업은 runner_cursor.py를 validation(검증) 전용으로 생성, enum / DB CHECK constraint(제약 조건)에 composer-2.5를 추가, dispatcher(디스패처)에 case를 1개 추가하는 것 등 총 3가지입니다. 지정된 task(태스크)가 존재하지 않으므로 배포하더라도 production에는 영향이 없으며, 만약 routing logic(라우팅 로직)에 버그가 발생하더라도 기존 task는 모두 Claude / Qwen으로 가기 때문에 blast radius(영향 범위)가 제로입니다.

Phase 2: Cursor Python SDK의 wire

Phase 2 (commit b1e7ebcd)에서 runner_cursor의 내부를 실제 구현으로 교체했습니다. Cursor의 Python SDK는 Bridge → Client → Agent → run.events()의 event-stream(이벤트 스트림) 구조를 가집니다:

# infrastructure/runners/runner_cursor.py
async def _run_cursor_session(prompt, workspace_dir, model_id):
bridge = await Bridge.launch(api_key=settings.CURSOR_API_KEY)
...

Bridge.launch를 통해 sidecar(사이드카) 형태의 Cursor process(프로세스)가 실행되며, 이를 window(윈도우)로 삼아 agent(에이전트)와 event stream(이벤트 스트림)으로 대화합니다. max_executions는 agent 내부의 tool call(도구 호출) 상한이며, Codens Purple의 retry cap(재시도 제한, fix_verify에서 agent를 몇 번 spawn(스폰)할 것인가)과는 별개의 layer(계층)에서 작동하는 cap입니다. 이를 초과하면 failure_reason="exceeded max executions"와 함께 run(실행)이 종료되며, fix_verify 측에서 다음 attempt(시도)를 다시 spawn(스폰)합니다.

CURSOR_API_KEY는 AWS Secrets Manager에 저장하고, ECS Fargate task definition(태스크 정의)에 secret ARN을 env(환경 변수)로 전달한 뒤, entrypoint script(엔트리포인트 스크립트)에서 resolve(해결)합니다:

# scripts/purple-job-entrypoint.sh
set -euo pipefail
unset AWS_PROFILE # Use task IAM role, not customer's profile
...

첫 번째 구현에서 실패했던 점은 aws secretsmanager get-secret-value가 고객의 AWS_PROFILE로부터 credential(자격 증명)을 가져오려 했다는 점입니다. Codens Purple은 태스크마다 고객의 AWS credential을 가지는 경우(고객의 repo에 배포하는 계열)가 있어 환경에 AWS_PROFILE이 설정되어 있을 수 있습니다. 이 경우 '고객의 IAM principal로 우리의 Secrets Manager를 호출'하게 되어 403 에러가 발생합니다. 수정 사항 (commit 6210a052)에서는 명시적으로 unset AWS_PROFILE을 수행합니다.

하고, ECS Fargate의 task IAM role이 container metadata endpoint를 통해 SDK에 전달되는 동작에 맡겼습니다. 이는 multi-tenant SaaS의 credential isolation (자격 증명 격리)의 전형적인 사례였습니다.

Canary: 1개 프로젝트만 flip

production rollout (운영 배포)은 Corevice org의 dogfood 프로젝트 1개만 flip 하는 canary (카나리) 방식으로 시작했습니다. runbook (실행 지침서)은 UPDATE purple_projects SET default_model = 'composer-2.5' WHERE slug = 'dogfood-composer-canary'; 이 한 줄이 전부입니다. 다른 org의 customer project는 기존 방식대로 Opus / Qwen으로 흐릅니다. 즉시 rollback (롤백) 또한 같은 방식으로 한 줄만 되돌리면 됩니다.

관찰 지표는 완주율, 1 attempt (시도) 당 verify pass (검증 통과)율, wall time (Cursor SDK는 bridge sidecar의 overhead가 있어 Claude의 HTTP API보다 느리므로, cost(비용)에서 이기더라도 운영에서 질 수 있음), 실제 per-task cost (태스크당 비용)입니다. 판단 기준은 사전에 설정해 두었습니다. "2주 동안 완주율이 Opus baseline의 70% 미만이면 hold (중단)", "per-task cost가 Opus baseline의 30% 이하이면 완주율이 낮더라도 다른 프로젝트로의 확대를 검토"와 같은 식입니다. 사전에 임계치(threshold)를 정하지 않고 지표를 보기 시작하면 좋은 숫자에 휘둘리기 쉬우므로, 출발점에서 선을 그어 두는 것이 나중에 훨씬 편합니다.

smoke 테스트에서 겪은 failure와 operator UX

실제 운영을 해보니 당연히 benchmark (벤치마크) 숫자와는 다른 현실이 보였습니다. v4-v17 테스트에서 16 failed / 9 done으로 절반 이상이 실패하여, production ready (운영 준비 완료) 상태와는 거리가 멀었습니다.

주요 failure mode (실패 모드)는 두 가지였습니다. bridge drop (Bridge sidecar로의 connection이 끊겨 run.events()가 중간에 멈추는 현상. workspace에 중간 단계의 diff가 남아 있음에도 status는 failed로 표시되어 commit 되지 않고 버려짐)과 max_executions (최대 실행 횟수) 도달 (agent가 tool을 계속 호출하여 max_executions=5에서 failure_reason="exceeded max executions (5)"`가 발생하는 경우)입니다.

commit 0f95f020에서 이 두 가지를 수정했습니다. bridge drop 감지 시 workspace의 git diff를 salvage (구조/복구)하여 PR draft로 저장하도록 했습니다. 완전히 실패하게 두는 대신 "중간까지 진행된 차분은 구한다"는 동작을 구현하여, operator (운영자)가 나중에 유용하다고 판단하면 수동으로 commit 승격이 가능하도록 했습니다. max_executions는 failure_reason의 표시 방식이 문제였습니다. 당초 Notion ticket과 Slack 알림에는 "exceeded max executions (5)"만 표시되어, operator가 "그래서 무엇 때문에 5번이나 tool을 호출한 거지?"라는 질문에 대한 답을 추적할 수 없었습니다. 동일한 commit에서 마지막 tool call의 error message를 append (추가)하여, "exceeded max executions (5) - last error: pytest exit 1 in tests/test_user.py::test_create" 형식으로 변경했습니다.

또한 1be0614f에서 entrypoint script가 secret resolve (비밀 정보 해결) 실패 시 silent (조용히)하게 die (종료)하던 문제를 수정했습니다. set -e가 적용되지 않아 subshell이 종료되어도 parent (부모 프로세스)가 인지하지 못했고, CURSOR_API_KEY가 unset (미설정) 상태인 채로 worker가 기동되어, bridge.launch()가 "API key 부정"으로 종료되지만 실제 root cause (근본 원인)는 IAM permission (권한) 부족인, 추적하기 어려운 연쇄 반응이 발생했습니다. set -euo pipefail을 넣어 secret resolve 단계에서 즉시 종료되도록 수정했습니다.

이것들은 전부 「reliability (신뢰성)의 근본적인 치료」가 아니라 「failure mode (장애 모드)를 발견하기 쉽게 만드는 operator UX (운영자 UX)」의 개선입니다. benchmark (벤치마크) 수치가 좋아도 production (운영 환경)에서 장애가 발생했을 때 operator (운영자)가 원인에 도달하지 못한다면 결국 그 lane (레인)은 쓸모가 없게 됩니다. observability (관찰 가능성) 확보는 canary (카나리) 배포의 첫 1주일 동안 가장 많은 시간을 할애한 작업이었습니다.

multi-model executor lane (멀티 모델 실행 레인)을 갖는 의미

Composer 2.5를 세 번째 lane으로 추가할 수 있었던 것은, Codens가 원래부터 task (태스크)별로 model (모델)을 전환할 수 있는 구조를 가지고 있었기 때문이며, 이것이 없었다면 거의 새로 만드는 것에 가까운 변경이었을 것입니다. provider (공급자)가 하나뿐이라면 해당 provider의 pricing (가격 정책) 개정이나 API 사양 변경, ratelimit policy (속도 제한 정책) 변경이 그대로 product (제품)의 리스크가 됩니다. multi-model (멀티 모델) 구조로 해두면 그 리스크를 분산할 수 있습니다. benchmark (벤치마크) 수치와는 별개인 「optionality (선택권)」라는 효능으로, SaaS를 운영하다 보면 1년에 한 번 정도 그 혜택을 실감하게 됩니다. Composer 2.5 lane은 아직 smoke phase (스모크 단계)라 reliability (신뢰성) 수치가 다 나오지는 않았지만, 「lane을 하나 추가한다」는 기계적인 작업으로서 구현할 수 있었다는 사실 자체가 지금까지의 multi-model (멀티 모델) 투자에 대한 return (수익)이었다고 생각합니다.

Codens는 https://www.codens.ai/ 에서 공개하고 있습니다.

Discussion

AI 자동 생성 콘텐츠

본 콘텐츠는 Zenn AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0