콘텐츠로 바로가기

Testing Pyramid & Isolation Physics

테스트의 유형별 비용과 속도를 고려한 효율적인 상호 비중 설계를 다루며, 외부 의존성을 완벽히 차단하여 무결성을 유지하는 테스트 격리 물리학을 배우는 학습 노드입니다.

Article
M

Me

hyunyoun's Blog

software-engineering-devopssoftware-engineeringdev-opsqaquality-assurancetesting-pyramidisolation-physicstesting11 min read

1. Overview

테스트 피라미드와 격리의 물리학(Testing Pyramid & Isolation Physics)은 "코드가 돌아가니까 배포하자"는 야만의 시대를 끝내고, 버그가 실서버(Production)에 도달하기 전에 무자비하게 박살 내버리는 자동화된 방어망의 구조역학을 해부합니다.

학습자는 UI를 클릭하는 느리고 비싼 테스트에 집착하다가 파멸하는 안티 패턴(아이스크림 콘)을 걷어차고, 빠르고 싼 단위 테스트(Unit)를 넓게 깔아 튼튼한 뼈대를 구축하는 **테스트 피라미드(Testing Pyramid)**의 중력 법칙을 뜯어봅니다. 나아가 내 로직을 검증하고 싶은데 외부 API가 응답하지 않아 테스트가 실패하는 억울함을 막기 위해, 가짜 객체를 투입하여 타겟만 완벽히 도려내는 격리(Isolation) 기법을 장악합니다. 마지막으로, Mock, Stub, Fake, Spy 등 이름만 들어도 헷갈리는 가짜 객체들(Test Doubles)의 쓰임새를 칼같이 분리하는 외과 수술적 모킹(Mocking) 역량을 확보합니다.

2. Scope & Boundaries

In-Scope

  • Testing Pyramid: Unit Tests (하단/빠름/저비용), Integration Tests (중단/DB연동), E2E Tests (상단/느림/고비용).
  • Test Isolation: 외부 의존성(DB, Network, File)으로부터 검증 대상(SUT, System Under Test)을 완벽히 분리하는 기법.
  • Test Doubles (가짜 객체): Dummy (전달용), Stub (하드코딩 응답), Spy (호출 기록), Mock (행위 검증), Fake (가짜 구현체, 예: 인메모리 DB).
  • Ice Cream Cone Anti-pattern: E2E 테스트만 잔뜩 쌓아놓고 Unit 테스트는 텅 빈 기형적 테스트 구조.

Out-of-Scope

  • TDD (Test-Driven Development): 테스트 코드를 '먼저' 짜서 설계를 주도하는 방법론 \rightarrow 09-04-02 TDD & AAA Pattern 영역으로 위임.
  • CI/CD 파이프라인 연동: 테스트를 젠킨스(Jenkins)에서 언제 돌릴 것인가의 타이밍 문제 \rightarrow 09-04-04 CI Gates 영역으로 분리.

Boundaries

  • Unit vs Integration: "이 테스트가 Unit 테스트인가요?"라는 질문의 기준은 명확합니다. 1) DB를 찌르는가? 2) 네트워크(외부 API)를 타는가? 3) 파일 시스템을 읽는가? 이 세 가지 중 하나라도 True라면 그것은 Unit 테스트가 아니라 Integration 테스트입니다. Unit 테스트는 오직 메모리(Memory) 안에서 빛의 속도(수 밀리초)로 실행되고 독립적이어야 한다는, 테스트의 물리적 경계선(I/O Boundary)을 명확히 긋습니다.

3. Counterexample

  • 아이스크림 콘의 녹아내림 (Ice Cream Cone Anti-pattern): 테스트 자동화를 하라니까, 개발팀이 Selenium이나 Cypress로 웹 브라우저를 띄워 회원가입부터 결제까지 클릭하는 E2E 테스트만 100개를 만들었습니다. 단위 테스트(Unit)는 0개입니다. 결과? 이 테스트를 한 번 돌릴 때마다 3시간이 걸립니다. 심지어 네트워크가 살짝 느리면 테스트가 실패합니다(Flaky Test). 개발자들은 테스트가 너무 느려서 코드를 수정해도 테스트를 돌리지 않고 배포해버립니다. E2E라는 꼭대기(고비용)에만 투자하여 무너져 내린 자동화의 재앙입니다.
  • 모의 객체의 배신 (Mocking Abuse): 결제 로직을 테스트합니다. 데이터베이스(Repository)도 Mock으로 만들고, 외부 API(PG)도 Mock으로 만들고, 심지어 내부의 할인 계산기(Calculator)까지 다 Mock으로 덮었습니다. 코드는 초록불(Pass)이 뜹니다. 하지만 실서버에 나가자마자 결제가 다 터집니다. 테스트 코드 안에서 '가짜'들끼리 춤을 추며 "내가 이렇게 물어보면 넌 이렇게 대답해"라고 짜고 친 고스톱(Over-mocking)을 쳤기 때문입니다. 격리(Isolation)의 본질을 잃고 시스템을 바보로 만든 모킹의 남용입니다.

