티스토리 뷰

안녕하세요, 여러분! 끊임없이 변화하는 기술의 물결 속에서 개발자라면 누구나 견고하고 유연하며 유지보수가 쉬운 코드를 꿈꿀 것입니다. 하지만 막상 코드를 작성하다 보면 예상치 못한 버그, 복잡한 의존성, 그리고 리팩토링에 대한 두려움에 직면하기 일쑤죠. 이러한 문제들은 개발 생산성을 저해하고 궁극적으로는 프로젝트의 성공까지 위협할 수 있습니다.

오늘 우리는 이러한 난관을 극복하고, 궁극적으로 개발 생산성 향상의 문을 열어줄 강력한 방법론인 TDD(테스트 주도 개발)에 대해 깊이 파고들어 볼 것입니다. TDD 방법론은 단순히 테스트 코드를 작성하는 것을 넘어, 코드의 설계와 구조, 그리고 장기적인 유지보수성까지 혁신적으로 개선하는 접근 방식입니다. 개발 입문자부터 숙련된 전문가까지, 코드 품질 향상에 관심 있는 모든 개발자 여러분께 이 글이 TDD의 본질을 이해하고 실제 개발에 적용하는 데 귀중한 지침이 되기를 바랍니다.

지금부터 TDD란 무엇인지, 그 핵심 원리는 어떻게 작동하는지, 그리고 왜 TDD를 우리의 개발 과정에 적극적으로 도입해야 하는지에 대한 모든 것을 함께 알아보겠습니다. 특히 파이썬 예제를 통해 TDD가 실제 코드 속에서 어떻게 살아 움직이는지 체험해 볼 예정이니, 코드를 통한 실질적인 학습을 기대하셔도 좋습니다.

 


TDD(테스트 주도 개발)란 무엇인가?

소프트웨어 개발 과정에서 코드의 품질과 안정성을 확보하는 것은 프로젝트의 성패를 좌우하는 핵심 요소입니다. 하지만 버그는 언제나 발생하고, 코드를 변경하는 것은 늘 조심스러운 작업입니다. 이러한 문제의식 속에서 2000년대 초반, 익스트림 프로그래밍(XP)의 한 부분으로 TDD(Test-Driven Development), 즉 테스트 주도 개발 방법론이 부상했습니다. TDD란 간단히 말해 "코드를 작성하기 전에 테스트 코드를 먼저 작성하는" 개발 방식입니다.

대부분의 개발자는 코드를 먼저 작성하고 나중에 해당 코드가 제대로 작동하는지 확인하기 위해 테스트 코드를 작성하거나 수동으로 기능을 검증하는 방식을 따릅니다. 하지만 TDD는 이 순서를 완전히 뒤집습니다. TDD의 창시자인 Kent Beck은 TDD를 "코드를 깨끗하게 유지하면서, 테스트를 통해 기능을 점진적으로 개발하는 방법"이라고 설명했습니다. 이는 단순한 테스트 기법이 아니라, 소프트웨어 설계에 깊이 관여하는 패러다임입니다.

테스트 퍼스트(Test-First) 철학의 본질

TDD의 핵심은 바로 '테스트 퍼스트(Test-First)' 철학에 있습니다. 이는 코드를 한 줄도 작성하기 전에, 구현하려는 기능이 어떤 동작을 해야 할지 정의하는 테스트 코드를 먼저 작성하는 것을 의미합니다. 마치 건축가가 건물을 짓기 전에, 건물이 어떤 구조와 기능을 가져야 하는지 상세한 설계도를 먼저 그리는 것과 같습니다. 이 설계도가 바로 '테스트 코드'인 셈입니다.

이러한 접근 방식은 기존 개발 방식과 몇 가지 중요한 차이를 만들어냅니다.

  1. 명확한 요구사항 정의: 코드를 작성하기 전에 '무엇을 만들 것인가'에 대한 테스트를 먼저 작성함으로써, 개발자는 구현할 기능의 요구사항과 기대 동작을 더 명확하게 이해하게 됩니다. 이는 모호함을 줄이고, 잘못된 기능을 구현할 위험을 최소화합니다.
  2. 설계 주도: 테스트는 코드의 인터페이스와 동작 방식에 대한 구체적인 명세를 제공합니다. 테스트를 통과하는 코드를 작성하기 위해서는 자연스럽게 테스트하기 쉬운, 즉 응집도가 높고 결합도가 낮은 모듈화된 코드를 설계하게 됩니다. 이는 TDD가 견고하고 유연한 코드 작성에 기여하는 가장 중요한 부분 중 하나입니다.
  3. 즉각적인 피드백: 테스트 코드가 먼저 존재하므로, 기능 구현 후에는 즉시 테스트를 실행하여 변경 사항이 기존 기능에 영향을 미치는지, 새로운 기능이 올바르게 작동하는지 확인할 수 있습니다. 이는 문제점을 조기에 발견하고 수정하는 데 결정적인 역할을 합니다.

기존 개발 방식과의 차이점

특징 기존 개발 방식 TDD(테스트 주도 개발) 방법론
순서 기능 구현 → 테스트 작성 (선택적) 테스트 작성 → 기능 구현
목표 기능 구현 → 버그 발견 기능 명세 → 기능 구현 → 설계 개선
테스트의 역할 코드 검증 도구 기능 정의, 설계 가이드, 코드 검증 도구
코드 품질 개발자의 역량과 노력에 따라 편차 큼 테스트 용이성을 고려한 설계로 높은 품질 유지 가능
피드백 기능 구현 후 또는 릴리스 임박하여 발생 기능 구현 중 실시간으로 발생
변경에 대한 태도 기존 코드 변경에 대한 부담과 두려움 존재 테스트의 안전망 덕분에 자신감 있는 변경 및 리팩토링

