fos-blog/study
01 / 홈02 / 카테고리03 / 시리즈
01 / 홈02 / 카테고리03 / 시리즈

카테고리

  • AI 페이지로 이동
    • RAG 페이지로 이동
    • agent 페이지로 이동
    • claude-code 페이지로 이동
    • harness 페이지로 이동
    • llm 페이지로 이동
    • ops 페이지로 이동
    • practice 페이지로 이동
  • algorithm 페이지로 이동
    • Alias Method: 가중치 랜덤 선택을 전처리 O(n), 추출 O(1)로 바꾸기
    • Welford's Online Algorithm: 값을 저장하지 않고 평균과 분산 계산하기
  • architecture 페이지로 이동
    • distributed-systems 페이지로 이동
    • domain 페이지로 이동
    • evolution 페이지로 이동
    • patterns 페이지로 이동
  • database 페이지로 이동
    • milvus 페이지로 이동
    • mysql 페이지로 이동
    • opensearch 페이지로 이동
    • qdrant 페이지로 이동
    • redis 페이지로 이동
    • vespa 페이지로 이동
    • DB Connection Pool Saturation과 Thread Pool 격리
    • 커넥션 풀 크기는 얼마나 조정해야 할까?
    • MyBatis 기본기 — XML Mapper, resultMap, 동적 SQL, 운영 패턴 정리
    • MyBatis와 JPA/Hibernate 트레이드오프 — 레거시 백엔드를 다루는 시니어 관점
    • 한국어 형태소 분석기 Nori vs Lindera — OpenSearch에서 Milvus로 갈 때 어휘 검색은 유지되나
    • 정규화와 역정규화: 무결성과 조회 비용 사이의 선택
    • 벡터 DB 5종, 아키텍처는 어떻게 다른가
    • 벡터 DB 5종을 실제로 벤치마크했다 — 같은 recall에서 QPS는 얼마나 갈리나
    • 벡터 DB 어떻게 고를까 — OpenSearch · Milvus · Qdrant · Vespa · pgvector 비교
    • 벡터 DB를 실제로 도입한 사례 — 빅테크 프로덕션
  • devops 페이지로 이동
    • docker 페이지로 이동
    • k8s 페이지로 이동
    • Envoy Proxy
    • Graceful Shutdown
    • 종료 신호는 어디서 멈추는가
  • http 페이지로 이동
    • HTTP Connection Pool
    • HTTPS와 TLS: 핸드셰이크, 인증서, 종료 지점
  • java 페이지로 이동
    • concurrency 페이지로 이동
    • jdbc 페이지로 이동
    • spring 페이지로 이동
    • spring-batch 페이지로 이동
    • testing 페이지로 이동
    • Java 동시성 락 정리 — 커머스 메뉴/프로모션 정책 캐시 갱신 관점
    • JVM 튜닝 실전: 메모리 구조부터 Virtual Threads, GC 튜닝, 프로파일링까지
    • 로그에 traceId 남기기 — MDC 부터 OpenTelemetry 까지
    • Java StampedLock — 읽기 폭주에도 쓰기가 밀리지 않는 락
    • Virtual Thread와 Project Loom
  • javascript 페이지로 이동
    • typescript 페이지로 이동
    • AbortController
    • Async Iterator와 제너레이터
    • CommonJS와 ECMAScript Modules
    • 제너레이터(Generator)
    • Http Client
    • Node 백엔드 운영 패턴 — Streams 백프레셔, pipe/pipeline, 멱등성 vs 분산 락
    • Node.js
    • `setImmediate()`
  • kafka 페이지로 이동
    • Kafka 클러스터 아키텍처: Broker, KRaft Controller와 복제
    • Kafka 기본 개념: Topic, Partition, Offset과 Segment
    • Kafka 실전 설계: 파티션 전략, 컨슈머 그룹, 전달 보장, 재시도, 순서 보장 트레이드오프
    • Kafka 파티션·리밸런스·컨슈머 지연 운영
    • Kafka를 로그로 이해하기: 복제, 재처리, 상태와 데이터 통합
    • Spring Kafka 컨슈머 오프셋 커밋과 트랜잭션 정렬: AckMode, manual ack, 멱등 처리
  • linux 페이지로 이동
    • fsync — 리눅스 파일 동기화 시스템 콜
    • SSH forced command 로 배포 키가 실행할 수 있는 명령 제한하기
  • mlops 페이지로 이동
    • llm-serving 페이지로 이동
    • model-router 페이지로 이동
    • Python CUDA 버전 생태계 — nvidia-smi, nvcc, pip, conda가 다 다른 버전을 말하는 이유
    • GPU 컨테이너의 CUDA 버전 호환성 — nvidia-smi부터 이미지 다이어트까지
    • Kubernetes GPU 노드에서 /run tmpfs가 꽉 차서 Pod가 안 뜰 때
    • GPU·CUDA·MPS 기초 — 자바 백엔드 개발자가 처음 만나는 그림
    • Multi-process GPU 워크로드 — 자바 ThreadPool 사용자가 만나는 모델 차이
    • ML 서비스 성능 분석 워크플로 — 자바 백엔드 트러블슈팅과 다른 점
    • 한 GPU 를 여러 프로세스가 나눠 쓰기 — Time-Slicing 과 MPS
  • network 페이지로 이동
    • Connection reset by peer는 누가 보낸 걸까 — 리버스 프록시 홉마다 TCP 연결은 따로 논다
    • L2(스위치)와 L3(라우터)의 역할 차이
    • L4와 VIP(Virtual IP Address)
    • IP Subnet
  • observability 페이지로 이동
    • Observability 입문: 시니어 백엔드가 장애를 탐지하고 대응하는 방식
    • K8s 위 Spring Boot 앱의 메트릭 수집
  • python 페이지로 이동
    • Python async/await — CompletableFuture·Reactor 와 다른 점, 그리고 blocking I/O 함정
    • Python 의존성 관리 — Java Maven/Gradle 사용자가 만나는 첫 충격
    • FastAPI 기초 — Spring Boot 사용자가 빠르게 익히는 법
    • Java 개발자를 위한 Python 심화 — OOP·데코레이터·컨텍스트 매니저
    • PyTorch 기초 — 텐서, 디바이스, 그리고 모델 로딩이 무거운 이유
    • Java 개발자를 위한 Python 문법 핵심
    • ThreadLocal 에서 contextvars 로 — Python 의 요청 컨텍스트 전파
    • OCR 동작 원리 — Layout · Text · Post-process 3단계
    • Python 서버의 RSS가 줄지 않는 이유: CPython, gc.collect(), malloc_trim
  • rabbitmq 페이지로 이동
    • RabbitMQ Basics — 실전 백엔드 관점에서 정리하는 메시지 브로커 기본기
    • RabbitMQ vs Kafka — 백엔드 메시징 선택 기준과 실전 운영 관점
  • resume 페이지로 이동
    • 경험 및 경력기술서
    • 김병태 경력기술서
    • 김병태 포트폴리오
  • task 페이지로 이동
    • ai-service-team 페이지로 이동
    • nsc-slot 페이지로 이동
    • sb-dev-team 페이지로 이동
    • the-future-company 페이지로 이동
  • thinking 페이지로 이동
    • 자기주도 학습: 질문에서 시작해 글로 검증하기
    • 좋은 일을 넘어 중요한 일을 하는 법
