본문 바로가기

개발 관련 지식

TDD는 정말 최고의 설계 도구일까? – Bob Martin vs Jim Coplien의 토론 정리

🎬 [0:00~1:01] Bob: 전문 개발자의 필수 TDD

TDD는 이제 전문 개발자라면 필수로 해야 할 행위라고 강조함.
Bob은 TDD를 단순한 개발 기법이 아니라, 개발자의 책임감과 전문성의 문제로 격상시켜 주장함.

 

🎬 [1:02~2:23] Bob: TDD 정의

Bob의 TDD 3법칙:

  1. Failing test first
    → 테스트가 먼저다. 실패하는 테스트 없이 프로덕션 코드 작성 금지
  2. Write only enough test to fail
    → 실패를 보장할 수 있을 정도까지만 테스트를 작성해야 한다
  3. Write only enough code to pass
    → 테스트를 통과하기 위해 최소한의 프로덕션 코드만 작성한다

TDD는 극단적으로 작은 단위의 사이클로 구성됨:
테스트 → 실패 → 최소 코드 → 통과 → 리팩터 → 반복
이 사이클은 30초~1분 단위로 반복되도록 설계됨.

 

🎬 [2:24~4:10] Jim: 아키텍처나 프레임워크 없이 TDD를 사용하는 것은 문제다

TDD만으로 개발을 시작하면 객체지향적 설계(OOP)가 아닌, 절차적 설계가 된다.
즉, 객체들의 협력보다는 함수 중심의 코드가 만들어짐.
이로 인해 소프트웨어 아키텍처가 bottom-up으로 쌓이며 망가지고, 리팩터링만으로는 되돌릴 수 없다고 지적.

 

🎬 [4:11~4:55] Jim: 절차적 설계는 도메인 모델을 적용하기 어렵고 GUI까지 불안정해짐

TDD는 작은 함수 단위로 반복되기 때문에, 객체보다는 함수 묶음 중심의 코드가 되고 도메인 모델링과 멀어짐

GUI(사용자 인터페이스)에 그 아키텍처가 그대로 투영되기 때문에, 결국 불안정한 GUI로 이어진다
도메인 모델로 아키텍처를 정하고 TDD를 진행하는 것을 대안으로 제시.

 

🎬 [6:00~7:01] Bob: 아키텍처는 점진적으로 형성된다

처음부터 완벽한 아키텍처를 만드는 것이 아니라, 코드와 테스트를 통해 점진적으로 발전해나가야 한다고 강조.
여러 실험과 반복(iteration)을 통해 좋은 설계가 자연스럽게 형성된다는 입장.

 

🎬 [7:16~9:00] Jim: 도메인 지식 없이 점진적인 설계는 위험하다

도메인 지식 없이 고객 피드백만으로 설계하면 잘못된 방향으로 갈 위험이 있음.

 

ex) Kent Beck이 TDD를 설명할 때 사용한 ‘예금 계좌’를 단순한 숫자 덧셈/뺄셈 기능으로 구현하고, 이후 코드를 덧붙여야 한다고 함
하지만, 실제 은행 시스템에서의 ‘예금 계좌’는 단순한 값이 아니라, 트랜잭션 로그를 기반으로 상태를 계산하는 프로세스이고, 감사, 세금, 보험 등 다양한 도메인 요구사항이 얽힌 복잡한 구조이기 때문에 단순한 객체나 값으로는 표현할 수 없음을 설명

→ TDD로만 접근하면 실제 시스템의 복잡성을 반영하지 못함


 도메인 모델을 기반으로 아키텍처를 먼저 설계하고, 이후 TDD로 구현하는 접근을 대안으로 주장.

 

🎬 [9:01~9:30] Jim: 코드 구현보다 구조 설계가 먼저다

구현보다 먼저, 도메인 지식을 담은 인터페이스를 설계해야 함.
→ 실제 구현은 필요할 때만 작성해야 한다는 Lean 원칙과도 일치함

더보기