4. Prerequisites

  • 객체지향과 의존성 주입 (Basic): Interface를 통해 의존성을 갈아 끼울 수 있는(DI) 코드 구조. (09-03-01 SOLID)
  • 에러 핸들링 (Basic): 예외(Exception) 처리의 기본 메커니즘.

5. Learning Map

Sequence Core Cluster Objective & Description Evidence (BoK)
1 The Testing Pyramid 피라미드 하단에 빠르고 싼 유닛 테스트를 수천 개 깔고, 꼭대기에 비싸고 느린 E2E를 조금만 얹는 중력 역학을 쥡니다. P1
2 Physics of Isolation 테스트가 외부 네트워크나 DB가 죽었다고 실패하는 억울함을 막기 위해, 시스템의 결합을 끊어내는 수술용 메스를 해부합니다. P5
3 The Anatomy of Test Doubles 모형 인형(Dummy)부터 앵무새(Stub), 첩자(Spy), 배우(Mock), 짝퉁(Fake)까지 가짜 객체 5형제의 본질적 차이를 뜯어봅니다. Industry
4 State vs Behavior Verification "결과값이 10이 나왔는가?(State)"를 검증하는 고전파와, "그 함수를 3번 호출했는가?(Behavior)"를 따지는 모키스트(Mockist)의 철학을 장악합니다. Industry

6. Learning Topics

Basic

Core Topic 01: 비용과 속도의 중력장 (The Testing Pyramid)

  • Why to Learn: 모든 코드를 브라우저를 띄워 E2E로 테스트하려는 무식함을 버리고, 버그가 발생한 위치를 0.1초 만에 바늘처럼 집어내는 효율적인 포트폴리오를 설계하기 위함입니다.
  • What to Learn:
    • Concepts: Testing Pyramid, Unit Test, Integration Test, E2E (End-to-End) Test, Test Execution Time, Flaky Test.
    • Skills: 프로젝트의 테스트 전략을 짤 때, Unit Test 70%, Integration 20%, E2E 10%의 비율로 리소스를 할당하고, CI 파이프라인의 실행 시간을 5분 이내로 통제하는 아키텍처 구성하기.
  • How to Learn:
    • 1단계: Unit (하단/빠름): 함수 하나, 클래스 하나만 떼어놓고 테스트합니다. DB 연결도 없습니다. 수천 개를 돌려도 1초면 끝납니다. 어디서 고장 났는지(Line 번호까지) 정확히 알려줍니다.
    • 2단계: Integration (중단/무거움): 내가 짠 코드와 진짜 DB(또는 Redis)를 연결해서 테스트합니다. SQL 문법이 맞는지, 트랜잭션이 잘 걸리는지 확인합니다. 수십 초가 걸립니다.
    • 3단계: E2E (꼭대기/비쌈): 유저처럼 로그인하고 버튼을 누릅니다. 셀레니움(Selenium)이 브라우저를 조작합니다. 네트워크 지연 때문에 툭하면 깨집니다(Flaky). 수십 분이 걸립니다.
    • 4단계: 아이스크림 콘 타파: 테스트를 거꾸로 짜면 지옥이 열립니다. 피라미드의 하단을 넓고 단단하게 다져야, E2E 테스트가 터졌을 때 디버깅 하느라 밤을 새우는 고통을 피할 수 있다는 물리 법칙을 해부합니다.
  • Implement: 테스트 전략 포트폴리오 ROI 시뮬레이터. Unit(1000개): 실패 시 0.1초 만에 "User.class 35라인 에러" 출력. E2E(1000개): 실패 시 1시간 만에 "결제 페이지 안 뜸 (원인 불명)" 출력. 두 방식의 피드백 루프 속도가 개발자의 퇴근 시간에 미치는 나비효과 렌더링.

Practical