이처럼 TDD는 개발의 순서와 테스트의 역할을 재정립함으로써, 단순히 버그를 줄이는 것을 넘어 소프트웨어 개발의 전반적인 품질과 효율성을 향상시키는 강력한 접근 방식입니다. 특히 개발 입문자에게는 테스트 가능한 코드를 작성하는 훈련을 통해 좋은 설계 습관을 길러주고, 기본적인 프로그래밍 지식이 있는 전공자 및 비전공자에게는 코드를 더 깊이 이해하고 통제하는 방법을 제시합니다. 이제 이 강력한 방법론의 핵심 엔진인 'Red-Green-Refactor' 사이클에 대해 자세히 알아보겠습니다.


TDD 핵심 사이클: Red-Green-Refactor 완벽 가이드

TDD는 무작정 테스트 코드를 먼저 작성하고 기능을 구현하는 것이 아닙니다. 이 과정은 엄격하게 정의된 세 가지 단계를 반복하는 Red-Green-Refactor 사이클을 통해 이루어집니다. 이 사이클은 TDD의 심장과 같으며, 코드를 점진적으로 발전시키고 품질을 향상시키는 핵심적인 메커니즘입니다.

1단계: Red (실패하는 테스트 작성)

TDD 사이클의 첫 시작은 바로 '빨간불'입니다. 이는 "새로운 기능을 추가하기 전에, 해당 기능이 아직 구현되지 않았음을 명확히 보여주는 실패하는 테스트 코드를 작성한다"는 의미입니다. 이 단계에서 중요한 것은 다음과 같습니다.

  • 기능의 명확한 정의: 구현하려는 기능이 무엇이고, 어떤 입력을 받았을 때 어떤 출력을 반환해야 하는지, 또는 어떤 부작용을 일으켜야 하는지를 테스트 코드로 구체적으로 정의합니다. 이는 요구사항을 명확히 하는 과정이기도 합니다.
  • 실패의 확인: 테스트 코드를 작성한 후에는 반드시 테스트를 실행하여 '실패'하는 것을 확인해야 합니다. 이 실패는 다음과 같은 두 가지 중요한 의미를 갖습니다.
    • 테스트의 유효성 확인: 작성한 테스트가 실제로 코드가 없을 때 혹은 잘못되었을 때 실패하는지를 확인하여, 테스트 코드 자체가 올바르게 작동하는지 검증합니다. 만약 코드가 없는데도 테스트가 통과한다면, 테스트 자체가 잘못 작성된 것입니다.
    • 기능의 부재 확인: 빨간불은 우리가 구현하려는 기능이 아직 존재하지 않음을 확실히 알려주는 신호입니다. 이는 우리가 앞으로 구현해야 할 목표를 명확하게 제시해 줍니다.
  • 최소한의 테스트: 한 번에 너무 많은 기능을 테스트하려 하지 말고, 단 하나의 '다음' 기능을 정의하는 최소한의 실패하는 테스트를 작성하는 것이 중요합니다.

이 'Red' 단계는 개발자에게 나침반과 같습니다. 어디로 가야 할지, 무엇을 만들어야 할지 명확한 방향을 제시해주며, 우리가 올바른 길을 가고 있는지 확인시켜 줍니다.

2단계: Green (테스트를 통과하는 코드 작성)

두 번째 단계는 '초록불'입니다. 'Red' 단계에서 작성한 실패하는 테스트를 '통과'시키기 위한 최소한의 코드를 작성하는 과정입니다. 이 단계에서 가장 중요한 원칙은 "오직 테스트를 통과시키는 것에만 집중하라"는 것입니다.

  • 최소한의 구현: 코드를 작성할 때, 지금 당장 실패하는 테스트를 통과시키기에 충분한 가장 단순하고 직관적인 방법으로 구현합니다. 완벽한 설계나 미래를 위한 확장을 고려하여 복잡한 코드를 작성할 필요가 없습니다. 예를 들어, add(1, 1)이 2를 반환해야 하는 테스트라면, return a + b와 같이 단순하게 구현하면 됩니다.
  • 빠른 통과: 가능한 한 빠르게 테스트를 통과시키는 것을 목표로 합니다. 이는 개발 주기를 짧게 가져가고, 즉각적인 피드백을 통해 개발자가 집중력을 잃지 않도록 돕습니다.
  • 설계 개선은 다음 단계에서: 이 단계에서는 코드의 아름다움이나 효율성, 중복 제거 등은 잠시 잊어둡니다. 그 모든 것은 다음 'Refactor' 단계에서 처리할 것입니다. 중요한 것은 오직 '테스트 통과'입니다.

이 'Green' 단계는 우리가 목표했던 기능을 성공적으로 구현했음을 확인시켜 주는 안도감과 성취감을 제공합니다. 이제 코드가 동작한다는 확신을 가지고 다음 단계로 나아갈 수 있습니다.

3단계: Refactor (코드 리팩토링)

마지막 단계는 '리팩토링'입니다. 테스트가 모두 초록불(성공)인 상태에서, 코드의 외부 동작은 변경하지 않으면서 내부 구조를 개선하여 코드의 품질을 향상시키는 과정입니다. 이제 우리는 완벽한 안전망, 즉 수많은 초록색 테스트들을 가지고 있으므로, 자신감을 가지고 코드를 개선할 수 있습니다.

  • 중복 제거: 비슷한 로직이나 코드 패턴이 여러 곳에 있다면, 함수나 클래스로 추상화하여 중복을 제거합니다.
  • 가독성 향상: 변수명, 함수명, 클래스명을 더 의미 있고 명확하게 변경하여 코드의 가독성을 높입니다.
  • 설계 개선: 모듈 간의 결합도를 낮추고 응집도를 높여, 더 유연하고 확장 가능한 구조로 만듭니다. 불필요한 복잡성을 제거하고, 디자인 패턴을 적용하여 코드를 더 깔끔하게 다듬을 수 있습니다.
  • 성능 최적화 (선택적): 때로는 리팩토링 과정에서 성능 최적화가 이루어질 수도 있지만, 이는 주된 목적이 아닙니다. 먼저 코드를 올바르게 만들고, 그 다음에 빠르게 만드는 것이 TDD의 철학입니다.
  • 지속적인 테스트 실행: 리팩토링하는 동안에도 주기적으로 모든 테스트를 실행하여, 변경 사항이 기존 기능에 예기치 않은 버그를 유발하지 않았는지 확인합니다. 테스트가 여전히 초록색이라면, 리팩토링이 성공적으로 이루어진 것입니다.

