본문으로 건너뛰기
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. 4장: 코드 스멜 감지와 기술 부채 정량화
2026년 3월 11일·AI / ML·

4장: 코드 스멜 감지와 기술 부채 정량화

LLM 기반 코드 스멜 탐지와 CodeScene Code Health 메트릭을 활용한 기술 부채 정량화를 학습합니다. 우선순위 기반 리팩터링 계획 수립까지 다룹니다.

17분938자7개 섹션
code-qualityaillmdevtools
공유
code-analysis4 / 10
12345678910
이전3장: 레거시 코드 이해와 문서화다음5장: LLM 기반 자동 리팩터링

학습 목표

  • 코드 스멜의 분류 체계와 각 유형의 위험성을 이해합니다
  • LLM을 활용한 코드 스멜 탐지 기법을 학습합니다
  • CodeScene Code Health 메트릭의 원리와 활용법을 파악합니다
  • 기술 부채를 정량화하고 우선순위 기반 리팩터링 계획을 수립합니다

코드 스멜이란

코드 스멜(Code Smell)은 Martin Fowler가 정의한 개념으로, 코드에 더 깊은 문제가 존재할 수 있음을 암시하는 표면적 징후입니다. 코드 스멜 자체가 버그는 아니지만, 유지보수를 어렵게 하고 결함 발생 확률을 높입니다.

코드 스멜 분류 체계

코드 스멜은 크게 다섯 가지 범주로 분류됩니다.

범주스멜 유형위험도
비대함(Bloaters)긴 메서드, 큰 클래스, 긴 매개변수 목록, 데이터 덩어리높음
객체지향 남용(OO Abusers)Switch 문, 거부된 유산, 대안 클래스중간
변경 방해(Change Preventers)산탄총 수술, 분산된 변경, 병렬 상속높음
불필요한 것(Dispensables)중복 코드, 죽은 코드, 추측성 일반화, 게으른 클래스중간
결합도(Couplers)기능 선망, 부적절한 친밀, 메시지 체인, 중간자높음

비대한 함수: 가장 흔한 스멜

bloated_function_example.py
python
def process_order(order_data: dict) -> dict:
    """전형적인 '긴 메서드' 코드 스멜 예시"""
    # 유효성 검사 (20줄)
    if not order_data.get("customer_id"):
        raise ValueError("고객 ID가 필요합니다")
    if not order_data.get("items"):
        raise ValueError("주문 항목이 필요합니다")
    if not isinstance(order_data["items"], list):
        raise TypeError("주문 항목은 리스트여야 합니다")
    for item in order_data["items"]:
        if not item.get("product_id"):
            raise ValueError("상품 ID가 필요합니다")
        if not item.get("quantity") or item["quantity"] <= 0:
            raise ValueError("수량은 양수여야 합니다")
 
    # 가격 계산 (15줄)
    subtotal = 0
    for item in order_data["items"]:
        price = get_product_price(item["product_id"])
        discount = get_discount(
            item["product_id"], order_data["customer_id"]
        )
        item_total = price * item["quantity"] * (1 - discount)
        subtotal += item_total
 
    tax = subtotal * 0.1
    shipping = 3000 if subtotal < 50000 else 0
    total = subtotal + tax + shipping
 
    # 재고 확인 및 차감 (10줄)
    for item in order_data["items"]:
        stock = check_stock(item["product_id"])
        if stock < item["quantity"]:
            raise ValueError(f"재고 부족: {item['product_id']}")
        decrease_stock(item["product_id"], item["quantity"])
 
    # 주문 저장 (10줄)
    order = {
        "customer_id": order_data["customer_id"],
        "items": order_data["items"],
        "subtotal": subtotal,
        "tax": tax,
        "shipping": shipping,
        "total": total,
        "status": "confirmed",
    }
    save_order(order)
 
    # 알림 발송 (5줄)
    send_email(order_data["customer_id"], "주문 확인", order)
    send_push_notification(order_data["customer_id"], "주문이 확인되었습니다")
 
    return order

이 함수는 유효성 검사, 가격 계산, 재고 관리, 데이터 저장, 알림 발송 등 다섯 가지 이상의 책임을 가지고 있습니다. 전형적인 단일 책임 원칙(SRP) 위반입니다.


LLM 기반 코드 스멜 탐지

규칙 기반 vs LLM 기반 탐지

전통적인 스멜 탐지 도구는 메트릭 임계값에 의존합니다. "메서드가 30줄 이상이면 긴 메서드"처럼 기계적입니다. 하지만 LLM은 코드의 의미를 고려하여 더 정교한 판단이 가능합니다.

smell_detector.py
python
from dataclasses import dataclass
from enum import Enum
 
 
class SmellSeverity(Enum):
    LOW = "low"
    MEDIUM = "medium"
    HIGH = "high"
    CRITICAL = "critical"
 
 
