09. 목 사용의 모범 사례
05편에서 단위 테스트 맥락의 목 사용 기준(관리/비관리 의존성)을 다뤘습니다. 이 편은 08편에서 다룬 통합 테스트 맥락에서 목을 어디에 배치해야 하는지를 다룹니다. 같은 목이라도 배치 위치에 따라 테스트의 신뢰도가 크게 달라집니다.
학습 목표
- 목을 애플리케이션 경계에 가장 가까운 지점에 배치해야 하는 이유를 설명할 수 있다.
- 목을 시스템 내부 깊숙이 걸었을 때 생기는 문제를 진단할 수 있다.
- 외부 API 실패·타임아웃 시나리오를 목으로 안전하게 재현하는 방법을 적용할 수 있다.
목은 애플리케이션 경계에 가장 가까운 곳에 건다
통합 테스트의 목적은 “여러 컴포넌트가 실제로 맞물리는가"를 확인하는 것입니다. 그런데 목을 시스템 내부 깊숙이 걸면 정작 검증하려던 연동 구간이 빠져버립니다.
| |
| |
이 테스트는 place_order() 호출 여부만 확인할 뿐, OrderService와 PaymentGateway, repository가 실제로 올바르게 연동되는지는 전혀 검증하지 않습니다. 목을 너무 안쪽(테스트 대상 그 자체)에 걸어버려서, 통합 테스트를 작성한 의미가 사라졌습니다.
| |
여기서는 실제로 통제할 수 없는 외부 결제 API(PaymentGateway.authorize)만 목으로 대체하고, OrderService와 repository는 실제 구현체를 그대로 사용합니다. 이렇게 하면 애플리케이션 내부의 연동은 그대로 검증하면서, 우리가 제어할 수 없는 외부 시스템만 예측 가능한 값으로 고정할 수 있습니다.
목을 안쪽에 걸수록 생기는 문제
flowchart LR
subgraph Bad["안쪽에 목: 검증 범위가 좁아짐"]
API1["/checkout"] -.->|"건너뜀"| OS1["OrderService"]
OS1 -.->|"목으로 대체"| PG1["PaymentGateway"]
end
subgraph Good["경계에 목: 내부 연동은 그대로 검증"]
API2["/checkout"] --> OS2["OrderService"]
OS2 --> Repo2["OrderRepository(실제)"]
OS2 -->|"목으로 대체"| PG2["PaymentGateway(외부 API)"]
end
목을 안쪽에 걸수록 다음 문제가 생깁니다.
- 검증 범위 축소: 목으로 대체한 컴포넌트 안쪽에서 일어나는 실제 버그(예: 리포지터리 쿼리 오류)를 통합 테스트가 놓친다.
- 거짓 양성 증가: 04편에서 다룬 것처럼, 안쪽 구현이 바뀔 때마다(리팩터링만 해도) 목 설정이 어긋나 테스트가 깨진다.
- 테스트가 실제 배포 구성을 반영하지 못함: 목으로 건너뛴 부분은 실제 운영 환경의 설정 오류를 사전에 잡지 못한다.
외부 API 실패 시나리오를 안전하게 재현하기
경계에 목을 배치하면 얻는 실질적인 이득이 하나 더 있습니다. 실제로 재현하기 어려운 실패 상황을 안전하게 시뮬레이션할 수 있다는 것입니다.
| |
실제 결제 API에 타임아웃이나 거절 응답을 강제로 발생시키는 것은 현실적으로 불가능하거나 매우 번거롭습니다. 경계에 목을 두면 이런 예외 상황을 코드 몇 줄로 안전하게 재현할 수 있고, 08편에서 언급한 “실패를 먼저 모델링하라"는 원칙을 통합 테스트 수준에서도 실천할 수 있습니다.
목 배치 판단 기준
| 상황 | 목 배치 여부 |
|---|---|
| 우리가 소유하지 않은 외부 API/서비스 | 경계에서 목으로 대체 |
| 우리가 소유한 DB(테스트용 인메모리/컨테이너 DB 사용 가능) | 실제 구현체 사용(목 불필요) |
| 테스트 대상 서비스 자체 또는 그 바로 안쪽 컴포넌트 | 목으로 대체하지 않음 — 이건 검증 대상이다 |
| 실제로 재현하기 어려운 실패 상황(타임아웃, 5xx) | 경계의 목에 side_effect로 주입 |
실무 체크리스트
- 목이 애플리케이션 경계(우리가 통제할 수 없는 외부 시스템)에 있는가, 아니면 테스트 대상 안쪽에 있는가?
- 통합 테스트가 실제로 여러 컴포넌트의 연동을 검증하고 있는가, 아니면 목 때문에 사실상 단위 테스트가 되어버렸는가?
- 외부 의존성의 실패 시나리오(타임아웃, 거절, 5xx)를 목으로 검증하고 있는가?
- 목으로 대체한 대상이 정말 “우리가 소유하지 않은” 시스템인가?
연습 과제
기초(★☆☆)
- 여러분의 프로젝트에서 통합 테스트가 테스트 대상 서비스 자체를 목으로 대체하고 있는 사례를 찾아, 경계로 목을 옮겨보세요.
중급(★★☆)
- 외부 API 연동 코드에 대해 타임아웃과 거절 응답 시나리오를 목으로 재현하는 테스트를 각각 작성해보세요.
고급(★★★)
- 여러 외부 시스템(결제, 알림, 배송 추적)을 연동하는 진입점에 대해, 각 시스템의 실패 조합(예: 결제는 성공, 알림은 실패)을 조합해 보상 로직을 검증하는 테스트를 설계해보세요.
요약
- 목은 테스트 대상 안쪽이 아니라 애플리케이션 경계(우리가 통제할 수 없는 외부 시스템)에 배치한다.
- 목을 안쪽에 걸수록 검증 범위가 좁아지고 거짓 양성이 늘어난다.
- 경계에 목을 두면 실제로 재현하기 어려운 실패 시나리오(타임아웃, 거절)를 안전하게 검증할 수 있다.
참고 문헌 및 출처(추천)
- Vladimir Khorikov, 『Unit Testing: Principles, Practices, and Patterns』(Manning, 2020) — 목의 배치 위치와 시스템 경계 논의
- Michael Nygard, 『Release It!』(2007, 2판 2018) — 외부 시스템 실패(타임아웃, 서킷 브레이커) 대응 패턴
- Martin Fowler, “Mocks Aren’t Stubs”(martinfowler.com, 2007)
다음 글
- 다음: 10. 데이터베이스 테스트하기
![Featured image of post [UnitTesting] 09. 목 사용의 모범 사례](/post/unittesting/mocking-best-practices/wordcloud_hu_6fc8e5b5a59264ff.webp)
![[UnitTesting] 07. 가치 있는 테스트로 리팩터링하기](/post/unittesting/refactoring-toward-valuable-tests/wordcloud_hu_4850486c5a378748.webp)
![[UnitTesting] 08. 통합 테스트: 언제, 왜 필요한가](/post/unittesting/why-integration-testing/wordcloud_hu_778f7cca10a80be9.webp)
![[UnitTesting] 09. 목 사용의 모범 사례](/post/unittesting/mocking-best-practices/wordcloud_hu_50c5a3e83dd32262.webp)
![[UnitTesting] 10. 데이터베이스 테스트하기](/post/unittesting/testing-the-database/wordcloud_hu_db0df40e8e982f87.webp)
![[UnitTesting] 11. 흔한 단위 테스트 안티패턴](/post/unittesting/unit-testing-anti-patterns/wordcloud_hu_cc3d2981ed7191c3.webp)
![[UnitTesting] 05. 목과 테스트 취약성](/post/unittesting/mocks-and-test-fragility/wordcloud_hu_58e71e5b0e44ed90.webp)
![[UnitTesting] 00. 단위 테스트, 왜 다시 배워야 하는가](/post/unittesting/unit-testing-overview/wordcloud_hu_84359c81f11cbee.webp)
![[OOAD] 06. 유스케이스 기반 분석 기법](/post/ooad/usecase-based-analysis-techniques/wordcloud_hu_2c99d93cee96e03f.webp)
![[OOAD] 18. 클린 아키텍처와 헥사고날 아키텍처](/post/ooad/clean-hexagonal-architecture/wordcloud_hu_38a408530414282.webp)