Core Topic 02: 진공 상태 만들기 (Physics of Isolation)

  • Why to Learn: 내 코드는 완벽한데 카카오 API 서버가 죽어서 내 테스트 코드가 FAIL을 뿜어내는 기가 막힌 연좌제를 끊어내고, 오직 내 코드의 잘못만 심판하기 위함입니다.
  • What to Learn:
    • Concepts: SUT (System Under Test, 검증 대상), DOC (Depended-On Component, 의존 컴포넌트), Deterministic (결정론적), I/O Boundary.
    • Skills: "날씨 API를 찔러서 비가 오면 알람을 보낸다"는 로직을 테스트할 때, 진짜 기상청 API를 찌르는(Nondeterministic) 행위를 끊어버리고, 인터페이스를 통해 '항상 비가 오는' 환경을 주입(DI)하여 진공 상태 만들기.
  • How to Learn:
    • 1단계: 결정론(Deterministic)의 붕괴: 어제는 통과했던 테스트가 오늘은 실패합니다. 기상청 API에서 오늘은 맑은 날씨를 리턴했기 때문입니다. 환경에 따라 결과가 바뀌면 그건 쓰레기 테스트입니다.
    • 2단계: I/O 절단술: 내 코드(SUT)가 기상청(DOC)과 직접 붙어 있으면 안 됩니다. WeatherService 인터페이스로 쪼갭니다(DIP).
    • 3단계: 격리 (Isolation): 테스트할 때 진짜 기상청 대신 FakeWeatherService를 밀어 넣습니다. 이 가짜 객체는 외부 인터넷을 타지 않고 메모리 안에서 무조건 "비 내림"을 리턴합니다. 완벽한 진공 상태(Isolation) 안에서 내 if(비가 오면) 로직이 정확히 동작하는지만 현미경으로 들여다보는 외과 수술을 뜯어봅니다.
  • Implement: 외부 결합도 절단(Decoupling) 및 의존성 주입 모사. Bad: public Alarm() { this.api = new RealWeatherAPI(); } \rightarrow 네트워크 단절 시 테스트 Fail (격리 0%). Good: public Alarm(WeatherAPI api) { this.api = api; } \rightarrow 테스트 시 new Alarm(new AlwaysRainAPI()) 주입 \rightarrow 네트워크 뽑고 테스트해도 100% Pass. 결정론적 세계의 구축 시각화.

Practical

Core Topic 03: 5가지 가짜들의 해부학 (The Anatomy of Test Doubles)

  • Why to Learn: 테스트할 때 아무 가짜나 대충 던져 넣으면 코드가 스파게티가 됩니다. 상황에 맞춰 깡통(Dummy)을 쓸지 스파이(Spy)를 쓸지 정확한 수술 도구를 고르기 위함입니다.
  • What to Learn:
    • Concepts: Test Doubles (테스트 대역), Dummy, Stub, Spy, Mock, Fake.
    • Skills: 파라미터 채우기용은 Dummy, "무조건 100원 리턴해"는 Stub, "메일 발송 함수가 2번 호출됐어?"는 Spy/Mock, 실제 DB 대신 빠른 H2 인메모리 DB를 띄우는 건 Fake로 구별하여 적용하기.
  • How to Learn:
    • 1단계: Dummy (깡통): 컴파일 에러만 막으려고 인자로 던지는 null이나 빈 객체입니다. 로직에 아무 영향도 안 줍니다.
    • 2단계: Stub (앵무새): 찌르면 정해진 대답만 합니다. "유저 정보 가져와" \rightarrow "무조건 홍길동." 상태를 강제 세팅할 때 씁니다.
    • 3단계: Spy (스파이): 진짜처럼 행동하는데, 뒤에서 기록을 남깁니다. "이 함수가 몇 번 호출됐고, 무슨 파라미터가 들어왔지?"를 나중에 추궁(Verify)할 수 있습니다.
    • 4단계: Mock (시나리오 배우): 행위(Behavior)를 검증하기 위해 미리 대본을 짜놓은 로봇입니다(프레임워크가 주로 만듦).
    • 5단계: Fake (가짜 구현체): 진짜 로직이 들어있지만, 프로덕션에는 못 쓰는 짝퉁입니다. (예: List로 구현한 인메모리 User DB). 이 5가지 도구를 헷갈려서 남용하는 순간 테스트 코드가 읽을 수 없는 쓰레기가 되는 원리를 해부합니다.
  • Implement: Test Double 탐지 퀴즈 로직. 상황: 결제 실패 시 EmailSender 객체를 통해 메일이 발송되는지 테스트하려 함. 선택 1(Stub): 메일 발송 성공(true)을 리턴하게 세팅함 \rightarrow 발송됐는지 "확인" 불가. 선택 2(Mock/Spy): verify(emailSender, times(1)).send() 로직 추가 \rightarrow 정답! 외부로 나가는 '행위'를 기록하고 검증하는 최적의 도구 매칭 렌더링.