FOS-BLOG · FOOTERall systems normal·v0.1 · 2026.04.27·seoul, kr
Ffos-blog/study

개발 학습 기록을 정리하는 블로그입니다. 공부하면서 기록하고, 기록하면서 다시 배웁니다.

visitors
01site
  • Home↗
  • Posts↗
  • Categories↗
  • Glossary↗
  • About↗
02policy
  • 소개/about
  • 개인정보처리방침/privacy
  • 연락처/contact
03categories
  • AI↗
  • Algorithm↗
  • DB↗
  • DevOps↗
  • Java/Spring↗
  • JS/TS↗
  • React↗
  • Next.js↗
  • System↗
04connect
  • GitHub@jon890↗
  • Source repositoryjon890/fos-study↗
  • RSS feed/rss.xml↗
  • Newsletter매주 1 회 · 한 편의 글→
© 2026 FOS Study. All posts MIT-licensed.
built with·Next.js·Tailwind v4·Geist·Pretendard·oklch
fos-blog/AI/GitHub Actions 로 PR 마다 A…
ai

GitHub Actions 로 PR 마다 AI 코드 리뷰를 자동으로 돌리는 워크플로 설계

PR 이 열리면 AI 가 먼저 리뷰를 달아 주는 워크플로는 만들기는 쉽지만 운영하면 손볼 곳이 계속 나온다. 댓글이 중복으로 쌓이고, 봇이 봇을 다시 불러내고, 프롬프트 파일이 포맷터에 깨지고, 리뷰어가 엉뚱한 파일을 PR 에 올리기도 한다. 이 글은 공개 저장소 nhncloud-cli 의 claude-code-review.yml 과 code-review-...

2026.10.02·7 min read·7 views

