DesignThroughTest

주니어 개발자들을 위한 패턴 언어 - 테스트를 검증 도구가 아닌 설계 도구로 사용하는 방법

The Story 1: The Imtestable Fortress vs. The User's Wish (Programming)

두 명의 개발자, 민수와 하나가 '주문 할인 계산기'를 만들고 있다.

민수의 접근 (Implementation First): 민수는 할인 로직의 모든 복잡한 경우의 수를 생각하며 DiscountManager 클래스를 구현했다. "데이터베이스에서 사용자 등급을 가져오고, 현재 진행 중인 이벤트를 API로 조회하고..." 코드를 다 짠 후, 그는 테스트를 작성하려고 했다. "어... 테스트에서 데이터베이스 연결을 어떻게 흉내 내지? API 서버가 죽으면 테스트도 실패하나?" 민수의 코드는 외부 세상과 너무 단단히 결합되어 있어서, 테스트를 하려면 거대한 설정이 필요했다. 결국 그는 테스트 작성을 포기했다.

하나의 접근 (Test First): 하나는 구현을 시작하기 전에, 자신이 이 코드를 어떻게 사용하고 싶은지 상상하며 테스트부터 적었다.

test "VIP user gets 10% discount" {
  calculator = DiscountCalculator(user_grade="VIP")
  price = calculator.apply(10000)
  assert price == 9000
}

"데이터베이스? API? 그건 나중에 생각하자. 지금 중요한 건 계산 로직이야." 하나는 테스트가 요구하는 대로 DiscountCalculator를 만들었다. 필요한 데이터는 생성자로 받게 설계되었다. 결과적으로 하나의 코드는 외부 의존성 없이 순수한 로직만 남았고, 사용하기 쉬웠으며, 테스트하기도 쉬웠다.

The Story 2: The Cardboard Sofa (Ordinary Life)

민수와 하나는 거실에 놓을 커다란 '코너 소파'를 사려고 고민 중이다.

민수의 구매 (The Buy First): 민수는 가구 매장에서 본 소파의 세련된 디자인에 매료되어 즉시 주문했다. "우리 거실에 놓으면 정말 멋질 거야." 며칠 뒤 소파가 배달되었지만, 문제가 생겼다. 소파가 생각보다 너무 커서 거실의 통로를 막아버렸고, TV와의 거리도 너무 가까워 눈이 아팠다. 민수는 거실 가구 배치를 전부 다시 고민하거나 비싼 위약금을 물고 반품해야 하는 상황에 처했다.

하나의 구매 (The Simulation First): 하나는 소파를 주문하기 전, 재활용 분리수거장에서 큰 종이 박스들을 모아왔다. 그녀는 소파가 놓일 자리에 박스들을 소파 크기만큼 쌓아두고 며칠을 생활해 보았다. "음, 박스가 여기 있으니 베란다 나갈 때 조금 불편하네. 소파 길이를 20cm만 줄인 모델로 사야겠어." 하나는 실제 물건을 들여놓기 전에 '사용 경험'을 먼저 테스트해 봄으로써, 자신의 거실에 가장 완벽하게 어울리는 설계를 찾아냈다. 소망하는 결과를 미리 시뮬레이션하는 것이 최선의 선택을 만드는 길임을 하나는 알고 있었다.

Context

새로운 기능을 구현해야 한다. 요구사항은 어느 정도 이해했다(TinyExperiment & TwoWorlds). 이제 코드를 작성하려고 한다.

일상적인 상황:

당신은 테스트를 숙제(Verification)로 여기고 있다. 하지만 테스트는 설계(Design)의 기회다.

Problem

구현을 먼저 하면, "작동하는 코드"는 얻을 수 있지만 "사용하기 편한 코드"는 얻기 힘들다.

구현 관점에서 코드를 짜면 내부 구현의 편의성을 우선하게 된다. "여기서 DB 바로 조회하면 편하겠네"라고 생각하는 순간 결합도는 높아지고, 나중에 이 코드를 사용하는 쪽(클라이언트)이나 테스트 코드에서 보면 사용하기가 끔찍하게 불편하다는 것을 발견한다.

Solution

코드를 작성하기 전에, 그 코드를 사용하는 테스트를 먼저 작성하라.

Allen Holub은 "Design by Coding"을 강조했다. 설계(Design)는 도면을 그리는 것이 아니라 코드를 작성하는 과정 그 자체다. 따라서 테스트를 먼저 작성하는 것은 구현 전에 설계를 수행하는 가장 구체적인 활동이다.

Principle 1: Coding IS Design (코딩이 곧 설계다)

실제 설계 결정은 코드를 타이핑하는 순간 일어난다.

Principle 2: Test is the First Client (테스트는 첫 번째 클라이언트다)

테스트 코드는 당신이 만드는 모듈의 첫 번째 사용자입니다.

Principle 3: Wishful Thinking (소망적 사고)

구현 방법을 고민하지 말고, "이런 인터페이스가 있었으면 좋겠다"는 소망을 코드로 적으십시오. 소망대로 테스트를 작성하면, 코드는 자연스럽게 느슨한 결합과 높은 응집도를 가지게 됩니다.

Principle 4: Externalize Dependencies (의존성 드러내기)

테스트를 먼저 짜면 숨겨진 의존성을 사용할 수 없습니다. 자연스럽게 필요한 모든 것을 인자나 생성자로 받게 되며, 이것이 바로 의존성 주입(DI)의 본질입니다.

Real Examples

Example 1: Decoupling Time

"오후 10시 이후에는 주문 불가" 로직에서 시스템 시간을 직접 쓰는 대신 Clock 인터페이스를 주입받도록 설계하여, 테스트에서 시간을 마음대로 조작할 수 있게 함.

Example 2: Interface Discovery

복잡한 파싱 로직을 구현하기 전, 파일 경로를 넘길지 File 객체를 넘길지 테스트 코드를 먼저 써보며 가장 사용하기 편한 API 구조를 결정함.

Common Pitfalls

"Test First takes too long" (너무 오래 걸려요)

처음에는 느리지만, 디버깅 시간과 나중에 구조를 뜯어고치는 시간을 포함하면 훨씬 빠릅니다.

Testing Implementation Details

테스트가 "어떻게(How)"를 검증하면 안 됩니다. 행동(Behavior)과 인터페이스(Interface)를 테스트하십시오.

Connection to Other Patterns

Signs of Success

The Ultimate Insight

테스트는 버그를 찾기 위한 것이 아닙니다. 테스트는 설계의 결함을 찾기 위한 것입니다.

테스트를 먼저 작성하는 순간, 당신은 구현자가 아니라 사용자가 됩니다. 사용자 관점에서 설계된 코드는 언제나 구현자 관점에서 설계된 코드보다 낫습니다.


CategoryPatternLanguage CategoryProgramming CategoryTDD CategoryDesign

DesignThroughTest (last edited 2025-12-30 09:01:03 by 정수)