본문으로 건너뛰기
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. 9장: CI/CD 통합과 지속적 코드 품질 관리
2026년 3월 21일·AI / ML·

9장: CI/CD 통합과 지속적 코드 품질 관리

LLM 기반 코드 분석을 CI/CD 파이프라인에 통합하는 방법을 학습합니다. PR별 자동 분석, 품질 게이트, 기술 부채 대시보드와 GitHub Actions 구축을 다룹니다.

16분1,364자9개 섹션
code-qualityaillmdevtools
공유
code-analysis9 / 10
12345678910
이전8장: 아키텍처 분석과 시각화다음10장: 실전 프로젝트 -- LLM 코드 분석 파이프라인 구축

학습 목표

  • PR별 코드 분석 자동화 파이프라인을 구축합니다
  • 품질 게이트 설계와 정책 수립 방법을 학습합니다
  • 기술 부채 트렌드 대시보드의 구조를 이해합니다
  • GitHub Actions를 활용한 리팩터링 제안 자동 코멘트를 구현합니다

지속적 코드 품질 관리의 원칙

코드 분석이 일회성 이벤트가 아닌 지속적인 프로세스가 되려면 CI/CD 파이프라인에 통합되어야 합니다. 모든 코드 변경이 자동으로 분석되고, 품질 기준을 충족하지 못하면 머지가 차단되는 체계가 필요합니다.

핵심 원칙 세 가지

첫째, 빠른 피드백. 개발자가 PR을 올린 직후 분석 결과를 받아야 합니다. 분석이 30분 걸리면 개발자는 다른 작업으로 넘어가고, 피드백의 효과가 급감합니다.

둘째, 점진적 개선. 기존 코드베이스의 모든 문제를 한꺼번에 차단하면 개발이 멈춥니다. "새로 추가되는 코드"에 대해서만 규칙을 적용하고, 기존 문제는 별도 계획으로 해소합니다.

셋째, 실행 가능한 제안. "복잡도가 높습니다"라는 경고보다 "이 함수를 세 개로 분리하세요"라는 구체적 제안이 효과적입니다. LLM이 바로 이 역할을 합니다.


PR별 코드 분석 자동화

GitHub Actions 워크플로우

.github/workflows/code-analysis.yml
yaml
name: Code Analysis
 
on:
  pull_request:
    branches: [main, develop]
    paths:
      - "src/**"
      - "lib/**"
 
permissions:
  contents: read
  pull-requests: write
 
jobs:
  analyze:
    runs-on: ubuntu-latest
    timeout-minutes: 15
 
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
 
      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"
 
      - name: 의존성 설치
        run: pip install -r requirements-analysis.txt
 
      - name: 변경 파일 목록 추출
        id: changed
        run: |
          FILES=$(git diff --name-only origin/main...HEAD -- '*.py' '*.ts')
          echo "files=$FILES" >> $GITHUB_OUTPUT
 
      - name: AST 기반 메트릭 분석
        run: |
          python scripts/analyze_metrics.py \
            --files="${{ steps.changed.outputs.files }}" \
            --output=metrics.json
 
      - name: 코드 스멜 탐지
        run: |
          python scripts/detect_smells.py \
            --metrics=metrics.json \
            --output=smells.json
 
      - name: LLM 심층 분석
        if: steps.changed.outputs.files != ''
        env:
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
        run: |
          python scripts/llm_analysis.py \
            --smells=smells.json \
            --output=analysis.json
 
      - name: 품질 게이트 평가
        id: gate
        run: |
          python scripts/quality_gate.py \
            --analysis=analysis.json \
            --policy=.quality-gate.yml
 
      - name: PR 코멘트 작성
        if: always()
        uses: actions/github-script@v7
        with:
          script: |
            const fs = require('fs');
            const analysis = JSON.parse(
              fs.readFileSync('analysis.json', 'utf8')
            );
            const report = buildReport(analysis);
 
            await github.rest.issues.createComment({
              owner: context.repo.owner,
              repo: context.repo.repo,
              issue_number: context.issue.number,
              body: report,
            });

분석 스크립트 구현