PR 이 열리면 AI 가 먼저 리뷰를 달아 주는 워크플로는 만들기는 쉽지만 운영하면 손볼 곳이 계속 나온다. 댓글이 중복으로 쌓이고, 봇이 봇을 다시 불러내고, 프롬프트 파일이 포맷터에 깨지고, 리뷰어가 엉뚱한 파일을 PR 에 올리기도 한다. 이 글은 공개 저장소 nhncloud-cli 의 claude-code-review.yml 과 code-review-prompt.txt 를 운영하며 정리한 설계 기준이다. 두 구현 방식의 선택, 실행 제어, 프롬프트 분리, 게시 방식, 자주 실패하는 곳을 순서대로 다룬다.

구현 방식 두 가지

구분marketplace actionself-hosted CLI
구성anthropics/claude-code-action 을 step 으로 사용runner 에 claude login 해 두고 워크플로가 claude 바이너리를 직접 호출
장점설정이 쉽다action 에 의존하지 않고 모델, 도구, 프롬프트를 모두 제어한다
비용action 이 내부에서 git add -A 같은 작업을 해 임시 파일이 PR 에 섞일 수 있다인증과 도구 차단을 직접 챙겨야 한다

이 글의 설계 기준은 두 방식에 공통으로 적용된다. 차이는 분리한 프롬프트를 모델에 어떻게 전달하느냐뿐이며, 아래 「프롬프트 전달」 절에서 비교한다.

실행 제어

트리거

  • pull_request: [opened] 로 PR 을 열 때 자동으로 리뷰한다.
  • issue_comment: [created] 로 PR 댓글에 /review 가 달리면 다시 리뷰한다. issue_comment 는 일반 이슈 댓글에도 발생하므로 github.event.issue.pull_request != null 로 PR 댓글만 걸러야 한다.

봇과 중복 실행 제어

  • 봇이 만든 이벤트는 제외한다. 사용하는 저장소의 워크플로는 dependabot[bot] 과 claude[bot] 을 명시해 제외한다. 봇 계정이 늘어날 것 같으면 endsWith(github.actor, '[bot]') 로 한꺼번에 제외하는 방식도 있다.
  • concurrency 그룹을 PR 번호로 잡고 cancel-in-progress: true 로 같은 PR 의 이전 실행을 취소한다. 푸시마다 리뷰가 겹쳐 쌓이는 일을 막는다.

권한

contents: read, pull-requests: write, issues: write 를 기본으로 둔다. checks: write 는 Check Run 을 만들 때만 필요하다. 아래 「자주 실패하는 곳」 에서 이유를 설명한다.

진행 표시

/review 로 시작한 실행은 사람이 결과를 기다리고 있다. 시작할 때 댓글에 eyes reaction 을 달고, 끝나면 성공 여부에 따라 +1 이나 -1 reaction 을 단다.

프롬프트 설계

.txt 파일로 분리한다

50줄이 넘는 프롬프트를 YAML 안에 heredoc 으로 넣으면 읽기 어렵다. 그래서 별도 파일로 분리하되 확장자는 .md 가 아니라 .txt 로 둔다. IDE 의 Markdown 포맷터가 *.lock 같은 glob 을 _.lock 으로, _x 같은 식별자를 \_x 로 바꿔 프롬프트를 깨뜨리기 때문이다. 프롬프트는 문서가 아니라 모델에 넣는 평문이다.

변수는 envsubst '$PR_NUMBER $REPO' 처럼 치환할 이름을 명시해 바꾼다. 이름을 주지 않으면 프롬프트 안의 다른 $ 표현이 모두 빈 값으로 바뀐다. 파일은 YAML 밖에 있어서 ${{ }} 식이 평가되지 않으므로 $VAR 형태의 자리표시자를 쓴다.

개방형으로 시작한다

"리뷰 관점 네 가지" 처럼 닫힌 번호 목록을 주면 모델이 그 목록만 체크리스트처럼 따라가 일반 버그를 놓친다. nhncloud-cli 의 프롬프트는 이 점을 반영해 로직, 타입, 안전처럼 일반 코드 리뷰 관점을 먼저 나열하고, 프로젝트 고유 규칙은 AGENTS.md 와 docs/pitfalls/INDEX.md 에서 읽어 오게 한다. 프롬프트에는 고정된 패턴 목록을 새로 만들지 않고, 규칙이 바뀌면 그 문서만 고치게 한다.

심각도를 표시한다

요약 댓글에서 🔴(머지 전에 고쳐야 하는 결함), 🟡(개선 항목), 잘된 점을 구분한다. 이 구분은 게시 후 점검에도 쓰인다. 심각도 표시가 하나도 없는 인라인 댓글은 형식을 어긴 것으로 보고 지울 수 있다.

프롬프트 전달