Advanced

Core Topic 04: 상태 vs 행위 검증의 철학 (State vs Behavior Verification)

  • Why to Learn: 무지성으로 "호출되었는가?(Mock)"만 검사하다가 테스트 코드가 구현에 강하게 결합되어 리팩터링조차 못하게 되는 '모키스트(Mockist)의 저주'를 극복하기 위함입니다.
  • What to Learn:
    • Concepts: State Verification (상태 검증), Behavior Verification (행위 검증), Classicist vs Mockist, Test Brittleness (테스트 취약성).
    • Skills: 덧셈 로직은 반환된 '결과값(10)'을 검증(State)하고, 외부 은행망으로 돈을 쏘는 로직은 transfer() 함수가 '호출됐는지'를 검증(Behavior)하는 양동 작전 구사하기.
  • How to Learn:
    • 1단계: 상태 검증 (Classicist): 고전파입니다. 과정은 신경 안 씁니다. add(3, 4)를 넣었을 때 리턴값이 7인가? DB에 저장하고 났을 때 Row 개수가 1이 되었는가? 객체의 최종 '상태'만 봅니다. 테스트가 튼튼합니다. 코드를 어떻게 리팩터링하든 결과만 같으면 통과합니다.
    • 2단계: 행위 검증 (Mockist): 과정에 집착합니다. order()를 호출했을 때 내부에서 discount.apply() 함수를 호출했는가? db.save()를 호출했는가? (Mock을 씁니다).
    • 3단계: 깨지기 쉬운 유리창 (Brittleness): 내부 구현을 행위 검증(Mock)으로 다 묶어놓으면 어떻게 될까요? 개발자가 discount.apply() 대신 더 좋은 newDiscount.calc()로 내부 코드를 싹 리팩터링했습니다. 결과는 똑같이 잘 나오는데 테스트 코드는 다 터집니다. "어? 너 왜 옛날 함수 호출 안 했어!" 라면서요. 꼭 외부로 나가는 부수 효과(메일 발송 등)에만 제한적으로 행위 검증을 쓰고, 나머지는 상태 검증으로 몰아야 테스트가 튼튼해진다는 궁극의 철학을 뜯어봅니다.
  • Implement: 리팩터링에 따른 테스트 파손도(Brittleness) 비교. Action: 내부 할인 로직 메서드명을 calc()에서 compute()로 리팩터링. State Test: assertEquals(9000, order.total) \rightarrow Pass (구현 방식에 무관심, 결과만 맞음). Behavior Test(Mock): verify(discountObj).calc() \rightarrow Fail (메서드명 바뀌었다고 테스트가 터짐). Mock 남용이 테스트 코드 유지보수를 지옥으로 밀어 넣는 역효과 시각화.

7. Terminology