class SmellCategory(Enum):
    BLOATER = "bloater"
    OO_ABUSER = "oo_abuser"
    CHANGE_PREVENTER = "change_preventer"
    DISPENSABLE = "dispensable"
    COUPLER = "coupler"
 
 
@dataclass
class CodeSmell:
    name: str
    category: SmellCategory
    severity: SmellSeverity
    location: str
    description: str
    suggestion: str
    confidence: float  # 0.0 ~ 1.0
 
 
SMELL_DETECTION_PROMPT = """다음 코드를 분석하고 코드 스멜을 탐지하세요.
 
코드:
---
{code}
---
 
메트릭 정보:
- 순환 복잡도: {complexity}
- 줄 수: {line_count}
- 매개변수 수: {param_count}
- 의존성 수: {dependency_count}
 
다음 코드 스멜 유형을 확인하세요:
1. 비대함: 긴 메서드, 큰 클래스, 긴 매개변수 목록
2. 객체지향 남용: Switch 문 남용, 거부된 유산
3. 변경 방해: 산탄총 수술, 분산된 변경
4. 불필요한 것: 중복 코드, 죽은 코드
5. 결합도: 기능 선망, 부적절한 친밀
 
각 스멜에 대해 다음을 제공하세요:
- 스멜 이름
- 심각도 (low/medium/high/critical)
- 해당 위치 (줄 번호 또는 함수명)
- 구체적 설명
- 리팩터링 제안
- 확신도 (0.0-1.0)"""
 
 
class LLMSmellDetector:
    """LLM을 활용한 코드 스멜 탐지기"""
 
    def __init__(self, llm_client, ast_analyzer):
        self.llm_client = llm_client
        self.ast_analyzer = ast_analyzer
 
    async def detect_smells(
        self, source: str, filepath: str
    ) -> list[CodeSmell]:
        # AST 기반 사전 분석
        metrics = self.ast_analyzer.analyze(source)
 
        # 메트릭 기반 1차 필터링
        candidates = self._filter_candidates(metrics)
 
        smells = []
        for candidate in candidates:
            # LLM 기반 심층 분석
            prompt = SMELL_DETECTION_PROMPT.format(
                code=candidate["code"],
                complexity=candidate["complexity"],
                line_count=candidate["line_count"],
                param_count=candidate["param_count"],
                dependency_count=candidate["dependency_count"],
            )
 
            response = await self.llm_client.generate(prompt)
            detected = self._parse_smells(response)
            smells.extend(detected)
 
        return self._deduplicate_and_rank(smells)
 
    def _filter_candidates(self, metrics: dict) -> list[dict]:
        """메트릭 임계값으로 1차 필터링"""
        candidates = []
        for func in metrics.get("functions", []):
            if (func["complexity"] > 5
                    or func["line_count"] > 20
                    or func["param_count"] > 4):
                candidates.append(func)
        return candidates
 
    def _deduplicate_and_rank(
        self, smells: list[CodeSmell]
    ) -> list[CodeSmell]:
        """중복 제거 및 심각도/확신도 기준 정렬"""
        seen = set()
        unique = []
        for smell in smells:
            key = (smell.name, smell.location)
            if key not in seen:
                seen.add(key)
                unique.append(smell)
 
        return sorted(
            unique,
            key=lambda s: (
                s.severity.value,
                -s.confidence,
            ),
            reverse=True,
        )
 
    def _parse_smells(self, response: str) -> list[CodeSmell]:
        # LLM 응답 파싱 (간략화)
        return []
Info

AST 메트릭으로 1차 필터링 후 LLM에 전달하는 2단계 접근은 비용 효율성과 정확도를 모두 확보합니다. 전체 코드베이스를 LLM에 전달하면 비용이 급증하지만, 메트릭 기반 필터링으로 분석 대상을 20-30%로 줄일 수 있습니다.


CodeScene Code Health 메트릭

CodeScene은 코드 건강도(Code Health)를 1-10 점수로 정량화하는 도구입니다. 이 메트릭의 핵심 발견들은 코드 품질 관리에 중대한 시사점을 제공합니다.

Code Health의 핵심 발견

메트릭불건강한 코드건강한 코드비율
결함 밀도높음낮음15배
개발 속도느림빠름2배
추정 불확실성높음낮음10배

불건강한 코드는 건강한 코드보다 15배 더 많은 결함을 유발합니다. 이는 코드 품질이 단순한 미학적 문제가 아니라 비즈니스 리스크임을 의미합니다.

Code Health 점수 계산 요소

핫스팟 분석

CodeScene의 가장 강력한 기능 중 하나는 핫스팟 분석(Hotspot Analysis)입니다. Git 히스토리를 분석하여 자주 변경되면서 동시에 복잡한 코드를 식별합니다.

