개발·프레임워크

BUILD / 5번째 글

AI 코딩 운용 원칙과 리뷰 자동화

AI 코딩 도구를 쓸 때 지킬 다섯 원칙과 프롬프트 패턴에서 출발해, 생성된 코드를 사람이 검증하는 법과 그 검증을 Claude API·GitHub Actions 리뷰봇으로 자동화하는 법, 팀 도입과 측정까지 한 줄기로 정리한다.

PALDYN Team52 MIN READ

지난 글에서 Aider가 AI의 편집을 커밋 단위로 Git 히스토리에 남기는 방식과 설계 모델·편집 모델을 나누는 Architect 모드를 봤다. Copilot, Cursor, Claude Code, Codex, Aider까지 다섯 도구를 차례로 훑었으니 이제 도구가 무엇이든 공통으로 적용되는 운용 원칙을 정리할 차례다. 좋은 도구를 쥐고도 잘못 쓰면 코드 품질이 떨어지고 보안 구멍이 생기며 시간이 새어 나간다.

이 글의 줄기는 하나다. AI가 만든 코드는 반드시 검증한다는 원칙이 있고, 그 검증을 처음에는 사람이 손으로 하다가 나중에는 리뷰봇과 CI로 옮긴다. 앞 절반은 원칙과 프롬프트를, 뒤 절반은 검증을 자동화하는 파이프라인과 그것을 팀에 들이는 법을 다룬다. 둘을 따로 떼어 놓으면 리뷰봇의 프롬프트에 무엇을 넣어야 하는지가 보이지 않는다 — 리뷰봇에 넣는 규칙은 결국 사람이 검증할 때 보던 목록 그대로다.

운용 원칙

AI 코딩 모범 사례 프레임워크

컨텍스트 최적화

AI 도구의 결과물은 넣어 준 컨텍스트의 품질에 비례한다. 여기서 컨텍스트는 모델이 답을 만들 때 볼 수 있는 모든 것이다 — 열려 있는 파일, 커서 주변의 코드, 타입 정의, 주석, 에러 메시지, 그리고 프로젝트 지침 파일. 작업 공간이 정돈되어 있으면 좋은 결과가 나오고 어지러우면 어지러운 결과가 나온다.

가장 값싼 개선은 함수 서명이다. def process(data): pass만 주면 모델은 data가 무엇인지 추측해야 하고, 추측이 틀리면 그럴듯하지만 엉뚱한 구현이 나온다. 타입 힌트와 목적을 적은 독스트링을 먼저 쓰고 본문을 비워 두면 모델이 채울 자리가 정확해진다.

from datetime import datetime
from typing import NamedTuple

class Order(NamedTuple):
    id: int
    customer_id: int
    amount: float
    status: str
    created_at: datetime

def process_daily_orders(orders: list[Order], date: datetime) -> dict[str, float]:
    """특정 날짜의 주문을 처리하고 상태별 합계를 반환
    - completed: 완료된 주문 금액 합계
    - refunded: 환불된 주문 금액 합계
    """
    ...

이 정도면 반환 딕셔너리의 키가 무엇이어야 하는지, 날짜 비교를 어느 필드로 해야 하는지까지 모델이 읽어 낸다. 예시 코드 하나, 실제 에러 메시지 한 줄, 관련 파일을 열어 두는 것도 같은 축의 일이다. 모델이 추측해야 하는 것을 하나씩 없애는 작업이라고 보면 된다.

프로젝트 수준의 컨텍스트는 매번 적지 않는다. CLAUDE.md나 Cursor의 .cursor/rules 같은 지침 파일에 한 번 적어 두면 그 저장소의 모든 세션에 자동으로 실린다 — 언어 버전, 테스트 명령, 금지 패턴, 커밋 규칙 같은 것이다. 뒤의 팀 도입 절에서 이 파일이 다시 나온다.

작업 분해와 반복 개선

복잡한 기능을 한 번에 요청하면 실패 확률이 높다. "결제 시스템 전체를 구현해줘"는 요구사항이 수십 개인데 한 문장이고, 모델은 그 수십 개를 제멋대로 정해 채운다. 결과가 틀렸을 때 어디가 틀렸는지 찾는 데 드는 시간이 처음부터 나눠 요청하는 시간보다 길다.

나누는 단위는 검증 가능한 한 걸음이다. 결제라면 Payment 모델 정의, 저장소 CRUD, 외부 결제 API를 부르는 서비스, POST /payments 엔드포인트, 그리고 단계마다의 단위 테스트 — 다섯 걸음이다. 걸음마다 완료 기준을 말로 적고, 중간 결과를 읽고, 테스트를 돌린 뒤 다음 걸음으로 간다. 걸음이 작으면 되돌릴 지점도 촘촘해서 세 번째 걸음이 틀렸을 때 두 번째까지는 남는다.

반복 개선은 같은 원리를 한 걸음 안에서 쓰는 것이다. 첫 결과물을 그대로 쓰는 것은 AI 코딩의 잠재력을 10%도 못 쓰는 일이다. 사용자 검색 API를 예로 들면, 첫 요청으로 뼈대를 받은 뒤 「이름 검색에 LIKE를 썼는데 한국어 풀텍스트 검색으로 바꿔줘」, 「페이지네이션 추가해줘」, 「결과를 Redis에 5분 캐시해줘」, 「느린 쿼리에 인덱스 추가해줘」로 이어 간다. 피드백마다 결과를 확인하고 다음 피드백을 낸다. 처음부터 완벽한 요구사항을 쓰려는 부담이 없어지고, 대신 각 단계에서 무엇이 달라졌는지가 눈에 보인다.

