"버그가 있습니다"에서 "이것이 근본 원인입니다"로 - 내 앱 리뷰 파이프라인에 AI 코드 분석 추가하기
요약
앱 리뷰와 크래시 데이터를 분석하여 버그의 근본 원인과 수정 방안을 제시하는 AI 코드 분석 파이프라인 구축 사례를 소개합니다. PydanticAI 에이전트를 활용해 코드베이스를 탐색하고, 다양한 LLM 백엔드를 플러그인 방식으로 지원하는 아키텍처를 구현했습니다.
핵심 포인트
- PydanticAI를 사용하여 구조화된 출력과 자동 검증 구현
- 리뷰 분류를 넘어 코드의 특정 파일과 라인을 지목하는 자동화
- Grok Build, Claude Code, OpenAI Codex 등 유연한 백엔드 교체 가능
- 수동 디버깅 과정을 줄여 개발 생산성 향상
요약 (TL;DR): 저는 버그와 크래시(Crash)를 분류하는 앱 리뷰 파이프라인을 구축했습니다. 유용하긴 했지만 "무엇이 잘못되었는지"에서 멈춰 있었습니다. 저는 이를 확장하여 "코드의 어디가 문제인지"와 "무엇을 변경해야 하는지"에 답할 수 있도록 만들었습니다. 코드베이스 탐색 도구를 갖춘 PydanticAI 에이전트를 사용했으며, 분석 엔진을 플러그인 방식으로 만들어 Grok Build, Claude Code 또는 OpenAI Codex를 백엔드로 사용할 수 있게 했습니다. 제가 어떻게 이 단계에 도달했는지, 그 과정에서 무엇이 고장 났는지, 그리고 최종 아키텍처가 어떻게 완성되었는지 소개합니다.
시작점
저에게는 앱의 리뷰와 크래시(Crash) 데이터를 모니터링하는 AppPulse라는 CLI 도구가 있습니다. 매일 아침 이 도구는 다음 작업을 수행합니다:
- Google Play / App Store에서 새로운 리뷰를 가져옵니다.
- LLM을 사용하여 각 리뷰를 분류합니다 — 버그(Bug), 크래시(Crash), 기능 요청(Feature request), 성능(Performance), 찬사(Praise).
- 리뷰를 Sentry/Firebase의 크래시 데이터와 상관 분석합니다.
- 저에게 요약본을 보냅니다.
apppulse run
# → "새로운 리뷰 12개: 버그 3개 (심각 1개), 기능 요청 2개, 찬사 7개"
...
이것은 정말 유용했습니다. 사용자들이 무엇에 대해 불평하고 있는지, 어떤 크래시가 가장 많은 사람에게 영향을 미치고 있는지 알 수 있었습니다. 하지만 파이프라인은 분류 단계에서 멈췄습니다. "10MB 이상의 이미지 업로드 시 크래시 발생"이라는 문구를 보았을 때, 저는 여전히 다음 과정을 거쳐야 했습니다:
- IDE에서 프로젝트를 엽니다.
- 업로드 관련 코드를 검색합니다.
- Sentry의 스택 트레이스(Stacktrace)를 대조합니다.
- 리뷰 내용을 실제 코드와 머릿속으로 매핑합니다.
- 무엇을 변경해야 할지 파악합니다.
버그 하나라면 괜찮습니다. 하지만 월요일 아침에 5개의 심각한 버그가 몰려 있다면, 수정을 시작하기도 전에 너무 많은 수동 작업이 필요합니다.
저는 파이프라인이 더 나아가기를 원했습니다. 분류된 리뷰를 가져와서 문제를 일으키는 특정 파일과 라인을 지목하고, 제가 즉시 실행에 옮길 수 있는 브리핑을 제공하기를 원했습니다.
첫 번째 버전: 코드베이스 도구를 갖춘 PydanticAI 에이전트
핵심 아이디어는 간단했습니다. LLM 에이전트에게 읽기 전용(Read-only) 도구를 통해 제 코드베이스에 대한 접근 권한을 주고, 제가 하는 것처럼 버그를 조사하게 하는 것이었습니다.
왜 PydanticAI인가?
LangGraph, LangChain, 그리고 직접 에이전트 루프 (agent loop)를 구현하는 방안을 고려했습니다. PydanticAI가 다음 세 가지 이유로 선택되었습니다:
-
기본적으로 제공되는 구조화된 출력 (Structured output). 저는 분석 결과가 제가 직접 파싱해야 하는 자유 형식의 텍스트가 아니라, 검증된 Pydantic 모델로 돌아오기를 원했습니다. PydanticAI는 LLM의 출력을 스키마 (schema)에 따라 자동으로 검증하며, 결과가 틀릴 경우 수정 지침과 함께 재시도합니다.
-
최소한의 의존성 (Minimal dependency footprint). AppPulse는 가벼운 CLI 도구입니다. 단일 에이전트의 선형 워크플로 (linear workflow)를 위해 LangChain의 방대한 의존성 트리 (dependency tree)를 끌어들이고 싶지 않았습니다.
-
이미 제가 사용하는 LLM 제공업체들을 지원합니다 — OpenAI, Anthropic, Ollama. 새로운 API 키가 필요 없습니다.
출력 모델 (The Output Model)
모든 분석은 검증된 AnalysisBrief를 생성합니다:
class AnalysisBrief(BaseModel):
issue_type: str # bug | crash | feature_request
summary: str # 한 단락 분량의 평가
...
이것은 분석 엔진과 파이프라인의 나머지 부분 사이의 계약 (contract)입니다. 분석이 프로세스 내부 (in-process)에서 실행되든 외부 코딩 에이전트를 통해 실행되든, 항상 동일한 구조를 생성합니다.
네 가지 내장 도구 (Four Built-in Tools)
에이전트는 네 가지 도구를 부여받으며, 이들은 모두 읽기 전용이고 로컬 코드베이스에서만 작동합니다:
@analysis_agent.tool
async def tool_search_code(ctx: RunContext[AnalysisDeps], keyword: str, file_extensions: str = "") -> str:
"""grep/ripgrep 검색 — 관련 코드가 어디에 있는지 찾습니다."""
...
모든 도구는 디렉토리 트래버설 (directory traversal) — 즉, ../../etc/passwd와 같은 장난을 방지하기 위해 code_path를 기준으로 경로를 검증합니다. 에이전트는 탐색할 수는 있지만 프로젝트 루트 (project root)를 절대 벗어날 수 없습니다.
V1의 문제점: 너무 많은 API 호출
첫 번째 버전은 작동했지만, 에이전트 비용이 많이 들었습니다. 가설을 세우기 전까지 디렉토리 목록 나열, 광범위한 용어 검색, 파일 전체 읽기 등 40~50번의 도구 호출 (tool calls)을 수행하곤 했습니다. gpt-4o-mini 요율로는 여전히 저렴한 편이지만, 제가 설정한 API 요청 제한 (API request limit)에 걸려 때로는 출력을 전혀 생성하지 못하기도 했습니다.
최적화 1: 레포 맵 (The Repo Map)
가장 큰 낭비는 에이전트(agent)가 프로젝트 구조를 파악하는 과정에서 발생했습니다. 에이전트는 조사를 시작하기도 전에 어디에 무엇이 있는지 이해하려고 list_files를 재귀적으로 호출하곤 했습니다.
해결책: 사전에 **레포 맵 (repo map)**을 생성하여 시스템 프롬프트 (system prompt)에 포함하는 것입니다. 레포 맵은 정규 표현식 (regex)을 통해 추출된 최상위 심볼 (classes, functions)과 함께 모든 소스 파일의 내용을 압축된 텍스트 개요로 보여줍니다:
src/services/
ImageUploadService.java — ImageUploadService, compress, validateSize, uploadToS3
UserService.java — UserService, getProfile, updatePreferences
...
이를 통해 에이전트는 도구 호출 (tool calls)을 수행하기 전에 일종의 "GPS"를 갖게 됩니다. 디렉토리를 무작정 나열하는 대신, 레포 맵을 읽고 업로드 충돌(crash)을 조사할 때 ImageUploadService.java로 바로 이동할 수 있습니다.
레포 맵 캐싱 (Caching the Repo Map)
레포 맵을 생성한다는 것은 전체 소스 트리 (source tree)를 탐색하고 정의를 확인하기 위해 모든 파일을 읽어야 함을 의미합니다. 중간 규모의 프로젝트(약 500개 파일)의 경우, 약 100~500ms가 소요됩니다. 일회성 작업으로는 괜찮지만, 매 분석마다 이 비용을 지불하고 싶지는 않았습니다.
캐시 전략은 git 기반의 핑거프린트 (fingerprint)를 사용합니다:
def _compute_fingerprint(root: Path) -> str:
# Git 레포지토리: HEAD 커밋 + 수정된 파일 수 (~5ms)
head = subprocess.run(["git", "rev-parse", "HEAD"], ...)
...
캐시 히트 (Cache hit): ~0ms. 캐시 미스 (Cache miss, 새로운 커밋 또는 커밋되지 않은 변경 사항): 재생성. 캐시는 워킹 트리 (working tree)를 변경하는 모든 git 작업 시 자동으로 무효화됩니다.
최적화 2: 점진적 도구 게이팅 (Progressive Tool Gating)
레포 맵이 있어도 에이전트는 때때로 관련 없는 파일을 찾거나, 연관된 패턴을 검색하거나, 엉뚱한 곳에 빠지는 등 영원히 탐색을 계속하곤 했습니다. 익숙한 상황인가요? 새벽 2시에 디버깅할 때 제가 하는 행동과 똑같습니다.
해결책은 에이전트의 도구를 점진적으로 제한하는 깔때기 (funnel) 구조를 도입하는 것입니다:
async def _prepare_tools(ctx: RunContext[AnalysisDeps], tool_defs: list[ToolDefinition]):
tool_call_count = count_tool_calls(ctx.messages)
...
| 단계 (Phase) | 도구 호출 (Tool Calls) | 사용 가능한 도구 (Available Tools) |
|---|---|---|
| 탐색 (Exploration) | 0-12 | 4개 도구 모두 사용 |
| ... |
이를 통해 평균 분석 도구 호출 횟수를 40회 이상에서 8-12회로 줄였으며, 품질 차이는 눈에 띄지 않았습니다. 에이전트가 핵심에 더 빠르게 도달하게 된 것입니다.
더 큰 단계: 플러그형 분석 백엔드 (Pluggable Analysis Backends)
프로세스 내(in-process) PydanticAI 에이전트는 단순한 버그에는 효과적이었습니다. 하지만 복잡한 다중 파일 문제(multi-file issues)를 다루거나 더 깊은 분석을 원할 때는, Grok Build나 Claude Code와 같은 완전한 코딩 에이전트(coding agent)를 코드베이스에 지정하고 구조화된 브리프(structured brief)를 바로 받고 싶다는 생각이 계속 들었습니다.
이 모든 도구는 헤드리스 모드(headless mode)를 지원합니다:
# Grok Build
grok -p "analyze this bug..." --cwd /path/to/code --tools "read_file,grep,list_dir" --yolo
...
과제: 각 에이전트는 출력 방식이 서로 다르며(JSON, 일반 텍스트, 구조화된 대화 등), 저는 최종적으로 검증된 AnalysisBrief가 필요했습니다.
2단계 파이프라인 (The Two-Stage Pipeline)
외부 백엔드를 위한 해결책은 2단계 파이프라인입니다:
1단계: 코딩 에이전트 (Coding Agent) → 원시 마크다운 (Raw Markdown)
2단계: 저렴한 LLM (구조화 도구) → 검증된 AnalysisBrief
1단계에서는 코딩 에이전트가 특정 섹션(요약(Summary), 근본 원인(Root Cause), 영향받는 파일(Affected Files), 제안된 변경 사항(Proposed Changes) 등)을 포함한 마크다운 분석을 생성하도록 프롬프트를 전달하여 실행합니다. 에이전트는 자체 도구를 사용하여 코드베이스를 탐색합니다. 에이전트는 grep, 파일 읽기, 코드 이해에 대한 완전한 권한을 가지고 있기 때문에 제가 만든 4개의 내장 도구보다 이 작업에 훨씬 더 뛰어납니다.
2단계는 단 한 번의 저렴한 gpt-4o-mini 호출로, 마크다운에서 정보를 추출하여 AnalysisBrief Pydantic 모델로 변환합니다. 도구 사용이나 탐색 없이, 오직 구조화된 추출(structured extraction)만 수행합니다. 비용은 1센트의 아주 작은 일부에 불과합니다.
1단계에서 생성된 원시 마크다운은 ~/.apppulse/analyses/raw/에 검사 가능한 아티팩트(artifact)로 저장됩니다. 만약 구조화된 출력이 잘못된 것처럼 보인다면, 코딩 에이전트가 정확히 무엇을 찾아냈는지 확인할 수 있습니다.
백엔드 아키텍처 (The Backend Architecture)
핵심 분석 흐름을 건드리지 않고도 새로운 백엔드 (backend)를 추가할 수 있기를 원했습니다. 이 아키텍처는 전략 패턴 (strategy pattern)을 사용하는 지연 로딩 (lazy-loaded) 레지스트리 (registry)를 사용합니다.
# backends/__init__.py
BACKEND_REGISTRY: dict[str, str] = {
"builtin": "apppulse.code_analysis.backends.builtin:BuiltinBackend",
...
백엔드 (backends)는 지연 로딩 (lazily) 방식으로 임포트됩니다. 예를 들어 Grok을 사용하지 않는다면, grok.py는 절대 로드되지 않습니다. 새로운 백엔드를 추가하는 방법은 세 단계입니다:
-
ExternalAgentBackend를 상속받는 모듈을 생성합니다. -
_build_command()를 구현하여 argv 리스트를 반환합니다. -
BACKEND_REGISTRY에 한 줄을 추가합니다.
다음은 Grok 백엔드의 전체 코드입니다:
class GrokBackend(ExternalAgentBackend):
name = "grok"
display_name = "Grok Build"
...
프롬프트 조립 (prompt assembly), 서브프로세스 관리 (subprocess management), 출력 파싱 (output parsing), 원시 아티팩트 (raw artifact) 저장, 그리고 Stage 2 구조화기 (structurer) 호출을 포함한 나머지 모든 작업은 ExternalAgentBackend 기본 클래스 (base class)에서 처리됩니다. 구체적인 백엔드 (concrete backend)는 자신의 CLI 명령어를 빌드하는 방법만 알고 있습니다.
백엔드 전환하기 (Switching Backends)
config.yaml에서:
code_analysis:
enabled: true
backend: "grok" # "builtin", "claude_code", 또는 "codex"로 교체 가능
...
설정 마법사 (setup wizard)가 사용자의 PATH에 설치된 CLI를 자동으로 감지하여 선택할 수 있게 해줍니다:
Available analysis backends:
1. builtin — Built-in (PydanticAI) — 가볍고 추가 도구가 필요 없음
2. grok — Grok Build — 헤드리스 모드 (headless mode), 읽기 전용 도구 액세스
...
출력 결과의 모습
기본 (builtin) 백엔드를 사용하든 외부 백엔드를 사용하든, 출력은 동일하게 검증된 구조를 가집니다:
$ apppulse analyze 42
Analyzing review #42 against MyApp Android codebase...
...
--output file 옵션을 사용하면 분석 결과를 마크다운 (markdown) 형식으로 저장하며, 하단에는 바로 붙여넣을 수 있는 프롬프트 섹션이 포함됩니다. 이를 코딩 에이전트 (coding agent)에게 직접 전달하여 수정을 구현하게 할 수 있습니다.
주의 사항
-
Config nesting (설정 중첩).
code_analysis블록은 앱 엔트리 (app entry) 아래에 중첩되는 것이 아니라, 설정 YAML의 **최상위 레벨 (top level)**에 위치해야 합니다. 저도 이 실수를 했습니다. 파서 (parser)는 루트 (root)에서code_analysis를 읽기 때문에, 이를apps[]아래에 중첩하면 조용히 무시되고 기본값 (defaults)으로 되돌아갑니다. -
prepare_tools는 매 턴 (every turn) 호출됩니다. 이 함수는 전체 메시지 히스토리 (message history)를 전달받으므로, 도구 호출 (tool calls) 횟수를 세는 작업은 모든 메시지에 대해 O(n)의 복잡도를 가집니다. 25턴 정도라면 무시할 만한 수준이지만, 요청 제한을 제거한다면 매우 긴 대화에서는 느려질 수 있습니다. -
외부 에이전트 (External agent)의 stdout 형식은 다양합니다.
--output-format json옵션을 사용한 Claude Code는 응답을 JSON 객체로 감싸서 출력하지만, Codex는 일반 텍스트 (plain text)를 출력합니다.ExternalAgentBackend._extract_text()메서드가 두 가지를 모두 처리하지만, 새로운 백엔드 (backend)를 추가한다면 해당 백엔드가 실제로 stdout에 무엇을 쓰는지 확인해야 합니다. -
모델 이름 접두사 (Model name prefix)가 중요합니다. PydanticAI는
openai-chat:gpt-4o-mini형식을 원합니다 (단순히gpt-4o-mini가 아님). 접두사가 없으면 지원 중단 경고 (deprecation warning)가 발생하고 잠재적으로 잘못된 프로바이더 (provider)가 선택될 수 있습니다.
다음에 고민하고 있는 것들
이 파이프라인 (pipeline)은 계속 진화하고 있습니다. 현재 고려 중인 사항들은 다음과 같습니다:
-
앱별 백엔드 설정 (Per-app backend config). 현재
code_analysis는 전역적 (global)입니다. 여러 앱을 운영한다면 하나에는 Grok을, 다른 하나에는 내장형 (builtin)을 사용하고 싶을 수 있습니다. 설정 구조가 이를 지원해야 합니다. -
배치 분석 (Batch analysis). 하나씩 처리하는 대신
apppulse analyze --category bug --severity critical명령어를 통해 모든 심각한 버그를 한 번에 분석하는 기능입니다. -
분석 히스토리 (Analysis history). 어떤 분석이 성공적인 수정 (fix)으로 이어졌는지 추적하여, 향후 분석 품질을 향상시키는 피드백 루프 (feedback loop)를 만드는 것입니다.
-
파이프라인 내 자동 분석 (Auto-analyze in pipeline).
apppulse run도중 모든 심각한 버그에 대해 분석을 실행하는 옵션입니다. 이를 통해 아침 요약 (morning digest)에 단순 분류뿐만 아니라 코드 수준의 브리핑 (briefs)이 포함되도록 할 수 있습니다.
요약 (Takeaways)
-
분류부터 시작하고, 그 다음 분석을 추가하세요. 리뷰를 카테고리별(버그 vs 기능 요청 vs 칭찬)로 분류하는 것이 기초입니다. 코드 분석은 그 위에 쌓이는 레이어입니다. 가치 있는 작업이지만, 먼저 양질의 분류가 이루어져야 합니다.
-
도구를 주기 전에 에이전트에게 지도를 제공하세요. 리포 맵 (repo map)은 도구 호출 (tool calls)을 60% 줄였습니다. 코드베이스를 눈먼 상태로 탐색하는 LLM은 방향을 잡는 데 예산의 대부분을 낭비할 것입니다. 구조를 먼저 제공하세요.
-
제한하지 말고 깔때기(Funnel) 구조를 만드세요. 엄격한 요청 제한보다는 점진적인 도구 게이팅 (tool gating)이 더 효과적입니다. 에이전트가 조사 도중에 끊기지 않고, 탐색에서 심층 분석, 그리고 출력으로 자연스럽게 전환되도록 해야 합니다.
-
백엔드를 강한 의존성이 아닌 하나의 전략으로 만드세요. 2단계 파이프라인 (탐색 → 구조화)은 탐색 엔진을 출력 형식으로부터 분리합니다. 오늘은 Grok과 Claude Code를 사용하지만, 내일은 파일을 읽고 마크다운 (markdown)을 작성할 수 있는 어떤 에이전트라도 사용할 수 있습니다.
-
원시 아티팩트 (raw artifacts)를 저장하세요. 외부 에이전트로부터 받은 원시 마크다운은 잘못된 분석을 디버깅할 때 매우 귀중합니다. 구조화된 출력 (structured output)이 잘못되어 보인다면, 원시 아티팩트를 통해 문제가 1단계 (잘못된 탐색)였는지 아니면 2단계 (잘못된 추출)였는지 알 수 있습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기