코드 리팩토링은 외부 동작을 변경하지 않으면서 내부 구조를 개선하는 과정입니다. 본 글에서는 파이썬 코드 리팩토링의 주요 원칙, 기법, 도구와 실제 사례를 체계적으로 살펴봅니다. SOLID 원칙, DRY, KISS 등 핵심 개념부터 실용적인 리팩토링 패턴까지 상세히 설명합니다.
오늘은 파이썬 코드 리팩토링에 대해 알아보겠습니다! 코드의 품질을 높이고 유지보수를 쉽게 만드는 핵심 기술에 대해 자세히 설명해 드릴게요.
리팩토링이란 무엇인가요? 🤔
여러분이 오래된 책장을 정리한다고 상상해보세요.
- 책은 그대로지만 배열과 구조가 바뀌어 찾기 쉬워지고
- 새 책을 추가하기도 더 편해지죠!
코드 리팩토링도 이와 같습니다:
- 프로그램의 외부 동작은 유지하면서
- 내부 구조만 개선하는 체계적인 작업
- 코드의 가독성, 유지보수성, 확장성을 높이는 과정
마틴 파울러의 정의에 따르면, "리팩토링은 소프트웨어의 외부 동작은 바꾸지 않으면서 내부 구조를 개선하여 코드를 더 이해하기 쉽고 수정하기 쉽게 만드는 작업"입니다.
왜 리팩토링이 필요한가요? 📝
1. 기술 부채 감소
# 리팩토링 전: 기술 부채가 쌓인 상태
def process_data(data):
# 500줄의 복잡한 코드...
result1 = 0
for item in data:
if item > 0:
result1 += item * 2
else:
result1 += abs(item) / 2
# 반복되는 유사한 로직...
result2 = 0
for item in data:
if item > 10:
result2 += item * 3
else:
result2 += abs(item) / 3
# 계속되는 반복...
return result1, result2
2. 버그 발견 용이성
코드가 간결하고 명확할수록 버그를 발견하고 수정하기 쉬워집니다.
3. 개발 속도 향상
초기에는 시간이 들지만, 장기적으로는 개발 속도가 크게 향상됩니다.
4. 코드 이해도 증가
팀원들이 코드를 더 쉽게 이해하고 협업할 수 있습니다.
리팩토링의 핵심 원칙 🌟
1. SOLID 원칙
SOLID는 객체지향 설계의 5가지 핵심 원칙입니다:
단일 책임 원칙 (Single Responsibility Principle)
# 리팩토링 전: 여러 책임이 섞인 클래스
class User:
def __init__(self, name, email):
self.name = name
self.email = email
def validate_email(self):
# 이메일 검증 로직...
return '@' in self.email
def save_to_database(self):
# 데이터베이스 저장 로직...
print(f"Saving {self.name} to database")
def send_welcome_email(self):
# 이메일 발송 로직...
print(f"Sending welcome email to {self.email}")
# 리팩토링 후: 책임이 분리된 클래스들
class User:
def __init__(self, name, email):
self.name = name
self.email = email
class EmailValidator:
@staticmethod
def validate(email):
return '@' in email
class UserRepository:
def save(self, user):
print(f"Saving {user.name} to database")
class EmailService:
def send_welcome_email(self, user):
print(f"Sending welcome email to {user.email}")
개방-폐쇄 원칙 (Open-Closed Principle)
# 리팩토링 전: 확장에 닫혀있는 코드
class Rectangle:
def __init__(self, width, height):
self.width = width
self.height = height
class AreaCalculator:
def calculate_area(self, shape):
if isinstance(shape, Rectangle):
return shape.width * shape.height
# 새 도형을 추가할 때마다 이 함수를 수정해야 함
# 리팩토링 후: 확장에 열려있고 수정에 닫혀있는 코드
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self):
pass
class Rectangle(Shape):
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self):
return 3.14 * self.radius * self.radius
# 새 도형을 추가할 때 AreaCalculator를 수정할 필요가 없음
class AreaCalculator:
def calculate_area(self, shape):
return shape.area()
리스코프 치환 원칙, 인터페이스 분리 원칙, 의존성 역전 원칙도 중요합니다.
2. DRY (Don't Repeat Yourself)
# 리팩토링 전: 중복 코드
def get_active_users():
users = database.query("SELECT * FROM users WHERE status = 'active'")
results = []
for user in users:
results.append({
'id': user.id,
'name': user.name,
'email': user.email
})
return results
def get_inactive_users():
users = database.query("SELECT * FROM users WHERE status = 'inactive'")
results = []
for user in users:
results.append({
'id': user.id,
'name': user.name,
'email': user.email
})
return results
# 리팩토링 후: 중복 제거
def get_users_by_status(status):
users = database.query(f"SELECT * FROM users WHERE status = '{status}'")
return [{'id': user.id, 'name': user.name, 'email': user.email} for user in users]
def get_active_users():
return get_users_by_status('active')
def get_inactive_users():
return get_users_by_status('inactive')
3. KISS (Keep It Simple, Stupid)
# 리팩토링 전: 복잡한 접근법
def get_day_type(date):
if date.weekday() == 0:
return "Weekday: Monday"
elif date.weekday() == 1:
return "Weekday: Tuesday"
elif date.weekday() == 2:
return "Weekday: Wednesday"
elif date.weekday() == 3:
return "Weekday: Thursday"
elif date.weekday() == 4:
return "Weekday: Friday"
elif date.weekday() == 5:
return "Weekend: Saturday"
elif date.weekday() == 6:
return "Weekend: Sunday"
# 리팩토링 후: 단순한 접근법
def get_day_type(date):
days = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
day_name = days[date.weekday()]
day_type = "Weekend" if date.weekday() >= 5 else "Weekday"
return f"{day_type}: {day_name}"
핵심 리팩토링 기법들 💫
1. 긴 함수 분리하기
# 리팩토링 전: 매우 긴 함수
def process_order(order_id, user_id, items, payment_info):
# 1. 주문 유효성 검사
if not order_id:
raise ValueError("Order ID is required")
# 사용자 정보 검증 (50줄 정도)...
# 2. 재고 확인
for item in items:
# 재고 확인 로직 (30줄 정도)...
# 3. 결제 처리
# 결제 처리 로직 (70줄 정도)...
# 4. 주문 저장
# 데이터베이스 저장 로직 (40줄 정도)...
# 5. 알림 발송
# 이메일, SMS 발송 로직 (30줄 정도)...
return order_confirmation
# 리팩토링 후: 책임별로 분리된 함수들
def validate_order(order_id, user_id):
if not order_id:
raise ValueError("Order ID is required")
# 사용자 정보 검증...
def check_inventory(items):
# 재고 확인 로직...
pass
def process_payment(payment_info, total_amount):
# 결제 처리 로직...
pass
def save_order(order_details):
# 데이터베이스 저장 로직...
pass
def send_notifications(user, order):
# 이메일, SMS 발송 로직...
pass
def process_order(order_id, user_id, items, payment_info):
validate_order(order_id, user_id)
check_inventory(items)
total_amount = sum(item.price for item in items)
payment_result = process_payment(payment_info, total_amount)
order_details = {
'order_id': order_id,
'user_id': user_id,
'items': items,
'payment_result': payment_result
}
order = save_order(order_details)
send_notifications(user_id, order)
return order
2. 복잡한 조건문 단순화
# 리팩토링 전: 복잡한 중첩 조건문
def calculate_insurance_premium(age, has_medical_condition, smoking, occupation):
premium = 0
if age < 25:
premium = 1000
if smoking:
premium += 300
if has_medical_condition:
premium += 500
if smoking:
premium += 200
elif age >= 25 and age < 50:
premium = 800
if smoking:
premium += 250
if has_medical_condition:
premium += 400
if smoking:
premium += 150
else:
premium = 1200
if smoking:
premium += 400
if has_medical_condition:
premium += 700
if smoking:
premium += 300
if occupation == "high_risk":
premium *= 1.5
return premium
# 리팩토링 후: 함수 추출 및 조건부 로직 간소화
def get_base_premium(age):
if age < 25:
return 1000
elif age < 50:
return 800
else:
return 1200
def get_smoking_surcharge(age, smoking):
if not smoking:
return 0
if age < 25:
return 300
elif age < 50:
return 250
else:
return 400
def get_medical_surcharge(age, has_medical_condition):
if not has_medical_condition:
return 0
if age < 25:
return 500
elif age < 50:
return 400
else:
return 700
def get_smoking_medical_surcharge(age, smoking, has_medical_condition):
if not (smoking and has_medical_condition):
return 0
if age < 25:
return 200
elif age < 50:
return 150
else:
return 300
def apply_occupation_factor(premium, occupation):
if occupation == "high_risk":
return premium * 1.5
return premium
def calculate_insurance_premium(age, has_medical_condition, smoking, occupation):
premium = get_base_premium(age)
premium += get_smoking_surcharge(age, smoking)
premium += get_medical_surcharge(age, has_medical_condition)
premium += get_smoking_medical_surcharge(age, smoking, has_medical_condition)
premium = apply_occupation_factor(premium, occupation)
return premium
3. 전략 패턴으로 조건문 대체
# 리팩토링 전: if-else 문을 사용한 할인 계산
def calculate_discount(customer_type, order_amount):
if customer_type == "regular":
if order_amount >= 1000:
return order_amount * 0.1
else:
return 0
elif customer_type == "premium":
if order_amount >= 500:
return order_amount * 0.15
else:
return order_amount * 0.05
elif customer_type == "vip":
return order_amount * 0.2
else:
return 0
# 리팩토링 후: 전략 패턴을 사용한 할인 계산
from abc import ABC, abstractmethod
class DiscountStrategy(ABC):
@abstractmethod
def calculate(self, order_amount):
pass
class RegularCustomerDiscount(DiscountStrategy):
def calculate(self, order_amount):
return order_amount * 0.1 if order_amount >= 1000 else 0
class PremiumCustomerDiscount(DiscountStrategy):
def calculate(self, order_amount):
return order_amount * 0.15 if order_amount >= 500 else order_amount * 0.05
class VIPCustomerDiscount(DiscountStrategy):
def calculate(self, order_amount):
return order_amount * 0.2
class DefaultDiscount(DiscountStrategy):
def calculate(self, order_amount):
return 0
class DiscountCalculator:
def __init__(self):
self.strategies = {
"regular": RegularCustomerDiscount(),
"premium": PremiumCustomerDiscount(),
"vip": VIPCustomerDiscount()
}
self.default_strategy = DefaultDiscount()
def calculate_discount(self, customer_type, order_amount):
strategy = self.strategies.get(customer_type, self.default_strategy)
return strategy.calculate(order_amount)
# 사용 예
calculator = DiscountCalculator()
discount = calculator.calculate_discount("premium", 750)
4. 딕셔너리를 활용한 코드 단순화
# 리팩토링 전: 조건문 사용
def get_tax_rate(country_code):
if country_code == "US":
return 0.07
elif country_code == "UK":
return 0.20
elif country_code == "DE":
return 0.19
elif country_code == "FR":
return 0.20
elif country_code == "JP":
return 0.10
else:
return 0.0
# 리팩토링 후: 딕셔너리 매핑 사용
TAX_RATES = {
"US": 0.07,
"UK": 0.20,
"DE": 0.19,
"FR": 0.20,
"JP": 0.10
}
def get_tax_rate(country_code):
return TAX_RATES.get(country_code, 0.0)
파이썬 리팩토링 도구 🛠️
리팩토링을 효과적으로 수행하기 위한 도구들이 있습니다:
1. IDE 내장 리팩토링 기능
- PyCharm: 메서드 추출, 변수 이름 변경, 클래스 이동 등 다양한 자동화된 리팩토링 기능 제공
- VS Code + Python 확장: 기본적인 리팩토링 기능 제공
2. 코드 품질 분석 도구
Wily: 코드 복잡도를 측정하고 리팩토링이 필요한 부분을 식별
pip install wily wily build . wily report cyclomatic your_module.py
Radon: 코드 복잡도 측정
pip install radon radon cc your_file.py
3. 린터 및 포맷터
- Pylint: 코드 스타일과 잠재적인 문제 식별
- Black: 자동으로 PEP 8 스타일 적용
- isort: 임포트 문 정렬
- autopep8: PEP 8 규칙에 맞게 코드 자동 수정
4. 자동화 도구
- Sourcery: AI를 활용한 자동 리팩토링 제안
- Prospector: 여러 린터를 결합하여 종합적인 코드 분석 제공
실전 리팩토링 가이드 ⚠️
1. 리팩토링 전 준비
# 테스트 코드 작성 예시
import unittest
from calculator import Calculator
class CalculatorTest(unittest.TestCase):
def setUp(self):
self.calc = Calculator()
def test_addition(self):
self.assertEqual(self.calc.add(2, 3), 5)
def test_subtraction(self):
self.assertEqual(self.calc.subtract(5, 2), 3)
# 더 많은 테스트 케이스...
if __name__ == '__main__':
unittest.main()
2. 단계별 리팩토링 방법
- 한 번에 하나의 변경사항만 적용하세요
- 각 단계 후 테스트를 실행하여 기능이 그대로인지 확인하세요
- 리팩토링과 새 기능 개발을 동시에 하지 마세요
- 변경사항은 작게 유지하고 자주 커밋하세요
3. 코드 리뷰 활용하기
- 팀원들에게 리팩토링 결과를 검토받으세요
- 특히 더 복잡해진 부분이 없는지 확인하세요
4. 지속적인 리팩토링 문화 만들기
- "보이스카우트 규칙": 코드를 발견했을 때보다 더 깨끗하게 남겨두세요
- 팀 내 리팩토링 시간을 정기적으로 할당하세요
실제 사례: 데이터 처리 코드 리팩토링 📱
리팩토링 전:
def analyze_data(filename):
data = []
with open(filename, 'r') as f:
for line in f:
if line.strip():
parts = line.strip().split(',')
if len(parts) >= 3:
name = parts[0]
try:
age = int(parts[1])
except ValueError:
age = 0
try:
score = float(parts[2])
except ValueError:
score = 0.0
data.append({'name': name, 'age': age, 'score': score})
# 통계 계산
total_age = 0
total_score = 0
for item in data:
total_age += item['age']
total_score += item['score']
if len(data) > 0:
avg_age = total_age / len(data)
avg_score = total_score / len(data)
else:
avg_age = 0
avg_score = 0
# 높은 점수 학생 찾기
high_scorers = []
for item in data:
if item['score'] > 90:
high_scorers.append(item['name'])
return {
'avg_age': avg_age,
'avg_score': avg_score,
'high_scorers': high_scorers,
'total_count': len(data)
}
리팩토링 후:
def parse_line(line):
parts = line.strip().split(',')
if len(parts) < 3:
return None
name = parts[0]
try:
age = int(parts[1])
except ValueError:
age = 0
try:
score = float(parts[2])
except ValueError:
score = 0.0
return {'name': name, 'age': age, 'score': score}
def calculate_average(data, key):
if not data:
return 0
return sum(item[key] for item in data) / len(data)
def find_high_scorers(data, threshold=90):
return [item['name'] for item in data if item['score'] > threshold]
def read_data(filename):
data = []
with open(filename, 'r') as f:
for line in f:
if line.strip():
item = parse_line(line)
if item:
data.append(item)
return data
def analyze_data(filename):
data = read_data(filename)
return {
'avg_age': calculate_average(data, 'age'),
'avg_score': calculate_average(data, 'score'),
'high_scorers': find_high_scorers(data),
'total_count': len(data)
}
마치며 🎁
파이썬 코드 리팩토링은 단순히 코드를 예쁘게 만드는 것이 아니라, 소프트웨어의 수명과 품질을 크게 향상시키는 투자입니다. 리팩토링을 일상적인 개발 습관으로 만든다면, 시간이 지날수록 코드베이스는 더 강력하고 유지보수하기 쉬워질 것입니다.
마틴 파울러의 말처럼 "코드를 더럽게 유지하면 단기적으로는 빠르게 진행할 수 있지만, 결국 생산성이 급격히 떨어지게 됩니다."
리팩토링은 마치 정원 가꾸기와 같습니다 - 지속적인 관리가 필요하지만, 그 결과는 명확하게 아름답고 건강한 환경을 만들어 냅니다.
궁금하신 점 있으시다면 댓글로 남겨주세요! 😊
참고 자료:
'800===Dev Docs and License > Clean Code' 카테고리의 다른 글
파이썬 코드 리팩토링 마스터 가이드 - 코드 테스트와 유지보수성 향상 🧹✨ (0) | 2025.01.07 |
---|---|
파이썬 코드 리팩토링 마스터 가이드 - 성능 최적화 핵심 가이드 🚀 (0) | 2025.01.07 |
파이썬 코드 리팩토링 마스터 가이드: 코드 구조 개선 🛠️ (0) | 2025.01.07 |
SOLID 원칙 - 객체지향 설계의 완벽 가이드 🧩 (1) | 2024.12.06 |
대형 메서드 분리 기법 - 복잡한 코드를 정복하는 리팩토링 전략 🛠️ (1) | 2024.12.06 |