'Refactor' 단계는 코드에 '생명'을 불어넣는 과정입니다. 초록불 테스트 덕분에 우리는 아무런 걱정 없이 코드를 다듬고, 더 아름답고 효율적인 코드로 만들어갈 수 있습니다. 이 과정은 코드 품질 향상에 직접적으로 기여하며, 장기적으로 프로젝트의 건강성을 책임집니다.

Red-Green-Refactor 사이클이 코드 품질과 설계를 개선하는 방법

이 세 단계는 끊임없이 반복되며, 개발자는 작은 단위의 기능을 구현하고 개선하는 과정을 통해 코드를 점진적으로 발전시킵니다. 이 사이클을 반복하면서 얻는 이점은 다음과 같습니다.

  • 점진적 개발: 한 번에 하나의 작은 기능에 집중하여 개발하므로, 복잡성을 관리하기 쉬워집니다.
  • 지속적인 품질 검증: 모든 변경 사항은 테스트에 의해 즉시 검증되므로, 버그가 발생하더라도 문제 발생 지점을 빠르게 파악하고 수정할 수 있습니다.
  • 견고한 설계: 테스트하기 쉬운 코드를 작성하려는 노력은 자연스럽게 모듈화되고, 의존성이 낮은, 즉 응집도는 높고 결합도는 낮은 코드를 만들게 됩니다. 이는 곧 견고하고 유연한 코드 작성으로 이어집니다.
  • 강력한 안전망: 리팩토링 시 항상 모든 테스트를 실행할 수 있으므로, 변경으로 인한 회귀 버그(Regression Bug) 발생 위험을 최소화하고 개발자가 자신감을 가지고 코드를 개선할 수 있도록 돕습니다.

TDD의 Red-Green-Refactor 사이클은 개발의 각 단계마다 명확한 목표와 피드백을 제공하여, 개발자가 더 체계적이고 자신감 있게 코드를 만들어나가도록 안내합니다. 이 사이클의 반복은 단순히 버그를 줄이는 것을 넘어, 소프트웨어의 설계 품질과 장기적인 유지보수성을 근본적으로 향상시키는 강력한 도구입니다.


TDD의 핵심 장점: 왜 TDD를 도입해야 하는가?

TDD는 단순히 "테스트를 먼저 작성하는" 개발 방법론을 넘어, 개발 프로세스와 결과물에 혁신적인 변화를 가져다줍니다. 많은 개발자가 초기 학습 곡선과 추가적인 테스트 코드 작성 시간 때문에 주저하기도 하지만, 장기적으로 볼 때 TDD 장점은 그 초기 투자 비용을 상회하고도 남습니다. 그렇다면 왜 우리는 TDD를 우리의 개발 과정에 적극적으로 도입해야 할까요? 여기 TDD가 가져다주는 핵심적인 이점들을 살펴보겠습니다.

1. 코드 품질 향상 및 버그 감소

가장 명백한 TDD의 이점은 바로 코드 품질 향상입니다. 테스트 코드가 먼저 작성되기 때문에, 개발자는 구현하려는 기능의 '기대 동작'을 명확히 정의하게 됩니다. 이는 모호함을 줄이고, 개발자가 요구사항을 정확히 이해했는지 빠르게 확인할 수 있도록 돕습니다.

  • 조기 버그 발견: 코드가 작성되자마자 테스트를 통해 검증되므로, 버그가 발생하더라도 매우 작은 단위에서 즉시 발견하고 수정할 수 있습니다. 이는 개발 수명 주기 후반에 버그를 찾아 수정하는 것보다 훨씬 비용 효율적입니다.
  • 회귀 버그 방지: 기존 기능에 대한 테스트가 항상 존재하므로, 새로운 기능을 추가하거나 기존 코드를 변경(리팩토링)할 때 발생하는 '회귀 버그'를 효과적으로 방지할 수 있습니다. 변경 사항이 기존 시스템을 망가뜨리지 않았음을 테스트가 보장해 줍니다.
  • 살아있는 문서: 잘 작성된 테스트 코드는 그 자체가 코드의 동작 방식과 사용법에 대한 가장 정확하고 최신화된 문서 역할을 합니다. 다른 개발자가 코드를 이해하고 사용할 때 큰 도움을 줍니다.

2. 견고하고 유연한 설계 유도

TDD는 개발자가 '테스트하기 쉬운 코드'를 작성하도록 유도하며, 이는 자연스럽게 더 나은 코드 설계로 이어집니다.

  • 테스트 용이성: 테스트하기 쉬운 코드는 일반적으로 모듈화가 잘 되어 있고, 각 부분이 독립적으로 작동하며, 외부 의존성이 낮습니다. 이는 곧 '높은 응집도(High Cohesion)'와 '낮은 결합도(Low Coupling)'라는 좋은 설계 원칙을 따르게 만듭니다.
  • 모듈성 증진: TDD는 작은 단위의 기능부터 구현하도록 강제하므로, 자연스럽게 기능을 작은 모듈로 분리하고 각 모듈이 명확한 책임을 갖도록 설계하게 됩니다. 이는 코드의 재사용성을 높이고 유지보수를 용이하게 합니다.
  • 확장성 및 유연성: 잘 설계된 모듈은 기능 추가나 변경 시 기존 코드에 미치는 영향을 최소화합니다. TDD를 통해 얻은 유연한 설계는 변화에 강하며, 장기적으로 프로젝트의 생명력을 연장시킵니다. 이러한 과정은 곧 견고한 코드 작성으로 이어집니다.

