800===Dev Docs and License/Clean Code

파이썬 코드 리팩토링 마스터 가이드 - 코드 테스트와 유지보수성 향상 🧹✨

블로글러 2025. 1. 7. 21:30

여러분은 오래된 집을 리모델링하는 과정을 생각해 본 적이 있으신가요? 🏠

  • 집의 기본 구조와 외관은 그대로 유지하면서 내부를 개선하고 현대화하는 작업입니다.
  • 코드 리팩토링도 이와 매우 유사합니다. 프로그램의 외부 동작은 변경하지 않으면서 내부 구조를 개선하는 과정이죠.

왜 필요한가?

파이썬 코드 리팩토링이 해결하는 문제들은 다음과 같습니다:

  1. 기술 부채(Technical Debt) 관리: 🧾 개발 과정에서 시간 압박이나 자원 제약으로 인해 발생한 임시방편적 코드 솔루션은 시간이 지날수록 유지보수 비용을 증가시킵니다.

  2. 코드 스멜(Code Smell) 제거: 🦨 코드 스멜은 더 깊은 문제를 나타내는 코드의 특성으로, 중복된 코드, 지나치게 긴 함수, 복잡한 조건문 등이 여기에 해당합니다.

  3. 유지보수성 향상: 🔧 시간이 지남에 따라 새로운 기능이 추가되고 버그가 수정되면서 코드는 점점 이해하고 수정하기 어려워질 수 있습니다.

  4. 테스트 용이성 개선: 🧪 잘 구조화되지 않은 코드는 테스트하기 어려우며, 이는 불충분한 테스트 커버리지로 이어질 수 있습니다.

기본 원리

파이썬 리팩토링의 핵심 원리를 알아볼까요?

테스트 주도 개발(TDD) 접근법

1. Red: 실패하는 테스트 작성
2. Green: 테스트를 통과하도록 최소한의 코드 작성
3. Refactor: 코드를 개선하면서 테스트가 계속 통과하는지 확인

점진적 변경

1. 작은 단위로 리팩토링 진행
2. 각 변경 후 테스트 수행
3. 변경사항 커밋 및 문서화
4. 다음 단계로 진행

실제 예제

파이썬 코드 리팩토링의 일반적인 기법과 그 효과를 살펴보겠습니다.

함수 추출

리팩토링 전:

def process_data(data):
    # 데이터 검증
    if not isinstance(data, list):
        raise TypeError("데이터는 리스트여야 합니다")
    if len(data) == 0:
        raise ValueError("데이터는 비어있을 수 없습니다")

    # 데이터 처리
    result = []
    for item in data:
        if isinstance(item, (int, float)):
            result.append(item * 2)
        else:
            result.append(0)

    # 결과 포맷팅
    formatted_result = []
    for item in result:
        formatted_result.append(f"값: {item}")

    return formatted_result

리팩토링 후:

def validate_data(data):
    if not isinstance(data, list):
        raise TypeError("데이터는 리스트여야 합니다")
    if len(data) == 0:
        raise ValueError("데이터는 비어있을 수 없습니다")

def process_item(item):
    if isinstance(item, (int, float)):
        return item * 2
    return 0

def format_item(item):
    return f"값: {item}"

def process_data(data):
    validate_data(data)
    processed = [process_item(item) for item in data]
    return [format_item(item) for item in processed]

복잡한 조건문 단순화

리팩토링 전:

def calculate_discount(customer, order_total):
    discount = 0
    if customer.type == "premium":
        if order_total >= 1000:
            discount = 0.15
        elif order_total >= 500:
            discount = 0.10
        else:
            discount = 0.05
    elif customer.type == "regular":
        if customer.years_active > 5:
            if order_total >= 1000:
                discount = 0.10
            elif order_total >= 500:
                discount = 0.05
            else:
                discount = 0.02
        else:
            if order_total >= 1000:
                discount = 0.05
            elif order_total >= 500:
                discount = 0.02
            else:
                discount = 0
    return order_total * (1 - discount)

리팩토링 후:

def get_premium_discount(order_total):
    if order_total >= 1000:
        return 0.15
    elif order_total >= 500:
        return 0.10
    return 0.05

def get_regular_discount(order_total, years_active):
    if years_active <= 5:
        return get_regular_new_discount(order_total)
    return get_regular_loyal_discount(order_total)

def get_regular_new_discount(order_total):
    if order_total >= 1000:
        return 0.05
    elif order_total >= 500:
        return 0.02
    return 0

def get_regular_loyal_discount(order_total):
    if order_total >= 1000:
        return 0.10
    elif order_total >= 500:
        return 0.05
    return 0.02

def calculate_discount(customer, order_total):
    if customer.type == "premium":
        discount = get_premium_discount(order_total)
    else:  # regular customer
        discount = get_regular_discount(order_total, customer.years_active)

    return order_total * (1 - discount)

의존성 주입 적용

리팩토링 전:

class DataAnalyzer:
    def analyze(self, data_id):
        # 하드코딩된 의존성
        fetcher = DataFetcher()
        data = fetcher.fetch(data_id)
        # 분석 로직
        return {'result': sum(data), 'count': len(data)}

리팩토링 후:

class DataAnalyzer:
    def __init__(self, data_fetcher):
        # 생성자를 통한 의존성 주입
        self.data_fetcher = data_fetcher

    def analyze(self, data_id):
        data = self.data_fetcher.fetch(data_id)
        # 분석 로직
        return {'result': sum(data), 'count': len(data)}

