
테스트 주도 개발(TDD)은 단순한 테스트 기법이 아닌 '디자인 도구'입니다. 파이썬 환경에서 유지보수가 쉬운 견고한 아키텍처를 구축하는 전문 가이드를 제안합니다.
1. 개요: TDD가 코드 구조에 미치는 영향
많은 개발자가 TDD(Test Driven Development)를 '코드를 짠 후 테스트를 만드는 것'의 순서만 바꾼 것으로 오해하곤 합니다. 하지만 진정한 TDD의 가치는 테스트를 먼저 작성함으로써 테스트하기 어려운 코드(Untestable Code)를 원천적으로 차단하는 데 있습니다. 파이썬은 동적 타입 언어로서 유연성이 높지만, 그만큼 런타임 에러에 취약합니다. TDD를 적용하면 객체 간의 의존성을 분리하고, 인터페이스(추상화)를 명확히 정의하게 되어 자연스럽게 SOLID 원칙이 준수되는 구조로 진화합니다. 본 글에서는 테스트 가능성을 극대화하는 7가지 실무 사례와 설계 전략을 다룹니다.
2. TDD 기반 설계 vs 전통적 설계의 구조적 차이 비교
테스트를 먼저 고려했을 때 코드 구조가 어떻게 변화하는지 핵심 지표를 통해 비교 분석합니다.
| 비교 항목 | 전통적 설계 (Big Upfront Design) | TDD 기반 설계 (Evolutionary Design) |
|---|---|---|
| 의존성 관리 | 함수 내부에서 직접 객체 생성 (강한 결합) | 의존성 주입(DI) 활용 (느슨한 결합) |
| 함수/메서드 크기 | 하나의 함수가 여러 책임을 가짐 (거대함) | 단위 테스트를 위해 작고 명확하게 분리됨 |
| 추상화 시점 | 미래를 예측하여 미리 추상화 도입 | 테스트 대역(Mock)이 필요한 시점에 도입 |
| 코드 가독성 | 구현 세부 사항에 집중 | 사용자 관점(API 인터페이스)에 집중 |
| 회귀 테스트 | 수동 확인 또는 사후 작성으로 누락 잦음 | 설계 자체가 테스트 문서가 되어 안정적임 |
3. TDD 실무 적용을 위한 7가지 해결 Example
파이썬의 unittest 또는 pytest를 활용하여 테스트 가능한 구조로 리팩토링하는 실무 예제입니다.
Example 1: 외부 API 의존성 분리 (Dependency Injection)
네트워크 상태에 의존하는 코드는 테스트가 어렵습니다. 인터페이스를 주입받는 구조로 설계합니다.
class PaymentGateway:
def process(self, amount):
# 실제 결제 API 호출 로직 (테스트 시 차단 필요)
pass
class OrderService:
def __init__(self, gateway):
self.gateway = gateway # 의존성 주입
def complete_order(self, amount):
return self.gateway.process(amount)
# Test Code (Mocking 활용)
def test_order_completion():
mock_gateway = MagicMock()
service = OrderService(mock_gateway)
service.complete_order(100)
mock_gateway.process.assert_called_with(100)
Example 2: 시간/날짜 의존성 해결 (Wrappers)
datetime.now()를 직접 사용하면 테스트 결과가 시간에 따라 변합니다. 이를 래핑합니다.
class Clock:
def now(self):
return datetime.now()
class DiscountPolicy:
def __init__(self, clock):
self.clock = clock
def is_happy_hour(self):
return 14 <= self.clock.now().hour <= 16
Example 3: 순수 함수와 비즈니스 로직의 격리
입출력이 명확한 순수 함수는 테스트가 가장 쉽습니다. I/O 작업과 로직을 분리하세요.
# Bad: 로직과 DB 저장이 섞임
def update_user_score(user_id, points):
user = db.fetch(user_id)
new_score = user.score + points # 로직
db.save(user_id, new_score) # I/O
# Good: 계산 로직만 분리
def calculate_score(current_score, points):
return current_score + points
Example 4: Factory Pattern을 활용한 테스트 대역 생성
다양한 환경(Dev, Test, Prod)에 맞는 객체 생성을 지원합니다.
class StorageFactory:
@staticmethod
def get_storage(env="prod"):
if env == "test":
return InMemoryStorage()
return S3Storage()
Example 5: 명령(Command)과 조회(Query)의 분리 (CQRS 기초)
상태를 변경하는 메서드와 값을 반환하는 메서드를 분리하면 테스트 가독성이 높아집니다.
class UserProfile:
def __init__(self):
self.email = ""
def update_email(self, new_email): # Command
self.email = new_email
def get_email_domain(self): # Query
return self.email.split('@')[-1]
Example 6: 예외 상황(Edge Case) 선제적 정의
TDD는 실패하는 테스트부터 시작합니다. 잘못된 입력에 대한 처리를 구조화합니다.
def test_withdraw_insufficient_funds():
account = Account(balance=50)
with pytest.raises(InsufficientFundsError):
account.withdraw(100)
Example 7: 파이썬 덕 타이핑을 활용한 프로토콜 정의
추상 클래스 없이도 인터페이스를 만족하는 가짜 객체를 만들어 테스트합니다.
class FakeLogger:
def log(self, msg):
self.last_msg = msg
def process_data(logger):
logger.log("Data Processed")
# 실제 로거 없이도 로직 검증 가능
4. 결론: 유지보수가 쉬운 파이썬 코드를 만드는 방법
TDD를 통해 설계된 코드 구조는 필연적으로 결합도가 낮고 응집도가 높습니다. 테스트를 작성하기 위해 코드를 쪼개고, 의존성을 외부에서 주입하게 되는 과정 자체가 훌륭한 리팩토링의 연속이기 때문입니다.
파이썬 프로젝트에서 TDD를 안착시키기 위한 3대 원칙을 기억하십시오.
- Small Steps: 아주 작은 단위의 실패하는 테스트부터 시작하십시오.
- Mocking Judiciously: 가짜 객체는 외부 경계(DB, API)에만 사용하고 내부 로직은 실제 객체로 테스트하십시오.
- Refactor Ruthlessly: 테스트 통과 후에는 코드 구조를 개선하는 'Green-to-Refactor' 단계를 절대 생략하지 마십시오.
'Artificial Intelligence > 60. Python' 카테고리의 다른 글
| [PYTHON] 믹스인(Mixin) 설계 시 상속 구조 문제를 해결하는 3가지 방법과 실무적 차이점 (0) | 2026.03.29 |
|---|---|
| [PYTHON] 상속보다 합성을 선택해야 하는 5가지 상황과 구조적 차이 해결 방법 (0) | 2026.03.29 |
| [PYTHON] 어댑터 패턴으로 레거시 코드를 통합하는 7가지 방법과 구조적 차이 해결 가이드 (0) | 2026.03.29 |
| [PYTHON] unittest와 pytest의 5가지 차이점 분석 및 pytest가 대세가 된 해결 방법 (0) | 2026.03.29 |
| [PYTHON] 외부 API 테스트를 위한 Mocking과 Patching의 3가지 차이점과 해결 방법 (0) | 2026.03.29 |