본문으로 건너뛰기
Kreath Archive
TechProjectsBooksAbout
TechProjectsBooksAbout

내비게이션

  • Tech
  • Projects
  • Books
  • About
  • Tags

카테고리

  • AI / ML
  • 웹 개발
  • 프로그래밍
  • 개발 도구

연결

  • GitHub
  • Email
  • RSS
© 2026 Kreath Archive. All rights reserved.Built with Next.js + MDX
홈TechProjectsBooksAbout
//
  1. 홈
  2. 테크
  3. 6장: PR 분석과 변경 영향도 예측
2026년 1월 29일·AI / ML·

6장: PR 분석과 변경 영향도 예측

PR의 변경 범위와 위험도를 AI로 분석하고, 리뷰어에게 구조화된 인사이트를 제공하는 시스템을 구축합니다.

17분895자7개 섹션
devtoolsautomationcode-qualitydevopsllm
공유
ai-dev-workflow6 / 10
12345678910
이전5장: AI 기반 문서화 자동화다음7장: GitHub Copilot 심층 활용 전략

PR 분석이 중요한 이유

코드 리뷰의 효과는 리뷰어가 변경 사항을 얼마나 잘 이해하느냐에 달려 있습니다. 대규모 PR에서 리뷰어는 수백 줄의 변경 사항을 일일이 읽으며 전체 맥락을 파악해야 합니다. 이 과정에서 중요한 변경을 놓치거나, 변경 간의 연관성을 파악하지 못하는 일이 발생합니다.

AI 기반 PR 분석 시스템은 리뷰어에게 구조화된 사전 정보를 제공합니다. 변경의 범위, 위험도, 주요 변경 사항, 검토가 필요한 핵심 포인트를 요약하여 리뷰어가 효율적으로 리뷰할 수 있도록 돕습니다.

text
PR 분석 시스템이 제공하는 정보:
 
  변경 요약:
    - PR이 무엇을 변경하는지 한 문단으로 요약
    - 변경의 동기와 목적
 
  변경 분류:
    - 기능 추가, 버그 수정, 리팩터링, 설정 변경 등 분류
    - 파일별 변경 유형과 중요도
 
  위험도 평가:
    - 전체 위험도 (Low / Medium / High / Critical)
    - 위험 요인별 상세 분석
 
  리뷰 가이드:
    - 핵심 검토 포인트
    - 추천 리뷰 순서
    - 주의가 필요한 파일

변경 분류 시스템

파일 변경의 의미적 분류

단순히 파일 확장자나 경로만으로 분류하는 것을 넘어, 변경의 실제 의미를 분석합니다.

src/change-classifier.ts
typescript
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))
}

변경 그룹핑

관련된 파일 변경을 논리적 그룹으로 묶어 리뷰어가 맥락을 파악하기 쉽게 합니다.

src/change-grouper.ts
typescript
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의 위험도를 다각적으로 평가합니다. 단순한 변경 줄 수가 아니라, 변경의 성격과 영향 범위를 종합적으로 고려합니다.

src/risk-assessor.ts
typescript
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),
  }
}

PR 리포트 생성

구조화된 리포트 형식

분석 결과를 리뷰어가 쉽게 소화할 수 있는 형식으로 구성합니다.

src/report-generator.ts
typescript
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
}

시각적 요소 활용

리포트에 시각적 요소를 포함하면 정보 전달이 더 효과적입니다.

src/visual-report.ts
typescript
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의 변경 내용에 따라 적합한 리뷰어를 자동으로 추천하는 기능을 구현합니다.

src/reviewer-recommender.ts
typescript
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)
}

GitHub Actions 통합

PR 분석 시스템을 GitHub Actions에 통합합니다.

.github/workflows/pr-analysis.yml
yaml
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.js
Tip

PR 분석 리포트는 PR 설명의 상단에 자동으로 추가하거나, 별도의 코멘트로 게시할 수 있습니다. 팀의 선호에 따라 선택하세요. PR 설명에 추가하면 항상 보이지만 수동 편집과 충돌할 수 있고, 코멘트로 게시하면 스크롤해야 보이지만 충돌이 없습니다.

정리

이 장에서는 PR 분석과 변경 영향도 예측 시스템을 구축했습니다. 변경의 의미적 분류, 위험도 평가, 구조화된 리포트 생성, 리뷰어 추천까지 리뷰 효율을 높이는 전체 파이프라인을 다루었습니다.

핵심 내용을 정리합니다.

  • 파일 변경을 의미적으로 분류하여 변경의 성격과 중요도를 파악합니다
  • 다각적 위험 요인을 정량화하여 종합 위험도를 산출합니다
  • 관련 파일을 논리적 그룹으로 묶어 리뷰 맥락을 제공합니다
  • 리뷰 가이드와 추천 리뷰 순서로 리뷰 효율을 높입니다
  • 변경 이력 기반으로 적합한 리뷰어를 자동 추천합니다

다음 장에서는 GitHub Copilot의 심층 활용 전략을 다룹니다. 인라인 자동 완성, 채팅, Agent Mode를 실전에서 효과적으로 활용하는 방법을 다룹니다.

이 글이 도움이 되셨나요?

관련 주제 더 보기

#devtools#automation#code-quality#devops#llm

관련 글

AI / ML

7장: GitHub Copilot 심층 활용 전략

GitHub Copilot의 인라인 자동 완성, Copilot Chat, Agent Mode를 실전에서 효과적으로 활용하는 전략과 팀 단위 도입 방법을 다룹니다.

2026년 1월 31일·19분
AI / ML

5장: AI 기반 문서화 자동화

코드 변경에 따라 API 문서, README, 변경 로그를 AI로 자동 갱신하는 시스템을 구축하고, 문서와 코드의 동기화를 유지하는 전략을 다룹니다.

2026년 1월 27일·19분
AI / ML

8장: Claude Code를 활용한 개발 자동화

Claude Code의 에이전트 기반 워크플로우를 활용하여 코드 생성, 리팩터링, 디버깅을 자동화하고, CI/CD에 통합하는 방법을 다룹니다.

2026년 2월 2일·17분
이전 글5장: AI 기반 문서화 자동화
다음 글7장: GitHub Copilot 심층 활용 전략

댓글

목차

약 17분 남음
  • PR 분석이 중요한 이유
  • 변경 분류 시스템
    • 파일 변경의 의미적 분류
    • 변경 그룹핑
  • 위험도 평가 엔진
    • 위험 요인 식별
  • PR 리포트 생성
    • 구조화된 리포트 형식
    • 시각적 요소 활용
  • 리뷰어 자동 추천
  • GitHub Actions 통합
  • 정리