LLM 코드 리뷰 자동화: PR 품질을 높이는 CI 파이프라인 구축

AI

LLM 코드 리뷰GitHub ActionsCI 자동화AI 코드 리뷰개발 생산성

이 글은 누구를 위한 것인가

  • PR 리뷰 병목으로 개발 속도가 느려진 팀
  • 코드 리뷰의 일관성과 품질을 높이고 싶은 팀 리더
  • LLM을 활용한 개발 도구 자동화에 관심 있는 DevOps 엔지니어

들어가며

코드 리뷰는 품질 게이트이지만 동시에 병목이다. 팀원이 바쁘면 PR이 며칠씩 대기하고, 리뷰를 받아도 리뷰어마다 기준이 달라 일관성이 없다.

LLM 기반 자동 리뷰는 이 문제를 해결한다. 24시간 즉시 피드백, 일관된 기준, 사람 리뷰어가 놓치기 쉬운 패턴 감지. 물론 LLM이 비즈니스 로직이나 아키텍처 판단을 대체할 수는 없지만, 명백한 버그, 보안 취약점, 코드 스타일 문제는 자동으로 잡을 수 있다.

이 글은 bluefoxdev.kr의 GitHub Actions CI/CD 자동화 가이드 를 참고하고, LLM 코드 리뷰 자동화 관점에서 확장하여 작성했습니다.


1. 아키텍처 설계

[LLM 코드 리뷰 CI 파이프라인]

PR 오픈/업데이트
      ↓
GitHub Actions 트리거
      ↓
변경된 파일 diff 추출
      ↓
파일별 LLM 리뷰 요청 (병렬)
      ↓
리뷰 결과 집계
      ↓
GitHub PR에 리뷰 코멘트 자동 포스팅
      ↓
심각도별 라벨 자동 부여
(critical/warning/suggestion)

2. GitHub Actions 워크플로우

# .github/workflows/ai-code-review.yml
name: AI Code Review

on:
  pull_request:
    types: [opened, synchronize]
    # 특정 파일 변경 시에만 실행
    paths:
      - 'src/**/*.ts'
      - 'src/**/*.tsx'
      - 'server/**/*.py'

jobs:
  ai-review:
    runs-on: ubuntu-latest
    permissions:
      pull-requests: write
      contents: read

    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          fetch-depth: 0  # 전체 히스토리 필요

      - name: Setup Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.12'

      - name: Install dependencies
        run: pip install anthropic PyGithub

      - name: Run AI Code Review
        env:
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          PR_NUMBER: ${{ github.event.pull_request.number }}
          BASE_SHA: ${{ github.event.pull_request.base.sha }}
          HEAD_SHA: ${{ github.event.pull_request.head.sha }}
        run: python .github/scripts/ai_review.py

3. 리뷰 스크립트 구현

# .github/scripts/ai_review.py
import os
import subprocess
import json
from github import Github
import anthropic

REVIEW_SYSTEM_PROMPT = """당신은 시니어 소프트웨어 엔지니어입니다.
코드 diff를 분석하여 다음 관점에서만 리뷰합니다:

1. 버그 및 논리 오류 (critical)
2. 보안 취약점 - SQL 인젝션, XSS, 인증 누락 (critical)  
3. 성능 문제 - N+1 쿼리, 불필요한 루프 (warning)
4. 에러 처리 누락 (warning)
5. 코드 품질 개선 (suggestion)

규칙:
- 명확한 문제만 지적합니다. 의견이나 스타일 선호는 제외합니다.
- 각 문제는 severity: critical/warning/suggestion 태그를 붙입니다.
- 문제가 없으면 "LGTM" 한 줄만 출력합니다.
- 줄 번호를 명시합니다.
- 한국어로 작성합니다.

출력 형식:
[severity] 줄 N: 문제 설명
수정 제안: 코드 예시"""

def get_changed_files() -> list[dict]:
    base = os.environ['BASE_SHA']
    head = os.environ['HEAD_SHA']
    
    result = subprocess.run(
        ['git', 'diff', '--name-only', f'{base}...{head}'],
        capture_output=True, text=True
    )
    
    files = []
    for filepath in result.stdout.strip().split('\n'):
        if not filepath:
            continue
        
        # 리뷰 대상 파일 필터링
        if not any(filepath.endswith(ext) for ext in ['.ts', '.tsx', '.py', '.go']):
            continue
        
        # 삭제된 파일 제외
        if not os.path.exists(filepath):
            continue
        
        diff_result = subprocess.run(
            ['git', 'diff', f'{base}...{head}', '--', filepath],
            capture_output=True, text=True
        )
        
        if diff_result.stdout:
            files.append({
                'path': filepath,
                'diff': diff_result.stdout,
            })
    
    return files


def review_file(client: anthropic.Anthropic, file: dict) -> dict:
    # 너무 큰 diff는 앞부분만
    diff = file['diff'][:8000]
    
    response = client.messages.create(
        model='claude-opus-4-7',
        max_tokens=1500,
        system=REVIEW_SYSTEM_PROMPT,
        messages=[{
            'role': 'user',
            'content': f"파일: {file['path']}\n\n```diff\n{diff}\n```"
        }]
    )
    
    review_text = response.content[0].text
    
    # 심각도 파악
    severity = 'suggestion'
    if '[critical]' in review_text.lower():
        severity = 'critical'
    elif '[warning]' in review_text.lower():
        severity = 'warning'
    
    return {
        'path': file['path'],
        'review': review_text,
        'severity': severity,
        'lgtm': review_text.strip() == 'LGTM',
    }


