본문으로 건너뛰기
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. 10장: 실전 프로젝트 - AI 개발 워크플로우 통합 시스템
2026년 2월 6일·AI / ML·

10장: 실전 프로젝트 - AI 개발 워크플로우 통합 시스템

전체 시리즈에서 다룬 AI 코드 리뷰, 테스트 생성, 문서화, PR 분석을 하나의 통합 시스템으로 구축하는 실전 프로젝트를 진행합니다.

21분1,166자6개 섹션
devtoolsautomationcode-qualitydevopsllm
공유
ai-dev-workflow10 / 10
12345678910
이전9장: AI 통합 CI/CD 파이프라인 구축

프로젝트 개요

이 장에서는 시리즈 전체에서 다룬 기술을 조합하여 실제로 동작하는 AI 통합 개발 워크플로우 시스템을 구축합니다. 하나의 GitHub 리포지토리에 설치하면, PR 생성 시 자동으로 코드 리뷰, 테스트 제안, 문서 영향 분석, 변경 위험도 평가를 수행하는 완전한 시스템입니다.

구축할 시스템의 기능

text
AI Dev Workflow System v1.0 기능:
 
  PR 생성 시 자동 실행:
    1. 변경 분류: 파일 변경을 의미적으로 분류
    2. 코드 리뷰: 버그, 보안, 성능 문제 식별
    3. 테스트 제안: 부족한 테스트 식별 및 생성 제안
    4. 문서 영향: 갱신이 필요한 문서 식별
    5. 위험도 평가: 변경의 종합 위험도 산출
    6. 통합 리포트: 모든 결과를 하나의 리포트로 제공
 
  설정 가능:
    - 프로젝트별 리뷰 기준 커스터마이징
    - 단계별 활성화/비활성화
    - 비용 예산 설정
    - 알림 설정

프로젝트 구조

text
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 환경에서 실행됩니다.

src/index.ts
typescript
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 클라이언트

LLM API 호출을 추상화하는 클라이언트입니다. 재시도, 비용 추적, 에러 처리를 포함합니다.

src/utils/llm-client.ts
typescript
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 }

변경 분류기

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

코드 리뷰어

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

테스트 제안기

src/stages/test-suggester.ts
typescript
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-pipeline.yml
yaml
# 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 네이밍
    - 에러 처리 필수

배포와 설치

새 프로젝트에 설치하는 방법

이 시스템을 새 프로젝트에 설치하는 단계를 정리합니다.

text
설치 단계:
 
  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을 생성하여 파이프라인 동작 확인

트러블슈팅

실전에서 자주 발생하는 문제와 해결 방법을 정리합니다.

text
문제: GitHub Actions에서 GITHUB_TOKEN 권한 부족
해결: 워크플로우의 permissions에
      pull-requests: write 권한 추가
 
문제: LLM 응답 파싱 실패
해결: JSON 블록 추출 로직 강화,
      응답에서 마크다운 코드 블록 패턴 매칭
 
문제: 대규모 PR에서 토큰 한도 초과
해결: 파일 우선순위 기반 선별적 리뷰,
      diff만 전송하고 전체 파일은 필요시에만
 
문제: 리뷰 코멘트의 줄 번호가 잘못됨
해결: diff 기반 줄 번호 매핑,
      유효하지 않은 줄 번호 필터링
 
문제: API 비용이 예상보다 높음
해결: 비용 추적 활성화,
      불필요한 단계 비활성화,
      소규모 PR에서 가벼운 모델 사용

발전 방향

단기 개선

text
단기 개선 (1-2개월):
 
  학습 기능:
    - 리뷰어가 무시한 피드백을 학습하여 정밀도 향상
    - 프로젝트별 패턴 학습
 
  더 나은 맥락:
    - Git blame 기반 코드 소유자 식별
    - 최근 이슈/버그 이력 참조
 
  UI 개선:
    - GitHub Check Run으로 상세 결과 표시
    - 접을 수 있는 섹션으로 리포트 구성

중기 발전

text
중기 발전 (3-6개월):
 
  자동 수정:
    - 코드 리뷰에서 발견된 문제를 자동 수정하는 PR 생성
    - 테스트 제안을 실제 테스트 파일로 생성
 
  팀 분석:
    - 팀 전체의 코드 품질 트렌드 분석
    - 자주 발생하는 문제 패턴 리포트
    - 개발자별 강점/약점 분석 (비공개)
 
  다른 플랫폼 지원:
    - GitLab CI 통합
    - Bitbucket Pipelines 통합

장기 비전

text
장기 비전:
 
  자율적 개발 보조:
    - 이슈 분석에서 구현까지 자동화
    - 코드 리뷰 피드백 자동 반영
    - 배포 후 문제 자동 감지 및 롤백
 
  조직 수준 품질 관리:
    - 마이크로서비스 간 API 호환성 검증
    - 보안 정책 자동 적용
    - 아키텍처 가이드라인 준수 검증

시리즈 정리

이 시리즈를 통해 AI 기반 개발 워크플로우의 전체 그림을 그리고, 각 구성 요소를 구축하고, 하나의 통합 시스템으로 완성했습니다.

시리즈 핵심 요약

text
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를 활용한 개발 워크플로우 혁신의 실용적인 가이드가 되기를 바랍니다.

이 글이 도움이 되셨나요?

관련 주제 더 보기

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

관련 글

AI / ML

9장: AI 통합 CI/CD 파이프라인 구축

코드 리뷰, 테스트 생성, 문서화, PR 분석을 하나의 CI/CD 파이프라인으로 통합하고, 품질 게이트와 비용 관리 전략을 수립합니다.

2026년 2월 4일·18분
AI / ML

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

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

2026년 2월 2일·17분
AI / ML

7장: GitHub Copilot 심층 활용 전략

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

2026년 1월 31일·19분
이전 글9장: AI 통합 CI/CD 파이프라인 구축

댓글

목차

약 21분 남음
  • 프로젝트 개요
    • 구축할 시스템의 기능
    • 프로젝트 구조
  • 핵심 구현
    • 진입점
    • LLM 클라이언트
    • 변경 분류기
    • 코드 리뷰어
    • 테스트 제안기
  • 설정 파일 명세
    • 프로젝트 설정
  • 배포와 설치
    • 새 프로젝트에 설치하는 방법
    • 트러블슈팅
  • 발전 방향
    • 단기 개선
    • 중기 발전
    • 장기 비전
  • 시리즈 정리
    • 시리즈 핵심 요약
    • 도입 시 고려 사항