scripts/analyze_metrics.py
python
#!/usr/bin/env python3
"""PR 변경 파일의 메트릭을 분석하는 스크립트"""
 
import argparse
import ast
import json
from pathlib import Path
 
 
def analyze_file(filepath: str) -> dict:
    source = Path(filepath).read_text()
    tree = ast.parse(source)
 
    functions = []
    for node in ast.walk(tree):
        if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
            complexity = calculate_complexity(node)
            functions.append({
                "name": node.name,
                "line_start": node.lineno,
                "line_end": node.end_lineno,
                "complexity": complexity,
                "param_count": len(node.args.args),
                "line_count": (node.end_lineno or node.lineno) - node.lineno + 1,
            })
 
    return {
        "filepath": filepath,
        "total_lines": len(source.splitlines()),
        "function_count": len(functions),
        "functions": functions,
        "avg_complexity": (
            sum(f["complexity"] for f in functions) / len(functions)
            if functions else 0
        ),
        "max_complexity": (
            max(f["complexity"] for f in functions)
            if functions else 0
        ),
    }
 
 
def calculate_complexity(node: ast.AST) -> int:
    complexity = 1
    for child in ast.walk(node):
        if isinstance(child, (ast.If, ast.While, ast.For, ast.ExceptHandler)):
            complexity += 1
        elif isinstance(child, ast.BoolOp):
            complexity += len(child.values) - 1
    return complexity
 
 
def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("--files", required=True)
    parser.add_argument("--output", required=True)
    args = parser.parse_args()
 
    files = [f.strip() for f in args.files.split() if f.strip()]
    results = []
 
    for filepath in files:
        if filepath.endswith(".py") and Path(filepath).exists():
            try:
                results.append(analyze_file(filepath))
            except SyntaxError:
                results.append({
                    "filepath": filepath,
                    "error": "구문 분석 실패",
                })
 
    with open(args.output, "w") as f:
        json.dump(results, f, indent=2, ensure_ascii=False)
 
 
if __name__ == "__main__":
    main()

품질 게이트 설계

게이트 정책 정의

품질 게이트는 코드가 머지되기 위해 충족해야 하는 최소 기준입니다. 정책은 YAML로 정의합니다.

.quality-gate.yml
yaml
# 품질 게이트 정책
version: 1
 
# 새 코드에만 적용되는 규칙
new_code:
  max_complexity: 15
  max_function_length: 50
  max_file_length: 400
  max_parameters: 5
  min_test_coverage: 0.8
 
# 전체 코드베이스 규칙 (경고만)
overall:
  max_avg_complexity: 8
  max_tech_debt_hours: 200
  min_code_health: 6
 
# 게이트 동작
actions:
  block:
    - new_code.max_complexity
    - new_code.max_function_length
  warn:
    - new_code.max_parameters
    - overall.max_avg_complexity
  info:
    - overall.max_tech_debt_hours
 
# 예외 허용
exceptions:
  files:
    - "**/*_test.py"
    - "**/*.test.ts"
    - "**/migrations/**"

품질 게이트 평가기

scripts/quality_gate.py
python
#!/usr/bin/env python3
"""품질 게이트 평가 스크립트"""
 
import argparse
import json
from dataclasses import dataclass
 
import yaml
 
 
@dataclass
class GateResult:
    passed: bool
    blockers: list[str]
    warnings: list[str]
    info: list[str]
 
 