# 프로덕션 환경 사용
analyzer = DataAnalyzer(DataFetcher())
result = analyzer.analyze(123)

# 테스트 환경 사용
from unittest.mock import Mock
mock_fetcher = Mock()
mock_fetcher.fetch.return_value = [1, 2, 3]
analyzer = DataAnalyzer(mock_fetcher)
result = analyzer.analyze(123)
assert result == {'result': 6, 'count': 3}

테스트 전략 구현

# pytest를 사용한 리팩토링된 코드 테스트 예시
import pytest
from customer import Customer
from discount_calculator import calculate_discount

# 테스트용 픽스처
@pytest.fixture
def premium_customer():
    return Customer(type="premium", years_active=3)

@pytest.fixture
def regular_new_customer():
    return Customer(type="regular", years_active=2)

@pytest.fixture
def regular_loyal_customer():
    return Customer(type="regular", years_active=7)

# 테스트 케이스
def test_premium_high_order(premium_customer):
    result = calculate_discount(premium_customer, 1200)
    expected = 1200 * 0.85  # 15% 할인
    assert result == expected

def test_premium_medium_order(premium_customer):
    result = calculate_discount(premium_customer, 600)
    expected = 600 * 0.90  # 10% 할인
    assert result == expected

def test_premium_small_order(premium_customer):
    result = calculate_discount(premium_customer, 300)
    expected = 300 * 0.95  # 5% 할인
    assert result == expected

Mock을 활용한 테스트

# Mock을 사용한 테스트 예시
import pytest
from unittest.mock import Mock, patch
from data_processor import process_data

def test_process_data_calls_api():
    # API에 대한 Mock 생성
    mock_api = Mock()
    mock_api.get_data.return_value = [1, 2, 3]

    # 모듈의 API 의존성 패치
    with patch('data_processor.api', mock_api):
        result = process_data('test_param')

        # API가 올바른 파라미터로 호출되었는지 확인
        mock_api.get_data.assert_called_once_with('test_param')

        # 결과가 예상대로인지 확인
        assert result == [2, 4, 6]  # 각 항목이 2배로 증가

다음은 표로 정리한 리팩토링 기법과 그 효과입니다:

리팩토링 기법 해결하는 문제 기대 효과
함수 추출 긴 함수, 코드 중복 가독성 향상, 재사용성 증가
조건문 단순화 복잡한 조건 로직 이해하기 쉬운 코드, 유지보수 용이
의존성 주입 강한 결합, 테스트 어려움 테스트 용이성 향상, 모듈화 개선
클래스 분리 과도한 책임을 가진 클래스 단일 책임 원칙 준수, 모듈성 개선
메서드 이동 잘못된 책임 할당 응집도 향상, 관련 기능 그룹화

주의사항 및 팁 💡

⚠️ 이것만은 주의하세요!

  1. 테스트 없이 리팩토링하지 마세요

    • 항상 충분한 테스트 케이스를 작성한 후 리팩토링을 시작하세요.
    • 각 변경 후에 테스트를 실행하여 기능이 그대로 유지되는지 확인하세요.
  2. 한 번에 너무 많은 변경을 시도하지 마세요

    • 리팩토링은 점진적으로 진행해야 합니다.
    • 작은 변경을 하고 테스트한 후 커밋하는 방식으로 진행하세요.
  3. 리팩토링과 새 기능 추가를 동시에 하지 마세요

    • 리팩토링과 기능 추가는 별도의 커밋으로 분리하세요.
    • 두 작업을 동시에 수행하면 문제가 발생했을 때 원인을 찾기 어렵습니다.

💡 꿀팁

  • 코드 분석 도구를 활용하세요 - pylint, flake8, SonarQube 등의 도구로 자동화된 코드 품질 검사를 수행하세요.
  • 보이스카우트 규칙을 적용하세요 - 코드를 처음 발견했을 때보다 더 깨끗하게 만들고 떠나세요.
  • 설계 패턴을 학습하세요 - 일반적인 문제에 대한 검증된 해결책을 적용하면 코드 품질이 향상됩니다.
  • 페어 프로그래밍을 시도하세요 - 동료와 함께 리팩토링하면 더 나은 결정을 내릴 수 있습니다.
  • 코드 리뷰를 활성화하세요 - 다른 사람의 관점에서 코드를 바라보면 놓친 부분을 발견할 수 있습니다.

마치며

지금까지 파이썬 코드 리팩토링에 대해 알아보았습니다. 코드 리팩토링은 시간이 지날수록 더욱 중요해지는 작업입니다. 처음에는 약간의 추가 노력이 필요하지만, 장기적으로는 유지보수성 향상과 버그 감소로 이어져 많은 시간과 자원을 절약할 수 있습니다. 🚀

효과적인

리팩토링을 위해 테스트 주도 개발(TDD)과 지속적인 통합(CI)을 일상적인 개발 프로세스에 통합하는 것을 권장합니다. 작은 변화부터 시작하여 점진적으로 코드베이스를 개선해 나가세요!

혹시 궁금한 점이 있으시거나, 더 알고 싶은 내용이 있으시면 댓글로 남겨주세요. 😊

참고 자료 🔖

  • Real Python: Refactoring Python Applications for Simplicity
  • Python Design Patterns
  • Clean Code in Python
  • Test-Driven Development with Python
  • ArjanCodes: Optimize Python Code for Better Maintenance

#파이썬 #리팩토링 #코드품질 #TDD #유지보수성

728x90