hotspot_analyzer.py
python
import subprocess
from collections import Counter
from dataclasses import dataclass
 
 
@dataclass
class Hotspot:
    filepath: str
    change_frequency: int
    complexity: float
    hotspot_score: float
    last_change: str
 
 
class HotspotAnalyzer:
    """Git 히스토리 기반 핫스팟 분석"""
 
    def __init__(self, repo_path: str, months: int = 6):
        self.repo_path = repo_path
        self.months = months
 
    def analyze(self) -> list[Hotspot]:
        # Git 로그에서 변경 빈도 추출
        change_freq = self._get_change_frequencies()
 
        # 각 파일의 복잡도 계산
        hotspots = []
        for filepath, freq in change_freq.most_common(50):
            complexity = self._calculate_file_complexity(filepath)
            score = freq * complexity  # 핫스팟 점수
 
            hotspots.append(Hotspot(
                filepath=filepath,
                change_frequency=freq,
                complexity=round(complexity, 2),
                hotspot_score=round(score, 2),
                last_change=self._get_last_change(filepath),
            ))
 
        return sorted(hotspots, key=lambda h: h.hotspot_score, reverse=True)
 
    def _get_change_frequencies(self) -> Counter:
        result = subprocess.run(
            [
                "git", "log",
                f"--since={self.months} months ago",
                "--name-only",
                "--pretty=format:",
            ],
            capture_output=True,
            text=True,
            cwd=self.repo_path,
        )
 
        files = [
            line.strip() for line in result.stdout.splitlines()
            if line.strip() and not line.startswith(".")
        ]
        return Counter(files)
 
    def _calculate_file_complexity(self, filepath: str) -> float:
        # AST 기반 복잡도 계산 (간략화)
        return 1.0
 
    def _get_last_change(self, filepath: str) -> str:
        result = subprocess.run(
            ["git", "log", "-1", "--format=%ci", "--", filepath],
            capture_output=True,
            text=True,
            cwd=self.repo_path,
        )
        return result.stdout.strip()

기술 부채 정량화

42%의 현실

Stripe의 조사에 따르면 전 세계 개발자들은 평균적으로 전체 개발 시간의 42%를 기술 부채에 소비합니다. 이를 비용으로 환산하면 다음과 같습니다.

  • 개발자 연봉이 1억 원이라면 약 4,200만 원이 기술 부채 처리에 소모됩니다
  • 10명 팀 기준 연간 4억 2천만 원의 숨겨진 비용이 발생합니다
  • 이 중 상당 부분은 자동화로 줄일 수 있습니다

기술 부채 점수 산출

debt_calculator.py
python
from dataclasses import dataclass
from enum import Enum
 
 
class DebtCategory(Enum):
    DESIGN = "design"
    CODE = "code"
    TEST = "test"
    DOCUMENTATION = "documentation"
    DEPENDENCY = "dependency"
 
 
@dataclass
class TechnicalDebt:
    category: DebtCategory
    description: str
    estimated_hours: float
    business_impact: str  # low, medium, high, critical
    affected_files: list[str]
    priority_score: float
 
 
class DebtCalculator:
    """기술 부채를 정량화하는 도구"""
 
    SEVERITY_WEIGHTS = {
        "critical": 4.0,
        "high": 3.0,
        "medium": 2.0,
        "low": 1.0,
    }
 
    def calculate_debt(
        self,
        smells: list[dict],
        hotspots: list[dict],
        test_coverage: float,
    ) -> list[TechnicalDebt]:
        debts = []
 
        # 코드 스멜 기반 부채
        for smell in smells:
            hours = self._estimate_remediation_hours(smell)
            debts.append(TechnicalDebt(
                category=DebtCategory.CODE,
                description=smell["description"],
                estimated_hours=hours,
                business_impact=smell["severity"],
                affected_files=smell.get("files", []),
                priority_score=self._calculate_priority(
                    hours, smell["severity"],
                    smell.get("change_frequency", 1),
                ),
            ))
 
        # 핫스팟 기반 부채
        for hotspot in hotspots:
            if hotspot["complexity"] > 15:
                debts.append(TechnicalDebt(
                    category=DebtCategory.DESIGN,
                    description=(
                        f"핫스팟 파일 리팩터링 필요: {hotspot['filepath']}"
                    ),
                    estimated_hours=hotspot["complexity"] * 0.5,
                    business_impact="high",
                    affected_files=[hotspot["filepath"]],
                    priority_score=hotspot["hotspot_score"],
                ))
 
        # 테스트 커버리지 부채
        if test_coverage < 0.6:
            debts.append(TechnicalDebt(
                category=DebtCategory.TEST,
                description=(
                    f"테스트 커버리지 부족: {test_coverage:.0%}"
                ),
                estimated_hours=(0.6 - test_coverage) * 100,
                business_impact="high",
                affected_files=[],
                priority_score=100 * (0.6 - test_coverage),
            ))
 
        return sorted(debts, key=lambda d: d.priority_score, reverse=True)
 
    def _estimate_remediation_hours(self, smell: dict) -> float:
        base_hours = {
            "long_method": 2.0,
            "large_class": 4.0,
            "duplicate_code": 3.0,
            "dead_code": 0.5,
            "feature_envy": 2.0,
            "shotgun_surgery": 6.0,
        }
        return base_hours.get(smell.get("type", ""), 2.0)
 
    def _calculate_priority(
        self,
        hours: float,
        severity: str,
        change_frequency: int,
    ) -> float:
        """우선순위 = 심각도 가중치 * 변경 빈도 / 수정 시간"""
        weight = self.SEVERITY_WEIGHTS.get(severity, 1.0)
        return (weight * change_frequency) / max(hours, 0.5)