3. 유지보수 용이성 및 리팩토링의 자신감

개발된 소프트웨어는 끊임없이 변화하고 발전해야 합니다. 이때 기존 코드를 변경하거나 기능을 확장하는 유지보수 작업은 개발자에게 큰 부담으로 다가오곤 합니다.

  • 안전한 리팩토링: TDD로 작성된 테스트는 강력한 안전망 역할을 합니다. 개발자는 수많은 테스트가 초록불을 유지하는 한, 코드의 내부 구조를 개선하거나 리팩토링하는 데 주저함이 없습니다. 테스트가 변경 사항이 외부 동작에 영향을 미치지 않음을 보장하기 때문입니다.
  • 쉬운 버그 수정: 버그가 발생했을 때, 해당 버그를 재현하는 테스트 케이스를 먼저 작성하고 그 테스트를 통과시키는 방식으로 버그를 수정합니다. 이는 버그가 다시 발생할 확률을 낮추고, 어떤 코드가 문제를 일으켰는지 명확히 파악하는 데 도움을 줍니다.
  • 개발 생산성 증가 (장기적 관점): 초기에는 테스트 코드 작성에 시간이 더 소요되는 것처럼 보일 수 있습니다. 하지만 장기적으로는 버그 수정에 드는 시간 감소, 리팩토링의 용이성, 새로운 기능 추가 시 안정성 확보 등으로 인해 전체적인 개발 생산성이 크게 향상됩니다. 디버깅에 쏟는 시간이 줄어들고, 변경에 대한 두려움이 사라져 개발 속도가 오히려 빨라집니다.

4. 개발자의 자신감 증대 및 스트레스 감소

TDD는 개발자 개인의 경험에도 긍정적인 영향을 미칩니다.

  • 불확실성 감소: 코드를 작성하기 전에 목표가 명확하므로, "무엇을 만들어야 할지", "어떻게 테스트해야 할지"에 대한 불확실성이 줄어듭니다.
  • 성취감: 작은 테스트를 통과시킬 때마다 즉각적인 성취감을 느끼며, 이는 개발 과정의 동기 부여로 이어집니다.
  • 업무 스트레스 감소: 버그에 대한 걱정이나 코드 변경에 대한 두려움이 줄어들어, 개발자는 더 창의적이고 편안한 마음으로 개발에 임할 수 있습니다.

이처럼 TDD는 단순히 기술적인 이점을 넘어, 개발 프로세스 전반에 걸쳐 긍정적인 파급효과를 가져옵니다. TDD 장점은 일시적인 편리함을 넘어, 장기적인 관점에서 프로젝트의 성공과 개발팀의 역량 강화에 크게 기여합니다. 이제 이론적인 이해를 넘어, 실제 코드를 통해 TDD의 힘을 직접 경험해 볼 시간입니다. TDD 예제 파이썬을 통해 Red-Green-Refactor 사이클이 어떻게 작동하는지 상세히 알아보겠습니다.


실제 예시로 배우는 TDD: 간단한 기능 구현하기 (Python 예제)

TDD의 이론적인 개념과 이점들을 살펴보았으니, 이제 실제 코드를 통해 TDD 예제 파이썬 방식으로 간단한 Calculator 클래스를 구현해 보겠습니다. 이 예제를 통해 Red-Green-Refactor 사이클이 어떻게 반복되는지 단계별로 명확하게 보여드리겠습니다. 우리는 unittest 모듈을 사용할 것입니다.

목표: 간단한 Calculator 클래스 구현하기

Calculator 클래스는 다음 기능을 제공해야 합니다:

  1. 두 수를 더하는 add 메서드
  2. 두 수를 빼는 subtract 메서드
  3. 두 수를 곱하는 multiply 메서드
  4. 두 수를 나누는 divide 메서드 (0으로 나누는 경우 예외 처리)

1단계: 프로젝트 초기 설정

먼저, 프로젝트 폴더를 만들고 두 개의 파일을 생성합니다.

  • calculator.py: 실제 Calculator 클래스가 구현될 파일
  • test_calculator.py: Calculator 클래스를 테스트할 파일
tdd_example/
├── calculator.py
└── test_calculator.py

calculator.py 파일은 처음에는 비어 있습니다.

2단계: add 기능 구현 (Red-Green-Refactor)

2.1. Red: 실패하는 테스트 작성 (덧셈)

test_calculator.py 파일에 Calculatoradd 메서드를 테스트할 코드를 작성합니다.

# test_calculator.py
import unittest
# from calculator import Calculator # 아직 Calculator 클래스가 없으므로 주석 처리

class TestCalculator(unittest.TestCase):
    def test_add_two_numbers(self):
        # 지금은 Calculator 클래스는 존재하지만, add 메서드가 아직 없어서 실패할 것임을 예상하는 코드를 작성합니다.
        # 이 테스트는 `AttributeError`를 발생시켜 'Red' 상태가 될 것입니다.
        calculator = Calculator() 
        with self.assertRaises(AttributeError):
            calculator.add(1, 2)

        print("Red: test_add_two_numbers - 실패 예상") # 콘솔에 표시

