테스트 더블 (Test Double) Mock과 Stub의 차이점

핵심 인사이트 (3줄 요약)

  1. 본질: 테스트 더블(Test Double)은 단위 테스트(Unit Test)를 수행할 때 실제 데이터베이스나 외부 API 등 제어하기 힘든 의존성을 대신하여 동작하는 '가짜 객체'의 총칭이며, 그중 대표적인 것이 **Stub(상태 기반 테스트)**과 **Mock(행위 기반 테스트)**이다.
  2. 가치: 외부 시스템의 장애나 네트워크 지연에 구애받지 않고 빠르고(Fast) 일관된(Deterministic) 단위 테스트를 작성할 수 있게 해주어, TDD(테스트 주도 개발)의 퀄리티와 피드백 루프 속도를 극적으로 향상시킨다.
  3. 융합: Stub은 "테스트 대상이 올바른 값을 반환하는가?"를 검증하는 데 쓰이고, Mock은 "테스트 대상이 외부 객체와 올바른 순서로 소통(메서드 호출)했는가?"를 검증하는 데 쓰이며, 클린 아키텍처(Clean Architecture)의 의존성 역전 원칙(DIP)과 결합되어 테스트 용이성(Testability)을 극대화한다.

Ⅰ. 개요 및 필요성 (Context & Necessity)

  • 개념: 영화 촬영을 할 때 위험한 장면에서 진짜 배우 대신 '스턴트 더블(Stunt Double)'이 연기하듯, 소프트웨어 테스트에서도 진짜 컴포넌트 대신 투입되는 가짜 컴포넌트를 제라드 메스자로스(Gerard Meszaros)가 '테스트 더블'이라고 명명했다. 테스트 더블의 5가지 종류는 Dummy, Fake, Stub, Spy, Mock이다.

  • 필요성: 회원 가입 로직을 테스트하려면 DB에 연결하고, 메일 서버(SMTP)에 연결해야 한다. 만약 메일 서버가 점검 중이면 내 코드에 버그가 없어도 테스트는 실패(False Negative)한다. 또한 DB에 매번 데이터를 넣었다 지웠다 하면 테스트가 너무 느려져서 개발자가 테스트 실행 버튼을 누르기 싫어지게 된다. 진짜 객체를 빠르고 통제 가능한 '가짜(Test Double)'로 갈아 끼워야만 완벽한 고립(Isolation) 테스트가 가능해진다.

  • 💡 비유: 조종사가 비행 연습을 할 때, 매번 진짜 수백억 원짜리 비행기를 타고 하늘로 올라가서 추락해 볼 수는 없습니다. 그래서 조종석과 똑같이 생겼지만 날지는 않는 '비행 시뮬레이터(Test Double)'에 앉아 연습을 하죠. 이때 버튼을 누르면 미리 입력된 풍속 수치를 화면에 띄워주는 것이 Stub이고, 비상 탈출 버튼을 매뉴얼 순서대로 정확하게 눌렀는지 기계가 감시하고 채점하는 것이 Mock입니다.

  • 등장 배경 및 발전 과정:

    1. 초기 단위 테스트의 어려움: 코드가 덩어리(Monolithic)로 짜여있어 외부 의존성을 끊어내고 테스트하기가 불가능에 가까웠다.
    2. 의존성 주입(DI)의 확산: Spring 프레임워크처럼 생성자로 객체를 주입(Inject)받는 패턴이 표준화되면서, 운영 시에는 '진짜 DB'를 넣고 테스트 시에는 '가짜 DB'를 밀어 넣는 것이 매우 쉬워졌다.
    3. Mockito 등 프레임워크의 대중화: 수동으로 가짜 클래스를 코딩하던 시절을 지나, Mockito(Java), Jest(Node.js) 같은 라이브러리가 등장하며 어노테이션 한 줄로 Mock과 Stub을 찍어내는 TDD 시대가 열렸다.
  • 📢 섹션 요약 비유: 실제 배우(외부 API)가 스케줄이 안 맞거나 너무 비싸서 못 올 때, 감독(테스터)이 지시한 대사만 딱딱 읽어주는 엑스트라(Stub)나, 주인공과 약속된 동작(합)을 정확히 맞추는지 감시해 주는 스턴트맨(Mock)을 대신 부르는 것과 같습니다.


Ⅱ. 아키텍처 및 핵심 원리 (Deep Dive)

테스트 더블 5가지 종류의 명확한 정의

종류역할특징실무 예시
1. Dummy파라미터 채우기용 (안 쓰임)로직이 없고 그냥 null이나 쓰레기값을 반환new Customer(dummyAuth)
2. Fake단순화된 진짜 동작 구현DB 대신 메모리(Map)에 데이터 저장 및 조회InMemoryUserRepository
3. Stub미리 준비된 응답(상태) 제공"A를 요청하면 B를 리턴해라"라고 미리 세팅됨환율 API 호출 시 무조건 '1300원' 반환
4. Spy객체의 행위(호출 기록) 몰래 감시실제 객체처럼 동작하되, 호출 횟수 등을 기록이메일 발송 메서드가 몇 번 불렸는지 카운트
5. Mock기대되는 행위(순서/호출) 검증사전에 "이 메서드가 불려야만 통과!"라고 룰 지정verify(emailService).send(...)