class QualityGate:
    def __init__(self, policy_path: str):
        with open(policy_path) as f:
            self.policy = yaml.safe_load(f)
 
    def evaluate(self, analysis: list[dict]) -> GateResult:
        blockers = []
        warnings = []
        info_items = []
 
        new_code_rules = self.policy.get("new_code", {})
 
        for file_result in analysis:
            if file_result.get("error"):
                continue
 
            filepath = file_result["filepath"]
 
            # 예외 파일 건너뛰기
            if self._is_exception(filepath):
                continue
 
            for func in file_result.get("functions", []):
                # 복잡도 검사
                max_cx = new_code_rules.get("max_complexity", 15)
                if func["complexity"] > max_cx:
                    blockers.append(
                        f"{filepath}:{func['line_start']} "
                        f"- {func['name']}의 순환 복잡도가 "
                        f"{func['complexity']}입니다 (최대: {max_cx})"
                    )
 
                # 함수 길이 검사
                max_len = new_code_rules.get("max_function_length", 50)
                if func["line_count"] > max_len:
                    blockers.append(
                        f"{filepath}:{func['line_start']} "
                        f"- {func['name']}이(가) {func['line_count']}줄입니다 "
                        f"(최대: {max_len})"
                    )
 
                # 매개변수 수 검사
                max_params = new_code_rules.get("max_parameters", 5)
                if func["param_count"] > max_params:
                    warnings.append(
                        f"{filepath}:{func['line_start']} "
                        f"- {func['name']}의 매개변수가 "
                        f"{func['param_count']}개입니다 (권장: {max_params}개 이하)"
                    )
 
        return GateResult(
            passed=len(blockers) == 0,
            blockers=blockers,
            warnings=warnings,
            info=info_items,
        )
 
    def _is_exception(self, filepath: str) -> bool:
        exceptions = self.policy.get("exceptions", {}).get("files", [])
        from fnmatch import fnmatch
        return any(fnmatch(filepath, pattern) for pattern in exceptions)
 
 
def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("--analysis", required=True)
    parser.add_argument("--policy", required=True)
    args = parser.parse_args()
 
    with open(args.analysis) as f:
        analysis = json.load(f)
 
    gate = QualityGate(args.policy)
    result = gate.evaluate(analysis)
 
    print(f"품질 게이트: {'통과' if result.passed else '실패'}")
 
    if result.blockers:
        print(f"\n차단 사항 ({len(result.blockers)}건):")
        for b in result.blockers:
            print(f"  - {b}")
 
    if result.warnings:
        print(f"\n경고 ({len(result.warnings)}건):")
        for w in result.warnings:
            print(f"  - {w}")
 
    if not result.passed:
        exit(1)
 
 
if __name__ == "__main__":
    main()
Tip

품질 게이트는 팀의 성숙도에 맞게 점진적으로 강화해야 합니다. 처음에는 극단적인 경우(복잡도 30 이상, 함수 100줄 이상)만 차단하고, 팀이 적응하면 기준을 점차 낮춥니다. 갑자기 엄격한 기준을 적용하면 개발 속도가 급감하고 팀의 반발을 초래합니다.


리팩터링 제안 자동 코멘트

LLM 기반 PR 리뷰 코멘트

품질 게이트를 통과하지 못한 코드에 대해 LLM이 구체적인 리팩터링 제안을 PR 코멘트로 작성합니다.

scripts/llm_analysis.py
python
#!/usr/bin/env python3
"""LLM 기반 심층 분석 및 리팩터링 제안"""
 
import argparse
import json
from anthropic import Anthropic
 
 
REVIEW_PROMPT = """코드 리뷰어로서 다음 코드 변경을 분석하세요.
 
파일: {filepath}
문제가 있는 함수:
{code}
 
감지된 메트릭:
- 순환 복잡도: {complexity}
- 줄 수: {line_count}
- 매개변수 수: {param_count}
 
다음 형식으로 리뷰를 작성하세요:
 
## 문제 요약
(한 문장으로 핵심 문제를 설명)
 
## 리팩터링 제안
(구체적인 개선 방안을 코드와 함께 제시)
 
### 예상 효과
(리팩터링 후 개선되는 메트릭)"""
 
 
def analyze_with_llm(
    smells_data: list[dict],
    api_key: str,
) -> list[dict]:
    client = Anthropic(api_key=api_key)
    results = []
 
    for file_data in smells_data:
        filepath = file_data["filepath"]
        for smell in file_data.get("smells", []):
            prompt = REVIEW_PROMPT.format(
                filepath=filepath,
                code=smell["code"],
                complexity=smell.get("complexity", "N/A"),
                line_count=smell.get("line_count", "N/A"),
                param_count=smell.get("param_count", "N/A"),
            )
 
            response = client.messages.create(
                model="claude-sonnet-4-20250514",
                max_tokens=1024,
                messages=[{"role": "user", "content": prompt}],
            )
 
            results.append({
                "filepath": filepath,
                "line": smell.get("line_start", 0),
                "review": response.content[0].text,
                "severity": smell.get("severity", "medium"),
            })
 
    return results
 
 
