
대규모 AI 코드 리뷰 오케스트레이션
요약
Cloudflare는 단일 모델의 한계를 극복하기 위해 OpenCode를 기반으로 한 CI-native AI 코드 리뷰 오케스트레이션 시스템을 구축했습니다. 보안, 성능, 품질 등 각 분야에 특화된 7개의 에이전트가 협업하여 중복을 제거하고 구조화된 리뷰를 제공합니다.
핵심 포인트
- 단일 거대 에이전트 대신 특화된 다중 에이전트 구조 채택
- CI/CD 파이프라인에 통합된 오케스트레이션 시스템 구축
- 보안, 성능, 문서화 등 분야별 전문 리뷰어 운영
- 확장성을 고려한 플러그인 기반 아키텍처 설계
코드 리뷰 (Code review)는 버그를 잡고 지식을 공유하는 환상적인 메커니즘이지만, 엔지니어링 팀의 병목 현상을 일으키는 가장 확실한 방법 중 하나이기도 합니다. 머지 리퀘스트 (Merge request)가 대기열에 쌓여 있고, 리뷰어가 결국 컨텍스트 스위칭 (Context-switch)을 하여 차이점 (Diff)을 읽고, 변수 명명에 대한 몇 가지 사소한 지적 (Nitpicks)을 남기면, 작성자가 이에 응답하며 이 사이클이 반복됩니다. 당사의 내부 프로젝트 전반에 걸쳐 첫 번째 리뷰까지 걸리는 중앙값 대기 시간은 종종 몇 시간 단위로 측정되었습니다.
우리가 AI 코드 리뷰 (AI code review) 실험을 처음 시작했을 때, 아마 대부분의 사람들이 선택하는 경로를 따랐습니다. 즉, 몇 가지 다른 AI 코드 리뷰 도구들을 시도해 보았고, 이러한 도구들 중 상당수가 꽤 잘 작동하며 심지어 상당한 수준의 커스터마이징 (Customisation)과 설정 가능성 (Configurability)을 제공한다는 것을 발견했습니다! 하지만 불행하게도, 계속해서 나타나는 공통적인 주제는 Cloudflare 규모의 조직에 필요한 충분한 유연성과 커스터마이징을 제공하지 못한다는 점이었습니다.
그래서 우리는 그다음으로 가장 명확한 경로로 뛰어들었습니다. 바로 git diff를 가져와서 미완성된 프롬프트 (Prompt)에 밀어 넣고, 대규모 언어 모델 (Large language model)에게 버그를 찾아달라고 요청하는 것이었습니다. 결과는 예상했던 대로 매우 소란스러웠습니다. 모호한 제안들이 쏟아졌고, 환각 (Hallucinated)된 구문 오류가 나타났으며
처음부터 거대한 단일(monolithic) 코드 리뷰 에이전트를 구축하는 대신, 우리는 오픈 소스 코딩 에이전트인 __OpenCode__를 중심으로 한 CI-native 오케스트레이션 (orchestration) 시스템을 구축하기로 결정했습니다. 오늘날 Cloudflare의 엔지니어가 머지 리퀘스트 (merge request)를 열면, 조율된 다양한 AI 에이전트들의 뷔페(smörgåsbord)와 같은 초기 검토를 거치게 됩니다. 방대하고 일반적인 프롬프트 (prompt)를 사용하는 단일 모델에 의존하는 대신, 우리는 보안, 성능, 코드 품질, 문서화, 릴리스 관리, 그리고 내부 엔지니어링 코덱스 (Engineering Codex) 준수 여부를 다루는 최대 7개의 특화된 리뷰어를 실행합니다. 이러한 전문가들은 코디네이터 (coordinator) 에이전트에 의해 관리되며, 이 에이전트는 발견된 내용의 중복을 제거하고, 문제의 실제 심각도를 판단하며, 단일화된 구조화된 리뷰 코멘트를 게시합니다.
우리는 수만 개의 머지 리퀘스트에 대해 이 시스템을 내부적으로 운영해 왔습니다. 이 시스템은 깨끗한 코드는 승인하고, 인상적인 정확도로 실제 버그를 찾아내며, 진정으로 심각한 문제나 보안 취약점이 발견되면 머지를 적극적으로 차단합니다. 이는 __Code Orange: Fail Small__의 일환으로 우리의 엔지니어링 회복탄력성 (resiliency)을 개선하는 수많은 방법 중 하나일 뿐입니다.
이 포스트는 우리가 이를 어떻게 구축했는지, 우리가 채택한 아키텍처 (architecture)는 무엇인지, 그리고 LLM을 CI/CD 파이프라인의 핵심 경로(critical path)에 배치하려 할 때, 더 결정적으로는 코드를 배포하려는 엔지니어들의 앞길에 배치하려 할 때 마주하게 되는 구체적인 엔지니어링 문제들에 대해 심층적으로 다룹니다.
아키텍처: 끝없이 확장되는 플러그인 구조
수천 개의 저장소 (repositories)에서 실행되어야 하는 내부 도구를 구축할 때, 버전 관리 시스템 (version control system)이나 AI 제공업체를 하드코딩하는 것은 6개월 안에 전체를 다시 작성하게 만드는 지름길입니다. 우리는 어떤 구성 요소도 다른 구성 요소에 대해 알 필요 없이, 현재의 GitLab은 물론 내일은 또 무엇이 될지 모를 시스템과 다양한 AI 제공업체 및 서로 다른 내부 표준 요구 사항을 지원해야 했습니다.
우리는 진입점(entry point)이 리뷰가 실행되는 방식을 정의하기 위해 함께 구성되는 플러그인들에게 모든 설정을 위임하는 조합 가능한 플러그인 아키텍처 (composable plugin architecture)를 기반으로 시스템을 구축했습니다. 머지 리퀘스트 (merge request)가 리뷰를 트리거할 때의 실행 흐름은 다음과 같습니다:
각 플러그인은 세 가지 라이프사이클 (lifecycle) 단계가 있는 ReviewPlugin 인터페이스를 구현합니다. 부트스트랩 (Bootstrap) 훅은 병렬로 실행되며 비치명적 (non-fatal)입니다. 즉, 템플릿 가져오기에 실패하더라도 리뷰는 해당 템플릿 없이 계속 진행됩니다. 구성 (Configure) 훅은 순차적으로 실행되며 치명적 (fatal)입니다. 왜냐하면 VCS 제공업체가 GitLab에 연결할 수 없다면 작업을 계속할 의미가 없기 때문입니다. 마지막으로, postConfigure는 구성이 조립된 후 원격 모델 오버라이드 (remote model overrides) 가져오기와 같은 비동기 작업을 처리하기 위해 실행됩니다.
ConfigureContext는 플러그인이 리뷰에 영향을 미칠 수 있도록 제어된 접점 (surface)을 제공합니다. 플러그인은 에이전트 (agents)를 등록하고, AI 제공업체 (AI providers)를 추가하며, 환경 변수 (environment variables)를 설정하고, 프롬프트 섹션 (prompt sections)을 주입하고, 세밀한 에이전트 권한 (fine-grained agent permissions)을 변경할 수 있습니다. 어떤 플러그인도 최종 구성 객체 (final configuration object)에 직접 접근할 수 없습니다. 플러그인들은 컨텍스트 API (context API)를 통해 기여하며, 코어 어셈블러 (core assembler)가 모든 것을 OpenCode가 소비하는 opencode.json 파일로 병합합니다.
이러한 격리 (isolation) 덕분에, GitLab 플러그인은 Cloudflare AI Gateway 설정을 읽지 않으며, Cloudflare 플러그인은 GitLab API 토큰에 대해 아무것도 알지 못합니다. 모든 VCS 특화된 결합 (coupling)은 단일 ci-config.ts 파일에 격리되어 있습니다.
다음은 일반적인 내부 리뷰를 위한 플러그인 명단입니다:
플러그인 (Plugin)
| 역할 (Responsibility)
|
|---|
@opencode-reviewer/gitlab | GitLab VCS 제공자, MR 데이터, MCP 코멘트 서버 |
@opencode-reviewer/cloudflare | AI Gateway 설정, 모델 티어 (model tiers), 페일백 체인 (failback chains) |
@opencode-reviewer/codex | 엔지니어링 RFC에 따른 내부 준수 사항 (compliance) 확인 |
@opencode-reviewer/braintrust | 분산 트레이싱 (distributed tracing) 및 관측성 (observability) |
@opencode-reviewer/agents-md | 저장소의 AGENTS.md가 최신 상태인지 검증 |
@opencode-reviewer/reviewer-config | Cloudflare Worker를 통한 리뷰어별 원격 모델 오버라이드 (overrides) |
@opencode-reviewer/telemetry | 비동기식 (fire-and-forget) 리뷰 추적 |
내부적으로 OpenCode를 사용하는 방법
우리가 OpenCode를 선호하는 코딩 에이전트 (coding agent)로 선택한 데에는 몇 가지 이유가 있습니다:
우리는 내부적으로 이를 광범위하게 사용하고 있으며, 이는 우리가 이미 작동 방식에 매우 익숙하다는 것을 의미합니다.
이것은 오픈 소스 (open source)이므로, 상류 (upstream)에 기능과 버그 수정을 기여할 수 있을 뿐만 아니라 문제를 발견했을 때 매우 쉽게 조사할 수 있습니다 (이 글을 쓰는 시점에 Cloudflare 엔지니어들은 상류에 45개 이상의 풀 리퀘스트 (pull requests)를 반영했습니다!).
훌륭한 __오픈 소스 SDK (open source SDK)__를 갖추고 있어, 완벽하게 작동하는 플러그인을 쉽게 구축할 수 있습니다.
하지만 가장 중요한 이유는, 이것이 서버 우선 (server first) 구조로 설계되어 텍스트 기반 사용자 인터페이스 (UI)와 데스크톱 앱이 그 위의 클라이언트 (client) 역할을 하기 때문입니다. 이는 우리에게 필수적인 요구 사항이었는데, CLI 인터페이스를 임의로 수정(hacking)하지 않고도 프로그래밍 방식으로 세션을 생성하고, SDK를 통해 프롬프트 (prompts)를 전송하며, 여러 개의 동시 세션으로부터 결과를 수집해야 했기 때문입니다.
오케스트레이션 (orchestration)은 두 개의 별도 계층에서 작동합니다:
코디네이터 프로세스 (The Coordinator Process): 우리는 Bun.spawn을 사용하여 OpenCode를 자식 프로세스 (child process)로 생성합니다. 우리는 코디네이터 프롬프트를 명령줄 인자 (command-line argument) 대신 stdin을 통해 전달합니다. 로그로 가득 찬 거대한 머지 리퀘스트 (merge request) 설명을 명령줄 인자로 전달하려고 시도해 본 적이 있다면, 아마도 리눅스 커널의 ARG_MAX 제한을 경험해 보았을 것입니다. 우리는 E2BIG 오류를 통해 이 사실을 꽤 빠르게 배웠습니다.
매우 거대한 머지 리퀘스트 (Merge Request)에 대해 CI 작업의 아주 적은 비율에서 오류가 나타나기 시작했습니다. 프로세스는 --format json 옵션과 함께 실행되므로, 모든 출력은 stdout을 통해 JSONL 이벤트로 전달됩니다:
const proc = Bun.spawn(
["bun", opencodeScript, "--print-logs", "--log-level", logLevel,
"--format", "json", "--agent", "review_coordinator", "run"],
...
리뷰 플러그인 (The Review Plugin): OpenCode 프로세스 내부에서 런타임 플러그인이 spawn_reviewers 도구를 제공합니다. 코디네이터 LLM (Coordinator LLM)이 코드를 리뷰할 시점이라고 결정하면 이 도구를 호출하며, 이 도구는 OpenCode의 SDK 클라이언트를 통해 서브 리뷰어 (Sub-reviewer) 세션들을 실행합니다:
const createResult = await this.client.session.create({
body: { parentID: input.parentSessionID },
query: { directory: dir },
...
각 서브 리뷰어는 자신만의 에이전트 프롬프트 (Agent Prompt)를 가진 개별적인 OpenCode 세션에서 실행됩니다. 코디네이터는 서브 리뷰어들이 어떤 도구를 사용하는지 보거나 제어하지 않습니다. 그들은 적절하다고 판단되는 대로 소스 파일을 읽거나, grep을 실행하거나, 코드베이스를 검색할 수 있는 자유를 가지며, 작업이 끝나면 단순히 발견한 내용을 구조화된 XML 형식으로 반환합니다.
JSONL이란 무엇이며, 우리는 이를 어디에 사용하는가?
이러한 시스템을 다룰 때 일반적으로 직면하게 되는 큰 과제 중 하나는 구조화된 로깅 (Structured Logging)의 필요성입니다. JSON은 환상적인 구조화된 형식이지만, 유효한 JSON 블롭 (JSON Blob)이 되려면 모든 데이터가 "닫혀(closed out)" 있어야 합니다. 이는 애플리케이션이 모든 것을 닫고 유효한 JSON 블롭을 디스크에 쓰기 전에 조기에 종료되는 경우 특히 문제가 되는데, 아이러니하게도 바로 이때 디버그 로그가 가장 절실히 필요할 때가 많습니다.
이것이 바로 우리가 __JSONL (JSON Lines)__를 사용하는 이유입니다. 이름 그대로 모든 줄이 유효하고 독립적인 JSON 객체인 텍스트 형식입니다. 표준 JSON 배열과 달리, 첫 번째 항목을 읽기 위해 문서 전체를 파싱할 필요가 없습니다. 한 줄을 읽고, 파싱한 뒤, 다음으로 넘어가면 됩니다. 이는 거대한 페이로드 (Payload)를 메모리에 버퍼링하거나, 마지막 닫는 괄호 ]가 나타나기를 기다릴 필요가 없음을 의미합니다.
자식 프로세스가 메모리 부족으로 인해 영원히 도착하지 않을 수도 있는 상황 말입니다.
실제 사례는 다음과 같습니다:
Stripped: authorization, cf-access-token, host
Added: cf-aig-authorization: Bearer <API_KEY>
cf-aig-metadata: {"userId": "<anonymous-uuid>"}
장시간 실행되는 프로세스로부터 구조화된 출력 (Structured Output)을 파싱해야 하는 모든 CI 시스템은 결국 JSONL과 같은 방식에 도달하게 됩니다. 하지만 저희는 바퀴를 새로 발명하고 싶지는 않았습니다. (그리고 OpenCode는 이미 이를 지원합니다!)
스트리밍 파이프라인 (The streaming pipeline)
저희는 코디네이터 (Coordinator)의 출력을 실시간으로 처리하지만, 느리지만 고통스러운 appendFileSync로 인한 디스크의 죽음을 방지하기 위해 매 100행(또는 50ms)마다 버퍼링 및 플러시 (Flush)를 수행합니다.
스트림이 흘러 들어옴에 따라 특정 트리거를 감시하며 관련 데이터를 추출합니다. 예를 들어, 비용을 추적하기 위해 step_finish 이벤트에서 토큰 사용량 (Token usage)을 추출하고, 재시도 로직 (Retry logic)을 시작하기 위해 error 이벤트를 사용합니다. 또한 출력 잘림 (Output truncation) 현상도 주의 깊게 살핍니다. 만약 step_finish가 reason: "length"와 함께 도착한다면, 모델이 max_tokens 제한에 도달하여 문장 중간에 끊겼음을 의미하므로 자동으로 재시도해야 합니다.
저희가 예측하지 못했던 운영상의 골칫거리 중 하나는 Claude Opus 4.7 또는 GPT-5.4와 같은 크고 고급화된 모델들이 때때로 문제를 생각하는 데 꽤 오랜 시간을 보낼 수 있다는 점이었으며, 이는 사용자들에게 작업이 멈춘 것처럼 보일 수 있습니다. 저희는 사용자들이 작업이 실제로 백그라운드에서 열심히 돌아가고 있음에도 불구하고, 리뷰어가 의도대로 작동하지 않는다고 불평하며 빈번하게 작업을 취소한다는 사실을 발견했습니다. 이를 해결하기 위해 저희는 30초마다 "Model is thinking... (Ns since last output)"를 출력하는 매우 간단한 하트비트 로그 (Heartbeat log)를 추가했으며, 이로 인해 문제가 거의 완전히 해결되었습니다.
하나의 거대한 프롬프트 대신 전문화된 에이전트 (Specialised agents)
하나의 모델에게 모든 것을 리뷰하도록 요청하는 대신, 리뷰를 도메인별 에이전트 (Domain-specific agents)로 분할했습니다. 각 에이전트는 무엇을 찾아야 하는지, 그리고 더 중요하게는 무엇을 무시해야 하는지를 정확히 알려주는 엄격하게 범위가 지정된 프롬프트 (Tightly scoped prompt)를 가집니다.
예를 들어, 보안 리뷰어(Security reviewer)는 오직 "공격 가능하거나 구체적으로 위험한(exploitable or concretely dangerous)" 문제만을 표시하도록 명시적인 지침을 받습니다:
## 표시할 항목 (What to Flag)
- 인젝션 취약점 (Injection vulnerabilities) (SQL, XSS, command, path traversal)
- 변경된 코드에서의 인증/인가 우회 (Authentication/authorisation bypasses)
...
LLM에게 무엇을 하지 말아야 하는지를 알려주는 것이 실제 프롬프트 엔지니어링 (Prompt engineering)의 가치가 존재하는 지점임이 밝혀졌습니다. 이러한 경계가 없다면, 개발자들이 즉시 무시하는 법을 배우게 될 추측성이고 이론적인 경고들이 폭포수처럼 쏟아지게 됩니다.
모든 리뷰어는 심각도 분류(Severity classification)가 포함된 구조화된 XML 형식으로 결과물을 생성합니다: critical (장애를 유발하거나 공격 가능한 경우), warning (측정 가능한 회귀 또는 구체적인 위험), 또는 suggestion (고려할 가치가 있는 개선 사항).
이를 통해 우리는 단순한 권고 텍스트를 파싱하는 대신, 후속 동작을 유도하는 구조화된 데이터(Structured data)를 다루게 됩니다.
우리가 사용하는 모델들
리뷰를 전문화된 도메인으로 나누었기 때문에, 모든 작업에 매우 비싸고 성능이 뛰어난 모델을 사용할 필요는 없습니다. 우리는 에이전트(Agent)의 작업 복잡도에 따라 모델을 할당합니다:
최상위 계층 (Top-tier): Claude Opus 4.7 및 GPT-5.4: 리뷰 코디네이터(Review Coordinator) 전용으로 예약되어 있습니다. 코디네이터는 다른 7개 모델의 출력을 읽고, 결과물을 중복 제거하며, 오탐(False positives)을 필터링하고, 최종 판단을 내리는 가장 어려운 작업을 수행합니다. 따라서 사용 가능한 가장 높은 추론 능력 (Reasoning capability)이 필요합니다.
표준 계층 (Standard-tier): Claude Sonnet 4.6 및 GPT-5.3 Codex: 주요 작업을 수행하는 서브 리뷰어들(코드 품질, 보안, 성능)을 위한 핵심 동력입니다. 이 모델들은 빠르고 상대적으로 저렴하며, 코드 내의 로직 오류와 취약점을 찾아내는 데 탁월합니다.
Kimi K2.5: 문서 리뷰어(Documentation Reviewer), 릴리스 리뷰어(Release Reviewer), AGENTS.md 리뷰어와 같이 가볍고 텍스트 비중이 높은 작업에 사용됩니다.
이것들은 기본 설정이지만, 모든 개별 모델 할당은 런타임(Runtime) 중에 reviewer-config를 통해 동적으로 재정의될 수 있습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 HN AI Posts의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기