구분marketplace actionself-hosted CLI
프롬프트사전 step 이 envsubst 로 치환해 $GITHUB_OUTPUT 의 멀티라인 output 으로 넘기고 action 의 prompt: 에 연결한다envsubst ... < prompt.txt | claude ... -p - 로 stdin 에 직접 연결한다
모델과 도구claude_args: '--model opus --allowedTools ... --disallowedTools ...'같은 플래그를 CLI 에 직접 준다

marketplace action 의 멀티라인 output 은 다음 형태다.

yaml
- id: prompt
  env:
    PR_NUMBER: ${{ env.PR_NUMBER }}
    REPO: ${{ github.repository }}
  run: |
    {
      echo 'text<<PROMPT_EOF'
      envsubst '$PR_NUMBER $REPO' < .github/workflows/code-review-prompt.txt
      echo 'PROMPT_EOF'
    } >> "$GITHUB_OUTPUT"

이후 action 에서 prompt: ${{ steps.prompt.outputs.text }} 로 받는다. 구분자(PROMPT_EOF)가 프롬프트 본문에 우연히 등장하면 output 이 거기서 끊기므로, 본문에 나올 수 없는 토큰으로 정한다.

모델 지정

모델을 claude-opus-4-7 같은 고정 태그로 쓰면 모델이 바뀔 때마다 워크플로를 고쳐야 한다. 설치된 CLI 나 action 이 그 태그를 모르면 실행도 실패한다. --model opus 별칭을 쓰면 CLI 가 인식하는 최신 Opus 를 따라가므로 버전이 올라가도 워크플로를 고칠 필요가 없다.

self-hosted CLI 방식에서는 본 실행 전에 claude --model <별칭> --print -p ok 를 한 번 호출해 모델 인식을 확인하면 실패 원인이 모델인지 프롬프트인지 바로 갈린다.

리뷰어 구성

구성장점비용
단일 opus 리뷰어한 에이전트가 타입, 컨벤션, 보안, 구조를 직접 보므로 판정이 일관되고 구성이 단순하다관점을 나누지 않으므로 속도와 토큰 절감은 기대하기 어렵다
병렬 specialist 네 개(sonnet 과 haiku 혼합)관점을 나눠 동시에 돌려 속도와 토큰을 줄일 수 있다결과를 합치는 orchestration 이 복잡하다

dooray-cli 에서 쓰던 병렬 specialist 방식을 nhncloud-cli 로 옮겼다가 단일 opus 리뷰어로 바꿨다. 현재 nhncloud-cli 의 프롬프트는 "서브 에이전트를 만들지 말고 현재 PR 을 직접 검토한다" 로 시작한다. 일관성과 단순함이 속도나 비용보다 중요하면 단일 리뷰어가 맞다. 병렬 구성은 속도와 비용 최적화가 목표일 때 고른다.

게시 방식

요약과 인라인 댓글을 리뷰 하나로 묶는다

초기 구성은 변경된 파일의 줄에 인라인 댓글을 달고, 전체 요약은 gh pr comment 일반 댓글 하나로 따로 올렸다. 현재 워크플로는 POST /repos/{repo}/pulls/{pr}/reviews 를 한 번 호출해 요약(body)과 인라인 발견(comments[])을 리뷰 하나로 올린다. 일반 댓글로 올리면 요약이 리뷰와 분리되기 때문이다.

  • 인라인 발견은 변경된 새 줄을 정확히 특정할 수 있을 때만 쓰고, 위치가 불확실하면 요약 본문의 해당 심각도 절에 적는다.
  • 발견이 없으면 comments 를 빈 배열로 두고 검토한 범위와 잘된 점만 body 에 남긴다.
  • 요청 본문은 mktemp 로 만든 임시 파일에 JSON 으로 쓰고 gh api ... --input 으로 넘긴다. 인자로 직접 넘기면 줄바꿈이 깨진다.

이전 리뷰를 정리한다

같은 PR 에서 다시 실행하면 이전 봇 댓글이 남아 중복된다. 그래서 매 실행 전에 이전 댓글을 정리한다.

  • 일반 댓글과 인라인 댓글은 REST API 로 삭제한다.
  • 제출된 COMMENT 리뷰는 REST 로 삭제할 수 없고 dismiss 도 APPROVED 와 CHANGES_REQUESTED 에만 되므로, GraphQL minimizeComment 로 접는다.
  • 목록은 페이지 단위로 내려오므로 --paginate 로 끝까지 훑어야 100개가 넘는 댓글도 정리된다.

읽기 전용으로 묶는다