우선순위 기반 리팩터링 계획

Tip

모든 기술 부채를 한꺼번에 해결하려 하지 마세요. 핫스팟 분석에서 변경 빈도가 높고 복잡도가 높은 코드를 우선 리팩터링하면, 적은 노력으로 최대의 효과를 얻을 수 있습니다. 이를 "전략적 기술 부채 관리"라고 합니다.

우선순위 공식은 다음과 같습니다.

우선순위 = (심각도 가중치 x 변경 빈도) / 수정 예상 시간

이 공식에 따르면 심각도가 높고, 자주 변경되며, 수정이 비교적 쉬운 코드가 최우선 대상이 됩니다. 반대로 심각하더라도 거의 변경되지 않는 코드는 우선순위가 낮아집니다.


정리

코드 스멜은 더 깊은 코드 품질 문제를 암시하는 표면적 징후이며, 비대함/객체지향 남용/변경 방해/불필요한 것/결합도의 다섯 가지 범주로 분류됩니다. LLM 기반 탐지는 AST 메트릭으로 1차 필터링 후 의미적 분석을 수행하여 전통 도구보다 정교한 탐지가 가능합니다.

CodeScene의 Code Health 메트릭은 불건강한 코드가 15배 더 많은 결함을 유발한다는 것을 정량적으로 증명했으며, 핫스팟 분석을 통해 우선순위 기반 리팩터링 계획을 수립할 수 있습니다. 기술 부채에 소모되는 42%의 개발 시간을 전략적으로 줄이는 것이 목표입니다.

다음 장 미리보기

5장에서는 탐지된 코드 스멜을 실제로 수정하는 LLM 기반 자동 리팩터링을 다룹니다. 함수 추출, 이름 변경, 메서드 이동 등 리팩터링 패턴과, RepoAI의 멀티에이전트 리팩터링 아키텍처, 그리고 37%에서 98%로 정밀도를 끌어올리는 검증 파이프라인을 학습합니다.

이 글이 도움이 되셨나요?

관련 주제 더 보기

#code-quality#ai#llm#devtools

관련 글

AI / ML

5장: LLM 기반 자동 리팩터링

LLM을 활용한 자동 리팩터링의 패턴, 멀티에이전트 아키텍처, 검증 파이프라인을 학습합니다. 37%에서 98%로 정밀도를 끌어올리는 실전 기법을 다룹니다.

2026년 3월 13일·15분
AI / ML

3장: 레거시 코드 이해와 문서화

LLM을 활용하여 레거시 코드베이스를 자동으로 탐색하고 문서화하는 기법을 학습합니다. 의존성 그래프 추출, 아키텍처 다이어그램 생성, 인라인 주석 자동 생성을 다룹니다.

2026년 3월 9일·18분
AI / ML

6장: 코드 마이그레이션 자동화

LLM을 활용한 언어/프레임워크 마이그레이션 자동화를 학습합니다. Java에서 Kotlin, React Class에서 Hooks로의 전환과 의미 보존 검증 기법을 다룹니다.

2026년 3월 15일·16분
이전 글3장: 레거시 코드 이해와 문서화
다음 글5장: LLM 기반 자동 리팩터링

댓글

목차

약 17분 남음
  • 학습 목표
  • 코드 스멜이란
    • 코드 스멜 분류 체계
    • 비대한 함수: 가장 흔한 스멜
  • LLM 기반 코드 스멜 탐지
    • 규칙 기반 vs LLM 기반 탐지
  • CodeScene Code Health 메트릭
    • Code Health의 핵심 발견
    • Code Health 점수 계산 요소
    • 핫스팟 분석
  • 기술 부채 정량화
    • 42%의 현실
    • 기술 부채 점수 산출
    • 우선순위 기반 리팩터링 계획
  • 정리
  • 다음 장 미리보기