두 원칙이 합쳐지면 프롬프트가 짧아진다. 긴 프롬프트 하나가 아니라 짧은 프롬프트 열 개이고, 열 개 사이마다 사람이 읽는 자리가 있다. 그 자리가 곧 뒤에서 다룰 검증이다.

보안과 윤리

AI 코딩 도구를 쓸 때 간과하기 쉬운 구멍이 프롬프트 자체에 있다. 실제 API 키, 비밀번호, 개인정보를 프롬프트에 넣는 것이다. STRIPE_SECRET_KEY='sk-live-...'로 결제 코드 작성해줘라고 적는 순간 그 키는 도구의 로그, 세션 기록, 경우에 따라 학습 데이터 파이프라인까지 흘러갈 수 있는 곳에 놓인다. 고객의 이름과 주민등록번호를 예시 데이터로 붙이는 것도 같은 일이다.

고치는 법은 단순하다. 실제 값 대신 자리표시자를 쓴다. 「os.environ['STRIPE_SECRET_KEY']로 결제 API를 호출하는 코드를 써줘」라고 하면 모델은 환경 변수를 읽는 코드를 만들고 키는 어디에도 남지 않는다. 예시 데이터는 지어낸 값으로 만들고, 실제 로그를 붙여야 하면 식별자를 가린 뒤 붙인다.

윤리 쪽에는 세 가지가 더 있다. 생성된 코드가 어디서 온 것인지 모르니 라이선스를 확인한다 — 유명한 라이브러리의 코드를 통째로 재현한 결과가 나올 수 있다. 팀 안팎에 AI가 만든 코드임을 공시한다 — 뒤의 AI 표시 절에서 다룬다. 그리고 도구가 제공하는 프라이버시 모드나 코드 유출 방지 설정을 켜 두어 회사 코드가 밖으로 나가는 경로를 줄인다.

검증 원칙

다섯 원칙 중 넷을 봤다. 남은 하나가 코드 검증이고, 이 글의 나머지가 전부 그것이다. 굳이 떼어 둔 이유는 나머지 넷이 검증에 기대고 있어서다. 컨텍스트를 잘 주면 검증에서 잡을 것이 줄고, 작업을 나누면 검증할 단위가 작아지며, 반복 개선의 매 단계가 검증이고, 보안 원칙은 검증 항목의 첫 줄이다.

거꾸로 검증을 빼면 넷이 무너진다. 컨텍스트가 좋아도 모델은 틀리고, 작은 단위로 나눴어도 읽지 않으면 틀린 채 쌓이며, 반복 개선은 무엇을 개선할지 모르니 멈춘다. 그래서 검증 원칙에 「절대 생략 불가」라는 꼬리표가 붙어 있었다. 이 원칙을 어기는 순간 AI 코딩은 생산성 도구가 아니라 버그 공장이 된다.

프레임워크 그림의 아래쪽에는 도구 선택 원칙이 붙어 있다. 인라인 자동완성은 Copilot, 대규모 멀티파일 편집은 Cursor, 에이전트와 CI 통합은 Claude Code, Git 히스토리와 로컬 모델은 Aider, 일회성 질문과 설명은 채팅 UI 쪽이다. 지난 다섯 편의 요약이고, 원칙 다섯은 그중 무엇을 골라도 같다.

프롬프트 패턴

효과적인 AI 코딩 프롬프트 패턴

구현 요청 템플릿

원칙을 프롬프트 한 장에 담으면 다섯 칸짜리 틀이 된다. 목적, 기술 스택, 요구사항, 예시 입출력, 제약사항이다.

## 기능: 이메일 인증

**목적**: 가입 시 이메일 소유를 확인한다

**기술 스택**: FastAPI, SQLAlchemy 2.0, Redis(토큰 TTL)

**요구사항**:
1. POST /auth/send-verification — 토큰을 만들어 메일 발송
2. GET /auth/verify?token=xxx — 토큰 검증 후 계정 활성화
3. 토큰은 24시간 뒤 만료, 만료 토큰은 400

**예시 입출력**:
- 입력: `{"email": "hong@example.com"}`
- 출력: `{"sent": true}`

**제약사항**: 타입 힌트 필수, 한국어 주석, 에러는 HTTPException

칸마다 하는 일이 다르다. 목적은 모델이 요구사항에 안 적힌 결정을 내려야 할 때 기준이 된다 — 「이메일 소유 확인」이 목적이면 토큰을 URL에 노출하는 설계와 본문에 담는 설계 중 무엇을 고를지 모델이 목적에 맞춰 정한다. 기술 스택은 버전까지 적는다. SQLAlchemy 1.4와 2.0은 쿼리 문법이 다르고, 버전을 빼면 모델이 학습 데이터에서 더 많이 본 쪽을 고른다. 요구사항은 번호를 붙여 하나씩 적고 엣지 케이스를 한 줄 넣는다. 예시 입출력은 요구사항의 모호함을 없애는 가장 짧은 방법이다 — 문장 세 줄보다 JSON 두 줄이 정확하다. 제약사항은 팀 규칙이고, 지침 파일에 있으면 생략해도 된다.

디버깅과 리팩토링 요청

디버깅 요청은 칸이 넷이다. 환경, 에러 메시지, 관련 코드, 기대 동작. 이 중 사람들이 가장 자주 빼먹는 것이 에러 메시지 원문과 기대 동작이다. 「안 돼요」 대신 sqlalchemy.exc.IntegrityError: UNIQUE constraint failed: users.email을 그대로 붙이고, 「중복 이메일이면 400을 돌려줘야 한다」를 함께 적는다. 에러 메시지는 모델이 원인을 좁히는 단서이고 기대 동작은 고친 결과가 맞는지 판정하는 기준이다. 둘 중 하나가 없으면 모델은 에러를 없애는 데만 집중해 예외를 삼키는 코드를 만들 수 있다 — 에러는 사라지고 버그는 남는다.

