학습 목표
- AI 기반 코드 스멜 탐지와 해결 방법 익히기
- 리팩토링 패턴과 베스트 프랙티스 이해하기
- 코드 복잡도 측정과 개선 전략 수립하기
- SOLID 원칙 적용과 디자인 패턴 구현하기
- 지속적인 코드 품질 관리 시스템 구축하기
AI 기반 코드 스멜 탐지
Cursor의 AI는 코드에서 나는 "나쁜 냄새"를 감지하고, 각 상황에 맞는 리팩토링 기법을 제안합니다. 단순한 스타일 문제부터 복잡한 아키텍처 개선까지 다양한 수준의 개선을 지원합니다.
일반적인 코드 스멜과 해결법
1. 긴 메서드 (Long Method)
❌ 문제가 있는 코드
function processOrder(order) {
// 주문 검증
if (!order.items || order.items.length === 0) {
throw new Error('Order must have items');
}
if (!order.customer) {
throw new Error('Order must have customer');
}
if (!order.customer.email || !order.customer.email.includes('@')) {
throw new Error('Invalid customer email');
}
// 재고 확인
let allItemsAvailable = true;
for (let item of order.items) {
const stock = inventory.getStock(item.productId);
if (stock < item.quantity) {
allItemsAvailable = false;
break;
}
}
if (!allItemsAvailable) {
throw new Error('Some items are out of stock');
}
// 가격 계산
let subtotal = 0;
for (let item of order.items) {
const product = products.find(p => p.id === item.productId);
subtotal += product.price * item.quantity;
}
// 할인 적용
let discount = 0;
if (order.couponCode) {
const coupon = coupons.find(c => c.code === order.couponCode);
if (coupon && coupon.expiryDate > new Date()) {
if (coupon.type === 'percentage') {
discount = subtotal * (coupon.value / 100);
} else {
discount = coupon.value;
}
}
}
// 세금 계산
const taxRate = 0.08;
const tax = (subtotal - discount) * taxRate;
// 배송비 계산
let shipping = 10;
if (subtotal > 100) {
shipping = 0;
}
// 최종 금액
const total = subtotal - discount + tax + shipping;
// 주문 저장
const orderId = generateOrderId();
database.save({
id: orderId,
...order,
subtotal,
discount,
tax,
shipping,
total,
status: 'pending',
createdAt: new Date()
});
// 이메일 전송
emailService.send(order.customer.email, 'Order Confirmation',
`Your order ${orderId} has been received. Total: $${total}`);
// 재고 업데이트
for (let item of order.items) {
inventory.reduce(item.productId, item.quantity);
}
return { orderId, total };
}
✅ AI 리팩토링 제안
// 메서드 추출과 책임 분리
class OrderProcessor {
async processOrder(order) {
this.validateOrder(order);
this.checkInventory(order.items);
const pricing = this.calculatePricing(order);
const savedOrder = await this.saveOrder(order, pricing);
await this.sendConfirmationEmail(savedOrder);
await this.updateInventory(order.items);
return {
orderId: savedOrder.id,
total: pricing.total
};
}
validateOrder(order) {
const validator = new OrderValidator();
validator.validate(order);
}
checkInventory(items) {
const checker = new InventoryChecker();
if (!checker.areItemsAvailable(items)) {
throw new Error('Some items are out of stock');
}
}
calculatePricing(order) {
const calculator = new PricingCalculator();
return calculator.calculate(order);
}
async saveOrder(order, pricing) {
const repository = new OrderRepository();
return await repository.save({
...order,
...pricing,
status: 'pending',
createdAt: new Date()
});
}
async sendConfirmationEmail(order) {
const emailer = new OrderEmailer();
await emailer.sendConfirmation(order);
}
async updateInventory(items) {
const updater = new InventoryUpdater();
await updater.reduceStock(items);
}
}
// 각 책임을 담당하는 별도 클래스들
class OrderValidator {
validate(order) {
this.validateItems(order.items);
this.validateCustomer(order.customer);
}
validateItems(items) {
if (!items?.length) {
throw new ValidationError('Order must have items');
}
}
validateCustomer(customer) {
if (!customer) {
throw new ValidationError('Order must have customer');
}
this.validateEmail(customer.email);
}
validateEmail(email) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
throw new ValidationError('Invalid customer email');
}
}
}
class PricingCalculator {
calculate(order) {
const subtotal = this.calculateSubtotal(order.items);
const discount = this.calculateDiscount(subtotal, order.couponCode);
const tax = this.calculateTax(subtotal - discount);
const shipping = this.calculateShipping(subtotal);
return {
subtotal,
discount,
tax,
shipping,
total: subtotal - discount + tax + shipping
};
}
calculateSubtotal(items) {
return items.reduce((sum, item) => {
const product = this.findProduct(item.productId);
return sum + (product.price * item.quantity);
}, 0);
}
calculateDiscount(subtotal, couponCode) {
if (!couponCode) return 0;
const coupon = this.findValidCoupon(couponCode);
if (!coupon) return 0;
return coupon.type === 'percentage'
? subtotal * (coupon.value / 100)
: coupon.value;
}
calculateTax(amount) {
const TAX_RATE = 0.08;
return amount * TAX_RATE;
}
calculateShipping(subtotal) {
const FREE_SHIPPING_THRESHOLD = 100;
const STANDARD_SHIPPING = 10;
return subtotal > FREE_SHIPPING_THRESHOLD ? 0 : STANDARD_SHIPPING;
}
}
개선 효과:
- 단일 책임 원칙 적용
- 테스트 가능성 향상
- 코드 재사용성 증가
- 유지보수성 개선
2. 중복 코드 (Duplicated Code)
❌ 중복된 코드
// userController.js
async function createUser(req, res) {
try {
const { email, password, name } = req.body;
if (!email || !email.includes('@')) {
return res.status(400).json({ error: 'Invalid email' });
}
if (!password || password.length < 8) {
return res.status(400).json({ error: 'Password too short' });
}
if (!name || name.trim().length === 0) {
return res.status(400).json({ error: 'Name is required' });
}
const user = await User.create({ email, password, name });
res.json({ success: true, user });
} catch (error) {
console.error('Error creating user:', error);
res.status(500).json({ error: 'Internal server error' });
}
}
// productController.js
async function createProduct(req, res) {
try {
const { name, price, description } = req.body;
if (!name || name.trim().length === 0) {
return res.status(400).json({ error: 'Name is required' });
}
if (!price || price <= 0) {
return res.status(400).json({ error: 'Invalid price' });
}
if (!description || description.trim().length === 0) {
return res.status(400).json({ error: 'Description is required' });
}
const product = await Product.create({ name, price, description });
res.json({ success: true, product });
} catch (error) {
console.error('Error creating product:', error);
res.status(500).json({ error: 'Internal server error' });
}
}
✅ DRY 원칙 적용
// validators.js
class ValidationError extends Error {
constructor(field, message) {
super(message);
this.field = field;
this.statusCode = 400;
}
}
const validators = {
required: (value, field) => {
if (!value || (typeof value === 'string' && !value.trim())) {
throw new ValidationError(field, `${field} is required`);
}
},
email: (value) => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(value)) {
throw new ValidationError('email', 'Invalid email format');
}
},
minLength: (value, length, field) => {
if (value.length < length) {
throw new ValidationError(field, `${field} must be at least ${length} characters`);
}
},
positive: (value, field) => {
if (value <= 0) {
throw new ValidationError(field, `${field} must be positive`);
}
}
};
// middleware/asyncHandler.js
const asyncHandler = (fn) => (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
// middleware/errorHandler.js
const errorHandler = (err, req, res, next) => {
console.error(`Error: ${err.message}`, err);
if (err instanceof ValidationError) {
return res.status(err.statusCode).json({
error: err.message,
field: err.field
});
}
res.status(500).json({ error: 'Internal server error' });
};
// controllers/baseController.js
class BaseController {
constructor(model, validationRules) {
this.model = model;
this.validationRules = validationRules;
}
validate(data) {
for (const [field, rules] of Object.entries(this.validationRules)) {
const value = data[field];
for (const rule of rules) {
if (typeof rule === 'function') {
rule(value, field);
} else if (typeof rule === 'object') {
rule.validator(value, rule.params, field);
}
}
}
}
create = asyncHandler(async (req, res) => {
this.validate(req.body);
const entity = await this.model.create(req.body);
res.json({ success: true, data: entity });
});
}
// controllers/userController.js
class UserController extends BaseController {
constructor() {
super(User, {
email: [validators.required, validators.email],
password: [
validators.required,
{ validator: validators.minLength, params: 8 }
],
name: [validators.required]
});
}
}
// controllers/productController.js
class ProductController extends BaseController {
constructor() {
super(Product, {
name: [validators.required],
price: [validators.required, validators.positive],
description: [validators.required]
});
}
}
코드 복잡도 분석과 개선
순환 복잡도 (Cyclomatic Complexity)
높은 복잡도 코드
// 복잡도: 12 (너무 높음!)
function calculateShippingCost(order) {
let cost = 0;
if (order.destination === 'domestic') {
if (order.weight <= 1) {
cost = 5;
} else if (order.weight <= 5) {
cost = 10;
} else if (order.weight <= 10) {
cost = 15;
} else {
cost = 20;
}
if (order.express) {
cost *= 1.5;
}
if (order.insurance) {
cost += 5;
}
} else {
if (order.weight <= 1) {
cost = 15;
} else if (order.weight <= 5) {
cost = 25;
} else {
cost = 40;
}
if (order.express) {
cost *= 2;
}
if (order.insurance) {
cost += 10;
}
}
if (order.fragile) {
cost += 10;
}
return cost;
}
리팩토링된 코드
// 전략 패턴으로 복잡도 감소
class ShippingCalculator {
constructor() {
this.strategies = {
domestic: new DomesticShippingStrategy(),
international: new InternationalShippingStrategy()
};
}
calculate(order) {
const strategy = this.strategies[order.destination] || this.strategies.domestic;
return strategy.calculate(order);
}
}
class BaseShippingStrategy {
calculate(order) {
let cost = this.getBaseCost(order.weight);
cost = this.applyExpressMultiplier(cost, order.express);
cost = this.addInsurance(cost, order.insurance);
cost = this.addFragileHandling(cost, order.fragile);
return cost;
}
addFragileHandling(cost, isFragile) {
return isFragile ? cost + 10 : cost;
}
}
class DomesticShippingStrategy extends BaseShippingStrategy {
getBaseCost(weight) {
const rates = [
{ maxWeight: 1, cost: 5 },
{ maxWeight: 5, cost: 10 },
{ maxWeight: 10, cost: 15 },
{ maxWeight: Infinity, cost: 20 }
];
return rates.find(rate => weight <= rate.maxWeight).cost;
}
applyExpressMultiplier(cost, isExpress) {
return isExpress ? cost * 1.5 : cost;
}
addInsurance(cost, hasInsurance) {
return hasInsurance ? cost + 5 : cost;
}
}
class InternationalShippingStrategy extends BaseShippingStrategy {
getBaseCost(weight) {
const rates = [
{ maxWeight: 1, cost: 15 },
{ maxWeight: 5, cost: 25 },
{ maxWeight: Infinity, cost: 40 }
];
return rates.find(rate => weight <= rate.maxWeight).cost;
}
applyExpressMultiplier(cost, isExpress) {
return isExpress ? cost * 2 : cost;
}
addInsurance(cost, hasInsurance) {
return hasInsurance ? cost + 10 : cost;
}
}
개선 결과:
- 각 메서드의 복잡도: 1-3 (이상적)
- 테스트 용이성 향상
- 새로운 배송 타입 추가 용이
- 비즈니스 로직 명확화
SOLID 원칙 적용
S - 단일 책임 원칙 (Single Responsibility)
❌ 원칙 위반
class User {
constructor(name, email) {
this.name = name;
this.email = email;
}
// 사용자 정보 관리
updateProfile(data) { /* ... */ }
// 인증 로직 (다른 책임)
authenticate(password) { /* ... */ }
// 이메일 전송 (다른 책임)
sendEmail(subject, body) { /* ... */ }
// 데이터베이스 저장 (다른 책임)
save() { /* ... */ }
}
✅ 원칙 적용
// 각 클래스가 하나의 책임만 가짐
class User {
constructor(name, email) {
this.name = name;
this.email = email;
}
updateProfile(data) {
Object.assign(this, data);
}
}
class UserAuthenticator {
authenticate(user, password) {
// 인증 로직만
}
}
class EmailService {
sendEmail(to, subject, body) {
// 이메일 전송만
}
}
class UserRepository {
save(user) {
// 데이터 저장만
}
}
O - 개방-폐쇄 원칙 (Open-Closed)
❌ 수정에 열려있음
class PaymentProcessor {
processPayment(amount, type) {
if (type === 'credit') {
// 신용카드 처리
} else if (type === 'debit') {
// 직불카드 처리
} else if (type === 'paypal') {
// PayPal 처리
}
// 새 결제 방식 추가 시 이 메서드 수정 필요
}
}
✅ 확장에는 열려있고 수정에는 닫혀있음
interface PaymentMethod {
process(amount: number): Promise;
}
class CreditCardPayment implements PaymentMethod {
async process(amount) {
// 신용카드 처리
}
}
class PayPalPayment implements PaymentMethod {
async process(amount) {
// PayPal 처리
}
}
// 새로운 결제 방식 추가 (기존 코드 수정 없음)
class CryptoPayment implements PaymentMethod {
async process(amount) {
// 암호화폐 처리
}
}
class PaymentProcessor {
async processPayment(amount: number, method: PaymentMethod) {
return await method.process(amount);
}
}
코드 품질 메트릭스
AI 기반 코드 품질 대시보드
유지보수성 지수
85
코드 변경의 용이성
인지적 복잡도
12
코드 이해의 난이도
결합도
Low
모듈 간 의존성
응집도
High
모듈 내 관련성
AI 개선 제안
UserService.js의 복잡도가 높습니다
150줄이 넘는 메서드를 작은 단위로 분리하세요.
OrderController가 너무 많은 의존성을 가집니다
8개의 서비스를 주입받고 있습니다. Facade 패턴 고려하세요.
실습: 레거시 코드 리팩토링
실제 프로젝트 리팩토링
문제가 많은 레거시 코드를 단계적으로 개선해봅시다.
리팩토링 대상 코드
// 전형적인 레거시 코드 예제
var utils = {
data: [],
init: function() {
var self = this;
$.ajax({
url: '/api/data',
success: function(response) {
self.data = response;
self.render();
}
});
},
render: function() {
var html = '';
for (var i = 0; i < this.data.length; i++) {
html += '';
html += '' + this.data[i].title + '
';
html += '' + this.data[i].description + '
';
if (this.data[i].status == 'active') {
html += 'Active';
} else if (this.data[i].status == 'pending') {
html += 'Pending';
} else {
html += 'Inactive';
}
html += '';
html += '';
}
document.getElementById('container').innerHTML = html;
},
delete: function(id) {
if (confirm('Are you sure?')) {
var self = this;
$.ajax({
url: '/api/data/' + id,
method: 'DELETE',
success: function() {
self.data = self.data.filter(function(item) {
return item.id != id;
});
self.render();
}
});
}
},
add: function(title, desc) {
if (title == '' || desc == '') {
alert('Please fill all fields');
return;
}
var self = this;
$.ajax({
url: '/api/data',
method: 'POST',
data: { title: title, description: desc },
success: function(response) {
self.data.push(response);
self.render();
}
});
}
};
리팩토링 단계
-
모던 JavaScript로 변환
- var → const/let
- 함수 표현식 → 화살표 함수
- jQuery → Fetch API
-
클래스 기반 구조로 변경
- ES6 클래스 사용
- 적절한 캡슐화
-
관심사 분리
- 데이터 관리
- UI 렌더링
- API 통신
- React/Vue 컴포넌트로 전환
AI 활용 팁
각 단계에서 Cursor의 도움을 받으세요:
- Cmd+K: "Convert to modern ES6+ syntax"
- Chat: "이 코드의 문제점을 분석하고 개선 방안을 제시해줘"
- Composer: "이 코드를 React 컴포넌트로 변환해줘"
핵심 정리
자동 코드 스멜 탐지
AI가 코드의 문제점을 자동으로 찾아내고 개선 방안을 제시합니다.
원칙 기반 리팩토링
SOLID, DRY, KISS 등의 원칙을 자동으로 적용하여 코드 품질을 향상시킵니다.
복잡도 관리
순환 복잡도와 인지적 복잡도를 측정하고 최적화합니다.
지속적인 품질 개선
코드 품질 메트릭을 모니터링하고 점진적으로 개선합니다.