설명: calculator.pyCalculator 클래스는 존재하지만 add 메서드가 없기 때문에 AttributeError가 발생할 것입니다. 어떤 경우든 테스트는 실패해야 합니다. 이것이 바로 'Red' 상태입니다.

터미널에서 테스트 실행: python -m unittest test_calculator.py
결과는 FAILED가 나와야 합니다.

2.2. Green: 테스트를 통과하는 코드 작성 (덧셈)

calculator.py 파일에 Calculator 클래스와 add 메서드를 최소한으로 구현하여 테스트를 통과시킵니다.

# calculator.py
class Calculator:
    def add(self, a, b):
        return a + b

이제 test_calculator.py에서 from calculator import Calculator 주석을 해제합니다.
test_calculator.pytest_add_two_numbers 메서드를 다음과 같이 수정합니다 (AttributeError를 기대하는 부분을 실제 assert로 변경).

# test_calculator.py
import unittest
from calculator import Calculator # 이제 Calculator 클래스를 임포트합니다.

class TestCalculator(unittest.TestCase):
    def test_add_two_numbers(self):
        calculator = Calculator()
        self.assertEqual(calculator.add(1, 2), 3) # 1 + 2 = 3
        self.assertEqual(calculator.add(-1, 1), 0) # 음수와 양수
        self.assertEqual(calculator.add(0, 0), 0) # 0과 0
        self.assertEqual(calculator.add(5, 5), 10) # 다른 양수 예제

        print("Green: test_add_two_numbers - 성공 예상")

다시 터미널에서 테스트 실행: python -m unittest test_calculator.py
이번에는 OK (모든 테스트 성공)가 나와야 합니다. 이것이 'Green' 상태입니다.

2.3. Refactor: 코드 리팩토링 (덧셈)

add 메서드는 현재 매우 간단하여 리팩토링할 부분이 많지 않습니다. 하지만 'Red-Green-Refactor' 사이클의 중요성을 이해하기 위해 이 단계를 명시적으로 거칩니다. 만약 add 메서드가 더 복잡한 로직을 가지고 있었거나, 다른 곳과 중복되는 부분이 있었다면 이 단계에서 코드를 더 깔끔하게 다듬었을 것입니다. 현재로서는 코드가 명확하고 효율적이므로, 변경할 것이 없습니다.

설명: 이 단계에서 코드를 더 명확하게 만들거나, 중복을 제거하거나, 성능을 개선할 수 있습니다. 현재는 그대로 둡니다.


3단계: subtract 기능 구현 (Red-Green-Refactor)

3.1. Red: 실패하는 테스트 작성 (뺄셈)

test_calculator.pysubtract 메서드를 테스트할 새로운 코드를 추가합니다.

# test_calculator.py 에 추가
    def test_subtract_two_numbers(self):
        calculator = Calculator()
        # 아직 subtract 메서드가 없으므로 AttributeError가 발생할 것을 기대합니다.
        with self.assertRaises(AttributeError):
            calculator.subtract(5, 2)

        print("Red: test_subtract_two_numbers - 실패 예상")

터미널에서 테스트 실행: python -m unittest test_calculator.py
결과는 FAILED가 나와야 합니다 (AttributeError로 인해). 이것이 'Red' 상태입니다.

3.2. Green: 테스트를 통과하는 코드 작성 (뺄셈)

calculator.py 파일에 subtract 메서드를 최소한으로 구현합니다.

# calculator.py 에 추가
    def subtract(self, a, b):
        return a - b

test_calculator.pytest_subtract_two_numbers 메서드를 다음과 같이 수정합니다.

# test_calculator.py 의 test_subtract_two_numbers 수정
    def test_subtract_two_numbers(self):
        calculator = Calculator()
        self.assertEqual(calculator.subtract(5, 2), 3)
        self.assertEqual(calculator.subtract(2, 5), -3)
        self.assertEqual(calculator.subtract(10, 0), 10)
        self.assertEqual(calculator.subtract(0, 0), 0)

        print("Green: test_subtract_two_numbers - 성공 예상")

다시 터미널에서 테스트 실행: python -m unittest test_calculator.py
결과는 OK가 나와야 합니다. 이것이 'Green' 상태입니다.

3.3. Refactor: 코드 리팩토링 (뺄셈)

subtract 메서드도 현재로서는 리팩토링할 부분이 없습니다.


4단계: multiply 기능 구현 (Red-Green-Refactor)

4.1. Red: 실패하는 테스트 작성 (곱셈)

test_calculator.pymultiply 메서드를 테스트할 새로운 코드를 추가합니다.

# test_calculator.py 에 추가
    def test_multiply_two_numbers(self):
        calculator = Calculator()
        # 아직 multiply 메서드가 없으므로 AttributeError가 발생할 것을 기대합니다.
        with self.assertRaises(AttributeError):
            calculator.multiply(3, 4)

        print("Red: test_multiply_two_numbers - 실패 예상")

터미널에서 테스트 실행: python -m unittest test_calculator.py
결과는 FAILED가 나와야 합니다. 이것이 'Red' 상태입니다.

4.2. Green: 테스트를 통과하는 코드 작성 (곱셈)

calculator.py 파일에 multiply 메서드를 최소한으로 구현합니다.

# calculator.py 에 추가
    def multiply(self, a, b):
        return a * b

test_calculator.pytest_multiply_two_numbers 메서드를 다음과 같이 수정합니다.

# test_calculator.py 의 test_multiply_two_numbers 수정
    def test_multiply_two_numbers(self):
        calculator = Calculator()
        self.assertEqual(calculator.multiply(3, 4), 12)
        self.assertEqual(calculator.multiply(-2, 3), -6)
        self.assertEqual(calculator.multiply(0, 5), 0)

        print("Green: test_multiply_two_numbers - 성공 예상")