리팩토링 요청은 목표를 측정 가능한 말로 적는 것이 전부다. 「깔끔하게 해줘」는 목표가 아니다. 순환 복잡도를 함수당 10 아래로, 90% 같은 get_user와 get_admin의 중복 제거, 중첩 if 최대 2단계, 시간 복잡도는 현재 O(n2)O(n^2) 이하로 유지 — 이렇게 적으면 결과가 목표를 채웠는지 사람이 셀 수 있다. 결과물로는 세 가지를 요구한다. 리팩토링된 코드, 무엇을 왜 바꿨는지의 설명, 그리고 기존 테스트가 통과하는지 확인이다. 세 번째가 없으면 리팩토링이 동작을 바꿨는지 알 길이 없다.

프롬프트 안티패턴

그림의 아래에 적힌 세 가지는 각각 원칙 하나씩을 어긴다. 「코드 만들어줘」는 컨텍스트가 없고, 「전체 시스템 설계해줘」는 분해가 없으며, 「완벽하게 만들어줘」는 기준이 없어 반복 개선을 시작할 수 없다. 좋은 프롬프트는 목적, 기술 스택, 구체적 요구사항, 제약사항에 예시를 더한 것이고, 이 다섯 중 셋 이상이 빠져 있으면 그 프롬프트는 다시 쓰는 편이 빠르다.

또 하나는 부정 지시만 있는 프롬프트다. 「for 문 쓰지 마」보다 「리스트 컴프리헨션으로 써」가 낫고, 「print 쓰지 마」보다 「logging 모듈의 logger.info로 써」가 낫다. 무엇을 하지 말라는 말은 대안이 여럿 남아 모델이 그중 엉뚱한 것을 고를 수 있지만, 무엇을 하라는 말은 답이 하나다.

사람의 검증

이해의 기준

「AI가 만든 코드는 반드시 읽고 이해해야 한다」에서 이해의 기준을 정해 두지 않으면 원칙이 빈말이 된다. 기준은 하나다 — 그 코드를 남에게 설명할 수 있는가. 이 함수가 왜 이 순서로 호출되는지, 이 예외를 왜 여기서 잡는지, 이 캐시 키가 왜 이렇게 생겼는지를 리뷰어에게 말로 설명할 수 없으면 아직 검증하지 않은 것이다. 코드를 이해하지 못하면 검증할 수 없고, 검증하지 않은 코드를 배포하면 장애가 난다. 여기서 「이해」는 「돌아간다」와 다르다. 테스트가 통과하는 것은 이해의 필요조건이지 충분조건이 아니다.

읽을 때 보는 자리는 정해져 있다. 첫째, 경계다. 빈 리스트, 0, 음수, None, 아주 큰 값에서 무슨 일이 나는지 코드를 눈으로 따라간다. 모델은 정상 경로를 잘 만들고 경계에서 자주 틀린다. 둘째, 부수 효과다. 파일을 쓰는지, 외부 API를 부르는지, 전역 상태를 바꾸는지를 찾는다. 셋째, 가정이다. 모델이 존재한다고 가정한 함수나 필드가 실제로 있는지 — 없는 메서드를 그럴듯하게 부르는 일이 드물지 않다. 넷째, 성능이다. 반복문 안의 쿼리, 매번 새로 여는 연결, 전체 목록을 메모리에 올리는 정렬이 그 자리다.

보안 안티패턴

보안은 눈으로 읽는 것보다 목록으로 훑는 것이 빠르고 빠짐이 없다. 생성 코드에서 자주 보이는 패턴은 여섯 가지쯤 된다.