Write 와 Edit 를 --disallowedTools 로 막아 리뷰어가 파일을 고치지 못하게 한다. marketplace action 의 wrapper 는 내부에서 git add -A 를 돌리므로, 에이전트가 디스크에 임시 파일을 만들면 그 파일이 PR 브랜치 커밋에 섞여 들어갈 수 있다. 요약 댓글도 파일로 만들지 않고 quoted HEREDOC 으로 표준 입력에만 흘리는 이유다.

자주 실패하는 곳

  • 줄바꿈이 깨진 댓글: --body "...\n..." 는 shell 이 \n 을 두 글자 그대로 전달한다. --body-file - 와 quoted HEREDOC 으로 실제 개행을 넘긴다.
  • 본문에 적은 명령어가 동작함: 댓글 본문의 /review 는 리뷰를 다시 실행시키고, @claude 는 봇 멘션으로 인식되며, #N 은 엉뚱한 이슈로 링크된다. 백틱으로 감싸 평문으로 만든다.
  • issue_comment 실행이 Checks 탭에 보이지 않음: 이 이벤트로 시작한 실행은 PR 의 Checks 탭에 자동으로 연결되지 않는다. head SHA 에 Check Run 을 직접 만들면 표시되지만 Checks API 는 GitHub App 인증을 요구한다. 일반 GITHUB_TOKEN 만 쓰는 구성에서는 Check Run 을 포기하고 reaction 으로 진행을 표시하는 편이 단순하다.
  • 더미 댓글: 모델이 지침을 무시하고 "test" 같은 댓글을 올린 적이 있다. 게시 후 jq 로 길이가 12자 미만이거나 자리표시자이거나 🔴, 🟡 표시가 없는 인라인 댓글을 삭제하는 단계를 if: always() 로 둔다. 프롬프트에 "더미를 올리지 말라" 고 적는 것만으로는 막히지 않았다.
  • 묻혀 버리는 실패: 부가 단계에 || true 를 붙이면 인증 오류가 job 성공 표시 뒤에 가려진다. || echo "::warning::..." 로 Annotations 에 드러내고 부가 작업은 계속 진행하게 한다.
  • 토큰 표기 혼동: github.token 과 secrets.GITHUB_TOKEN 은 같은 값이다. 표기를 바꿔도 권한은 바뀌지 않으므로 401, 403 은 표기가 아니라 호출한 API 와 호스트에서 원인을 찾는다.
  • 모델 태그: CLI 나 action 이 모르는 태그를 쓰면 실행이 실패한다. 위 「모델 지정」 의 별칭을 쓴다.

정리

결정선택
구현 방식설정이 쉬우면 marketplace action, 모델과 도구를 직접 제어해야 하면 self-hosted CLI
프롬프트.txt 파일 분리, envsubst 로 이름을 지정해 치환, 일반 리뷰를 먼저 요구
모델--model opus 별칭
게시요약과 인라인을 리뷰 한 건으로, 매 실행 전 이전 리뷰 정리
방어읽기 전용 도구, 더미 댓글 삭제 단계, 실패는 warning 으로 노출

자동 리뷰가 통과해도 머지 여부는 사람이 판단한다. 리뷰 봇이 제안한 명령과 정규식을 그대로 적용할 때 생기는 문제는 리뷰 봇이 제안한 명령과 정규식은 실제 데이터에 먼저 돌려 본다에서 다룬다.

on this page
  • 01구현 방식 두 가지
  • 02실행 제어
  • 트리거
  • 봇과 중복 실행 제어
  • 권한
  • 진행 표시
  • 03프롬프트 설계
  • `.txt` 파일로 분리한다
  • 개방형으로 시작한다
  • 심각도를 표시한다
  • 프롬프트 전달
  • 04모델 지정
  • 05리뷰어 구성
  • 06게시 방식
  • 요약과 인라인 댓글을 리뷰 하나로 묶는다
  • 이전 리뷰를 정리한다
  • 읽기 전용으로 묶는다
  • 07자주 실패하는 곳
  • 08정리
tags
#study#insights

이런 글도

  • ai

    하네스 회고는 새 문서로 쌓지 않고 종류에 맞는 기존 단일 소스에 환원한다

    2026.10.02
  • ai

    에이전트가 배운 회피 패턴은 조건을 통과한 것만 파일로 쌓고 주기적으로 지운다

    2026.10.02
  • ai

    ADR 은 되돌리기 어려운 결정의 이유만 남기고, 대안 기각은 옵션마다 줄을 나눈다

    2026.10.02
  • ai

    스킬 문서는 반복 실행을 스크립트로 내리고 같은 지시를 한 곳에서만 소유하게 쓴다

    2026.10.02

댓글 (0)