다시 터미널에서 테스트 실행: python -m unittest test_calculator.py
결과는 OK가 나와야 합니다. 이것이 'Green' 상태입니다.

4.3. Refactor: 코드 리팩토링 (곱셈)

multiply 메서드도 현재로서는 리팩토링할 부분이 없습니다.


5단계: divide 기능 구현 및 예외 처리 (Red-Green-Refactor)

5.1. Red: 실패하는 테스트 작성 (나눗셈 및 0으로 나누기)

test_calculator.pydivide 메서드를 테스트할 새로운 코드를 추가합니다. 특히 0으로 나누는 경우를 함께 고려합니다.

# test_calculator.py 에 추가
    def test_divide_two_numbers(self):
        calculator = Calculator()
        # 아직 divide 메서드가 없으므로 AttributeError가 발생할 것을 기대합니다.
        with self.assertRaises(AttributeError):
            calculator.divide(10, 2)

        print("Red: test_divide_two_numbers - 실패 예상 (나눗셈)")

    def test_divide_by_zero_raises_error(self):
        calculator = Calculator()
        # 아직 divide 메서드가 없으므로 AttributeError가 발생할 것을 기대합니다.
        with self.assertRaises(AttributeError): 
            calculator.divide(10, 0)

        print("Red: test_divide_by_zero_raises_error - 실패 예상 (0 나눗셈)")

터미널에서 테스트 실행: python -m unittest test_calculator.py
결과는 FAILED가 나와야 합니다. 이것이 'Red' 상태입니다.

5.2. Green: 테스트를 통과하는 코드 작성 (나눗셈 및 0으로 나누기)

calculator.py 파일에 divide 메서드를 최소한으로 구현하고, 0으로 나누는 경우를 처리합니다.

# calculator.py 에 추가
class Calculator:
    def add(self, a, b): # 기존 메서드들
        return a + b

    def subtract(self, a, b): # 기존 메서드들
        return a - b

    def multiply(self, a, b): # 기존 메서드들
        return a * b

    def divide(self, a, b):
        if b == 0:
            raise ValueError("0으로 나눌 수 없습니다.")
        return a / b

test_calculator.pytest_divide_two_numberstest_divide_by_zero_raises_error 메서드를 다음과 같이 수정합니다.

# test_calculator.py 의 test_divide_two_numbers 수정
    def test_divide_two_numbers(self):
        calculator = Calculator()
        self.assertEqual(calculator.divide(10, 2), 5)
        self.assertEqual(calculator.divide(7, 2), 3.5)
        self.assertEqual(calculator.divide(-10, 2), -5)
        self.assertEqual(calculator.divide(0, 5), 0) # 0을 다른 수로 나누기

        print("Green: test_divide_two_numbers - 성공 예상")

# test_calculator.py 의 test_divide_by_zero_raises_error 수정
    def test_divide_by_zero_raises_error(self):
        calculator = Calculator()
        with self.assertRaisesRegex(ValueError, "0으로 나눌 수 없습니다."):
            calculator.divide(10, 0)

        print("Green: test_divide_by_zero_raises_error - 성공 예상")

다시 터미널에서 테스트 실행: python -m unittest test_calculator.py
결과는 OK가 나와야 합니다. 이것이 'Green' 상태입니다.

5.3. Refactor: 코드 리팩토링 (나눗셈)

divide 메서드는 0으로 나누는 경우에 대한 예외 처리가 명확하게 되어 있어, 현재로서는 큰 리팩토링 필요성은 없어 보입니다.


완성된 코드 (참고용)

calculator.py

class Calculator:
    def add(self, a, b):
        return a + b

    def subtract(self, a, b):
        return a - b

    def multiply(self, a, b):
        return a * b

    def divide(self, a, b):
        if b == 0:
            raise ValueError("0으로 나눌 수 없습니다.")
        return a / b

test_calculator.py

import unittest
from calculator import Calculator

class TestCalculator(unittest.TestCase):
    def test_add_two_numbers(self):
        calculator = Calculator()
        self.assertEqual(calculator.add(1, 2), 3)
        self.assertEqual(calculator.add(-1, 1), 0)
        self.assertEqual(calculator.add(0, 0), 0)
        self.assertEqual(calculator.add(5, 5), 10)
        print("Green: test_add_two_numbers - 성공")

    def test_subtract_two_numbers(self):
        calculator = Calculator()
        self.assertEqual(calculator.subtract(5, 2), 3)
        self.assertEqual(calculator.subtract(2, 5), -3)
        self.assertEqual(calculator.subtract(10, 0), 10)
        self.assertEqual(calculator.subtract(0, 0), 0)
        print("Green: test_subtract_two_numbers - 성공")

    def test_multiply_two_numbers(self):
        calculator = Calculator()
        self.assertEqual(calculator.multiply(3, 4), 12)
        self.assertEqual(calculator.multiply(-2, 3), -6)
        self.assertEqual(calculator.multiply(0, 5), 0)
        print("Green: test_multiply_two_numbers - 성공")

    def test_divide_two_numbers(self):
        calculator = Calculator()
        self.assertEqual(calculator.divide(10, 2), 5)
        self.assertEqual(calculator.divide(7, 2), 3.5)
        self.assertEqual(calculator.divide(-10, 2), -5)
        self.assertEqual(calculator.divide(0, 5), 0)
        print("Green: test_divide_two_numbers - 성공")

    def test_divide_by_zero_raises_error(self):
        calculator = Calculator()
        with self.assertRaisesRegex(ValueError, "0으로 나눌 수 없습니다."):
            calculator.divide(10, 0)
        print("Green: test_divide_by_zero_raises_error - 성공")

if __name__ == '__main__':
    unittest.main()