패턴 위험
eval(, exec( 코드 인젝션 — 입력 문자열이 곧 실행된다
subprocess.run(..., shell=True) 셸 인젝션 — 인자에 ;가 들어오면 다른 명령이 붙는다
password = "...", api_key = "..." 비밀값 하드코딩 — 커밋되는 순간 히스토리에 영구히 남는다
md5(, sha1( 취약한 해시 — 비밀번호 저장에 쓰면 안 된다
문자열 결합으로 만든 SQL SQL 인젝션
입력을 그대로 HTML에 넣는 템플릿 XSS

여기서 인젝션(injection)은 데이터 자리에 들어온 문자열이 코드로 해석되는 것을 통칭하는 말이다. SQL이면 SQL 인젝션, 셸이면 셸 인젝션이다. 모델이 이런 코드를 만드는 이유는 악의가 아니라 학습 데이터에 그런 예제가 많아서다 — 튜토리얼은 편의를 위해 shell=True를 쓰고 비밀번호를 변수에 박아 둔다.

목록을 손으로 훑는 것은 출발점이고, 실제로는 도구에 맡긴다. 정적 분석(static analysis)은 코드를 실행하지 않고 소스만 읽어 패턴을 찾는 검사이고, 파이썬이면 bandit, 여러 언어를 함께 쓰면 semgrep이 이 표의 항목을 대부분 잡는다. 여기에 린터와 타입 체커를 더하면 문법·스타일·타입 오류까지 사람이 읽기 전에 걸러진다. 이 도구들을 CI에 두면 검증의 첫 관문이 자동으로 선다 — 뒤의 리뷰봇 절에서 「자동 검사」 단계가 바로 이것이다.

코드 리뷰 요청

사람의 검증을 돕는 데도 AI를 쓸 수 있다. 코드를 만든 세션과 별개로 리뷰만 시키는 프롬프트를 던지는 것이다. 관점을 넷으로 나누고 심각도를 셋으로 나눠 달라고 한다.

## 코드 리뷰 요청

다음 코드를 리뷰해줘: [코드]

관점: 보안(인젝션·인증 누락) / 성능(N+1·불필요한 루프·누수) /
가독성(이름·함수 크기·주석) / 테스트 가능성(의존성 주입·모킹)

심각도: 🔴 Critical 반드시 수정 · 🟡 Warning 개선 권장 · 🔵 Info 선택

관점을 나누는 이유는 모델이 한 번에 한 렌즈만 잘 쓰기 때문이다. 「리뷰해줘」라고만 하면 가장 눈에 띄는 것 서너 개를 말하고 끝나는데, 관점 넷을 주면 넷을 각각 돈다. 심각도를 나누는 이유는 사람이 읽을 순서를 정하기 위해서다 — Critical부터 읽고 Info는 시간이 남으면 본다. 이 세 딱지가 그대로 다음 절의 리뷰봇 시스템 프롬프트로 넘어간다. 사람이 손으로 던지던 프롬프트를 PR이 열릴 때마다 자동으로 던지게 만드는 것이 리뷰봇이다.

N+1 쿼리는 목록을 한 번 가져온 뒤 항목마다 다시 쿼리를 날려 쿼리 수가 항목 수에 비례하는 패턴이다. ORM을 쓸 때 가장 흔한 성능 문제이고 모델이 특히 자주 만든다 — 코드만 보면 깔끔하게 읽히기 때문이다.

사람 검증의 한계

사람의 검증에도 한계가 있고, 그 한계가 자동화의 이유다. 첫째, 피로다. 하루에 PR 열 개를 같은 집중력으로 읽을 수 없고, 열 번째 PR의 shell=True는 눈에 안 들어온다. 둘째, 일관성이다. 같은 코드를 월요일의 나와 금요일의 나가 다르게 판정하고, 리뷰어 둘이면 기준이 둘이다. 셋째, 범위다. 사람은 diff만 보고 프로젝트 전체의 규칙을 매번 떠올리지 못한다.

기계가 잘하는 것이 정확히 이 셋이다. 지치지 않고, 같은 기준을 적용하고, 규칙 목록 전체를 매번 대조한다. 반대로 기계가 못하는 것 — 이 변경이 비즈니스에 맞는가, 아키텍처가 이 방향으로 가도 되는가, 팀이 이런 코드를 원하는가 — 은 사람에게 남는다. 다음 절은 이 분업을 파이프라인으로 세운다.

리뷰봇 구축

AI 코드 리뷰 프로세스

리뷰 파이프라인

앞 절에서 사람 검증의 한계로 피로·일관성·범위 셋을 봤다. 팀 단위로 보면 여기에 하나가 더 붙는다 — 처리량이다. 리뷰어가 바쁘면 PR이 쌓이고, 쌓인 PR은 서로 충돌을 만들며, 충돌을 푸는 동안 리뷰는 더 밀린다. 앞의 셋이 리뷰의 품질이 흔들리는 문제였다면 이것은 리뷰가 아예 시작되지 않는 문제다.

리뷰봇은 이 넷을 한꺼번에 겨냥한다. PR이 열리는 순간 첫 리뷰가 붙으므로 대기가 사라지고, 프롬프트가 곧 기준이므로 판정이 요일에 따라 달라지지 않으며, 반복 지적을 기계가 맡으니 사람의 집중력이 로직에 남는다. 그림의 파이프라인이 그 배치다 — PR 생성, 자동 검사(린팅·타입 체크·단위 테스트), AI 리뷰(보안·성능·로직), 사람 리뷰(비즈니스 로직·최종 승인).

네 단계를 늘어놓은 순서 자체가 설계다. 자동 검사가 AI 리뷰보다 앞에 있는 이유는 싼 검사를 먼저 돌리기 위해서다. 앞 절에서 본 bandit·semgrep과 린터·타입 체커가 이 자리에 서고, 이들이 잡을 것을 모델에 물어보면 토큰만 쓴다. AI 리뷰가 사람 리뷰보다 앞에 있는 이유는 사람이 읽기 시작할 때 기계적 지적이 이미 정리되어 있게 하려는 것이다. 그리고 사람이 마지막인 이유는 다음 표가 설명한다.

역할 분담

검토 항목 AI 사람
보안 취약점 패턴 맡는다 보완
코딩 컨벤션 맡는다 —
기본 에러 처리 맡는다 —
N+1 쿼리 패턴 맡는다 —
비즈니스 로직 타당성 보조 맡는다
아키텍처 적합성 보조 맡는다
팀 문화·맥락 — 맡는다
최종 승인 책임 — 맡는다

표의 위 넷은 패턴이고 아래 넷은 판단이다. 패턴은 코드 안에 답이 있어서 diff만으로 판정할 수 있고, 판단은 코드 밖의 것 — 요구사항, 로드맵, 팀의 취향 — 을 알아야 한다. 모델에게 「아키텍처 적합성」을 물으면 그럴듯한 답을 하지만 그 답은 일반론이지 이 팀의 답이 아니다. 그래서 「보조」다 — 사람이 판단할 재료를 정리해 줄 수는 있어도 판단을 대신하지는 못한다.

마지막 줄은 규칙이 아니라 선언이다. 최종 승인 책임은 사람에게 있다. AI 리뷰가 통과시켰다는 것은 면책 사유가 안 되고, 그래서 AI 리뷰가 아무리 좋아져도 사람 리뷰 단계를 없애지 않는다. 뒤에 나올 「Critical만 CI를 막는다」는 규칙도 같은 선 위에 있다 — 기계는 막을 수 있지만 통과시키지는 못한다.

리뷰 스크립트와 CI

리뷰봇의 뼈대는 스크립트 하나다. diff를 얻고, 시스템 프롬프트에 리뷰 형식을 적어 모델에 넘기고, 결과를 파일로 남기고, Critical이 있으면 비정상 종료한다.

import subprocess, sys
import anthropic

SYSTEM = """당신은 시니어 소프트웨어 엔지니어다. 코드 변경을 리뷰해 다음 형식으로 보고한다.
## 요약
## 이슈
- 🔴 Critical: [파일:라인] 문제와 수정 방법
- 🟡 Warning: [파일:라인] 문제와 개선 방법
- 🔵 Info: [파일:라인] 제안
## 잘된 점
{context}"""

def pr_diff(base: str = "main") -> str:
    return subprocess.run(["git", "diff", f"{base}...HEAD"],
                          capture_output=True, text=True).stdout

def review(diff: str, context: str = "") -> str:
    client = anthropic.Anthropic()
    msg = client.messages.create(
        model="claude-opus-5",
        max_tokens=16000,
        system=SYSTEM.format(context=context),
        messages=[{"role": "user",
                   "content": f"다음 diff를 리뷰해줘.\n\n<diff>\n{diff}\n</diff>"}],
    )
    return "".join(b.text for b in msg.content if b.type == "text")

if __name__ == "__main__":
    diff = pr_diff()
    if not diff:
        sys.exit(0)
    out = review(diff)
    open("review_output.txt", "w").write(out)
    print(out)
    sys.exit(1 if "🔴" in out else 0)

짧지만 결정이 넷 들어 있다. git diff main...HEAD의 점 세 개는 머지 베이스(두 브랜치가 갈라진 지점)부터의 변경만 잡는다는 뜻이다. 점 두 개면 main의 최신 커밋과 비교하므로 남이 main에 올린 변경이 내 PR의 diff에 섞인다. 시스템 프롬프트의 {context} 자리는 다음 절에서 팀 규칙이 들어갈 곳이다. 출력을 표준 출력에만 찍지 않고 파일로 남기는 이유는 다음 단계가 그 파일을 읽어 PR 코멘트로 올리기 때문이다. 그리고 종료 코드 — 🔴가 있으면 1을 돌려 CI를 붉게 만든다. Warning과 Info는 종료 코드에 영향을 주지 않는다.

이 스크립트를 PR마다 돌리는 것이 GitHub Actions의 일이다.

# .github/workflows/ai-code-review.yml
name: AI Code Review
on:
  pull_request:
    types: [opened, synchronize]
    paths: ['**.py', '**.ts', '**.go']

jobs:
  ai-review:
    runs-on: ubuntu-latest
    permissions:
      pull-requests: write
      contents: read
    steps:
      - uses: actions/checkout@v7
        with:
          fetch-depth: 0
      - uses: actions/setup-python@v7
        with:
          python-version: '3.12'
      - run: pip install anthropic
      - env:
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
        run: python scripts/ai_review.py
      - if: always()
        uses: actions/github-script@v9
        with:
          script: |
            const body = require('fs').readFileSync('review_output.txt', 'utf8');
            await github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner, repo: context.repo.repo,
              body: `## 🤖 AI 코드 리뷰\n\n${body}\n\n> [AI Generated] 이 리뷰는 자동 생성되었습니다.`,
            });

AI 코드 리뷰 자동화

여기에도 함정이 셋 있다. fetch-depth: 0이 없으면 체크아웃이 얕은 복제(최근 커밋 하나만 받는 것)라 머지 베이스가 없어 git diff main...HEAD가 빈 문자열을 돌려주고, 스크립트는 「변경 없음」으로 조용히 통과한다. permissions의 pull-requests: write가 없으면 코멘트 단계가 권한 오류로 죽는다. 그리고 코멘트 단계의 if: always()가 없으면 스크립트가 종료 코드 1로 끝난 PR — 정작 리뷰가 가장 필요한 PR — 에 코멘트가 안 달린다. paths 필터는 문서만 고친 PR에 토큰을 쓰지 않으려는 장치이고, synchronize는 PR에 커밋이 추가될 때마다 다시 돌리라는 뜻이다.

API 키는 저장소 시크릿에 두고 환경 변수로만 넘긴다. 첫 절의 보안 원칙이 여기서도 그대로다. 코멘트 끝의 [AI Generated] 꼬리표는 그림의 운영 팁에도 적혀 있듯 리뷰어가 이것이 기계의 의견임을 알고 읽게 하려는 것이다. 사람이 쓴 코멘트와 섞이면 기계의 오판이 사람의 권위를 얻는다.

Claude Code의 리뷰 명령

스크립트를 직접 세우지 않아도 리뷰를 받는 길이 있다. Claude Code는 현재 브랜치의 변경을 리뷰하는 슬래시 명령을 갖고 있고 파일 경로나 브랜치를 인자로 받는다. 무엇보다 -p 옵션으로 표준 입력을 받으므로 diff를 파이프로 넣을 수도 있다.

claude "/code-review"
claude "/code-review app/services/payment.py"
git diff HEAD~1 | claude -p "이 변경의 보안·성능 이슈를 분석해줘"

첫 줄은 현재 브랜치의 변경 전체를, 둘째 줄은 경로 하나만 리뷰한다. 셋째 줄은 위 스크립트의 핵심을 한 줄로 줄인 것이다. 로컬에서 커밋 전에 한 번 돌리는 용도로는 이것으로 충분하고, PR마다 자동으로 돌리려면 위의 Actions가 필요하다. 둘의 관계는 「내가 던지는 리뷰 요청」과 「PR이 던지는 리뷰 요청」이다 — 프롬프트는 같고 트리거만 다르다. 손으로 만든 스크립트 대신 벤더가 제공하는 GitHub Action을 쓰는 길도 있고, 그림의 워크플로가 그 예다. 어느 쪽이든 다음 절의 운영 규칙은 같다.

리뷰봇 운영

팀 규칙 주입

리뷰봇의 품질은 프롬프트의 품질이다. 「시니어 엔지니어처럼 리뷰해」만으로는 일반론이 나오고, 일반론은 절반이 이 팀에 안 맞는다. 시스템 프롬프트의 {context} 자리에 팀 규칙을 넣는 것이 첫 번째 튜닝이다. 규칙은 세 묶음으로 갈린다.

묶음 예
필수 확인 항목 보안(SQL 인젝션·XSS·CSRF·인증 누락), DB(N+1·인덱스 미사용·트랜잭션 누락), 에러(예외 처리 누락·빈 catch), 비밀값 하드코딩
팀 컨벤션 PEP 8과 타입 힌트, 한국어 주석, RESTful 설계와 알맞은 HTTP 상태 코드, 새 기능에는 테스트
금지 패턴 print() 대신 logging, SELECT * 금지, 3단계 넘는 중첩 if 금지

첫 묶음은 앞 절의 보안 안티패턴 표와 거의 같다 — 사람이 훑던 목록이 그대로 기계의 목록이 된다. 둘째와 셋째는 이 팀만의 것이다. 이 두 묶음이 없으면 리뷰봇은 「타입 힌트가 없다」를 지적하지 않는다. 모델은 이 팀이 타입 힌트를 필수로 한다는 것을 모른다.

두 번째 튜닝은 도메인 지식 주입이다. 「결제 금액은 항상 양수여야 한다」, 「관리자만 삭제할 수 있다」, 「주문 상태는 created, paid, shipped 순서로만 바뀐다」 같은 비즈니스 규칙을 프롬프트에 적으면 리뷰 품질이 눈에 띄게 오른다. 이 규칙들은 코드 어디에도 안 적혀 있어서 모델이 diff만 보고는 알 수 없는 것들이고, 그래서 역할 분담 표에서 「비즈니스 로직」이 사람 몫이었다. 규칙을 열 줄 적어 주면 그중 일부가 기계 몫으로 넘어온다. 적을수록 사람 리뷰가 가벼워진다.

지침 파일과의 관계도 정리해 둔다. CLAUDE.md에 적은 팀 규칙은 코드를 만들 때 쓰이고, 리뷰봇의 시스템 프롬프트는 코드를 검사할 때 쓰인다. 둘이 다르면 만들 때 허용한 것을 검사할 때 잡는 일이 생기므로, 규칙의 원본을 한 곳에 두고 리뷰 스크립트가 그 파일을 읽어 {context}에 넣는 편이 낫다.

거짓 경보 관리

거짓 경보(false positive)는 문제가 아닌 것을 문제라고 보고하는 것이다. 리뷰봇이 경고를 너무 많이 내면 개발자는 읽지 않게 되고, 읽지 않으면 진짜 경고도 묻힌다. 경고 스무 개 중 열여덟 개가 취향 문제면 나머지 둘의 SQL 인젝션도 스크롤에 밀려 지나간다. 리뷰봇이 실패하는 가장 흔한 방식이 정확도가 아니라 신뢰 상실이다.

그래서 처음에는 Critical만 켠다. 보안과 비밀값처럼 반박의 여지가 없는 항목만 보고하게 하고 Warning과 Info는 프롬프트에서 뺀다. 두어 주 돌리며 팀의 피드백을 받는다 — 「이건 우리 팀에서는 괜찮은 패턴이다」가 나오면 프롬프트에 예외를 적고, 「이건 잡아 줬으면 했다」가 나오면 항목을 더한다. 신뢰가 쌓인 뒤 Warning을, 그다음 Info를 연다. 그림의 운영 팁이 「팀 피드백으로 프롬프트 지속 개선」이라 적은 것이 이 되풀이다.

CI를 막는 것도 Critical뿐이다. Warning으로 빌드를 붉게 만들면 개발자는 경고를 고치는 대신 리뷰봇을 끄는 법을 찾는다. 막는 것은 좁게, 알리는 것은 넓게가 원칙이고, 넓히는 속도는 팀이 읽어 주는 속도에 맞춘다.

diff 크기 제한

diff가 1,000줄을 넘으면 리뷰 품질이 떨어진다. 모델의 컨텍스트에 들어가지 않아서가 아니다 — 들어가긴 한다. 문제는 긴 diff에서 모델이 앞부분과 뒷부분을 함께 놓고 보는 힘이 약해지는 것이고, 사람이 큰 PR을 대충 읽는 것과 같은 이유다. 파일 A의 변경과 파일 F의 변경이 서로 모순되는 것을 잡으려면 둘을 동시에 들고 있어야 하는데, 사이에 3,000줄이 끼면 그 연결이 끊긴다.

대응은 두 갈래다. 하나는 큰 PR을 작게 쪼개도록 유도하는 것이다 — 리뷰봇이 「이 PR은 1,000줄을 넘어 리뷰 정확도가 떨어집니다. 나눌 수 있으면 나눠 주세요」를 첫 줄에 적게 한다. 작업 분해 원칙이 PR 크기에서 다시 나오는 셈이다. 다른 하나는 파일별로 나눠 리뷰하는 것이다. 파일마다 따로 요청을 보내면 파일 안의 문제는 잘 잡히지만 파일 사이의 문제는 놓치므로, 두 방식을 섞어 쓴다 — 파일별 리뷰를 기본으로 하고, 파일 사이 관계는 사람이 본다.

어느 쪽이든 자르는 것보다 나누는 것이 낫다. 처음 세운 스크립트에는 4,000줄이 넘으면 뒤를 잘라 내는 처리가 있었는데, 잘린 뒤쪽에 무엇이 있는지 모델도 사람도 모른다는 점에서 위험하다. 잘라야 한다면 「잘랐다」를 리뷰 첫 줄에 크게 적어 사람이 뒷부분을 직접 보게 한다.

비용 계산

리뷰봇의 비용은 셈이 단순하다. 요청 하나의 비용은 입력 토큰 수에 입력 단가를, 출력 토큰 수에 출력 단가를 곱해 더한 것이다.

cost=Tin⋅pin+Tout⋅pout106\text{cost} = \frac{T_{in} \cdot p_{in} + T_{out} \cdot p_{out}}{10^6}

TinT_{in} 은 diff와 시스템 프롬프트를 합친 입력 토큰 수, ToutT_{out} 은 리뷰 출력 토큰 수이고, 단가 pp 는 100만 토큰당 달러다. 토큰 수는 영문 코드 기준으로 대략 4자에 1토큰으로 어림한다. 1,000줄 diff가 4만 자쯤이면 1만 토큰이고, 시스템 프롬프트 1,000토큰을 더해 Tin≈11,000T_{in} \approx 11{,}000, 리뷰 출력을 넉넉히 Tout≈2,000T_{out} \approx 2{,}000 으로 잡는다.

예컨대 입력 100만 토큰에 5달러, 출력 100만 토큰에 25달러인 모델이면 한 번의 리뷰가 0.055+0.05≈0.110.055 + 0.05 \approx 0.11 달러다. 하루 PR 스무 개에 커밋이 추가될 때마다 다시 돌아 하루 마흔 번이라면 월 20영업일에 약 85달러다. 입출력 단가가 모두 절반인 한 급 아래 모델로 내리면 비용도 절반이 된다. 단가는 모델과 시기마다 다르므로 스크립트에 박아 두지 말고 가격표를 보고 그때그때 대입한다. 정확한 토큰 수가 필요하면 어림 대신 API의 토큰 세기 엔드포인트를 쓴다.

리뷰 한 번의 비용

셈에서 보이는 것이 있다. 비용의 절반이 출력에서 나온다. 출력 토큰이 입력의 5분의 1인데 단가가 다섯 배라서다. 그래서 거짓 경보를 줄이는 것은 신뢰의 문제이자 비용의 문제다 — Info 열 개를 안 쓰게 하면 출력이 줄고, 출력이 줄면 비용이 준다. 그리고 paths 필터로 문서 PR을 거르는 것, 자동 검사가 실패한 PR에는 리뷰를 안 돌리는 것도 같은 방향의 절약이다. 린터가 실패한 PR을 모델에 넘기면 린터가 잡을 것을 토큰을 써서 다시 잡는다.

팀 도입과 측정

지침 파일

개인이 아니라 팀이 도구를 들일 때 첫 일은 지침 파일을 저장소에 넣는 것이다. 첫 절에서 본 CLAUDE.md나 .cursor/rules를 개인 홈 디렉터리가 아니라 저장소 루트에 두고 커밋한다. 그러면 팀원 모두의 AI가 같은 규칙을 읽고, 규칙이 바뀌면 PR로 바뀌며, 누가 언제 왜 바꿨는지 히스토리에 남는다. 개인 설정으로 두면 열 사람의 AI가 열 가지 스타일로 코드를 만들고, 리뷰봇이 그 차이를 전부 지적하게 된다.

지침 파일에 넣을 것은 리뷰봇 프롬프트의 세 묶음과 같다 — 필수 확인 항목, 팀 컨벤션, 금지 패턴. 여기에 「테스트 명령은 npm test」, 「커밋 메시지는 한국어로」처럼 도구가 알아야 움직일 수 있는 것을 더한다. 처음부터 완벽하게 쓰려 하지 말고, 리뷰봇이 반복해서 지적하는 것을 지침 파일에 옮기는 식으로 키운다. 리뷰봇이 같은 것을 세 번 지적했다면 그것은 리뷰 항목이 아니라 생성 규칙이어야 한다.

프롬프트도 공유 자산이다. 앞 절의 구현·디버깅·리팩토링 템플릿과 팀에서 잘 먹힌 프롬프트를 한 파일에 모아 두면 새로 온 사람이 첫 주에 쓸 수 있다. 성공한 패턴을 기록해 두는 것이 프레임워크 그림의 「반복 개선」 칸에 적힌 「프롬프트 라이브러리」다.

PR의 AI 표시

AI가 만든 코드임을 PR 설명에 적는다. 리뷰어가 더 신중하게 읽도록 하려는 것이고, 첫 절의 공시 원칙이기도 하다.

## AI 지원 작업 여부
- [x] 이 PR의 일부는 AI 도구(Claude Code)로 생성했다
- AI 생성 비율: 약 60%
- 직접 검토한 사항: 보안 로직, DB 쿼리 최적화

세 줄 중 셋째 줄이 실제로 쓸모 있는 줄이다. 「직접 검토한 사항」은 작성자가 어디를 읽었는지의 선언이고, 리뷰어는 그 밖의 곳을 먼저 본다. 작성자가 보안 로직을 봤다고 적었으면 리뷰어는 에러 처리와 경계값으로 간다. 이 줄이 비어 있으면 검증 원칙이 지켜지지 않았다는 뜻이므로 리뷰어가 그 자리에서 되물을 수 있다. AI 생성 비율은 정확할 필요가 없다 — 「거의 전부」와 「일부」를 가르는 정도면 되고, 그 차이가 리뷰의 밀도를 정한다.

이 표시는 벌이 아니다. 「AI로 만들었다」가 감점 요소가 되면 사람들은 표시를 안 하고, 표시가 없으면 리뷰어는 무엇을 더 볼지 모른다. 표시가 정보로만 쓰인다는 것을 팀이 믿어야 이 칸이 채워진다.

도입 효과 지표

도입 효과는 재지 않으면 모른다. 측정하지 않으면 「빨라진 것 같다」와 「버그가 는 것 같다」가 같은 무게로 싸운다. 다섯 지표를 도입 전후로 비교한다.

지표 무엇을 재는가 어디서 얻는가
PR 사이클 타임 PR 생성부터 병합까지 걸린 시간 Git 호스팅의 PR 데이터
버그 발생률 AI 생성 코드에서 나온 버그 수 ÷ 변경량 이슈 트래커, PR 서술의 AI 표시
리뷰 소요 시간 리뷰어가 PR 하나에 쓴 시간 첫 코멘트까지의 시간, 리뷰 세션 수
개발자 만족도 도구가 일을 쉽게 했는가 분기 설문
보안 이슈 수 SAST 도구가 잡은 이슈 수 CI의 정적 분석 리포트

지표 다섯이 서로를 견제한다. 사이클 타임만 보면 리뷰를 대충 하는 팀이 이기고, 버그율만 보면 아무것도 안 바꾸는 팀이 이긴다. 사이클 타임이 줄면서 버그율과 보안 이슈가 늘지 않아야 도입이 성공한 것이고, 셋 중 하나가 어긋나면 어느 원칙이 안 지켜지는지를 되짚는다 — 버그율이 올랐으면 검증이, 보안 이슈가 올랐으면 보안 원칙이 새는 것이다. 버그율의 분모에 「AI 생성 코드」를 따로 세우려면 앞 절의 PR 표시가 있어야 한다. 그 표시가 데이터의 출처다.

한 가지 더, 리뷰봇 자체의 지표를 둔다. 리뷰봇이 낸 Critical 중 개발자가 실제로 고친 비율이다. 이 비율이 낮으면 거짓 경보가 많다는 뜻이고 프롬프트를 좁힐 때다. 반대로 사람 리뷰어가 잡은 것 중 리뷰봇이 놓친 패턴을 세어 두면 다음에 프롬프트에 넣을 항목이 된다.

생산성 배율기

AI 코딩 도구를 처음 쓰는 개발자가 흔히 빠지는 함정은 「AI가 다 만들어 줄 것」이라는 기대다. 현실은 다르다. AI 코딩 도구는 생산성 배율기(productivity multiplier)다. 곱하는 것이지 더하는 것이 아니어서, 실력 있는 개발자가 쓰면 훨씬 더 효과적이고 검증할 줄 모르는 사람이 쓰면 틀린 코드가 빠르게 쌓인다. 곱하는 수가 0이면 결과도 0이다.

이 글의 모든 규칙이 그 관점에서 나온다. 컨텍스트를 주는 것도, 작업을 나누는 것도, 리뷰봇에 팀 규칙을 넣는 것도 결국 사람이 아는 것을 도구에 옮기는 일이다. 도구는 그 지식을 대체하지 않고 그 지식이 코드가 되는 속도를 올린다. 그래서 리뷰봇을 아무리 잘 세워도 역할 분담 표의 마지막 줄은 사람에게 남는다.

지금까지는 AI로 코드를 만들고 검사하는 쪽이었다. 다음 글부터는 방향이 바뀐다 — AI를 부품으로 넣은 서비스를 만드는 쪽이다. 첫 주제는 가장 흔한 형태인 챗봇 서비스로, 대화 이력을 어떻게 들고 다니고, 문서 검색을 어떻게 붙이고, 응답을 스트리밍으로 흘리며, 안전 필터를 어디에 두는지를 아키텍처부터 배포까지 본다. 이 글에서 세운 리뷰봇도 결국 API를 부르고 결과를 후처리하는 작은 서비스였다 — 그 뼈대가 그대로 커진다.


읽어주셔서 감사합니다. 😊

LATEST

개발·프레임워크의 최신 글

개발·프레임워크2026.05.28

Google Gemini SDK 활용 가이드

google-genai 패키지의 Client 하나로 Gemini API를 부르는 법 — 응답 객체와 finish_reason, 생성 설정과 구조화 출력, 인라인 데이터와 Files API, 도구 호출 왕복, 안전 설정과 재시도, 대화 비용과 컨텍스트 캐싱까지 정리한다.

26 MIN
개발·프레임워크2026.05.28

OpenAI SDK 완전 정복

Python openai 패키지로 OpenAI API를 다루는 법 — 클라이언트 설정과 재시도, 메시지와 응답 구조, Responses API 대응, 모델 고르는 축, 도구 호출 루프, 구조화 출력, 임베딩·이미지 입력, 토큰 비용과 한도까지 정리

30 MIN