Stub vs Mock의 구조적/철학적 차이점

가장 헷갈리기 쉬운 **Stub(상태 기반 검증)**과 **Mock(행위 기반 검증)**의 차이를 마틴 파울러(Martin Fowler)의 관점에서 완벽히 구분해 보자.

  ┌───────────────────────────────────────────────────────────────┐
  │         테스트 패러다임 비교: Stub vs Mock (마틴 파울러 모델)          │
  ├───────────────────────────────────────────────────────────────┤
  │                                                               │
  │  [ 1. Stub: 상태 기반 검증 (State Verification) ]               │
  │    목적: 테스트 대상이 "올바른 결과값"을 만들어 내는가?                 │
  │                                                               │
  │        (1. 준비된 데이터 응답)                                       │
  │   [Stub Mail Service] ────▶ [ Order Service ] ◀──── (2. 로직 호출)│
  │                                    │                  [ Test ]    │
  │                                    ▼ (3. 결과값 비교)              │
  │                     결과 객체의 상태(Status)가 "Success"인가?        │
  │                     (메일 서비스가 진짜 불렸는지는 관심 없음!)           │
  │                                                               │
  │  [ 2. Mock: 행위 기반 검증 (Behavior Verification) ]            │
  │    목적: 테스트 대상이 "외부 객체와 올바르게 소통(호출)" 했는가?          │
  │                                                               │
  │         (3. 감시 및 검증)                                           │
  │   [Mock Mail Service] ◀──── [ Order Service ] ◀──── (1. 로직 호출)│
  │           ▲                        │                  [ Test ]    │
  │           └────────────────────────┼──────────────────────┘       │
  │                                    │                              │
  │                     (2. 상호 작용 / sendMail() 호출)                 │
  │                                                               │
  │   ▶ 검증 로직: "Mock 객체야, OrderService가 네 안의 sendMail() 메서드를 │
  │              정확히 1번, 올바른 이메일 파라미터를 넣어서 호출했니?"       │
  └───────────────────────────────────────────────────────────────┘

[다이어그램 해설] Stub은 그저 멍청하게 준비된 대답(예: "재고 있음")만 던져주는 인형이다. 테스트 코드는 OrderService를 실행한 후 반환된 Order 객체의 최종 '상태(State)'가 제대로 바뀌었는지만 검증(assert)한다. 반면 Mock은 능동적인 감시자다. OrderService가 외부 API인 MailService에 이메일을 전송하는 로직이 있다고 할 때, 테스트 환경에서는 메일을 진짜 쏠 수가 없다. 이때 Mock Mail Service를 찔러 넣고, 나중에 테스트 코드가 Mock에게 **"너 아까 sendMail() 호출받았어? 몇 번 받았어?"**라고 행위(Behavior) 자체를 물어보아 검증(verify)한다.


Ⅲ. 실무 적용 및 기술사적 판단

실무 시나리오

  1. 시나리오 — 외부 결제 API(PG) 연동 테스트: 사용자가 10,000원을 결제할 때 PG사 서버를 찌르는 로직을 테스트해야 한다. 진짜 PG사 API를 찌르면 매번 내 카드로 1만 원씩 결제되었다가 취소되는 환장할 사태가 벌어진다.

    • 판단: 외부 시스템과의 연동은 상태 기반 검증(Stub)으로 1번, 행위 기반 검증(Mock)으로 1번 각각 테스트해야 한다.
    • 해결책 (Stub 적용): 먼저 PG사 API가 정상 응답(200 OK)을 줄 때와 잔고 부족(400) 에러를 줄 때를 Stubbing(when().thenReturn()) 하여, 내 비즈니스 로직이 각 상황에서 상태를 잘 변경하는지 테스트한다.
    • 해결책 (Mock 적용): 내 결제 서비스가 PG사 API를 찌를 때 금액과 카드번호 파라미터를 엉뚱하게 넘기지는 않았는지, 취소 시 cancel() 메서드를 빼먹지 않고 딱 1번만 불렀는지를 Mocking(verify()) 하여 행위를 철저히 검증한다.
  2. 시나리오 — 과도한 Mocking (Mockist TDD의 함정): 한 개발자가 모든 데이터베이스 Repository와 하위 Service들을 전부 Mock 객체로 떡칠(Over-mocking)하여 테스트 코드를 짰다. 덕분에 속도는 광속이 되었지만, 나중에 DB 스키마가 바뀌거나 구현 로직을 조금만 수정(Refactoring)해도 수백 개의 테스트 코드가 일제히 깨지며(Brittle Test) 테스트 코드를 버려야 하는 상황이 되었다.

    • 판단: Mock은 객체 간의 결합(Coupling) 즉, 내부 구현 방식을 테스트 코드에 강제로 노출시킨다. 내부 로직(메서드명 등)이 바뀌면 Mock 테스트도 깨진다.
    • 해결책: 가급적 Mock보다는 상태 기반 테스트(Classicist TDD)와 Fake 객체를 우선시해야 한다. 데이터베이스 계층은 Mocking하는 것보다 H2 같은 In-memory DB를 띄우거나 Testcontainers(Docker)를 띄워 **실제와 가까운 환경(Fake)**에서 통합 단위 테스트를 돌리는 것이 리팩토링에 훨씬 강인(Robust)하다. 외부 통제 불가능한 API(메일, SMS, PG)에 한해서만 제한적으로 Mock을 사용하는 것이 성숙한 아키텍트의 설계다.