TDD 예제 파이썬을 통해 우리는 Calculator 클래스의 각 기능을 구현하면서 Red-Green-Refactor 사이클을 반복하는 과정을 직접 경험했습니다. 각 기능을 구현할 때마다 먼저 실패하는 테스트를 작성하여 목표를 명확히 하고, 최소한의 코드로 이를 통과시킨 후, 필요하다면 코드를 개선하는 과정을 거쳤습니다. 이 방식은 우리가 작성하는 코드가 항상 테스트에 의해 검증되고, 점진적으로 견고해진다는 것을 보여줍니다.


TDD 도입 시 고려사항 및 흔한 오해

TDD는 의심할 여지 없이 강력한 개발 방법론이지만, 모든 개발 상황에 만능 해결책은 아닙니다. TDD를 효과적으로 프로젝트에 적용하기 위해서는 몇 가지 고려사항과 흔한 오해들을 명확히 이해하고 있어야 합니다. 무분별한 도입은 오히려 생산성 저하로 이어질 수 있기 때문입니다.

1. 초기 학습 곡선과 생산성 저하

TDD는 '테스트 퍼스트'라는 익숙하지 않은 사고방식을 요구합니다. 특히 개발 입문자나 TDD에 익숙하지 않은 개발자에게는 초기 학습 곡선이 존재합니다. 처음에는 다음과 같은 어려움을 겪을 수 있습니다.

  • 테스트 코드 작성 시간: 기능 코드 외에 테스트 코드를 작성하는 데 추가적인 시간이 소요됩니다.
  • 테스트 가능한 설계 고민: '어떻게 하면 이 코드를 테스트하기 쉽게 만들 수 있을까?'에 대한 고민이 익숙하지 않아 개발 속도가 느려질 수 있습니다.
  • 작은 단위 테스트 분리: 테스트를 너무 크게 만들거나, 너무 작게 분리하는 기준을 잡기 어려울 수 있습니다.

이러한 초기 비용 때문에 단기적으로는 개발 속도가 느려지는 것처럼 느껴질 수 있습니다. 하지만 이는 TDD의 장점을 누리기 위한 '투자'로 봐야 합니다. 숙련될수록 테스트 코드 작성 속도는 빨라지고, 좋은 설계 습관이 몸에 배어 장기적인 생산성 향상으로 이어집니다.

2. 모든 상황에 TDD를 적용할 필요는 없다 (균형의 중요성)

TDD는 매우 유용하지만, 모든 종류의 프로젝트나 모든 기능에 100% 적용해야 하는 것은 아닙니다.

  • 간단한 스크립트 또는 프로토타이핑: 일회성으로 사용되거나 빠르게 검증해야 하는 간단한 스크립트, 혹은 아직 요구사항이 불분명하여 자주 변경될 가능성이 높은 프로토타입 단계에서는 TDD의 엄격한 사이클이 오히려 발목을 잡을 수 있습니다.
  • UI/UX 코드: 사용자 인터페이스(UI)나 사용자 경험(UX) 관련 코드는 백엔드 로직처럼 명확한 입력과 출력을 정의하기 어려운 경우가 많습니다. 물론 UI 컴포넌트 단위의 테스트는 중요하지만, TDD의 '테스트 퍼스트'를 엄격하게 적용하기 어려운 부분이 있습니다.
  • 외부 시스템 연동: 데이터베이스, 외부 API, 레거시 시스템 등 제어하기 어려운 외부 시스템과 연동되는 부분은 테스트 환경을 구축하는 것이 복잡하고 어렵습니다. 이런 경우 Mock이나 Stub과 같은 테스트 대역을 적극적으로 활용해야 하며, 때로는 통합 테스트에 더 집중하는 것이 효율적일 수 있습니다.

중요한 것은 적절한 균형을 찾는 것입니다. 프로젝트의 특성, 팀의 숙련도, 기능의 중요도 등을 고려하여 TDD를 유연하게 적용해야 합니다. 핵심 비즈니스 로직, 복잡한 알고리즘, 안정성이 중요한 모듈 등에는 TDD를 적극적으로 활용하고, 그렇지 않은 부분에는 보다 가벼운 테스트 전략을 사용하는 것이 현명합니다.

3. 흔한 오해: TDD는 단지 테스트를 작성하는 기술이다?

TDD에 대한 가장 흔한 오해 중 하나는 "TDD는 단순히 테스트를 많이 작성하는 것"이라는 생각입니다. 하지만 TDD는 테스트를 통해 설계를 이끌어 나가는(Test-Driven Design) 방법론입니다.

  • 테스트는 설계의 안내자: TDD에서 테스트 코드는 단순히 구현된 코드를 검증하는 수단이 아니라, 코드를 어떻게 설계해야 하는지에 대한 가이드라인을 제공합니다. 테스트하기 쉬운 코드를 작성하려는 노력은 자연스럽게 응집도를 높이고 결합도를 낮추는, 즉 좋은 객체 지향 설계를 유도합니다.
  • 작은 단위의 명확한 책임: TDD는 작은 기능 단위로 테스트를 작성하고 구현하도록 유도합니다. 이는 각 모듈이 명확한 책임을 가지도록 하여, 코드의 응집도를 높이고 관리하기 쉽게 만듭니다.
  • 리팩토링의 핵심: TDD의 마지막 단계인 Refactor는 코드의 내부 구조를 개선하는 데 필수적입니다. 테스트가 존재하기 때문에 개발자는 자신감을 가지고 코드를 더 아름답고 효율적으로 다듬을 수 있으며, 이는 소프트웨어의 장기적인 건강성을 보장합니다.

4. 레거시 코드에 TDD 적용의 어려움

