LLM을 활용한 자동 리팩터링의 패턴, 멀티에이전트 아키텍처, 검증 파이프라인을 학습합니다. 37%에서 98%로 정밀도를 끌어올리는 실전 기법을 다룹니다.
리팩터링(Refactoring)은 외부 동작을 변경하지 않으면서 코드의 내부 구조를 개선하는 작업입니다. LLM은 Martin Fowler가 정리한 주요 리팩터링 패턴을 코드 맥락에 맞게 적용할 수 있습니다.
가장 빈번하게 적용되는 리팩터링 패턴입니다. 4장에서 본 process_order 함수를 예로 들어보겠습니다.
# 리팩터링 전: 하나의 함수에 모든 로직
def process_order(order_data: dict) -> dict:
# 유효성 검사
if not order_data.get("customer_id"):
raise ValueError("고객 ID가 필요합니다")
if not order_data.get("items"):
raise ValueError("주문 항목이 필요합니다")
# ... 50줄 이상의 코드# 리팩터링 후: 책임별로 분리
def process_order(order_data: dict) -> dict:
"""주문 처리의 오케스트레이션 함수"""
validate_order(order_data)
pricing = calculate_pricing(order_data["items"], order_data["customer_id"])
verify_and_reserve_stock(order_data["items"])
order = save_order(order_data, pricing)
notify_customer(order_data["customer_id"], order)
return order
def validate_order(order_data: dict) -> None:
"""주문 데이터의 유효성을 검사합니다"""
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"]:
_validate_order_item(item)
def _validate_order_item(item: dict) -> None:
if not item.get("product_id"):
raise ValueError("상품 ID가 필요합니다")
if not item.get("quantity") or item["quantity"] <= 0:
raise ValueError("수량은 양수여야 합니다")
@dataclass
class OrderPricing:
subtotal: float
tax: float
shipping: float
total: float
def calculate_pricing(
items: list[dict], customer_id: str
) -> OrderPricing:
"""주문 금액을 계산합니다"""
subtotal = sum(
get_product_price(item["product_id"])
* item["quantity"]
* (1 - get_discount(item["product_id"], customer_id))
for item in items
)
tax = subtotal * 0.1
shipping = 3000 if subtotal < 50000 else 0
return OrderPricing(
subtotal=subtotal,
tax=tax,
shipping=shipping,
total=subtotal + tax + shipping,
)명확한 이름은 코드의 가독성을 극적으로 향상시킵니다. LLM은 코드의 맥락을 이해하여 의미 있는 이름을 제안할 수 있습니다.
# 리팩터링 전
def proc(d, f=None, t=None):
r = []
for i in d:
if f and not f(i):
continue
if t:
i = t(i)
r.append(i)
return r
# 리팩터링 후
def filter_and_transform(
items: list,
predicate: Callable | None = None,
transformer: Callable | None = None,
) -> list:
results = []
for item in items:
if predicate and not predicate(item):
continue
if transformer:
item = transformer(item)
results.append(item)
return results기능 선망(Feature Envy) 스멜이 감지되면 메서드를 더 적절한 클래스로 이동합니다.
// 리팩터링 전: OrderService가 Product의 내부에 지나치게 의존
class OrderService {
calculateItemPrice(product: Product, quantity: number): number {
const basePrice = product.price * quantity;
const discount = product.category === "premium"
? basePrice * 0.1
: product.category === "sale"
? basePrice * 0.2
: 0;
return basePrice - discount;
}
}// 리팩터링 후: 가격 계산 로직을 Product로 이동
class Product {
price: number;
category: string;
calculatePrice(quantity: number): number {
const basePrice = this.price * quantity;
return basePrice - this.getDiscount(basePrice);
}
private getDiscount(basePrice: number): number {
const discountRates: Record<string, number> = {
premium: 0.1,
sale: 0.2,
};
return basePrice * (discountRates[this.category] ?? 0);
}
}
class OrderService {
calculateItemPrice(product: Product, quantity: number): number {
return product.calculatePrice(quantity);
}
}RepoAI는 멀티에이전트 방식으로 리팩터링을 수행하는 프레임워크입니다. 단일 LLM 호출이 아닌 여러 전문 에이전트의 협력으로 정확도를 높입니다.
from dataclasses import dataclass
from abc import ABC, abstractmethod
@dataclass
class RefactorPlan:
target_files: list[str]
changes: list[dict]
rationale: str
test_strategy: str
@dataclass
class RefactorResult:
original_code: dict[str, str]
refactored_code: dict[str, str]
compilation_passed: bool
tests_passed: bool
reflection_notes: list[str]
iterations: int
class BaseAgent(ABC):
def __init__(self, llm_client):
self.llm_client = llm_client
@abstractmethod
async def execute(self, context: dict) -> dict:
pass
class PlannerAgent(BaseAgent):
"""1단계: 리팩터링 계획 수립"""
async def execute(self, context: dict) -> dict:
prompt = f"""다음 코드의 리팩터링 계획을 수립하세요.
코드 스멜: {context['smells']}
영향받는 파일: {context['files']}
의존성 그래프: {context['dependencies']}
다음을 포함하세요:
1. 변경할 파일과 변경 내용
2. 변경 순서 (의존성 고려)
3. 각 변경의 근거
4. 테스트 전략"""
plan = await self.llm_client.generate(prompt)
return {"plan": plan}
class GeneratorAgent(BaseAgent):
"""2단계: 리팩터링된 코드 생성"""
async def execute(self, context: dict) -> dict:
prompt = f"""다음 계획에 따라 리팩터링된 코드를 생성하세요.
원본 코드:
{context['original_code']}
리팩터링 계획:
{context['plan']}
이전 시도의 피드백:
{context.get('reflection', '첫 번째 시도')}
규칙:
- 외부 동작은 변경하지 않습니다
- 기존 테스트가 모두 통과해야 합니다
- 타입 안전성을 유지합니다"""
code = await self.llm_client.generate(prompt)
return {"refactored_code": code}
class CompilerAgent(BaseAgent):
"""3단계: 컴파일 검증"""
async def execute(self, context: dict) -> dict:
# 실제로는 subprocess로 컴파일러를 실행
code = context["refactored_code"]
compilation_result = await self._compile(code)
return {
"compilation_passed": compilation_result["success"],
"errors": compilation_result.get("errors", []),
}
async def _compile(self, code: str) -> dict:
# TypeScript: tsc --noEmit
# Python: py_compile + mypy
# Java: javac
return {"success": True}
class TesterAgent(BaseAgent):
"""4단계: 테스트 실행"""
async def execute(self, context: dict) -> dict:
test_result = await self._run_tests(context["refactored_code"])
return {
"tests_passed": test_result["all_passed"],
"failed_tests": test_result.get("failures", []),
"coverage": test_result.get("coverage", 0),
}
async def _run_tests(self, code: str) -> dict:
# pytest, jest, junit 등 실행
return {"all_passed": True}
class ReflectionAgent(BaseAgent):
"""5단계: 실패 시 자기 반성 및 피드백"""
async def execute(self, context: dict) -> dict:
prompt = f"""리팩터링이 실패했습니다. 원인을 분석하세요.
컴파일 오류: {context.get('compile_errors', '없음')}
실패한 테스트: {context.get('failed_tests', '없음')}
이전 리팩터링 코드: {context['refactored_code']}
다음을 제공하세요:
1. 실패 원인 분석
2. 수정 방향
3. Generator Agent에 전달할 구체적 지시"""
reflection = await self.llm_client.generate(prompt)
return {"reflection": reflection}class RefactorOrchestrator:
"""멀티에이전트 리팩터링 오케스트레이터"""
MAX_ITERATIONS = 5
def __init__(self, llm_client):
self.planner = PlannerAgent(llm_client)
self.generator = GeneratorAgent(llm_client)
self.compiler = CompilerAgent(llm_client)
self.tester = TesterAgent(llm_client)
self.reflector = ReflectionAgent(llm_client)
async def refactor(
self,
original_code: dict[str, str],
smells: list[dict],
dependencies: dict,
) -> RefactorResult:
# 1단계: 계획 수립
context = {
"smells": smells,
"files": list(original_code.keys()),
"dependencies": dependencies,
"original_code": original_code,
}
plan_result = await self.planner.execute(context)
context.update(plan_result)
iterations = 0
reflection_notes = []
while iterations < self.MAX_ITERATIONS:
iterations += 1
# 2단계: 코드 생성
gen_result = await self.generator.execute(context)
context.update(gen_result)
# 3단계: 컴파일 검증
compile_result = await self.compiler.execute(context)
if not compile_result["compilation_passed"]:
context["compile_errors"] = compile_result["errors"]
reflect_result = await self.reflector.execute(context)
context.update(reflect_result)
reflection_notes.append(
f"반복 {iterations}: 컴파일 실패"
)
continue
# 4단계: 테스트 실행
test_result = await self.tester.execute(context)
if test_result["tests_passed"]:
return RefactorResult(
original_code=original_code,
refactored_code=context["refactored_code"],
compilation_passed=True,
tests_passed=True,
reflection_notes=reflection_notes,
iterations=iterations,
)
# 5단계: 반성
context["failed_tests"] = test_result["failed_tests"]
reflect_result = await self.reflector.execute(context)
context.update(reflect_result)
reflection_notes.append(
f"반복 {iterations}: 테스트 실패 "
f"({len(test_result['failed_tests'])}건)"
)
raise RuntimeError(
f"{self.MAX_ITERATIONS}회 반복 후에도 리팩터링 실패"
)LLM이 생성한 리팩터링 코드의 초기 기능적 정확도는 약 37%입니다. 이는 3건 중 2건이 어딘가에 기능적 오류를 포함한다는 의미입니다. 하지만 체계적인 검증 파이프라인을 거치면 98%까지 정밀도가 상승합니다.
| 단계 | 방법 | 검출하는 오류 |
|---|---|---|
| 구문 검증 | 파서/컴파일러 | 구문 오류, 누락된 괄호 |
| 타입 검증 | TypeScript tsc, mypy | 타입 불일치, 누락된 import |
| 단위 테스트 | pytest, jest | 기능적 오류, 엣지 케이스 |
| 자기 반성 | LLM 재분석 | 논리적 오류, 의미 변경 |
| 통합 테스트 | E2E 테스트 | 모듈 간 호환성 |
검증 파이프라인 없는 LLM 리팩터링은 위험합니다. 37%의 초기 정확도는 사람의 검토 없이는 프로덕션에 적용할 수 없는 수준입니다. 자동화된 검증 단계가 LLM 리팩터링의 실용성을 결정합니다.
Moderne은 단일 리포지토리가 아닌 수천 개 리포지토리에 걸쳐 리팩터링을 수행하는 플랫폼입니다.
Moderne의 핵심은 OpenRewrite 엔진입니다. 선언적 레시피(Recipe)를 정의하면 AST 수준에서 코드를 변환합니다.
# Spring Boot 2에서 3으로 마이그레이션 레시피
type: specs.openrewrite.org/v1beta/recipe
name: org.openrewrite.java.spring.boot3.UpgradeSpringBoot_3_0
displayName: Migrate to Spring Boot 3.0
description: >
Spring Boot 2.x 애플리케이션을 3.0으로 마이그레이션합니다.
Jakarta EE 전환, 설정 변경, 더 이상 사용하지 않는 API 교체를 포함합니다.
recipeList:
- org.openrewrite.java.spring.boot3.RemoveEnableBatchProcessing
- org.openrewrite.java.migrate.jakarta.JavaxMigrationToJakarta
- org.openrewrite.java.spring.boot3.ConfigurationOverEnableSecurityModerne은 이러한 레시피에 LLM을 결합하여, 레시피가 커버하지 못하는 복잡한 변환(비즈니스 로직 관련 변경 등)을 처리합니다.
LLM 기반 자동 리팩터링은 함수 추출, 이름 변경, 메서드 이동 등 전통적 리팩터링 패턴을 코드 맥락에 맞게 자동 적용합니다. RepoAI 같은 멀티에이전트 시스템은 계획-생성-검증-반성의 루프를 통해 정확도를 높이며, 검증 파이프라인을 통해 초기 37%의 정확도를 98%까지 끌어올릴 수 있습니다.
핵심은 LLM의 출력을 무조건 신뢰하지 않고, 체계적인 검증 단계를 거치는 것입니다. Moderne 같은 플랫폼은 이를 대규모로 자동화하여 수천 개 리포지토리에 일관된 리팩터링을 적용합니다.
6장에서는 리팩터링의 특수한 형태인 코드 마이그레이션 자동화를 다룹니다. Java에서 Kotlin으로, React Class 컴포넌트에서 Hooks로의 전환 등 언어/프레임워크 마이그레이션을 LLM으로 자동화하는 기법과 의미 보존 검증 방법을 학습합니다.
이 글이 도움이 되셨나요?
관련 주제 더 보기
LLM을 활용한 언어/프레임워크 마이그레이션 자동화를 학습합니다. Java에서 Kotlin, React Class에서 Hooks로의 전환과 의미 보존 검증 기법을 다룹니다.
LLM 기반 코드 스멜 탐지와 CodeScene Code Health 메트릭을 활용한 기술 부채 정량화를 학습합니다. 우선순위 기반 리팩터링 계획 수립까지 다룹니다.
SAST와 LLM을 결합한 보안 취약점 탐지, OWASP Top 10 자동 검출, 취약점 자동 수정 제안과 CI/CD 보안 게이트 구축을 학습합니다.