PR의 변경 범위와 위험도를 AI로 분석하고, 리뷰어에게 구조화된 인사이트를 제공하는 시스템을 구축합니다.
코드 리뷰의 효과는 리뷰어가 변경 사항을 얼마나 잘 이해하느냐에 달려 있습니다. 대규모 PR에서 리뷰어는 수백 줄의 변경 사항을 일일이 읽으며 전체 맥락을 파악해야 합니다. 이 과정에서 중요한 변경을 놓치거나, 변경 간의 연관성을 파악하지 못하는 일이 발생합니다.
AI 기반 PR 분석 시스템은 리뷰어에게 구조화된 사전 정보를 제공합니다. 변경의 범위, 위험도, 주요 변경 사항, 검토가 필요한 핵심 포인트를 요약하여 리뷰어가 효율적으로 리뷰할 수 있도록 돕습니다.
PR 분석 시스템이 제공하는 정보:
변경 요약:
- PR이 무엇을 변경하는지 한 문단으로 요약
- 변경의 동기와 목적
변경 분류:
- 기능 추가, 버그 수정, 리팩터링, 설정 변경 등 분류
- 파일별 변경 유형과 중요도
위험도 평가:
- 전체 위험도 (Low / Medium / High / Critical)
- 위험 요인별 상세 분석
리뷰 가이드:
- 핵심 검토 포인트
- 추천 리뷰 순서
- 주의가 필요한 파일단순히 파일 확장자나 경로만으로 분류하는 것을 넘어, 변경의 실제 의미를 분석합니다.
interface SemanticChange {
file: string
// 변경의 의미적 분류
classification: ChangeClassification
// 변경의 중요도 (0-10)
importance: number
// 관련 변경 그룹
group: string
// 변경 요약
summary: string
}
type ChangeClassification =
| "new-feature" // 새로운 기능 추가
| "feature-modification" // 기존 기능 수정
| "bug-fix" // 버그 수정
| "refactoring" // 코드 구조 개선
| "performance" // 성능 최적화
| "security" // 보안 관련 변경
| "dependency" // 의존성 변경
| "configuration" // 설정 변경
| "test" // 테스트 변경
| "documentation" // 문서 변경
| "infrastructure" // 인프라/빌드 변경
async function classifyChanges(
changes: FileChange[]
): Promise<SemanticChange[]> {
// LLM을 사용하여 각 파일 변경의 의미를 분석
const prompt = `다음 PR의 파일 변경 목록을 분석하여,
각 파일 변경의 의미적 분류, 중요도, 변경 요약을 제공해 주세요.
## 변경 파일 목록
` + changes.map(c => {
return "### " + c.filename
+ " (" + c.additions + " additions, "
+ c.deletions + " deletions)\n"
+ "```diff\n" + c.patch + "\n```"
}).join("\n\n") + `
## 분류 기준
- new-feature: 새로운 기능이나 엔드포인트 추가
- feature-modification: 기존 기능의 동작 변경
- bug-fix: 버그 수정 (조건문, 에러 처리 등)
- refactoring: 동작 변경 없는 코드 구조 개선
- performance: 성능 관련 최적화
- security: 보안 관련 변경
- dependency: 패키지 추가/제거/버전 변경
- configuration: 설정 파일 변경
- test: 테스트 추가/수정
- documentation: 문서 변경
- infrastructure: CI/CD, 빌드 설정 변경
## 중요도 기준 (0-10)
- 0-2: 포맷팅, 주석 등 영향 없는 변경
- 3-4: 테스트, 문서 등 간접적 변경
- 5-6: 기능 수정, 리팩터링
- 7-8: 핵심 기능 변경, 새 기능 추가
- 9-10: 보안 수정, 데이터 스키마 변경, Breaking Change
JSON 배열 형식으로 응답하세요.
`
const result = await callLLM(prompt)
return JSON.parse(extractJsonBlock(result))
}관련된 파일 변경을 논리적 그룹으로 묶어 리뷰어가 맥락을 파악하기 쉽게 합니다.
interface ChangeGroup {
name: string
description: string
files: SemanticChange[]
reviewPriority: number // 리뷰 우선순위 (1이 가장 높음)
}
function groupRelatedChanges(
changes: SemanticChange[]
): ChangeGroup[] {
const groups: ChangeGroup[] = []
// 같은 기능에 속하는 파일 그룹핑
// 예: UserService.ts, UserController.ts, user.test.ts
const featureGroups = new Map<string, SemanticChange[]>()
for (const change of changes) {
const groupKey = change.group || inferGroup(change.file)
if (!featureGroups.has(groupKey)) {
featureGroups.set(groupKey, [])
}
featureGroups.get(groupKey)?.push(change)
}
let priority = 1
// 중요도 높은 그룹부터 정렬
const sortedGroups = Array.from(featureGroups.entries())
.sort((a, b) => {
const maxA = Math.max(...a[1].map(c => c.importance))
const maxB = Math.max(...b[1].map(c => c.importance))
return maxB - maxA
})
for (const [name, files] of sortedGroups) {
groups.push({
name,
description: summarizeGroup(files),
files,
reviewPriority: priority++,
})
}
return groups
}PR의 위험도를 다각적으로 평가합니다. 단순한 변경 줄 수가 아니라, 변경의 성격과 영향 범위를 종합적으로 고려합니다.
interface RiskAssessment {
overallRisk: "low" | "medium" | "high" | "critical"
overallScore: number // 0-100
factors: RiskFactor[]
recommendations: string[]
}
interface RiskFactor {
name: string
score: number // 0-100
weight: number // 가중치
description: string
mitigations: string[] // 위험 완화 방법
}
function assessRisk(
changes: SemanticChange[],
groups: ChangeGroup[],
metadata: PRMetadata
): RiskAssessment {
const factors: RiskFactor[] = []
// 1. 변경 규모 위험도
const totalChanges = changes.reduce(
(sum, c) => sum + c.importance, 0
)
factors.push({
name: "변경 규모",
score: Math.min(totalChanges * 5, 100),
weight: 0.15,
description: changes.length + "개 파일에 걸친 변경",
mitigations: totalChanges > 50
? ["PR을 더 작은 단위로 분할하는 것을 고려하세요"]
: [],
})
// 2. 핵심 경로 변경 위험도
const criticalPathChanges = changes.filter(
c => c.importance >= 8
)
factors.push({
name: "핵심 경로 변경",
score: criticalPathChanges.length * 25,
weight: 0.25,
description: criticalPathChanges.length
+ "개의 핵심 파일이 변경됨",
mitigations: criticalPathChanges.length > 0
? ["핵심 파일의 변경을 주의 깊게 검토하세요"]
: [],
})
// 3. 보안 관련 변경
const securityChanges = changes.filter(
c => c.classification === "security"
)
factors.push({
name: "보안 관련 변경",
score: securityChanges.length > 0 ? 80 : 0,
weight: 0.25,
description: securityChanges.length > 0
? "보안 관련 코드가 변경됨"
: "보안 관련 변경 없음",
mitigations: securityChanges.length > 0
? ["보안 전문가의 추가 리뷰를 권장합니다"]
: [],
})
// 4. 테스트 커버리지
const hasTests = changes.some(
c => c.classification === "test"
)
const hasLogicChanges = changes.some(
c => c.classification === "new-feature"
|| c.classification === "feature-modification"
|| c.classification === "bug-fix"
)
factors.push({
name: "테스트 커버리지",
score: hasLogicChanges && !hasTests ? 60 : 0,
weight: 0.15,
description: hasLogicChanges && !hasTests
? "로직 변경이 있지만 테스트가 포함되지 않음"
: "테스트가 포함됨",
mitigations: hasLogicChanges && !hasTests
? ["변경된 로직에 대한 테스트를 추가하세요"]
: [],
})
// 5. 의존성 변경
const depChanges = changes.filter(
c => c.classification === "dependency"
)
factors.push({
name: "의존성 변경",
score: depChanges.length * 15,
weight: 0.10,
description: depChanges.length > 0
? depChanges.length + "개의 의존성이 변경됨"
: "의존성 변경 없음",
mitigations: depChanges.length > 0
? ["변경된 의존성의 보안 취약점과 호환성을 확인하세요"]
: [],
})
// 6. DB 스키마 변경
const dbChanges = changes.filter(
c => c.file.includes("migration")
|| c.file.includes("schema")
)
factors.push({
name: "데이터베이스 변경",
score: dbChanges.length > 0 ? 70 : 0,
weight: 0.10,
description: dbChanges.length > 0
? "데이터베이스 스키마가 변경됨"
: "DB 변경 없음",
mitigations: dbChanges.length > 0
? [
"마이그레이션의 롤백 가능 여부를 확인하세요",
"대용량 테이블의 경우 무중단 마이그레이션을 고려하세요",
]
: [],
})
// 종합 위험도 계산
const overallScore = factors.reduce(
(sum, f) => sum + f.score * f.weight, 0
)
const overallRisk =
overallScore >= 70 ? "critical" :
overallScore >= 50 ? "high" :
overallScore >= 30 ? "medium" : "low"
return {
overallRisk,
overallScore: Math.round(overallScore),
factors,
recommendations: generateRecommendations(factors),
}
}분석 결과를 리뷰어가 쉽게 소화할 수 있는 형식으로 구성합니다.
function generatePRReport(
summary: string,
groups: ChangeGroup[],
risk: RiskAssessment,
reviewGuide: ReviewGuide
): string {
let report = "## PR Analysis Report\n\n"
// 1. 변경 요약
report += "### Summary\n\n"
report += summary + "\n\n"
// 2. 위험도 요약
const riskBadge = {
low: "Low",
medium: "Medium",
high: "High",
critical: "Critical",
}
report += "### Risk Assessment: "
+ riskBadge[risk.overallRisk]
+ " (" + risk.overallScore + "/100)\n\n"
report += "| Factor | Score | Detail |\n"
report += "|--------|-------|--------|\n"
for (const factor of risk.factors) {
if (factor.score > 0) {
report += "| " + factor.name
+ " | " + factor.score
+ " | " + factor.description + " |\n"
}
}
report += "\n"
// 3. 변경 그룹별 요약
report += "### Change Groups\n\n"
for (const group of groups) {
report += "**" + group.reviewPriority + ". "
+ group.name + "**\n"
report += group.description + "\n"
report += "Files: "
+ group.files.map(f => "`" + f.file + "`").join(", ")
+ "\n\n"
}
// 4. 리뷰 가이드
report += "### Review Guide\n\n"
report += "**Recommended review order:**\n"
for (const step of reviewGuide.steps) {
report += step.order + ". "
+ step.description + "\n"
for (const file of step.files) {
report += " - `" + file + "`\n"
}
}
report += "\n"
// 5. 주의 사항
if (risk.recommendations.length > 0) {
report += "### Recommendations\n\n"
for (const rec of risk.recommendations) {
report += "- " + rec + "\n"
}
}
return report
}리포트에 시각적 요소를 포함하면 정보 전달이 더 효과적입니다.
function generateMermaidDiagram(
groups: ChangeGroup[]
): string {
let diagram = "```mermaid\ngraph LR\n"
for (const group of groups) {
const nodeId = group.name.replace(/\s/g, "_")
diagram += " " + nodeId + "["
+ group.name + "<br/>"
+ group.files.length + " files"
+ "]\n"
// 그룹 간 의존성 표시
for (const dep of findGroupDependencies(group, groups)) {
const depId = dep.replace(/\s/g, "_")
diagram += " " + nodeId + " --> " + depId + "\n"
}
}
diagram += "```"
return diagram
}
function generateFileTreeView(
changes: SemanticChange[]
): string {
// 파일 트리 형식으로 변경 사항 표시
const tree = buildFileTree(changes)
let view = "```text\n"
function renderNode(
node: TreeNode,
prefix: string,
isLast: boolean
): void {
const connector = isLast ? "+-- " : "|-- "
const indicator = node.change
? " [" + node.change.classification + "]"
: ""
view += prefix + connector + node.name + indicator + "\n"
const childPrefix = prefix + (isLast ? " " : "| ")
const children = Array.from(node.children.values())
children.forEach((child, i) => {
renderNode(child, childPrefix, i === children.length - 1)
})
}
for (const [name, node] of tree.children) {
renderNode(node, "", true)
}
view += "```"
return view
}PR의 변경 내용에 따라 적합한 리뷰어를 자동으로 추천하는 기능을 구현합니다.
interface ReviewerRecommendation {
username: string
reason: string
expertise: string[]
recentActivity: number // 최근 리뷰 횟수
confidence: number // 추천 신뢰도 (0-1)
}
async function recommendReviewers(
changes: SemanticChange[],
teamMembers: TeamMember[]
): Promise<ReviewerRecommendation[]> {
const recommendations: ReviewerRecommendation[] = []
for (const member of teamMembers) {
let relevanceScore = 0
// 1. 변경 파일의 최근 기여자 확인
const authoredFiles = changes.filter(
c => member.recentFiles.includes(c.file)
)
relevanceScore += authoredFiles.length * 30
// 2. 전문 분야와 변경 분류 매칭
const matchingExpertise = changes.filter(c => {
return member.expertise.some(
e => e === c.classification
)
})
relevanceScore += matchingExpertise.length * 20
// 3. 현재 리뷰 부하 고려 (리뷰 중인 PR 수)
const loadPenalty = member.activeReviews * 10
relevanceScore -= loadPenalty
if (relevanceScore > 20) {
recommendations.push({
username: member.username,
reason: buildRecommendationReason(
authoredFiles, matchingExpertise
),
expertise: member.expertise,
recentActivity: member.recentReviewCount,
confidence: Math.min(relevanceScore / 100, 1),
})
}
}
return recommendations
.sort((a, b) => b.confidence - a.confidence)
.slice(0, 3)
}PR 분석 시스템을 GitHub Actions에 통합합니다.
name: PR Analysis
on:
pull_request:
types: [opened, synchronize]
permissions:
contents: read
pull-requests: write
jobs:
analyze:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-node@v4
with:
node-version: "20"
- run: npm install
- name: Analyze PR
env:
GITHUB_TOKEN: $GITHUB_TOKEN_VALUE
ANTHROPIC_API_KEY: $ANTHROPIC_KEY_VALUE
run: node scripts/analyze-pr.js
- name: Post analysis report
env:
GITHUB_TOKEN: $GITHUB_TOKEN_VALUE
run: node scripts/post-report.jsPR 분석 리포트는 PR 설명의 상단에 자동으로 추가하거나, 별도의 코멘트로 게시할 수 있습니다. 팀의 선호에 따라 선택하세요. PR 설명에 추가하면 항상 보이지만 수동 편집과 충돌할 수 있고, 코멘트로 게시하면 스크롤해야 보이지만 충돌이 없습니다.
이 장에서는 PR 분석과 변경 영향도 예측 시스템을 구축했습니다. 변경의 의미적 분류, 위험도 평가, 구조화된 리포트 생성, 리뷰어 추천까지 리뷰 효율을 높이는 전체 파이프라인을 다루었습니다.
핵심 내용을 정리합니다.
다음 장에서는 GitHub Copilot의 심층 활용 전략을 다룹니다. 인라인 자동 완성, 채팅, Agent Mode를 실전에서 효과적으로 활용하는 방법을 다룹니다.
이 글이 도움이 되셨나요?
GitHub Copilot의 인라인 자동 완성, Copilot Chat, Agent Mode를 실전에서 효과적으로 활용하는 전략과 팀 단위 도입 방법을 다룹니다.
코드 변경에 따라 API 문서, README, 변경 로그를 AI로 자동 갱신하는 시스템을 구축하고, 문서와 코드의 동기화를 유지하는 전략을 다룹니다.
Claude Code의 에이전트 기반 워크플로우를 활용하여 코드 생성, 리팩터링, 디버깅을 자동화하고, CI/CD에 통합하는 방법을 다룹니다.