def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("--smells", required=True)
    parser.add_argument("--output", required=True)
    args = parser.parse_args()
 
    import os
    api_key = os.environ.get("ANTHROPIC_API_KEY")
    if not api_key:
        print("ANTHROPIC_API_KEY가 설정되지 않았습니다")
        exit(1)
 
    with open(args.smells) as f:
        smells_data = json.load(f)
 
    results = analyze_with_llm(smells_data, api_key)
 
    with open(args.output, "w") as f:
        json.dump(results, f, indent=2, ensure_ascii=False)
 
 
if __name__ == "__main__":
    main()

PR 코멘트 포맷

생성된 분석 결과를 PR 코멘트로 변환하면 다음과 같은 형태가 됩니다.

markdown
## Code Analysis Report
 
### 품질 게이트: 실패
 
**차단 사항 (2건)**
- `src/services/order.py:45` - process_order의 순환 복잡도가 23입니다 (최대: 15)
- `src/services/order.py:45` - process_order이(가) 78줄입니다 (최대: 50)
 
---
 
### process_order 리팩터링 제안
 
**문제 요약**: 주문 처리 함수가 유효성 검사, 가격 계산, 재고 관리, 알림까지
다섯 가지 책임을 가지고 있어 순환 복잡도가 23에 달합니다.
 
**리팩터링 제안**: 각 책임을 별도 함수로 추출하고,
오케스트레이션 패턴을 적용하세요.
 
**예상 효과**: 순환 복잡도 23에서 5 이하로, 함수 길이 78줄에서 15줄 이하로 감소

기술 부채 트렌드 대시보드

트렌드 데이터 수집

scripts/collect_trends.py
python
#!/usr/bin/env python3
"""분석 결과를 시계열 데이터로 저장"""
 
import json
from datetime import datetime
from pathlib import Path
 
 
def collect_trend_data(analysis_path: str, output_dir: str):
    with open(analysis_path) as f:
        analysis = json.load(f)
 
    today = datetime.now().strftime("%Y-%m-%d")
    trend_entry = {
        "date": today,
        "total_files": len(analysis),
        "total_functions": sum(
            len(f.get("functions", []))
            for f in analysis if not f.get("error")
        ),
        "avg_complexity": round(
            sum(
                f.get("avg_complexity", 0)
                for f in analysis if not f.get("error")
            ) / max(len(analysis), 1),
            2,
        ),
        "max_complexity": max(
            (f.get("max_complexity", 0) for f in analysis),
            default=0,
        ),
        "high_complexity_count": sum(
            1 for f in analysis
            for func in f.get("functions", [])
            if func.get("complexity", 0) > 15
        ),
        "smell_count": sum(
            len(f.get("smells", []))
            for f in analysis
        ),
    }
 
    # 기존 트렌드 데이터에 추가
    output_path = Path(output_dir) / "trends.json"
    trends = []
    if output_path.exists():
        trends = json.loads(output_path.read_text())
 
    trends.append(trend_entry)
 
    # 최근 90일만 유지
    if len(trends) > 90:
        trends = trends[-90:]
 
    output_path.write_text(
        json.dumps(trends, indent=2, ensure_ascii=False)
    )

트렌드 시각화

Info

트렌드 데이터는 단순한 현재 상태보다 훨씬 가치 있습니다. "현재 코드 스멜이 10건"보다 "지난 8주간 코드 스멜이 25건에서 10건으로 감소"가 팀의 노력을 더 잘 보여줍니다. 이런 가시적 개선은 코드 품질 투자에 대한 경영진의 지지를 확보하는 데에도 도움이 됩니다.


SonarQube + AI 통합

SonarQube Quality Gate와 LLM 결합

기존 SonarQube 파이프라인에 LLM 분석을 추가하여 더 정교한 코드 리뷰를 구현합니다.

.github/workflows/sonar-ai.yml
yaml
name: SonarQube + AI Analysis
 
on:
  pull_request:
    branches: [main]
 
