들어가며 🎯
Part 3에서는 코드의 테스트 용이성과 유지보수성을 높이는 핵심 리팩토링 기법들을 자세히 살펴보겠습니다.
1. 단위 테스트를 위한 리팩토링 🔍
1.1 의존성 주입 패턴 적용
from abc import ABC, abstractmethod
from typing import List, Optional
import datetime
# 개선 전
class UserService:
def get_active_users(self):
db = Database() # 직접 의존성 생성 - 테스트 어려움
return db.query("SELECT * FROM users WHERE is_active = 1")
# 개선 후
class DatabaseInterface(ABC):
"""데이터베이스 작업을 추상화하는 인터페이스입니다."""
@abstractmethod
def query(self, sql: str) -> List[dict]:
pass
class UserService:
"""사용자 관련 비즈니스 로직을 처리하는 서비스 클래스입니다."""
def __init__(self, database: DatabaseInterface):
self.database = database # 의존성 주입
def get_active_users(self) -> List[dict]:
"""활성 사용자 목록을 조회합니다."""
return self.database.query("SELECT * FROM users WHERE is_active = 1")
# 테스트 코드
class MockDatabase(DatabaseInterface):
"""테스트를 위한 Mock 데이터베이스입니다."""
def query(self, sql: str) -> List[dict]:
# 테스트용 더미 데이터 반환
return [
{"id": 1, "name": "User1", "is_active": 1},
{"id": 2, "name": "User2", "is_active": 1}
]
def test_get_active_users():
"""사용자 서비스 테스트."""
mock_db = MockDatabase()
service = UserService(mock_db)
users = service.get_active_users()
assert len(users) == 2
assert users[0]['name'] == "User1"
1.2 테스트 용이한 클래스 설계
from dataclasses import dataclass
from datetime import datetime, timedelta
from typing import List, Optional
@dataclass
class OrderItem:
"""주문 항목을 나타내는 데이터 클래스입니다."""
product_id: str
quantity: int
price: float
class Order:
"""주문을 처리하는 클래스입니다."""
def __init__(self,
order_id: str,
items: List[OrderItem],
created_at: Optional[datetime] = None):
self.order_id = order_id
self.items = items
self.created_at = created_at or datetime.now()
self.status = "CREATED"
@property
def total_amount(self) -> float:
"""주문 총액을 계산합니다."""
return sum(item.price * item.quantity for item in self.items)
@property
def is_valid(self) -> bool:
"""주문의 유효성을 검사합니다."""
return bool(self.items) and all(
item.quantity > 0 and item.price > 0
for item in self.items
)
def can_be_modified(self) -> bool:
"""주문 수정 가능 여부를 확인합니다."""
return (
self.status == "CREATED" and
datetime.now() - self.created_at < timedelta(hours=24)
)
# 테스트 코드
def test_order_validation():
"""주문 유효성 검사 테스트."""
# 유효한 주문 테스트
valid_items = [
OrderItem("P1", 2, 1000),
OrderItem("P2", 1, 500)
]
order = Order("O1", valid_items)
assert order.is_valid
assert order.total_amount == 2500
# 무효한 주문 테스트
invalid_items = [
OrderItem("P1", 0, 1000), # 수량 0
OrderItem("P2", 1, -500) # 음수 가격
]
invalid_order = Order("O2", invalid_items)
assert not invalid_order.is_valid
2. 에러 처리와 로깅 개선 🚨
2.1 커스텀 예외 클래스
from typing import Optional
import logging
class OrderError(Exception):
"""주문 관련 기본 예외 클래스입니다."""
pass
class InvalidOrderError(OrderError):
"""유효하지 않은 주문 예외입니다."""
pass
class OrderProcessingError(OrderError):
"""주문 처리 중 발생한 예외입니다."""
def __init__(self, message: str, order_id: Optional[str] = None):
super().__init__(message)
self.order_id = order_id
class OrderProcessor:
"""주문 처리를 담당하는 클래스입니다."""
def __init__(self):
self.logger = logging.getLogger(__name__)
def process_order(self, order: Order):
"""주문을 처리합니다."""
try:
if not order.is_valid:
raise InvalidOrderError("유효하지 않은 주문입니다")
self._validate_stock(order)
self._process_payment(order)
self._update_inventory(order)
except InvalidOrderError as e:
self.logger.warning(
f"주문 유효성 검사 실패 - Order ID: {order.order_id}",
exc_info=True
)
raise
except Exception as e:
self.logger.error(
f"주문 처리 실패 - Order ID: {order.order_id}",
exc_info=True
)
raise OrderProcessingError(
f"주문 처리 중 오류 발생: {str(e)}",
order.order_id
)
2.2 컨텍스트 매니저 활용
from contextlib import contextmanager
import time
from typing import Generator
@contextmanager
def operation_logger(operation_name: str) -> Generator:
"""작업 시간과 결과를 로깅하는 컨텍스트 매니저입니다."""
logger = logging.getLogger(__name__)
start_time = time.time()
try:
logger.info(f"{operation_name} 시작")
yield
elapsed = time.time() - start_time
logger.info(f"{operation_name} 완료 (소요시간: {elapsed:.2f}초)")
except Exception as e:
elapsed = time.time() - start_time
logger.error(
f"{operation_name} 실패 "
f"(소요시간: {elapsed:.2f}초): {str(e)}"
)
raise
class InventoryManager:
"""재고 관리를 담당하는 클래스입니다."""
def update_stock(self, order: Order):
"""주문에 따라 재고를 업데이트합니다."""
with operation_logger("재고 업데이트"):
for item in order.items:
self._update_item_stock(item)
3. 코드 문서화와 타입 힌트 📚
3.1 문서화 예시
from typing import Dict, List, Optional, Union
import json
class APIClient:
"""REST API와 통신하는 클라이언트 클래스입니다.
이 클래스는 외부 API와의 통신을 담당하며, 재시도 로직과
에러 처리를 포함합니다.
Attributes:
base_url: API 기본 URL
timeout: 요청 타임아웃 시간 (초)
max_retries: 최대 재시도 횟수
Example:
>>> client = APIClient("https://api.example.com")
>>> response = client.get("/users/1")
>>> print(response['name'])
"""
def __init__(self,
base_url: str,
timeout: int = 30,
max_retries: int = 3):
self.base_url = base_url
self.timeout = timeout
self.max_retries = max_retries
def get(self,
endpoint: str,
params: Optional[Dict] = None) -> Dict:
"""API GET 요청을 수행합니다.
Args:
endpoint: API 엔드포인트 경로
params: URL 쿼리 파라미터
Returns:
API 응답 데이터를 딕셔너리로 반환
Raises:
APIError: API 호출 실패시 발생
ValueError: 잘못된 인자 전달시 발생
"""
url = f"{self.base_url}{endpoint}"
return self._request("GET", url, params=params)
3.2 타입 힌트와 Validation
from typing import TypeVar, Generic
from pydantic import BaseModel, validator
from datetime import datetime
T = TypeVar('T')
class Page(BaseModel, Generic[T]):
"""페이지네이션 결과를 담는 제네릭 클래스입니다."""
items: List[T]
total: int
page: int
size: int
class UserCreate(BaseModel):
"""사용자 생성 요청 모델입니다."""
username: str
email: str
birth_date: datetime
@validator('username')
def username_must_be_valid(cls, v: str) -> str:
"""사용자명 유효성을 검사합니다."""
if len(v) < 3:
raise ValueError("사용자명은 3자 이상이어야 합니다")
if not v.isalnum():
raise ValueError("사용자명은 영문과 숫자만 가능합니다")
return v
@validator('email')
def email_must_be_valid(cls, v: str) -> str:
"""이메일 형식을 검사합니다."""
if '@' not in v:
raise ValueError("올바른 이메일 형식이 아닙니다")
return v
class UserService:
"""사용자 관련 비즈니스 로직을 처리하는 서비스입니다."""
def get_users(self,
page: int = 1,
size: int = 10) -> Page[UserCreate]:
"""사용자 목록을 페이지네이션하여 조회합니다."""
users = self._fetch_users(page, size)
total = self._count_users()
return Page(
items=users,
total=total,
page=page,
size=size
)
정리 🎁
Part 3에서는 코드의 품질을 높이는 세 가지 핵심 영역을 다루었습니다:
단위 테스트 용이성
- 의존성 주입을 통한 테스트 용이한 구조
- 명확한 클래스 설계로 테스트 케이스 작성 용이성 확보
에러 처리와 로깅
- 체계적인 예외 처리 구조
- 컨텍스트 매니저를 활용한 리소스 관리
문서화와 타입 시스템
- 명확한 문서화로 코드 이해도 향상
- 타입 힌트를 통한 안정성 확보
이러한 기법들을 적절히 조합하여 적용하면, 더 안정적이고 유지보수가 용이한 코드를 작성할 수 있습니다.
References:
- Python Testing with pytest (Brian Okken) - Chapter 3, 5
- Clean Code in Python (Mariano Anaya) - Chapter 4, 7
- Python Type Hints Documentation (https://docs.python.org/3/library/typing.html)
- pytest Documentation (https://docs.pytest.org/)
- Python Logging Cookbook (https://docs.python.org/3/howto/logging-cookbook.html)
728x90
'800===Dev Docs and License > Clean Code' 카테고리의 다른 글
파이썬 코드 리팩토링 마스터 가이드 - Part 2: 성능 최적화 핵심 가이드 💫 (0) | 2025.01.07 |
---|---|
파이썬 코드 리팩토링 마스터 가이드 - Part 1: 코드 구조 개선 🎯 (0) | 2025.01.07 |
파이썬 코드 리팩토링의 핵심 가이드 🎯 (0) | 2025.01.07 |
SOLID 원칙 완벽 가이드 🚀 (1) | 2024.12.06 |
코드 품질 향상을 위한 대형 메서드 분리 기법 🚀 (1) | 2024.12.06 |