Term (EN / ko, abbr) 1문장 정의 단계(기본/권장/실무/심화) 역할/맥락 관련 개념 유사/대비/함께 사용 오해 포인트 Evidence(Primary/Secondary/Industry) Flags(core)
Testing Pyramid (테스트 피라미드) 빠르고 저렴한 단위 테스트(Unit)를 밑바닥에 수천 개 깔고, 무겁고 느린 E2E(End-to-End) 테스트를 꼭대기에 최소화하여 자동화의 가성비를 극대화하는 물리 법칙입니다. 기본 테스트 포트폴리오 비중 설계 Unit / Integration / E2E Ice Cream Cone Anti-pattern 피라미드라고 해서 중간이나 꼭대기를 안 해도 된다는 뜻이 아님. 비용과 피드백 속도에 비례해 비율을 조절(7:2<1> 등)하라는 뜻임 P1:CS2023 core
Test Isolation (테스트 격리) 내 코드(SUT)를 검증할 때, 외부 DB나 기상청 API 같은 통제 불능의 변수(의존성)를 가짜 객체로 대체하여 완벽한 진공(결정론적) 상태를 만드는 외과 수술입니다. 권장 환경에 의존하지 않는 무결점 테스트 환경 구축 SUT / Deterministic / Test Double Dependency Injection (DI) 격리를 빡세게 할수록 '내부 로직'은 증명되지만, '실제 시스템 융합'은 알 수 없게 되므로 반드시 통합(Integration) 테스트가 병행되어야 함 P5:SFIA core
Test Double (테스트 대역/가짜 객체) 스턴트맨(대역)처럼 실제 객체 대신 투입되어, 하드코딩된 답을 뱉거나(Stub) 호출된 횟수를 몰래 기록(Spy/Mock)하는 5가지 가짜 객체들의 총칭입니다. 실무 외부 시스템 연동 차단 및 제어 Stub / Spy / Mock / Fake / Dummy Mock (특정 프레임워크 객체) 현업에선 전부 뭉뚱그려 "Mocking 한다"고 부르지만, 공학적으로 Stub(상태 리턴)과 Mock(행위 검증)은 완전히 목적이 다름 Industry core
State vs Behavior Verification (상태 vs 행위 검증) 객체지향 테스트의 두 종교로, "실행 후 값이 10인가?(상태)"를 따지는 고전파와 "내부에서 그 함수를 호출했는가?(행위)"를 따지는 Mockist파의 이념 대립입니다. 심화 검증 대상의 본질(결과 vs 과정) 선택 Assert / Verify / Mockist Brittleness (테스트 취약성) 내부 로직(과정)에 Mock(행위 검증)을 남발하면 코드를 리팩터링할 때마다 테스트가 다 깨지는 끔찍한 결합도(Brittleness) 폭발을 맞음 Industry core

8. References

Primary

  • [P1] CS2023 - Software Engineering (SE) - Software Verification and Validation (Testing levels)
  • [P5] SFIA - Testing (TEST) - Test strategy and Isolation

Secondary

  • [xUnit Test Patterns] Gerard Meszaros - Test Doubles (Dummy, Stub, Spy, Mock, Fake)
  • [Succeeding with Agile] Mike Cohn - The Test Automation Pyramid

Industry

  • [MartinFowler.com] - The Practical Test Pyramid
  • [MartinFowler.com] - Mocks Aren't Stubs (State vs Behavior Verification)

9. Final Checklist

Primary

  • UI를 직접 클릭하는 E2E 테스트만 작성하던 조직이 겪게 되는 "느린 피드백 루프(1시간 소요)"와 "변덕스러운 실패(Flaky Test)"의 저주를, 테스트 피라미드의 하단(Unit Test) 강화로 어떻게 역전시키는지 증명할 수 있는가?
  • 결제 모듈을 테스트할 때, 외부 망에 연결된 실제 PG사 API를 호출하는 대신 인터페이스(DIP)를 뚫고 가짜 객체(Test Double)를 주입하여, 네트워크가 단절된 비행기 안에서도 100% 통과(Pass)하는 격리된(Isolated) 세계를 설계할 수 있는가?

Secondary

  • 고정된 날씨 값을 반환하는 'Stub'과, 이메일 발송 메서드가 정확히 1번 호출되었는지 검증하는 'Spy/Mock'의 차이를 이해하고, 상황에 맞게 5가지 Test Double(Dummy, Fake 포함)을 골라 쓰는 외과적 정밀도를 갖추었는가?
  • 실제 데이터베이스 대신 H2나 HashMap을 이용해 구현한 가짜(Fake) 저장소 객체가 프로덕션 코드에선 절대 쓰이지 않지만, 단위 테스트의 속도를 밀리초(ms) 단위로 끌어올리는 인메모리 아키텍처의 효용성을 논증할 수 있는가?

Industry

  • 코드 내부의 로직이 조금만 바뀌어도 깨져버리는 '행위 검증(Behavior Verification / Mock 남용)'의 취약성(Brittleness)을 비판하고, 객체의 상태(State)를 검증하는 클래시스트(Classicist) 방식으로 리팩터링 내성을 확보할 수 있는가?
  • "테스트 커버리지가 100%니까 완벽하다"는 착각에 빠진 조직에, '상태 기반의 유닛 테스트(격리)'만으로는 실제 DB나 API 게이트웨이와 맞물릴 때 터지는 연결 버그를 절대 잡을 수 없음을 경고하고 '통합 테스트(Integration)'와의 하이브리드 전략을 세울 수 있는가?

Testing & QA

1 / 5