✅ Lean 개발 원칙 7가지

  1. 낭비 제거(Eliminate Waste)
    • 사용되지 않는 기능, 불필요한 코드, 과도한 문서화 등 ‘가치 없는 활동’을 없애는 것
  2. 지속적인 학습과 지식 창출(Amplify Learning)
    • 작은 단위로 시도하고 피드백을 받아가며 개선
    • 실험과 반복(iteration)을 통해 ‘배움’을 축적
  3. 결정은 늦출수록 좋다(Decide as Late as Possible)
    • 불확실한 상태에서 성급하게 결정하지 않음
    • 필요한 정보를 최대한 얻은 뒤 Just-in-time에 결정
  4. 빠른 납품(Deliver as Fast as Possible)
    • 빠르게 기능을 제공하고 피드백을 받는 것이 더 나은 품질을 만든다
  5. 팀 간 협업 강화(Empower the Team)
    • 개발자에게 자율성과 책임을 부여
    • 수평적이고 개방적인 팀 문화를 지향
  6. 무결점 품질 내재화(Build Integrity In)
    • 테스트와 리팩토링을 통해 처음부터 품질을 설계
    • 사용자와 개발자의 관점에서 일관된 시스템을 만드는 것
  7. 전체 시스템 최적화(Optimize the Whole)
    • 부분 최적화가 아닌, 전체 플로우(기획 → 개발 → 운영)를 최적화

 

🎬 [9:36~10:18] Bob: 인터페이스는 테스트와 요구사항을 기반으로 점진적으로 구성해야 한다

“필요할지도 모른다”는 이유로 인터페이스에 미리 많은 메서드를 넣는 것은 낭비

 

→ 인터페이스의 메서드는 아래의 조건이 있을 때, 정의 되어야 함

  • 테스트가 요구할 때
  • 시스템 요구사항이 있을 때

그리고, 구조적 요구에 의해 인터페이스가 분리되어 정의되어야 함

 

💡 “인터페이스는 실제 사용과 구조적 요구에 의해 자연스럽게 분리되고 정제되어야 한다”는 점에서, TDD와 아키텍처의 점진적 발전이 함께 가야 한다는 그의 철학이 잘 드러나는 부분임.

 

🎬 [10:19~11:45] Jim: 무의미한 객체 설계는 도메인 이해 부족 때문

  • 객체의 의미는 그 객체가 수행하는 행위(메서드)에서 비롯된다.
    단지 클래스나 이름만으로 객체를 정의하려 해선 안 되고, 그 객체가 무엇을 할 수 있는지를 기준으로 판단해야 한다
  • 잘못 객체를 정의한 예시: 통신 시스템 리팩터링 프로젝트
    "복구(Recovery)" 기능을 구현하기 위해 어떤 사람이 '복구 객체'를 만들었지만, Jim은 이게 도메인 지식이 부족해서 잘못 설계된 예라고 지적.
    복구는 객체가 아니라 기능이나 프로세스에 가깝고, 명확한 멤버 함수가 존재하지 않아 객체로 보기 어렵다는 것.

💡 결론적으로, 초기에 도메인 지식을 기반으로 객체의 인터페이스와 해당 메서드에 대한 명확한 정의를 내리는 것은 필요하다.

 

🎬 [11:01~12:07] Bob & Jim: 작게 시작하고 실행하면서 설계를 발전시켜라

  • Bob: 객체의 의미는 있어야 하지만, 최소한으로 부여해야 한다
  • Jim: 실행 가능한 코드에서 설계를 발전시키는 방식에 동의 거대한 구조를 미리 설계하는 것은 위험

💡 이 부분에서는 TDD 방식의 점진적인 설계 방향과 도메인 기반 접근 방식이 충돌 없이 조화될 수 있는 지점을 보여지는 것 같음

 

🎬 [13:37~14:16] Jim: 책에 나오는 TDD를 그대로 믿지 마라

실제로 성공하는 개발자들은 교과서적 TDD만을 따르지 않음

문제는, 새로 배우는 사람들이 TDD를 검색하거나 책을 통해 "아키텍처는 오직 테스트에서만 비롯되어야 한다"는 우직한 믿음은 위험

 

🎬 [14:42~15:04] Bob: 테스트되지 않은 코드는 무책임하다

단 한 줄의 코드라도 테스트 없이 배포하는 것은 무책임하다”
→ 이를 방지하는 최선의 방법이 TDD 실천

 

🎬 [15:04~17:38] Jim: TDD보다는 CDD

유닛 테스트란 본질적으로 함수의 입력값에 대해 일부 샘플만을 택해 실행하는 방식이다.
예를 들어, 32비트 정수형 함수라면 2³²개의 입력값이 존재하지만, 우리는 그중 몇 개만을 테스트할 뿐이다.
이는 ‘운이 좋으면 버그를 잡는다’ 수준의, 매우 휴리스틱(Heuristic) 한 접근이다.

