
PR Sentinel 구축하기 - 멀티 에이전트 AI 코드 리뷰어
요약
멀티 에이전트 시스템을 활용하여 코드 리뷰를 자동화하는 'PR Sentinel' 구축 과정을 소개합니다. 보안, 성능, 품질을 담당하는 세 개의 독립적인 에이전트를 LangGraph로 구현하여 리뷰의 정확도를 높이고 지연 시간을 단축했습니다.
핵심 포인트
- 단일 에이전트의 환각 문제를 해결하기 위해 역할을 분리한 멀티 에이전트 구조 채택
- Security, Performance, Quality를 담당하는 세 가지 특화 에이전트 설계
- LangGraph와 asyncio.gather를 활용한 병렬 실행으로 지연 시간 60% 이상 단축
- LLM API 선택 및 에이전트 간 협업 아키텍처 구축의 실무적 경험 공유
석사 과정을 시작했을 때, 저는 실제 코드 리뷰(Code Review)가 그리워진다는 사실을 꽤 빨리 깨달았습니다. 당신이 미처 확인하지 못한 것들을 누군가가 실제로 잡아내 주는 그런 종류의 리뷰 말입니다.
이전 회사에서는 잘못 명명된 변수, 규모가 커지면 터져버릴 쿼리, 확인하는 것을 잊어버린 접근 제어(Access Control)와 같은 어처구니없는 실수들을 잡아내 줄 누군가가 항상 있었습니다. 하지만 대학원에서 과제 프로젝트를 만들고 개인 사이드 프로젝트를 진행할 때는, 그 안전망이... 그냥 사라져 버렸습니다. 오직 저와 제 코드뿐이었죠.
AI와 RAG(Retrieval-Augmented Generation)에 대한 열풍이 불고 있는 것을 보고, 저는 생각했습니다. '내가 직접 만들어보는 건 어떨까?' 그렇게 PR Sentinel이 시작되었습니다. 누군가 내 뒤에서 코드를 살펴봐 주던 환경이 그리워 직접 만든, 다소 이기적인 도구입니다.
이것을 구축하기 위해 실제로 어떤 과정이 필요했는지 소개합니다.
아키텍처: 세 개의 에이전트, 하나의 리뷰
첫 번째 중요한 결정은 '하나의 AI 에이전트를 쓸 것인가, 아니면 여러 개를 쓸 것인가?'였습니다.
저는 단순히 아키텍처 측면의 이점 때문만이 아니라, 여러 개의 에이전트를 사용하는 방식을 택했습니다. LLM(Large Language Models)은 환각(Hallucination)을 일으킵니다. 하나의 모델에게 SQL 인젝션(SQL Injection), N+1 쿼리(N+1 queries), 그리고 함수 이름이 적절한지를 동시에 확인하라고 요청하면, 세 가지 모두를 형편없이 수행할 것입니다. 그래서 저는 작업을 세 개의 집중된 에이전트로 나누었습니다:
- Security Agent (보안 에이전트) — 하드코딩된 비밀 정보, 인젝션 벡터(Injection vectors), 깨진 접근 제어(Broken access controls)
- Performance Agent (성능 에이전트) — N+1 쿼리, 최적화되지 않은 루프, 메모리 누수(Memory leaks)
- Quality Agent (품질 에이전트) — DRY(Don't Repeat Yourself) 위반, 명명 규칙, 모듈성(Modularity)
각 에이전트는 LangGraph를 사용하여 독립적으로 실행되며, asyncio.gather()를 통해 병렬로 실행됩니다. 결과는 최종 리뷰를 조립하는 어그리게이터 노드(Aggregator node)로 전달됩니다. 순차적으로 실행하는 것과 비교했을 때, 이 방식은 전체 지연 시간(Latency)을 60% 이상 단축합니다.
하지만 아키텍처를 결정하는 것은 쉬운 부분이었습니다. 이것이 실제로 '작동'하게 만드는 데는 꽤 오랜 시간이 걸렸습니다.
실제로 무엇이 (많이) 망가졌나
멀티 에이전트 (multi-agent) 기술에 도달하기도 전에, 어떤 LLM API를 사용할지 결정하는 데만 엄청난 시간을 허비했습니다. Claude? Gemini? 모든 API는 서로 다른 설정, 서로 다른 속도 제한 (rate limits), 그리고 서로 다른 특이점 (quirks)을 가지고 있었습니다. 저는 매번 모델을 바꾸고 그때마다 다시 배워야 했습니다.
그다음은 LangGraph 자체였습니다. 그 이면에 있는 사이클 (cycle)을 이해하고, 그래프에 노드 (nodes)를 추가하고, 도구 (tools)를 연결하고, 조건부 엣지 (conditional edges)를 설정하는 등의 작업 말입니다. 저는 진심으로 이 과정에서 여러 번 실수를 저질렀습니다. 문서는 매우 깔끔해 보이지만, 실제로 구축할 때는 전혀 그렇지 않습니다.
제가 예상치 못한 방식으로 처음 망가진 부분은 바로 도구 호출 (tool calling)이 작동하지 않았다는 것이었습니다. 에이전트들은 변경된 파일들을 리뷰하는 것은 잘 해냈지만, 임베디드 벡터 스토어 (embedded vector store)를 사용하여 코드베이스의 나머지 부분을 탐색하지는 못했습니다. 즉, 실제적인 문맥 (context)이 없었다는 뜻입니다. 그저 디프 (diff)에 대한 표면적인 피드백만 제공할 뿐이었습니다.
한동안 이를 알아차리지 못했습니다. 생성된 리뷰들을 직접 주의 깊게 읽어본 후에야 비로소 그들이 전체적인 그림을 놓치고 있다는 사실을 깨달았습니다. 코드를 파헤쳐 보니, 그것은 오타 때문이었습니다. 잘못 입력된 키 (key) 하나가 도구로 하여금 아무런 에러 없이 조용히 아무것도 반환하지 않게 만들었던 것입니다. 에러 메시지도 없었습니다. 그저 아무것도 없었을 뿐입니다.
그때 저는 모든 단계에서 로깅 (logging)을 하는 것이 얼마나 중요한지 배웠습니다. 지나고 나면 당연한 일이지만, 배우는 과정은 고통스러웠습니다.
Thundering Herd: API 폭주 문제 해결하기
에이전트들을 병렬로 실행하기 시작하자 새로운 문제가 나타났습니다. 세 개의 에이전트가 정확히 같은 밀리초(millisecond)에 API 요청을 동시에 날려버렸고, 즉시 Gemini의 속도 제한에 걸려 429 및 503 에러가 발생했습니다.
해결책은 **무작위 지터 (randomized jitter)**를 적용한 지수 백오프 (exponential backoff)였습니다:
import random
import time
...
실제로 중요한 것은 지터 (jitter) 부분입니다. 지터가 없다면 세 에이전트 모두 정확히 동일한 시간만큼 백오프를 하게 되고, 재시도 시점에 다시 충돌하게 됩니다. 무작위 오프셋 (random offset)을 주면 에이전트들의 실행 시점을 적절히 분산시킬 수 있습니다.
그리고 맞습니다. 무료 티어 API 제한은 매우 가혹합니다. 이런 시스템을 구축하고 있다면, 테스트용 PR (Pull Request)로 API를 몰아치기 전에 사용할 모델을 신중하게 선택하십시오.
점진적 RAG (Incremental RAG): 의도치 않은 최적화
에이전트들이 단순히 변경 사항(diff)뿐만 아니라 전체 코드베이스를 이해할 수 있도록, 저는 벡터 데이터베이스 (Vector Database)로 Pinecone을 통합했습니다. 모든 파일은 청크(chunk)로 나뉘고 임베딩(embedding)되어, 에이전트들이 리뷰를 작성하기 전에 관련 코드를 의미론적(semantically)으로 검색할 수 있습니다.
단순한 접근 방식은 모든 PR(Pull Request)마다 전체 리포지토리를 다시 벡터화하는 것이었습니다. 하지만 그런 방식은 사용량 청구서를 보고 인생의 선택을 재고하기 전까지, 약 두 번의 테스트 실행 정도만 버틸 수 있을 것입니다.
그래서 저는 대신 점진적 수집(incremental ingestion) 방식을 구축했습니다. 웹훅(webhook)이 발생하면 백엔드는 files_changed 페이로드를 확인하여, 해당 파일들에 대한 기존 임베딩만 삭제하고 새로운 버전으로 다시 임베딩합니다:
for file_path in changed_file_names:
delete_file_chunks(file_path, namespace)
...
이 방식은 전체 재수집(full re-ingestion)과 비교했을 때 토큰 사용량을 99% 이상 절감하면서도, Pinecone을 리포지토리와 동기화된 상태로 유지해 줍니다.
여기서 몇 주 동안 발견하지 못한 미묘한 버그가 하나 있었습니다. 수집 과정에서 PR의 헤드 브랜치(head branch) 대신 메인 브랜치(main branch)를 읽고 있었던 것입니다. 제 테스트 PR 대부분은 새로운 파일이 아닌 수정된 파일만 포함하고 있었기에, 로그에 명확하게 드러나지 않았습니다. 새로운 파일들은 그저 조용히 건너뛰어지고 있었습니다. 결국 해결했지만, 이는 조건이 딱 맞아떨어져 숨어버릴 때만 나타나는 종류의 버그입니다.
GitHub App 인증: 실제로 설치 가능하게 만들기
초기 버전은 하드코딩된 개인 액세스 토큰(Personal Access Token)을 사용했습니다. 로컬 테스트용으로는 괜찮지만, 다른 사람이 사용하려 한다면 완전히 사용할 수 없는 방식입니다. 사용자들이 자신의 토큰을 직접 넘겨줘야 하기 때문입니다.
저는 적절한 GitHub App 아키텍처로 전환했습니다. 이제 서버는 RSA 개인 키(private key)를 보유합니다. 누군가 앱을 설치하면 GitHub은 installation_id를 보내고, 백엔드는 동적으로 JWT를 서명하여 이를 범위가 제한된(scoped) 60분 유효 액세스 토큰(access token)으로 교환합니다.
사용자는 '설치(Install)'를 클릭하기만 하면 됩니다. 그게 끝입니다. 토큰 설정도, API 키 공유도 필요 없습니다.
이 흐름에 대한 GitHub 문서는... 괜찮은 편입니다. 하지만 공백이 꽤 많아서 예상했던 것보다 이 작업에 더 많은 시간을 소비했습니다. 만약 GitHub App을 처음 구축한다면, 인증 계층(auth layer)을 위해 추가 시간을 따로 확보해 두십시오.
내가 다르게 했을 것들
솔직히 말하자면? 구축하기 전에 더 다양한 RAG (Retrieval-Augmented Generation) 아키텍처를 더 신중하게 탐색했을 것입니다. 현재의 청킹 (chunking) 방식은 미숙합니다. 글자 수 기준으로 나누는 방식은 함수가 중간에 잘릴 수 있음을 의미하며, 이는 검색 품질 (retrieval quality)을 저하시킵니다. 저는 이를 tree-sitter를 사용한 AST (Abstract Syntax Tree) 기반 청킹으로 교체하여, 청크가 항상 완전한 함수 및 클래스와 일치하도록 만들고 싶습니다.
또한, 구축 도중에 제한 사항(limits)에 부딪혀 API를 교체하는 대신, 성능(capability), 속도 제한 (rate limits), 그리고 비용 사이의 균형을 고려하며 어떤 모델이 실제 유스케이스 (use case)에 적합한지 초기에 더 많은 시간을 들여 고민했을 것입니다.
깨달음의 순간
잘못된 API 선택, 소리 없는 오타, 실수로 인한 재-벡터화 (re-vectorizations), 몇 주 동안 지속된 PR 브랜치 버그 등 이 모든 과정을 거친 후, 그 모든 노력이 가치 있게 느껴진 순간이 있었습니다.
테스트 PR을 하나 열었습니다. Pinecone 네임스페이스 (namespace)가 업데이트되었습니다. 에이전트 하나가 작동했습니다. 리뷰 결과가 돌아왔습니다. 작동했습니다.
그리고 비동기 멀티 에이전트 (async multi-agent) 흐름을 연결하여 세 개의 에이전트가 동시에 작동하는 것을 지켜보았습니다.
그 순간이었습니다. 실제로 작동하는 것을 보는 것! 데모로서가 아니라, 원래 의도한 대로 수행하는 실제 시스템으로서 작동하는 것을 보는 순간, 그동안의 모든 고장 난 부분들이 의미 있게 느껴졌습니다.
여러분의 리포지토리 (repos)에서 직접 시도해보고 싶다면, **PR Sentinel GitHub App**을 공개적으로 설치할 수 있습니다. 구현 내용을 자세히 살펴보고 싶다면 소스 코드는 제 GitHub에 있습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기