def post_review(reviews: list[dict]) -> None:
    g = Github(os.environ['GITHUB_TOKEN'])
    
    # repo 정보
    repo_name = os.environ.get('GITHUB_REPOSITORY')
    pr_number = int(os.environ['PR_NUMBER'])
    
    repo = g.get_repo(repo_name)
    pr = repo.get_pull(pr_number)
    
    # 리뷰 요약 생성
    critical_count = sum(1 for r in reviews if r['severity'] == 'critical')
    warning_count = sum(1 for r in reviews if r['severity'] == 'warning')
    lgtm_count = sum(1 for r in reviews if r['lgtm'])
    
    summary = f"""## AI 코드 리뷰 결과

| 구분 | 수 |
|------|---|
| 🔴 Critical | {critical_count} |
| 🟡 Warning | {warning_count} |
| ✅ LGTM | {lgtm_count}/{len(reviews)} |

> 이 리뷰는 AI가 자동으로 생성했습니다. 최종 판단은 사람 리뷰어가 합니다.

---
"""
    
    # 파일별 리뷰 추가
    for review in reviews:
        if not review['lgtm']:
            severity_icon = {'critical': '🔴', 'warning': '🟡', 'suggestion': '🔵'}.get(review['severity'], '⚪')
            summary += f"\n### {severity_icon} `{review['path']}`\n\n{review['review']}\n\n---\n"
    
    # PR 코멘트 포스팅
    pr.create_issue_comment(summary)
    
    # 라벨 추가
    if critical_count > 0:
        pr.add_to_labels('ai-review: critical')
    elif warning_count > 0:
        pr.add_to_labels('ai-review: warning')
    else:
        pr.add_to_labels('ai-review: approved')


def main():
    client = anthropic.Anthropic(api_key=os.environ['ANTHROPIC_API_KEY'])
    
    changed_files = get_changed_files()
    print(f"리뷰 대상 파일: {len(changed_files)}개")
    
    if not changed_files:
        print("리뷰할 파일 없음")
        return
    
    reviews = []
    for file in changed_files:
        print(f"리뷰 중: {file['path']}")
        review = review_file(client, file)
        reviews.append(review)
    
    post_review(reviews)
    print("리뷰 완료")


if __name__ == '__main__':
    main()

4. 리뷰 품질을 높이는 프롬프트 전략

4.1 컨텍스트 추가

def build_context_aware_prompt(file: dict, repo_context: str) -> str:
    return f"""프로젝트 컨텍스트:
{repo_context}

파일: {file['path']}
언어: {detect_language(file['path'])}

변경 내용:
```diff
{file['diff']}

이 변경사항에서 실제로 존재하는 문제만 지적하세요. 리팩토링 제안이나 스타일 선호는 제외합니다."""

repo_context 예시:

"TypeScript + NestJS + PostgreSQL 백엔드

Zod로 입력 검증, Prisma ORM 사용

Jest 테스트 커버리지 80% 목표"


### 4.2 Few-shot 예시로 일관성 확보

```python
FEW_SHOT_EXAMPLES = """
예시 1 - Critical 버그:
[critical] 줄 42: users 배열이 비어있을 때 users[0]에 접근하면 TypeError가 발생합니다.
수정 제안: if (!users.length) return null;을 먼저 체크하세요.

예시 2 - Security:
[critical] 줄 78: SQL 쿼리에 사용자 입력이 직접 삽입되어 SQL 인젝션 취약점이 있습니다.
수정 제안: Prisma의 파라미터 바인딩을 사용하세요: where: { id: userId }

예시 3 - LGTM:
LGTM
"""

5. 비용 최적화 전략

[파일당 평균 LLM 비용 추정]
claude-opus-4-7: 입력 $3/1M + 출력 $15/1M 토큰

diff 500줄 = ~2,000 입력 토큰 + 리뷰 500 출력 토큰
파일당 약 $0.014

PR 10개 파일 = $0.14
월 PR 100개 = $14 (매우 저렴)

비용 절감 추가 전략:

def should_review_file(filepath: str, diff: str) -> bool:
    # 자동 생성 파일 제외
    if any(filepath.endswith(x) for x in [
        '.lock', '.min.js', '.generated.ts', 'schema.prisma'
    ]):
        return False

    # 너무 작은 변경 제외 (공백, 주석만)
    meaningful_lines = [
        l for l in diff.split('\n')
        if l.startswith(('+', '-')) and not l.startswith(('+++', '---'))
        and l.strip() not in ('+', '-', '')
    ]
    if len(meaningful_lines) < 3:
        return False

    return True

6. 팀 적용 가이드

[점진적 도입 전략]

1단계: 알림 전용 (2주)
   → 리뷰 결과를 Slack으로만 전달, PR에 코멘트 없음
   → 팀이 리뷰 품질 평가

2단계: PR 코멘트 시작 (2주)
   → "AI 제안" 라벨로 구분, 의무사항 아님
   → 팀 피드백 수집

3단계: Critical 자동 블로킹 (선택)
   → Critical 이슈 발견 시 merge 차단
   → 팀 합의 후 적용
# Critical 이슈 시 merge 차단 (선택적)
# .github/workflows/ai-code-review.yml 추가
- name: Block on Critical Issues
  if: ${{ env.HAS_CRITICAL == 'true' }}
  run: |
    echo "Critical 이슈가 발견되었습니다. 리뷰 후 merge 해주세요."
    exit 1

마무리

LLM 코드 리뷰 자동화의 핵심은 사람 리뷰어를 대체하는 것이 아니라 보완하는 것이다. LLM이 명백한 버그, 보안 이슈, 에러 처리 누락을 빠르게 잡아주면 사람 리뷰어는 아키텍처, 비즈니스 로직, 코드 설계에 집중할 수 있다.

월 $10~20 수준의 LLM 비용으로 PR 병목을 줄이고 코드 품질을 높일 수 있다면 대부분의 팀에서 투자 대비 효과가 크다.