*휴리스틱: 완벽하지는 않지만, 실용적이고 빠르게 문제를 해결하기 위한 경험 기반의 방법이나 규칙

 

Design by Contract
여기서 중요한 건, 코드의 입력조건(Precondition), 출력조건(Postcondition), 불변조건(Invariant) 을 명확하게 선언하는 것이다.

이 방식은 다음과 같은 특징을 가진다:

  • TDD처럼 깊은 사고를 유도하며
  • 외부 인터페이스 중심 설계를 돕는다
  • ✅ 하지만 무작위성 없이, 입력 전체 범위를 커버한다
  • ✅ 테스트가 아닌, 의도를 기준으로 코드의 정합성을 검증한다

즉, TDD는 함수가 ‘잘 작동하는지’를 확인한다면,
Design by Contract는 함수가 ‘이런 조건에서 이런 결과를 보장해야 한다’는 명세 그 자체를 중심에 둔다.

 

CDD 실행 방식

  1. 계약 명세 작성 (pre/post 조건)
  2. 입력값 자동 생성
  3. pre 조건 만족 → 테스트 실행
  4. post 조건 위반 → 버그로 간주

✅ 실제 Eiffel 언어에서 이 방식 적용 → 일주일 만에 7개 버그 발견

 

TDD가 주로 클래스 수준에서 테스트가 이뤄지기 때문에, 그 테스트가 비즈니스 레벨의 의도와 연결되기 어렵다고 지적한다.

반면, CDD는 함수의 ‘기능이 아니라 의도’를 코드에 직접 명시하기 때문에, 그 의도가 비즈니스 요구사항과 직접 연결될 수 있다는 점에서 우위를 가진다.

 

🎬 [17:38~18:42] Bob: 계약 기반 설계와 TDD는 결국 같은 일을 한다. 단, 의존성 방향에서는 TDD가 훨씬 더 깔끔하다.

계약 기반 설계 (Design by Contract) TDD(Unit Test)
Precondition → 인자 유효성 검증 테스트에서 인자 유효성 체크
Postcondition → 반환값 검증 예상 결과와의 일치 검증
Invariant → 클래스의 상태 조건 상태 변화 검증

📌 TDD는 테스트 코드 → 프로덕션 코드로 단방향 의존
→ 코드를 깨끗하고 구조적으로 유지할 수 있음

 

계약 기반은 프로덕션 코드에 로직이 섞여버림
→ 코드가 더러워지고, 양방향 의존처럼 보일 수 있음

 

🎬 [18:44~20:11] Jim vs Bob: "코드 자체가 설계다" vs "테스트는 버릴 수 없는 도구다"

🎙 Jim:

  • “설계는 코드 자체다” (UML도, 테스트도 아님)
  • Lean 철학에서는 테스트조차 부가물(waste)
  • 테스트 코드가 많아질수록 버그도 많아지고 속도는 느려짐
    • 대표 사례: Ada 컴파일러 프로젝트 → TDD를 적용했더니 테스트 코드 양이 늘었고, 오히려 버그 수가 증가

✅ 계약 기반(assertion) 방식은 더 결합도가 좋다

  • assertion은 코드 안에 자연스럽게 녹아들어
  • 인터페이스 의미와 구현이 강하게 연결됨
  • 반면, 테스트는 코드 외부에서 관리되며,→ 결합도가 약하고, 복잡하고, 관리가 어렵다

🎙 Bob: “지저분한 테스트가 있다고, 도구 자체를 버릴 순 없다”

Bob은 Jim의 주장을 일부 인정한다.
하지만, 그렇다고 해서 테스트라는 도구 자체를 부정할 순 없다고 말한다.

핵심은 테스트를 어떻게 ‘잘’ 쓰느냐

 

📜 좋은 테스트는 ‘사양’이 될 수 있다

  • Bob은 **잘 쓰인 테스트는 assertion처럼 명확한 사양(specification)**이 될 수 있다고 강조한다
  • 코드 외부에 있더라도 기능 요구사항을 명확히 표현할 수 있다.

🎙 Jim의 마무리

“나는 테스트 자체를 반대하는 게 아니다. 대부분이 잘못 사용하고 있는 게 문제다.”
→ 목적 없는 테스트, 사양도 아닌 테스트가 혼란만 키운다고 지적

 

 

참고자료

https://www.youtube.com/watch?v=eRxc4PD6RN0

https://armadillo-dev.github.io/design/programming/desing-by-contract/

https://www.youtube.com/watch?v=U1vySD_wG78