이미 존재하는, 테스트 코드가 없는 방대한 양의 레거시 코드에 TDD를 적용하는 것은 매우 어렵습니다. 테스트하기 어렵게 설계된 레거시 코드는 작은 변경에도 전체 시스템에 어떤 영향을 미칠지 예측하기 어렵기 때문입니다.

  • Characterization Test: 이런 경우, 기존 시스템의 '현재 동작'을 파악하는 테스트(Characterization Test)를 먼저 작성하는 것이 일반적입니다. 이 테스트는 버그를 발견하기 위함이 아니라, 코드 변경 시 기존 동작이 바뀌지 않았음을 보장하는 안전망을 구축하는 데 목적이 있습니다.
  • 점진적 적용: 한 번에 모든 레거시 코드를 TDD로 전환하려 하기보다는, 새로운 기능을 추가하거나 기존 코드를 수정하는 영역부터 TDD를 점진적으로 적용하는 것이 현실적입니다.

5. TDD를 효과적으로 도입하기 위한 팁

  • 작게 시작하기: TDD를 처음 접한다면, 작고 독립적인 기능부터 TDD 방식으로 구현해 보세요. 계산기, 문자열 처리 함수 등 명확한 입출력을 가진 기능이 좋습니다.
  • 페어 프로그래밍: 숙련된 TDD 개발자와 함께 페어 프로그래밍을 하면 TDD 사고방식을 빠르게 익힐 수 있습니다.
  • 지속적인 학습과 연습: TDD는 연습을 통해 숙련되는 기술입니다. TDD 카타(Kata)나 코딩 챌린지를 통해 꾸준히 연습하는 것이 중요합니다.
  • 팀 전체의 합의: 팀 프로젝트에 TDD를 도입하려면 모든 팀원의 이해와 합의가 필수적입니다. 함께 학습하고, 서로의 경험을 공유하며 점진적으로 정착시켜 나가야 합니다.

TDD는 단기적인 생산성 저하를 감수하더라도, 장기적으로는 견고하고 유연한 코드 작성을 가능하게 하며, 개발 생산성을 비약적으로 향상시키는 강력한 도구입니다. 위의 고려사항들을 충분히 숙지하고 현명하게 접근한다면, TDD는 여러분의 개발 여정에 큰 전환점이 될 것입니다.


TDD로 더 나은 개발자가 되기 위한 첫걸음

오늘 우리는 TDD(테스트 주도 개발)라는 강력한 개발 방법론에 대해 깊이 있게 탐구했습니다. TDD란 단순히 테스트 코드를 먼저 작성하는 것을 넘어, 테스트 주도 개발 방법론이 제공하는 '테스트 퍼스트' 철학을 통해 코드의 설계, 품질, 유지보수성, 그리고 궁극적으로 개발 생산성 향상까지 이끌어내는 과정임을 이해했습니다.

Red-Green-Refactor 사이클은 TDD의 핵심 엔진으로, 작은 단위의 실패하는 테스트를 작성하고, 이를 통과시키는 최소한의 코드를 구현한 뒤, 안전하게 코드를 리팩토링하는 과정을 반복하며 코드를 점진적으로 개선해 나갑니다. 이 과정에서 우리는 TDD 장점들을 분명히 확인할 수 있었습니다.

  • 코드 품질 향상: 버그를 조기에 발견하고, 회귀 버그를 방지하며, 테스트 코드를 통해 살아있는 문서를 확보합니다.
  • 견고하고 유연한 설계: 테스트하기 쉬운 코드를 작성하려는 노력이 자연스럽게 높은 응집도와 낮은 결합도를 가진 모듈화된 설계를 유도하여, 견고한 코드 작성의 기반을 마련합니다.
  • 유지보수 용이성: 강력한 테스트 안전망 덕분에 자신감을 가지고 코드를 리팩토링하고 변경할 수 있으며, 장기적인 개발 생산성을 보장합니다.

또한, TDD 예제 파이썬을 통해 Calculator 클래스의 각 기능을 구현하며 TDD 사이클이 실제 개발 과정에서 어떻게 적용되는지 생생하게 체험했습니다. 이는 개발 입문자부터 기본적인 프로그래밍 지식이 있는 모든 개발자에게 TDD의 실제적인 가치를 보여주는 좋은 예시가 되었으리라 생각합니다.

물론 TDD가 만능은 아니며, 초기 학습 곡선, 모든 상황에 대한 적용의 한계 등 고려해야 할 사항들도 분명히 존재합니다. 하지만 이러한 제약 사항들을 이해하고 현명하게 접근한다면, TDD는 여러분의 개발 역량을 한 단계 더 성장시키는 데 결정적인 역할을 할 것입니다.

더 나은 개발자가 되기 위한 여정은 끊임없는 학습과 실천의 연속입니다. TDD는 단순히 기술적인 스킬을 넘어, 코드를 대하는 태도와 문제 해결 방식에 대한 깊은 통찰을 제공합니다. 오늘 이 글을 통해 얻은 지식을 바탕으로, 주저하지 말고 오늘부터 작은 프로젝트에 TDD를 적용해보세요. 실패를 두려워하지 마세요. 실패하는 테스트는 여러분이 나아갈 방향을 명확히 제시해 줄 것입니다.

TDD를 통해 여러분의 코드가 더욱 견고하고 유연해지며, 개발 과정이 더욱 즐겁고 자신감 넘치게 변화하기를 진심으로 응원합니다.


참고 자료 및 다음 단계:

  • "테스트 주도 개발 (TDD)" - 마틴 파울러: https://martinfowler.com/bliki/TestDrivenDevelopment.html
  • "Test-Driven Development by Example" - Kent Beck (TDD 창시자의 저서)
  • TDD Kata 연습: TDD 기술을 연마할 수 있는 다양한 코딩 챌린지들을 검색하여 꾸준히 연습해 보세요. (예: FizzBuzz Kata, String Calculator Kata)
반응형
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2026/01   »
1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30 31
글 보관함