전체 시리즈에서 다룬 AI 코드 리뷰, 테스트 생성, 문서화, PR 분석을 하나의 통합 시스템으로 구축하는 실전 프로젝트를 진행합니다.
이 장에서는 시리즈 전체에서 다룬 기술을 조합하여 실제로 동작하는 AI 통합 개발 워크플로우 시스템을 구축합니다. 하나의 GitHub 리포지토리에 설치하면, PR 생성 시 자동으로 코드 리뷰, 테스트 제안, 문서 영향 분석, 변경 위험도 평가를 수행하는 완전한 시스템입니다.
AI Dev Workflow System v1.0 기능:
PR 생성 시 자동 실행:
1. 변경 분류: 파일 변경을 의미적으로 분류
2. 코드 리뷰: 버그, 보안, 성능 문제 식별
3. 테스트 제안: 부족한 테스트 식별 및 생성 제안
4. 문서 영향: 갱신이 필요한 문서 식별
5. 위험도 평가: 변경의 종합 위험도 산출
6. 통합 리포트: 모든 결과를 하나의 리포트로 제공
설정 가능:
- 프로젝트별 리뷰 기준 커스터마이징
- 단계별 활성화/비활성화
- 비용 예산 설정
- 알림 설정ai-dev-workflow/
src/
index.ts # 진입점
pipeline/
context.ts # 공통 분석 맥락 구축
orchestrator.ts # 파이프라인 오케스트레이터
quality-gate.ts # 품질 게이트
stages/
change-classifier.ts # 변경 분류
code-reviewer.ts # AI 코드 리뷰
test-suggester.ts # 테스트 제안
doc-analyzer.ts # 문서 영향 분석
risk-assessor.ts # 위험도 평가
report/
generator.ts # 통합 리포트 생성
github-poster.ts # GitHub 코멘트 게시
utils/
diff-parser.ts # diff 파싱
llm-client.ts # LLM API 클라이언트
cost-tracker.ts # 비용 추적
config-loader.ts # 설정 파일 로더
.github/
workflows/
ai-pipeline.yml # GitHub Actions 워크플로우
.ai-pipeline.yml # 파이프라인 설정 파일
package.json
tsconfig.json파이프라인의 진입점을 구현합니다. GitHub Actions 환경에서 실행됩니다.
import { buildPipelineContext } from "./pipeline/context"
import { runPipeline } from "./pipeline/orchestrator"
import { generateReport } from "./report/generator"
import { postToGitHub } from "./report/github-poster"
import { loadConfig } from "./utils/config-loader"
async function main(): Promise<void> {
// 환경 변수에서 PR 정보 추출
const prNumber = parseInt(
process.env.PR_NUMBER || "0", 10
)
const repo = process.env.GITHUB_REPOSITORY || ""
const [owner, repoName] = repo.split("/")
if (!prNumber || !owner || !repoName) {
console.error("Missing required environment variables")
process.exit(1)
}
console.log(
"Starting AI pipeline for PR #" + prNumber
)
// 설정 로드
const config = loadConfig()
// 파이프라인 맥락 구축
const context = await buildPipelineContext({
owner,
repo: repoName,
prNumber,
token: process.env.GITHUB_TOKEN || "",
apiKey: process.env.ANTHROPIC_API_KEY || "",
config,
})
console.log(
"Analyzed " + context.changes.length + " changed files"
)
// 파이프라인 실행
const result = await runPipeline(context)
console.log(
"Pipeline completed in "
+ (result.totalDuration / 1000).toFixed(1) + "s"
)
// 리포트 생성
const report = generateReport(result, context)
// GitHub에 게시
await postToGitHub({
owner,
repo: repoName,
prNumber,
report,
qualityGate: result.qualityGate,
token: process.env.GITHUB_TOKEN || "",
})
// 품질 게이트 실패 시 비정상 종료
if (!result.qualityGate.passed) {
console.error("Quality gate failed:")
for (const issue of result.qualityGate.blockingIssues) {
console.error(" - " + issue)
}
process.exit(1)
}
console.log("Pipeline completed successfully")
}
main().catch(error => {
console.error("Pipeline failed:", error)
process.exit(1)
})LLM API 호출을 추상화하는 클라이언트입니다. 재시도, 비용 추적, 에러 처리를 포함합니다.
import Anthropic from "@anthropic-ai/sdk"
interface LLMResponse {
text: string
inputTokens: number
outputTokens: number
cost: number
}
interface LLMClientOptions {
model: string
maxRetries: number
timeout: number
}
class LLMClient {
private client: Anthropic
private options: LLMClientOptions
private totalTokens: number = 0
private totalCost: number = 0
constructor(apiKey: string, options?: Partial<LLMClientOptions>) {
this.client = new Anthropic({ apiKey })
this.options = {
model: options?.model || "claude-sonnet-4-20250514",
maxRetries: options?.maxRetries || 3,
timeout: options?.timeout || 60000,
}
}
async complete(
system: string,
userMessage: string,
maxTokens: number = 4096
): Promise<LLMResponse> {
let lastError: Error | null = null
for (let attempt = 0; attempt < this.options.maxRetries; attempt++) {
try {
const response = await this.client.messages.create({
model: this.options.model,
max_tokens: maxTokens,
system,
messages: [{ role: "user", content: userMessage }],
})
const text = response.content[0].type === "text"
? response.content[0].text
: ""
const inputTokens = response.usage.input_tokens
const outputTokens = response.usage.output_tokens
const cost = this.calculateCost(
inputTokens, outputTokens
)
this.totalTokens += inputTokens + outputTokens
this.totalCost += cost
return { text, inputTokens, outputTokens, cost }
} catch (error) {
lastError = error as Error
if (attempt < this.options.maxRetries - 1) {
const delay = Math.pow(2, attempt) * 1000
await new Promise(resolve => setTimeout(resolve, delay))
}
}
}
throw lastError
}
private calculateCost(
inputTokens: number,
outputTokens: number
): number {
// Claude Sonnet 기준 가격 (2026년 기준)
const inputCostPer1k = 0.003
const outputCostPer1k = 0.015
return (
(inputTokens / 1000) * inputCostPer1k
+ (outputTokens / 1000) * outputCostPer1k
)
}
getUsageStats(): { tokens: number; cost: number } {
return {
tokens: this.totalTokens,
cost: this.totalCost,
}
}
}
export { LLMClient }
export type { LLMResponse }import { LLMClient } from "../utils/llm-client"
interface ClassifiedChange {
file: string
classification: string
importance: number
summary: string
group: string
}
async function classifyChanges(
changes: FileChange[],
llm: LLMClient
): Promise<ClassifiedChange[]> {
// 소규모 변경은 규칙 기반으로 빠르게 분류
const ruleBasedResults: ClassifiedChange[] = []
const needsAI: FileChange[] = []
for (const change of changes) {
const ruleResult = classifyByRules(change)
if (ruleResult) {
ruleBasedResults.push(ruleResult)
} else {
needsAI.push(change)
}
}
// AI 분류가 필요한 파일
if (needsAI.length === 0) {
return ruleBasedResults
}
const systemPrompt = `파일 변경 사항을 분류하세요.
각 파일에 대해 다음을 제공하세요:
- classification: new-feature, feature-modification, bug-fix, refactoring, performance, security 중 하나
- importance: 0-10 (변경의 중요도)
- summary: 변경 요약 (한 문장)
- group: 관련 파일 그룹명
JSON 배열로 응답하세요.`
const userPrompt = needsAI.map(c => {
return "File: " + c.filename + "\n"
+ "Status: " + c.status + "\n"
+ "Additions: " + c.additions + "\n"
+ "Deletions: " + c.deletions + "\n"
+ "Diff:\n" + c.patch.slice(0, 500)
}).join("\n---\n")
const response = await llm.complete(systemPrompt, userPrompt)
const aiResults = JSON.parse(
extractJsonBlock(response.text)
) as ClassifiedChange[]
return [...ruleBasedResults, ...aiResults]
}
function classifyByRules(
change: FileChange
): ClassifiedChange | null {
const path = change.filename.toLowerCase()
// 테스트 파일
if (path.includes("test") || path.includes("spec")) {
return {
file: change.filename,
classification: "test",
importance: 3,
summary: "테스트 파일 변경",
group: "tests",
}
}
// 설정 파일
if (path.match(/\.(json|ya?ml|toml)$/)
&& !path.includes("package")) {
return {
file: change.filename,
classification: "configuration",
importance: 2,
summary: "설정 파일 변경",
group: "config",
}
}
// 문서 파일
if (path.endsWith(".md")) {
return {
file: change.filename,
classification: "documentation",
importance: 1,
summary: "문서 파일 변경",
group: "docs",
}
}
// 규칙으로 분류할 수 없는 경우
return null
}import { LLMClient } from "../utils/llm-client"
interface ReviewComment {
file: string
line: number
severity: "critical" | "warning" | "suggestion"
category: string
comment: string
suggestion?: string
}
interface ReviewResult {
summary: string
comments: ReviewComment[]
overallRisk: "low" | "medium" | "high"
}
async function reviewCode(
context: PipelineContext,
llm: LLMClient
): Promise<ReviewResult> {
// 비즈니스 로직 변경만 심층 리뷰
const logicChanges = context.changes.filter(
c => c.classification === "new-feature"
|| c.classification === "feature-modification"
|| c.classification === "bug-fix"
)
if (logicChanges.length === 0) {
return {
summary: "비즈니스 로직 변경이 없어 심층 리뷰를 건너뜁니다.",
comments: [],
overallRisk: "low",
}
}
// 파일 그룹별 병렬 리뷰
const groups = groupByDirectory(logicChanges)
const groupResults = await Promise.all(
groups.map(group => reviewFileGroup(group, context, llm))
)
// 결과 병합
const allComments = groupResults.flatMap(r => r.comments)
const overallRisk = determineOverallRisk(groupResults)
// 요약 생성
const summaryResponse = await llm.complete(
"코드 리뷰 결과를 2-3문장으로 요약하세요.",
JSON.stringify({
totalComments: allComments.length,
criticalCount: allComments.filter(
c => c.severity === "critical"
).length,
categories: [...new Set(allComments.map(c => c.category))],
})
)
return {
summary: summaryResponse.text,
comments: allComments,
overallRisk,
}
}
async function reviewFileGroup(
files: AnalyzedChange[],
context: PipelineContext,
llm: LLMClient
): Promise<ReviewResult> {
const systemPrompt = `시니어 엔지니어로서 코드를 리뷰하세요.
리뷰 기준:
1. 로직 오류, 엣지 케이스 누락
2. 보안 취약점
3. 성능 비효율
4. 가독성과 유지보수성
JSON 형식으로 응답:
{
"comments": [
{
"file": "파일 경로",
"line": 줄번호,
"severity": "critical|warning|suggestion",
"category": "bug|security|performance|readability",
"comment": "피드백",
"suggestion": "개선 코드 (선택)"
}
],
"overallRisk": "low|medium|high"
}`
const userPrompt = files.map(f => {
const fullContent = context.fileCache.get(f.file) || ""
return "## " + f.file + "\n"
+ "### Full file:\n```\n"
+ fullContent.slice(0, 3000) + "\n```\n"
+ "### Changes:\n```diff\n"
+ f.diff + "\n```"
}).join("\n\n---\n\n")
const response = await llm.complete(
systemPrompt, userPrompt, 4096
)
return JSON.parse(extractJsonBlock(response.text))
}import { LLMClient } from "../utils/llm-client"
interface TestSuggestion {
file: string
testFile: string
description: string
testCode: string
priority: "high" | "medium" | "low"
}
interface TestSuggestionResult {
suggestions: TestSuggestion[]
coverageGaps: string[]
}
async function suggestTests(
context: PipelineContext,
llm: LLMClient
): Promise<TestSuggestionResult> {
// 로직 변경이 있는 파일 중 테스트가 없는 것 식별
const logicFiles = context.changes.filter(c => {
return c.classification === "new-feature"
|| c.classification === "feature-modification"
|| c.classification === "bug-fix"
})
const testFiles = context.changes.filter(
c => c.classification === "test"
)
// 테스트가 이미 포함된 로직 파일 제외
const untestedFiles = logicFiles.filter(lf => {
const baseName = lf.file
.replace(/\.(ts|js|tsx|jsx)$/, "")
return !testFiles.some(tf => {
return tf.file.includes(baseName)
})
})
if (untestedFiles.length === 0) {
return { suggestions: [], coverageGaps: [] }
}
const systemPrompt = `코드 변경을 분석하여 필요한 테스트를 제안하세요.
각 제안은 다음을 포함:
- file: 대상 소스 파일
- testFile: 테스트 파일 경로
- description: 테스트 설명
- testCode: 실행 가능한 테스트 코드 (vitest 사용)
- priority: high (로직 변경), medium (리팩터링), low (유틸리티)
JSON 형식:
{
"suggestions": [...],
"coverageGaps": ["설명1", "설명2"]
}`
const userPrompt = untestedFiles.map(f => {
const content = context.fileCache.get(f.file) || ""
return "## " + f.file + "\n"
+ "```\n" + content.slice(0, 2000) + "\n```\n"
+ "Changes:\n```diff\n" + f.diff + "\n```"
}).join("\n\n---\n\n")
const response = await llm.complete(
systemPrompt, userPrompt, 4096
)
return JSON.parse(extractJsonBlock(response.text))
}# AI Development Pipeline 설정
# 기본 설정
version: 1
model: claude-sonnet-4-20250514
# 활성화할 단계
stages:
change-classification:
enabled: true
code-review:
enabled: true
focus: [correctness, security, performance]
maxComments: 15
test-suggestion:
enabled: true
onlyForLogicChanges: true
doc-analysis:
enabled: true
risk-assessment:
enabled: true
# 파일 필터
files:
exclude:
- "*.lock"
- "*.min.*"
- "dist/**"
- ".github/**"
reviewPriority:
high: ["src/core/**", "src/auth/**"]
low: ["src/utils/**", "scripts/**"]
# 품질 게이트
qualityGate:
codeReview:
maxCriticalIssues: 0
maxWarningIssues: 10
testing:
requireTestsForLogicChanges: true
documentation:
requireDocUpdate: false
# 비용 관리
budget:
monthlyLimit: 50.0
perPRLimit: 1.0
warningThreshold: 0.8
# 프로젝트 맥락
context:
codeStyle: |
- TypeScript strict mode
- function 선언 사용
- camelCase 네이밍
- 에러 처리 필수이 시스템을 새 프로젝트에 설치하는 단계를 정리합니다.
설치 단계:
1. 리포지토리 시크릿 설정
- ANTHROPIC_API_KEY: Anthropic API 키
2. 워크플로우 파일 복사
- .github/workflows/ai-pipeline.yml
3. 설정 파일 생성
- .ai-pipeline.yml (프로젝트에 맞게 커스터마이징)
4. 의존성 설치
- npm install @anthropic-ai/sdk @octokit/rest
5. 소스 코드 복사
- src/ 디렉토리 전체
6. 테스트 PR 생성
- 소규모 PR을 생성하여 파이프라인 동작 확인실전에서 자주 발생하는 문제와 해결 방법을 정리합니다.
문제: GitHub Actions에서 GITHUB_TOKEN 권한 부족
해결: 워크플로우의 permissions에
pull-requests: write 권한 추가
문제: LLM 응답 파싱 실패
해결: JSON 블록 추출 로직 강화,
응답에서 마크다운 코드 블록 패턴 매칭
문제: 대규모 PR에서 토큰 한도 초과
해결: 파일 우선순위 기반 선별적 리뷰,
diff만 전송하고 전체 파일은 필요시에만
문제: 리뷰 코멘트의 줄 번호가 잘못됨
해결: diff 기반 줄 번호 매핑,
유효하지 않은 줄 번호 필터링
문제: API 비용이 예상보다 높음
해결: 비용 추적 활성화,
불필요한 단계 비활성화,
소규모 PR에서 가벼운 모델 사용단기 개선 (1-2개월):
학습 기능:
- 리뷰어가 무시한 피드백을 학습하여 정밀도 향상
- 프로젝트별 패턴 학습
더 나은 맥락:
- Git blame 기반 코드 소유자 식별
- 최근 이슈/버그 이력 참조
UI 개선:
- GitHub Check Run으로 상세 결과 표시
- 접을 수 있는 섹션으로 리포트 구성중기 발전 (3-6개월):
자동 수정:
- 코드 리뷰에서 발견된 문제를 자동 수정하는 PR 생성
- 테스트 제안을 실제 테스트 파일로 생성
팀 분석:
- 팀 전체의 코드 품질 트렌드 분석
- 자주 발생하는 문제 패턴 리포트
- 개발자별 강점/약점 분석 (비공개)
다른 플랫폼 지원:
- GitLab CI 통합
- Bitbucket Pipelines 통합장기 비전:
자율적 개발 보조:
- 이슈 분석에서 구현까지 자동화
- 코드 리뷰 피드백 자동 반영
- 배포 후 문제 자동 감지 및 롤백
조직 수준 품질 관리:
- 마이크로서비스 간 API 호환성 검증
- 보안 정책 자동 적용
- 아키텍처 가이드라인 준수 검증이 시리즈를 통해 AI 기반 개발 워크플로우의 전체 그림을 그리고, 각 구성 요소를 구축하고, 하나의 통합 시스템으로 완성했습니다.
1장: 전체 그림
- AI가 개발 수명 주기의 각 단계에 통합되는 방식
- 규칙 기반과 AI 기반 자동화의 상호 보완
2장: 코드 리뷰 원리
- LLM의 코드 이해 방식과 맥락의 중요성
- 시스템 아키텍처 설계
3장: 코드 리뷰 구현
- GitHub Actions 기반 자동 코드 리뷰 구축
- 점진적 리뷰와 설정 커스터마이징
4장: 테스트 생성
- 코드 분석 기반 테스트 전략 수립
- 뮤테이션 테스트로 품질 검증
5장: 문서화 자동화
- API 문서, CHANGELOG, README 자동 갱신
- 코드-문서 일관성 검증
6장: PR 분석
- 변경 의미 분류와 위험도 평가
- 리뷰 가이드와 리뷰어 추천
7장: GitHub Copilot
- 인라인 완성, Chat, Agent Mode 활용 전략
- 팀 단위 도입과 생산성 측정
8장: Claude Code
- CLAUDE.md 설계와 에이전트 기반 자동화
- CI/CD 통합과 MCP 연동
9장: CI/CD 통합
- 공통 분석 계층과 파이프라인 오케스트레이션
- 품질 게이트와 비용 관리
10장: 실전 프로젝트
- 전체 시스템의 구현과 배포
- 발전 방향AI 기반 개발 워크플로우를 도입할 때 기억해야 할 핵심 원칙을 정리합니다.
AI는 도구이지 대체재가 아닙니다. AI 코드 리뷰는 인간 리뷰를 보완하는 것이지 대체하는 것이 아닙니다. 아키텍처 결정, 비즈니스 로직 검증, 설계 판단은 여전히 인간의 영역입니다.
점진적으로 도입하세요. 모든 기능을 한 번에 도입하기보다, 코드 리뷰부터 시작하여 팀이 익숙해진 후 테스트 생성, 문서화 자동화를 순차적으로 추가하는 것이 효과적입니다.
결과를 측정하세요. AI 도구의 효과를 주관적 인상이 아닌 객관적 데이터로 평가하세요. 리뷰 시간 단축, 버그 감소, 테스트 커버리지 향상 등의 메트릭을 추적하세요.
비용을 관리하세요. LLM API 비용은 사용량에 비례합니다. 예산을 설정하고, 비용 대비 효과를 정기적으로 검토하세요.
보안에 주의하세요. API 키를 안전하게 관리하고, AI에 전달되는 코드에 민감 정보가 포함되지 않도록 주의하세요. AI가 생성한 코드의 보안 문제를 항상 검증하세요.
이 시리즈가 AI를 활용한 개발 워크플로우 혁신의 실용적인 가이드가 되기를 바랍니다.
이 글이 도움이 되셨나요?
코드 리뷰, 테스트 생성, 문서화, PR 분석을 하나의 CI/CD 파이프라인으로 통합하고, 품질 게이트와 비용 관리 전략을 수립합니다.
Claude Code의 에이전트 기반 워크플로우를 활용하여 코드 생성, 리팩터링, 디버깅을 자동화하고, CI/CD에 통합하는 방법을 다룹니다.
GitHub Copilot의 인라인 자동 완성, Copilot Chat, Agent Mode를 실전에서 효과적으로 활용하는 전략과 팀 단위 도입 방법을 다룹니다.