도입 체크리스트

  • 코드 설계적: 내 코드가 테스트 더블을 주입받을 수 있는 구조(의존성 주입, 생성자 주입)로 짜여 있는가? 클래스 안에서 new 외부API()를 직접 생성해 버리면(강결합) 아무리 뛰어난 Mock 프레임워크를 가져와도 가짜 객체를 갈아 끼울 방법이 없다.

Ⅳ. 기대효과 및 결론

정량/정성 기대효과

구분테스트 더블 미사용 (통합 테스트)테스트 더블 (Mock/Stub) 사용개선 효과
정량 (속도)DB, 네트워크 연결로 건당 수백 ms 소요메모리 연산으로 건당 1ms 미만 소요수천 개의 단위 테스트 실행 시간 10초 이내로 극적 단축
정성 (안정성)외부 서버 죽으면 내 테스트도 실패 (Flaky)외부와 완벽히 격리된 독립 환경 (Isolated)거짓 양성/음성 제거로 테스트 신뢰도 100% 확보
정성 (설계)스파게티 코드 양산 (강결합)더블을 넣기 위한 강제 인터페이스 분리 유도SOLID 원칙(DIP)을 자연스럽게 준수하는 클린 아키텍처 달성

테스트 더블(Mock과 Stub)은 단순히 테스트를 편하게 해주는 도구가 아니다. 내 코드가 외부의 더러운 세상(DB, Web, 프레임워크)과 얼마나 강력하게 결합되어 있는지를 고발하는 엑스레이(X-ray)다. 기술사는 "Mocking이 힘들다"고 불평하는 개발자에게 테스트 도구의 사용법을 가르칠 것이 아니라, "당신의 코드가 강결합되어 있기 때문에 가짜 객체를 넣기 힘든 것"이라는 아키텍처적 진실을 짚어주고, 인터페이스(Port)를 분리하도록 헥사고날(Hexagonal) 설계로 유도해야 한다.


📌 관련 개념 맵 (Knowledge Graph)

개념 명칭관계 및 시너지 설명
TDD (테스트 주도 개발)테스트 코드를 먼저 짜고 본 코드를 짜는 개발 방법론으로, 빠른 피드백을 위해 테스트 더블(Mock/Stub)이 절대적으로 필요하다.
의존성 주입 (DI, Dependency Injection)외부 의존성을 클래스 외부에서 넣어주는 패턴으로, 이게 없으면 테스트 더블을 찔러 넣는 것 자체가 불가능해진다 (IoC 컨테이너).
Testcontainers (테스트컨테이너)Mocking의 맹점(가짜 통과)을 없애기 위해, 테스트가 돌 때만 Docker로 진짜 DB나 Redis를 띄워버리는 최신 TDD 트렌드 도구다.
Flaky Test (불안정한 테스트)어떨 땐 성공하고 어떨 땐 실패하는 악마 같은 테스트. 주로 네트워크, 시간, 난수 등에 의존할 때 발생하며, 테스트 더블로 통제(고정)하여 해결한다.
Mockito / JestJava와 Node.js 생태계에서 Mock과 Stub 객체를 동적으로 아주 쉽게 생성해 주고 검증해 주는 사실상 표준 라이브러리들이다.

👶 어린이를 위한 3줄 비유 설명

  1. 축구 선수가 슛 연습을 하려고 훈련장에 갔어요. 진짜 골키퍼를 매번 부르면 돈도 들고 골키퍼가 다칠 수도 있잖아요?
  2. 그래서 골대 앞에 가만히 서 있는 '나무판자 사람'을 세워두고 슛 연습을 하는데, 이 나무판자가 바로 **'Stub(스텁)'**이에요.
  3. 그런데 코치님이 "너 아까 왼발로 몇 번 찼는지, 제대로 된 폼으로 찼는지 다 비디오로 녹화해서 검사할 거야!"라고 카메라를 달아두고 나를 감시하면, 그 카메라는 **'Mock(목)'**이랍니다!