LLM 기반 코드 스멜 탐지와 CodeScene Code Health 메트릭을 활용한 기술 부채 정량화를 학습합니다. 우선순위 기반 리팩터링 계획 수립까지 다룹니다.
코드 스멜(Code Smell)은 Martin Fowler가 정의한 개념으로, 코드에 더 깊은 문제가 존재할 수 있음을 암시하는 표면적 징후입니다. 코드 스멜 자체가 버그는 아니지만, 유지보수를 어렵게 하고 결함 발생 확률을 높입니다.
코드 스멜은 크게 다섯 가지 범주로 분류됩니다.
| 범주 | 스멜 유형 | 위험도 |
|---|---|---|
| 비대함(Bloaters) | 긴 메서드, 큰 클래스, 긴 매개변수 목록, 데이터 덩어리 | 높음 |
| 객체지향 남용(OO Abusers) | Switch 문, 거부된 유산, 대안 클래스 | 중간 |
| 변경 방해(Change Preventers) | 산탄총 수술, 분산된 변경, 병렬 상속 | 높음 |
| 불필요한 것(Dispensables) | 중복 코드, 죽은 코드, 추측성 일반화, 게으른 클래스 | 중간 |
| 결합도(Couplers) | 기능 선망, 부적절한 친밀, 메시지 체인, 중간자 | 높음 |
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) 위반입니다.
전통적인 스멜 탐지 도구는 메트릭 임계값에 의존합니다. "메서드가 30줄 이상이면 긴 메서드"처럼 기계적입니다. 하지만 LLM은 코드의 의미를 고려하여 더 정교한 판단이 가능합니다.
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 []AST 메트릭으로 1차 필터링 후 LLM에 전달하는 2단계 접근은 비용 효율성과 정확도를 모두 확보합니다. 전체 코드베이스를 LLM에 전달하면 비용이 급증하지만, 메트릭 기반 필터링으로 분석 대상을 20-30%로 줄일 수 있습니다.
CodeScene은 코드 건강도(Code Health)를 1-10 점수로 정량화하는 도구입니다. 이 메트릭의 핵심 발견들은 코드 품질 관리에 중대한 시사점을 제공합니다.
| 메트릭 | 불건강한 코드 | 건강한 코드 | 비율 |
|---|---|---|---|
| 결함 밀도 | 높음 | 낮음 | 15배 |
| 개발 속도 | 느림 | 빠름 | 2배 |
| 추정 불확실성 | 높음 | 낮음 | 10배 |
불건강한 코드는 건강한 코드보다 15배 더 많은 결함을 유발합니다. 이는 코드 품질이 단순한 미학적 문제가 아니라 비즈니스 리스크임을 의미합니다.
CodeScene의 가장 강력한 기능 중 하나는 핫스팟 분석(Hotspot Analysis)입니다. Git 히스토리를 분석하여 자주 변경되면서 동시에 복잡한 코드를 식별합니다.
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()Stripe의 조사에 따르면 전 세계 개발자들은 평균적으로 전체 개발 시간의 42%를 기술 부채에 소비합니다. 이를 비용으로 환산하면 다음과 같습니다.
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)모든 기술 부채를 한꺼번에 해결하려 하지 마세요. 핫스팟 분석에서 변경 빈도가 높고 복잡도가 높은 코드를 우선 리팩터링하면, 적은 노력으로 최대의 효과를 얻을 수 있습니다. 이를 "전략적 기술 부채 관리"라고 합니다.
우선순위 공식은 다음과 같습니다.
우선순위 = (심각도 가중치 x 변경 빈도) / 수정 예상 시간
이 공식에 따르면 심각도가 높고, 자주 변경되며, 수정이 비교적 쉬운 코드가 최우선 대상이 됩니다. 반대로 심각하더라도 거의 변경되지 않는 코드는 우선순위가 낮아집니다.
코드 스멜은 더 깊은 코드 품질 문제를 암시하는 표면적 징후이며, 비대함/객체지향 남용/변경 방해/불필요한 것/결합도의 다섯 가지 범주로 분류됩니다. LLM 기반 탐지는 AST 메트릭으로 1차 필터링 후 의미적 분석을 수행하여 전통 도구보다 정교한 탐지가 가능합니다.
CodeScene의 Code Health 메트릭은 불건강한 코드가 15배 더 많은 결함을 유발한다는 것을 정량적으로 증명했으며, 핫스팟 분석을 통해 우선순위 기반 리팩터링 계획을 수립할 수 있습니다. 기술 부채에 소모되는 42%의 개발 시간을 전략적으로 줄이는 것이 목표입니다.
5장에서는 탐지된 코드 스멜을 실제로 수정하는 LLM 기반 자동 리팩터링을 다룹니다. 함수 추출, 이름 변경, 메서드 이동 등 리팩터링 패턴과, RepoAI의 멀티에이전트 리팩터링 아키텍처, 그리고 37%에서 98%로 정밀도를 끌어올리는 검증 파이프라인을 학습합니다.
이 글이 도움이 되셨나요?
관련 주제 더 보기
LLM을 활용한 자동 리팩터링의 패턴, 멀티에이전트 아키텍처, 검증 파이프라인을 학습합니다. 37%에서 98%로 정밀도를 끌어올리는 실전 기법을 다룹니다.
LLM을 활용하여 레거시 코드베이스를 자동으로 탐색하고 문서화하는 기법을 학습합니다. 의존성 그래프 추출, 아키텍처 다이어그램 생성, 인라인 주석 자동 생성을 다룹니다.
LLM을 활용한 언어/프레임워크 마이그레이션 자동화를 학습합니다. Java에서 Kotlin, React Class에서 Hooks로의 전환과 의미 보존 검증 기법을 다룹니다.