jobs:
  sonar:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
 
      - name: SonarQube 스캔
        uses: sonarsource/sonarqube-scan-action@v2
        env:
          SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
          SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}
 
      - name: SonarQube 결과 대기
        uses: sonarsource/sonarqube-quality-gate-action@v1
        env:
          SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
 
      - name: SonarQube 이슈 추출
        run: |
          curl -s -u "${{ secrets.SONAR_TOKEN }}:" \
            "${{ secrets.SONAR_HOST_URL }}/api/issues/search?componentKeys=my-project&resolved=false" \
            > sonar-issues.json
 
      - name: LLM 기반 이슈 분석 및 수정 제안
        env:
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
        run: |
          python scripts/enhance_sonar_issues.py \
            --issues=sonar-issues.json \
            --output=enhanced-analysis.json
 
      - name: PR 코멘트 작성
        uses: actions/github-script@v7
        with:
          script: |
            const fs = require('fs');
            const analysis = JSON.parse(
              fs.readFileSync('enhanced-analysis.json', 'utf8')
            );
            // PR 코멘트 작성 로직

정리

CI/CD에 코드 분석을 통합하면 코드 품질 관리가 일회성 활동이 아닌 지속적 프로세스가 됩니다. 핵심은 빠른 피드백, 점진적 개선, 실행 가능한 제안의 세 가지 원칙입니다.

GitHub Actions 워크플로우로 PR별 자동 분석을 구축하고, YAML 기반 품질 게이트로 머지 기준을 정의하며, LLM이 구체적인 리팩터링 제안을 PR 코멘트로 제공합니다. 트렌드 대시보드는 시간에 따른 코드 품질 변화를 가시화하여 팀의 개선 노력을 증명합니다.

다음 장 미리보기

마지막 10장에서는 지금까지 학습한 모든 기법을 종합하여 실전 LLM 코드 분석 파이프라인을 구축합니다. AST 추출에서 스멜 감지, 리팩터링 제안, 검증, 적용까지 전체 과정을 하나의 프로젝트로 완성하고, 레거시 프로젝트 현대화 사례와 도입 가이드를 제공합니다.

이 글이 도움이 되셨나요?

관련 주제 더 보기

#code-quality#ai#llm#devtools

관련 글

AI / ML

10장: 실전 프로젝트 -- LLM 코드 분석 파이프라인 구축

AST 추출부터 코드 스멜 감지, 리팩터링 제안, 검증, 적용까지 전체 파이프라인을 구축하는 실전 프로젝트입니다. 레거시 프로젝트 현대화 사례와 도입 가이드를 포함합니다.

2026년 3월 23일·22분
AI / ML

8장: 아키텍처 분석과 시각화

LLM을 활용한 아키텍처 분석, 순환 의존성 감지, 레이어 위반 탐지, 마이크로서비스 경계 제안과 아키텍처 다이어그램 자동 생성을 학습합니다.

2026년 3월 19일·17분
AI / ML

7장: 보안 취약점 분석과 자동 수정

SAST와 LLM을 결합한 보안 취약점 탐지, OWASP Top 10 자동 검출, 취약점 자동 수정 제안과 CI/CD 보안 게이트 구축을 학습합니다.

2026년 3월 17일·16분
이전 글8장: 아키텍처 분석과 시각화
다음 글10장: 실전 프로젝트 -- LLM 코드 분석 파이프라인 구축

댓글

목차

약 16분 남음
  • 학습 목표
  • 지속적 코드 품질 관리의 원칙
    • 핵심 원칙 세 가지
  • PR별 코드 분석 자동화
    • GitHub Actions 워크플로우
    • 분석 스크립트 구현
  • 품질 게이트 설계
    • 게이트 정책 정의
    • 품질 게이트 평가기
  • 리팩터링 제안 자동 코멘트
    • LLM 기반 PR 리뷰 코멘트
    • PR 코멘트 포맷
  • 기술 부채 트렌드 대시보드
    • 트렌드 데이터 수집
    • 트렌드 시각화
  • SonarQube + AI 통합
    • SonarQube Quality Gate와 LLM 결합
  • 정리
  • 다음 장 미리보기