458. 테스트 더블 (Test Double) 5가지 개념 (xUnit 테스트 패턴)
핵심 인사이트 (3줄 요약)
- 본질: 테스트 더블(Test Double)은 단위 테스트(Unit Test)를 짤 때, 내 코드가 의존하고 있는 진짜 객체(DB, 외부 API 등)를 가져오기 힘들거나 느릴 때, 이를 완벽하게 흉내 내어 나를 속이는 **5가지 형태의 가짜 대역 배우(Dummy, Stub, Spy, Mock, Fake)**를 통칭하는 객체지향 테스팅 아키텍처다.
- 가치: "테스트는 무조건 0.1초 만에 끝나야 하고, 인터넷이 끊겨도 돌아가야 한다"는 TDD의 절대 철칙을 수호한다. 진짜 카카오페이 결제 API를 붙여 테스트하면 돈이 빠져나가고 느리지만, 가짜 배우(Mock)를 꽂아(DI) 속이면 돈 한 푼 안 들고(비용 제로), 내가 원하는 극단적 에러 상황(500 에러)을 맘대로 조작하여 완벽하게 통제된 실험실을 만들 수 있다.
- 융합: 제어의 역전(IoC)과 의존성 주입(DI) 아키텍처가 선행되어야만 이 가짜 배우들을 외부에서 쏙 주입할 수 있으며, Mockito 같은 자동화 프레임워크와 결합하여 MSA(마이크로서비스) 환경에서 남의 서버가 죽어 있어도 내 서버의 격리된 테스트를 100% 보장해 내는 DevOps 방어선의 핵심이다.
Ⅰ. 개요 및 필요성 (Context & Necessity)
-
개념: '더블(Double)'은 영화 촬영장 용어로, 위험한 액션 씬을 대신 찍어주는 '스턴트 대역 배우(Stunt Double)'에서 유래했다. 진짜 객체를 쓰면 너무 위험하거나(DB 삭제), 너무 느리거나(네트워크 통신), 아직 상대방이 안 만들어졌을 때, 내 코드를 속여 먹기 위해 투입하는 가짜 객체 5형제 세트다.
-
필요성: 내가
주문(Order)함수를 짰는데, 이 함수 안에서이메일 발송(EmailSender)객체를 호출한다. 테스트 코드를 짤 때 진짜 이메일 발송 객체를 쓰면 어떻게 될까? 테스트를 10,000번 돌릴 때마다 내 고객들에게 결제 완료 이메일 10,000통이 진짜로 날아가는 핵폭탄 대참사가 벌어진다. 게다가 인터넷이 끊기면 테스트도 실패한다(불안정성). 아키텍트는 "테스트 환경에서는 절대 진짜 이메일 발송기를 쓰지 말고, 이메일을 날리는 척만 하는 '가짜 배우(Mock)'를 넣어라!"라고 지시해야 한다. 이것이 테스트 더블이 존재하는 유일하고도 거대한 이유다. -
💡 비유: 테스트 더블은 **'소방 훈련용 마네킹'**과 같습니다. 소방관(내가 짠 코드)이 심폐소생술 훈련(단위 테스트)을 해야 하는데, 진짜 사람(진짜 DB 객체)을 데려다 갈비뼈를 부러뜨리며 연습할 수는 없습니다. 그래서 사람의 무게와 가슴 압박 느낌만 완벽하게 흉내 낸 '마네킹(테스트 더블)'을 눕혀놓고 안전하게 수만 번 훈련을 하는 것입니다.
-
등장 배경 및 발전 과정:
- 강한 결합의 지옥: 과거에는 테스트 더블을 쓸 수 없었다. 코드 안에
new EmailSender()가 본드로 붙어(하드코딩) 있어서 가짜를 쑤셔 넣을 구멍이 없었다. - DI의 발명과 Mock의 대중화: 의존성 주입(DI) 원칙이 퍼지며 인터페이스를 통해 부품을 외부에서 꽂을 수 있게 되자,
Mock객체를 주입하여 시스템을 속이는 기법이 전성기를 맞았다. - 제라드 메스자로스의 정리 (xUnit): 현장에서 가짜 객체를 부르는 용어(Mock, Stub 등)가 너무 혼재되어 혼란이 오자, 제라드 메스자로스가 『xUnit Test Patterns』라는 책에서 이 가짜 배우들을 역할에 따라 정확히 5가지(Dummy, Stub, Spy, Mock, Fake)로 분류하여 만천하의 용어를 통일해 버렸다.
- 강한 결합의 지옥: 과거에는 테스트 더블을 쓸 수 없었다. 코드 안에
-
📢 섹션 요약 비유: 영화 촬영장에서 톰 크루즈(핵심 로직)가 절벽에서 떨어지는 씬(단위 테스트)을 찍을 때, 진짜 톰 크루즈를 밀어버리면 영화사(회사)가 망합니다. 그래서 체격이 비슷한 5명의 스턴트맨(더블 5형제) 중 상황에 맞는 놈을 골라 가발을 씌워 떨어뜨리고, 카메라(프레임워크)를 속이는 완벽한 특수 효과입니다.
Ⅱ. 아키텍처 및 핵심 원리 (Deep Dive)
1. 테스트 더블 5총사의 명확한 구분 (암기 필수)
면접과 실무에서 가장 많이 혼동하는 5가지 대역 배우의 '정확한 역할' 차이다.
| 대역 배우 | 행동 방식 (Behavior) | 사용 목적 (When to use) | 실생활 비유 |
|---|---|---|---|
| 1. Dummy (더미) | 깡통. 아무 짓도 안 함. 호출되면 그냥 예외 뿜음. | 파라미터를 채워야 컴파일 에러가 안 날 때, "자리만 채우는 용도". | 교통사고 실험용 플라스틱 마네킹 (앉아만 있음) |
| 2. Stub (스텁) | 대사(Return 값)가 정해진 로봇. "안녕"하면 항상 "10"을 줌. | 내 로직을 돌리기 위해 "외부에서 특정한 정상/에러 값을 무조건 받아야 할 때". | 자판기 흉내. 버튼 누르면 무조건 캔 콜라만 툭 뱉어냄. |
| 3. Spy (스파이) | Stub처럼 값을 주면서, 내가 몇 번 불렸는지 몰래 **"기록(Log)"**함. | 이메일 발송 함수를 탔을 때 진짜로 이메일을 안 쏘고 "발송 함수가 딱 1번 제대로 불렸나?" 검증할 때. | 뒷조사 흥신소. "네, 톰 크루즈가 식당에 총 3번 들어갔습니다." 라고 장부에 적어옴. |
| 4. Mock (목) | 대본(Expectation)대로 행동하는 연기자. 대본과 다르게 부르면 연기 멈춤(Fail). | **"행위 검증(Behavior Verification)"**의 끝판왕. "파라미터 A를 넣어서 정확히 1번 호출해야 해!"라는 행위 자체를 검사. | 연극배우. 감독이 준 대본대로 안 하고 애드립 치면 감독이 **"컷! (테스트 실패)"**을 외침. |
| 5. Fake (페이크) | 진짜 DB는 아니지만 메모리 맵(HashMap)으로 진짜처럼 작동하는 가짜 엔진. | H2 같은 **"인메모리 DB"**를 써서 진짜 쿼리처럼 돌아가는데 엄청 빠른 통합 테스트 환경을 만들 때. | 영화 세트장의 가짜 은행. 진짜 돈은 없지만 문도 열리고 금고도 돌아감. |
2. 상태 검증(State) vs 행위 검증(Behavior)의 차이 (핵심 철학)
테스트 더블을 이해하는 본질적인 양대 산맥이다.
-
상태 검증 (State Verification): "그래서 계산기 돌리고 나서 결과가
5가 나왔어?" ➡ Stub, Fake가 주로 쓴다. 결괏값만 맞으면 중간 과정은 신경 안 쓴다. -
행위 검증 (Behavior Verification): "결과는 모르겠고, 계산하는 과정에서
DB.save()함수를 정확히 1번 호출한 거 확실해?" ➡ Mock, Spy가 쓴다. 값보다 '메서드 호출 행위' 자체를 깐깐하게 따진다. -
📢 섹션 요약 비유: 상태 검증(Stub)은 엄마가 아이에게 "수학 시험 100점(결괏값) 맞아왔니?"라고 결과만 확인하는 것이고, 행위 검증(Mock)은 방에 CCTV(스파이)를 달아놓고 "너 아까 게임 안 하고 수학 문제집(메서드) 정확히 3시간 풀었어?"라고 과정(행위) 자체를 깐깐하게 감시하는 것입니다.
Ⅲ. 융합 비교 및 다각도 분석
1. Mock (목) vs Stub (스텁) 최후의 승부
현업 개발자들이 가장 많이 섞어 쓰는 두 단어의 결정적 차이.
| 척도 | Stub (스텁) | Mock (목) |
|---|---|---|
| 역할 | 값을 제공하는 자판기 (입력값 세팅) | 행위를 감시하는 채점관 (동작 세팅) |
| 테스트 코드의 모습 | when(DB.find()).thenReturn("Kim");(네가 불리면 Kim을 던져라) | verify(DB).save("Kim");(너 방금 save 함수 Kim 넣고 불린 거 맞아?) |
| 에러(Fail) 발생 시점 | 마지막에 assertEquals(result, "Kim") 할 때 비로소 터짐. | Mock에 세팅된 대본과 다르게 함수가 불리는 그 찰나의 순간에 즉시 터짐. |
| 결합도 | 내부 구현에 관심 없음 (약한 결합) | 대상 객체가 무슨 함수를 부르는지 내부를 빤히 알아야 함 (강한 결합의 위험) |
과목 융합 관점
-
아키텍처 (의존성 주입, DI): 테스트 더블은 객체지향 5대 원칙의 **DIP(의존 역전 원칙)**와 완벽하게 융합된다. 만약 코드 안에
private KakaoPay pay = new KakaoPay();라고 강하게 본드칠(하드코딩)이 되어 있다면? 밖에서 아무리 가짜MockKakaoPay를 만들어도 저 코드 안으로 뚫고 들어갈 주사 바늘(생성자)이 없다. 테스트 더블(가짜 배우)을 꽂아 넣기 위해선, 반드시 모든 부품이 생성자를 통해 주입(DI)받도록 설계구조를 뜯어고쳐야(Refactoring) 한다. 테스트를 짜려고 시도하는 순간 아키텍처가 우아해지는 기적이 바로 TDD의 힘이다. -
마이크로서비스 (MSA) / 인프라: MSA 환경에서 내 주문(Order) 서버를 테스트하려는데 옆 팀의 결제(Pay) 서버가 죽어있다. 이러면 3일 내내 내 테스트도 멈춘다(종속성 폭발). 이를 타파하기 위해 외부 인프라 통신(HTTP) 전체를 가로채어 가짜 JSON 응답을 내려주는 WireMock이나 Fake API Server를 세팅한다. 옆 팀 서버가 터지든 말든, 나는 완벽하게 격리된(Isolated) 내 방(로컬)에서 빛의 속도로 1만 번의 테스트를 돌릴 수 있는 절대 방어막을 구축하게 된다.
-
📢 섹션 요약 비유: 내가 아이언맨 팔을 고치는 정비사(단위 테스트)입니다. Stub은 "팔에서 빔이 나가는지 확인하기 위해, 가짜 빔(리턴값)을 쏴주는 버튼"이고, Mock은 "내가 빔 버튼을 누를 때, 팔 안의 전선(로직)이 정확히 배터리(DB)에서 전기를 빨아먹는 행위를 하는지 감시하는 CCTV"입니다. DI(의존성 주입)는 팔을 몸통에서 분리할 수 있게 해주는 나사못입니다. 이 나사가 없으면 톰 크루즈(시스템 전체)를 데려와서 통째로 고쳐야 합니다.
Ⅳ. 실무 적용 및 기술사적 판단
실무 시나리오
-
시나리오 — 너무 많은 Mock에 잡아먹힌 깨지기 쉬운 테스트 (Fragile Test): TDD에 심취한 주니어가
Mockito라이브러리를 쓰면서 모든 의존성을 싹 다 Mock으로 발라버렸다. "DB 불리면 이거 줘, 이메일 불리면 이거 해"라며 대본(verify)을 100줄이나 짰다. 나중에 이메일 발송 함수의 이름이send()에서dispatch()로 1글자 바뀌었다. 진짜 비즈니스 로직은 멀쩡히 잘 도는데, 대본과 달라졌다고 1,000개의 Mock 테스트가 일제히 피를 토하며(Fail) 다 터져버렸다.- 아키텍트의 해결책: 전형적인 Mock 남용에 의한 과도한 구현 결합(Over-specified Implementation)의 저주다. 행위 검증(Mock)은 내부 구조를 속속들이 감시하기 때문에, 코드를 리팩토링할 때마다 대본을 다 고쳐야 하는 최악의 피곤함(유지보수 지옥)을 낳는다. 아키텍트는 룰을 정해야 한다. "단순히 값을 계산하는 객체는 진짜 객체나 Stub을 써라(상태 검증). 오직 이메일 발송, 결제 API 쏘기처럼 한 번 불리면 돌이킬 수 없는 파괴적인 행위(Side Effect)를 하는 놈의 통로에만 가끔씩 Mock(행위 검증)을 박아라."
-
시나리오 — 느려 터진 H2 인메모리 DB(Fake)와 100% Mock의 딜레마: 백엔드 팀장이 "우리는 단위 테스트 짤 때 무조건
Mock만 쓴다. DB 쿼리 날리는 건 다 가짜(Stub)로 대체해!"라고 룰을 세웠다. 단위 테스트 1만 개가 1초 만에 도는 미친 속도를 얻었다. 그런데 오픈 당일, SQL 쿼리 문법(JOIN)이 틀려서 라이브 서버가 와르르 무너졌다. Mock이 가짜 데이터를 완벽하게 리턴해 주니까, 정작 진짜 쿼리(SQL)가 썩었는지 맞았는지는 테스트가 한 번도 검증하지 않은 것이다.- 아키텍트의 해결책: 단위 테스트(Unit)와 통합 테스트(Integration)의 균형 상실이다. 비즈니스 로직(if, for문)은 Mock으로 순수하게 격리해서 빛의 속도로 테스트하는 게 맞다. 하지만 DB와 맞닿는 끝단 코드(Repository, DAO)는 Mock으로 속이면 절대 안 된다. 거기는 스프링과 결합하여 아주 가벼운 인메모리 가짜 DB인 **H2 Database (Fake 객체의 거대 버전)**를 띄워서, "진짜 SQL 쿼리가 날아갔을 때 DB가 욕을 안 하고 표를 만들어 주는지" 진짜 흙탕물 통합 테스트를 거쳐야만 라이브 장애를 막을 수 있다. (테스트 피라미드 원칙)
도입 체크리스트
- 기술적: 팀이 사용하는 프레임워크가 Mockito의 흑마법을 이해하는가? Java 생태계에서 Mock을 쓰려면
Mockito가 국룰이다.@Mock,@InjectMocks어노테이션 두 개만 박으면 프레임워크가 알아서 가짜 로봇을 생성하고 내 서비스 객체 배때지에 알아서 푹 꽂아(DI) 준다. 이 마법 같은 어노테이션 세팅 문법을 팀원들이 숨 쉬듯 쓸 수 있도록 템플릿화 시켜야 TDD가 날아오른다. - 설계적: 당신의 더미(Dummy)는 진짜 더미인가? 회원가입 함수를 테스트하는데, 파라미터로
User(이름, 나이, 주소, 핸드폰)객체를 억지로 채워 넣으라며 10줄의 코드를 낭비한다(테스트 가독성 파괴). 이 함수는 '이름'만 검사하고 나머진 안 보는데 말이다. 이때는null을 넘기거나 내용이 빈 깡통 객체(new DummyUser())를 던져서, "이 테스트에서는 이 값은 안 중요해!"라는 의도를 코드로 투명하게 표현해야 한다.
안티패턴
-
테스트 코드를 위한 운영 코드 수정 (Production Code Tainted for Test): 테스트를 짜다 보니 가짜 객체(Mock)를 꽂을 수가 없다며, 뜬금없이 운영 소스코드(Production) 안에
if (isTestMode) { return FakeDB; }라는 끔찍한 분기문을 심어놓는 대재앙 안티패턴. 해커가 이 파라미터를 조작해 라이브 서버를 테스트 모드로 돌려버리면 회사가 멸망한다. "테스트의 흔적이 운영 코드에 단 1바이트라도 남아서는 안 된다. 오직 의존성 주입(DI) 구조로만 우아하게 분리하라." -
📢 섹션 요약 비유: Mock에 너무 강하게 결합된 테스트 코드는, 영화감독이 스턴트맨(Mock)에게 "너 절벽에서 떨어질 때 무조건 왼발부터 구르고 공중에서 3바퀴 반 돌아!"라고 대본을 100줄 써놓은 것과 같습니다. 나중에 영화 스토리(로직)가 바뀌어서 2바퀴만 돌아도 되는데, 스턴트맨이 대본(테스트 코드)과 다르다며 촬영을 거부(에러 뿜음)합니다. 감독은 스턴트맨 달래느라 며칠 밤을 새웁니다(유지보수 지옥). 행동은 느슨하게 검증해야 합니다.
Ⅴ. 기대효과 및 결론
정량/정성 기대효과
| 구분 | 진짜 DB/API에 엉겨 붙어 테스트하는 환경 (AS-IS) | Mock/Stub 등 테스트 더블로 격리된 환경 (TO-BE) | 개선 효과 |
|---|---|---|---|
| 정량 | DB 띄우고 네트워크 타느라 단위 테스트 1개당 2초 소요 | Mock 주입으로 메모리에서 0.001초 컷 | 테스트 전체 수행 시간 99% 초극강 단축 (수십 분 -> 수십 초) |
| 정량 | 옆 팀 결제 서버 뻗으면 내 테스트도 1주일간 Fail | Mock 서버로 외부 API 응답 조작하여 365일 100% 성공 | 외부 인프라 종속성에 의한 개발 및 배포 지연 리드타임 0일 |
| 정성 | "DB 데이터 꼬일까 봐 테스트 돌리기 무서워" | "아무렇게나 때려도 내 격리된 방이라 안전해!" | 실패를 두려워하지 않는 거침없는 리팩토링과 TDD 문화의 정착 |
미래 전망
- AI 몹(Auto Mocking) 생성의 일상화: "이 인터페이스에 맞는 Stub 코드 50줄 좀 짜줘"는 가장 지루한 노가다였다. 이제 깃허브 코파일럿(Copilot)이 테스트 파일의 이름만 봐도, 당신의 서비스가 어떤 인터페이스에 의존하는지 1초 만에 스캔하고, 완벽한 행동 대본(when, thenReturn)이 박힌
Mockito코드를 100줄씩 토해내는 자동 목킹 시대가 열렸다. 개발자는 로직만 짜면 된다. - 클라우드 기반 섀도잉 (Traffic Shadowing): 기존 테스트 더블이 '가짜 배우'를 쓰는 거라면, 미래의 넷플릭스 아키텍처는 아예 운영 서버에 들어오는 1만 명의 '진짜 트래픽'을 거울(Shadow)처럼 몰래 복사해서, 내 로컬이나 스테이징에 떠 있는 새 서버 코드에 동시에 쏴버린다(가짜 객체가 필요 없음). 가장 현실적이고 가장 무자비한 라이브 테스트 더블의 끝판왕이 인프라 레벨(Istio 등)에서 진화 중이다.
참고 표준
- xUnit Test Patterns (Gerard Meszaros): 전 세계 모든 개발자가 혼용해 쓰던 가짜 객체 용어를 Dummy, Stub, Spy, Mock, Fake 5원소로 칼같이 정의하고 통일시킨 소프트웨어 테스팅의 바이블 도서.
- Mockito / Sinon.js / Jest: 자바, 자바스크립트 등 각 언어 생태계에서 "나를 속여줘!"를 가장 쉽고 우아하게 어노테이션과 함수 몇 개로 처리해 주는 글로벌 표준 테스트 더블 프레임워크들.
테스트 더블(Test Double)은 소프트웨어 공학이 발명해 낸 **'가장 완벽한 무균 실험실(Clean Room)'**이다. 세상의 모든 버그는 내가 짠 코드(내부)와 남이 짠 세상(외부 DB, API)이 진흙탕처럼 섞여 도는 통합 환경의 혼돈 속에서 싹튼다. 기술사는 외부의 혼돈(네트워크 지연, 500 에러)을 철저하게 가짜 배우(Mock/Stub)로 차단하고, 오직 내가 짠 '순수한 비즈니스 로직(Core)'만이 투명한 유리상자 안에서 빛의 속도로 춤을 출 수 있는 무대를 만들어 주어야 한다. 외부의 폭풍 속에서도 내 테스트의 불빛이 흔들리지 않고 언제나 녹색 불(Green)을 영롱하게 밝힐 수 있는 우아한 격리(Isolation), 그것이 테스트 더블이 선사하는 아키텍트의 특권이다.
- 📢 섹션 요약 비유: 테스트 더블 없이 하는 코딩은 **'진짜 총알을 쏘며 하는 군사 훈련'**입니다. 훈련(테스트)하다가 아군(DB)이 진짜로 죽습니다. 테스트 더블을 쓰는 것은 **'페인트탄과 레이저건'**을 들고 모의 시가전 세트장(Fake/Mock)에서 싸우는 것입니다. 1만 번을 쏴도 아무도 죽지 않고 돈도 안 들지만, 누가 명중시켰고 전술이 훌륭했는지(로직 검증)는 완벽하게 증명해 낼 수 있는 위대한 시뮬레이터입니다.
📌 관련 개념 맵 (Knowledge Graph)
| 개념 명칭 | 관계 및 시너지 설명 |
|---|---|
| 의존성 주입 (DI) | 테스트 더블(가짜 칩)을 내 스마트폰 배때지에 꽂아 넣으려면 반드시 선행되어야 하는 객체지향 최고의 발명품. DI가 없으면 가짜 객체를 주입할 구멍 자체가 없다. (이전 장 337번) |
| 단위 테스트 (Unit Test) | 단위(Unit)를 완벽하게 고립시키기 위해, 나를 둘러싼 모든 세상을 Mock과 Stub으로 잘라내고 철저히 나 혼자 방에 가두고 테스트하는 격리 철학. |
| TDD (테스트 주도 개발) | "실패하는 테스트부터 짠다"는 철학. 상대방 팀이 아직 DB 코드를 안 짰어도, 나는 쿨하게 가짜 DB 로봇(Stub)을 세워두고 내 코드를 빛의 속도로 완성해 나가는 동시 개발의 마법. |
| Mockito | Java 세상에서 저 5명의 대역 배우(Stub, Spy, Mock)를 단 2줄의 코드로 마법처럼 소환해서 내 코드에 버무려 주는 전 세계 1위 오픈소스 도우미. |
| 상태 검증 vs 행위 검증 | Stub(상태 검증)이 "결과값인 빵이 5개 나왔네? 합격!"이라면, Mock(행위 검증)은 "빵 굽는 함수가 정확히 1번 불렸네? 합격!"이라고 따지는 철학적 잣대의 차이. |
👶 어린이를 위한 3줄 비유 설명
- 내가 의사 선생님이 되는 수술 연습을 해야 하는데, 내 동생(진짜 객체)을 수술대 위에 눕혀놓고 진짜 칼로 연습할 수는 없잖아요? (너무 위험하고 큰일 남!)
- 그래서 동생이랑 똑같이 생기고 피 대신 물감이 터지는 **'고무 마네킹 인형(가짜 배우)'**을 눕혀놓고, 내 마음대로 100번이고 1,000번이고 찌르며 연습을 했어요.
- 이렇게 진짜 데이터베이스나 프로그램을 쓰면 큰일이 나거나 너무 느릴 때, 진짜인 척 나를 속여주는 5가지의 가짜 마네킹 로봇 친구들을 **'테스트 더블(Test Double)'**이라고 부른답니다!