← 이전 화면 돌아가기

Backend

2023-05-17

개발자를 위한 S/W Test 개념 2편 - TDD 핵심과 개발자에게 좋은 테스트의 기준

테스트 주도 개발(TDD)의 개념부터 본질 파헤치기

개발자를 위한 S/W Test 개념 (2)

TDD 핵심과 개발자에게 좋은 테스트의 기준

안녕하세요, ONDA에서 호텔 운영관리 솔루션을 개발하고 있는 백엔드 개발자 Gunner(거너, 정재훈)입니다. 

지난 1편에서 소프트웨어 테스트의 종류에 대해 다양한 관점으로 살펴봤습니다. BDD와 TDD 같은 테스트 도구에 대해서도 다뤄봤는데요. 

간혹 TDD와 테스트를 혼용하여 그 의미가 불분명해지는 경우가 있습니다. TDD의 개념과 개발자 사이에 일어났던 논쟁의 쟁점을 이해하고, 개발자에게 좋은 소프트웨어 테스트는 무엇일지 차근차근 다뤄보겠습니다. 

1. 테스트 주도 개발(Test-Driven Development, TDD)이란?

먼저 TDD에 대한 개념을 알아보겠습니다. TDD는 켄트 벡(Kent Beck)의 ⌜Test-Driven Development By Example⌟ 책1 (이하 TDDBE)에 소개된 소프트웨어 설계 기법으로 테스트를 통해 개발을 주도해가는 방법입니다.

*TDD를 테스트 방법으로 보는 경우도 많지만, 개인적으로는 설계 방법에 더 가깝다고 생각합니다.

TDDBE에서 주장하는 TDD 방법은 간단해요.

1. 실패하는 테스트를 먼저 작성한 뒤
2. 이를 최대한 빨리 통과시키는 코드를 작성하고
3. 리팩토링하라는 것

책에서는 아래 이미지처럼 테스트가 실패할 때 뜨는 적색과 성공할 때 뜨는 녹색에서 기인해 'Red-Green-Refactor 사이클'이라고 부릅니다.

Smalltalk SUnit의 TestRunner 결과

TDDBE 책에서는 Money 클래스를 개발하면서 통화 단위 등을 구현해 나가는데요. 예를 들어 달러($) 기준으로 개발하여 테스트가 성공하지만, 다른 통화 단위를 적용했을 때 실패하며 이를 이어 나가기 위한 클래스를 만들어 가는 과정을 보여준 것이지요.

실패하는 테스트를 먼저 작성하면서 Red-Green-Refactor 사이클을 보여주고, Test-First로 만들어진 자동화된 테스트들은 Money 클래스의 설계 방향을 주도(drive)할 뿐 아니라 새로운 기능이 추가될 때 기존 기능이 잘 동작하는지 체크하는 안전망 역할도 합니다.

또 하나 주목할 점은 TDDBE에서 켄트는 아주 작은 스텝을 밟아나간다는 것인데요. 후에 켄트는 한 인터뷰를 통해 ‘반드시 작은 스텝을 밟으라는 뜻은 아니며, 좋은 설계 아이디어가 떠오르지 않을 때 작은 스텝을 밟아 나가다가 확신이 생기면 큰 스텝을 밟을 수 있도록 감각을 기르게 하는 것이 목적’이라고 밝혔습니다. 

결론적으로 TDDBE에서 말하는 TDD의 핵심은 Red-Green-Refactor 사이클이며, 이를 통해 좋은 설계와 자동화된 테스트를 만들어 나가는 것이죠. 

Test-Driven Development by 켄트 벡, 2002년 11월

2. TDD, 개발자들로부터 논쟁을 일으키다

TDD가 등장한 뒤 개발자들 사이에서는 많은 논쟁이 오고 갔습니다. 주로 ‘TDD를 이용하여 개발해야 한다’와 ‘TDD는 실무에 맞지 않는다’는 의견이 대부분이었는데요. 

‘TDD를 이용하여 개발해야 한다’는 입장을 보인 개발자들은 TDD를 통해 발생하는 유용한 점, 즉 Test-First를 통해 진행되는 좋은 설계, 개발 진행과 함께 만들어지는 자동화된 테스트 등을 주장했지만, TDD는 실무에 맞지 않는다’는 입장을 보인 개발자들은 Test-First로 디자인(설계)하는 방법에 공감하지 않는다고 주장했죠. 

(1) 켄트는 왜 TDD를 주장했을까

자세한 이야기를 하기에 앞서 켄트가 TDD를 주장하게 된 배경에 대해 잠깐 알아볼까요?

켄트가 주로 사용했던 언어는 ‘Smalltalk’이고, 실제로 이 언어를 이용하여 유명한 프로젝트인 C32 를 진행하면서 XP(eXtreme Programming)도 적용했죠. Smalltalk은 VM(Virtual Machine, 가상 머신)에서 실행되는 언어로서 개발 환경 자체를 하나의 IDE(Integrated Development Environment, 통합 개발 환경)로 볼 수 있으며, 소스 코드가 하나의 통합 이미지로 저장되는 것이 특징입니다.

GemStone Smalltalk 플랫폼

Smalltalk는 객체 지향적으로 만들어진 언어로, 아래와 같이 TDD를 적용하기에 좋은 환경과 요건을 갖추고 있었습니다.

  • 코드 브라우저에서 코드를 바로 실행할 수 있다.
  • 수많은 작은 객체가 상호 메시지를 주고받으며 작업하도록 설계되어 있다.
  • Smalltalk는 MVC 패턴의 원조였지만 MVC를 디자인할 때 지금의 Java 프로젝트와 같이 레이어 개념으로 파일을 구분하지 않았으며, 소스 코드 전체를 하나의 이미지 형태로 저장한다.
  • xUnit의 원조가 SUnit이다.

허나 보통의 Java Spring 프로젝트는 그렇지 않죠.

  • 코드를 실행하기 위해서는 스프링 컨텍스트를 올려야 한다.
  • Smalltalk와 비교하면 상대적으로 클래스를 많이 나누지 않는다.
  • DDD(Domain-Driven Design), 헥사고날 아키텍처 등의 영향으로 레이어를 명확하게 두며, 레이어마다 파일을 분리해서 따로 둔다. 이는 테스트해야 할 범위가 확장되는 것으로 생각할 수 있다.
  • JUnit은 SUnit의 포팅이다. 3

Smalltalk는 Java처럼 '파일 - 클래스' 순으로 찾을 수 없고, 객체 간 상호 작용이 많아 이를 자동화된 테스트로 확인하는 과정이 필요했다고 볼 수 있는데요. 또 다르게 말하면 Smalltalk는 단위 테스트를 빠르게 실행하고 즉각적인 피드백을 통해 개발하기 매우 좋은 환경이기도 했던 것이죠. 

(2) ‘TDD는 죽었다. 테스트여 영원하라’

2000년대 초반 TDD 개념이 등장하면서 개발자들 사이에서 많은 논의가 있었고, 앞서 말한 것과 같이 이를 적극적으로 수용하는 그룹과 그렇지 않은 그룹으로 나뉘었습니다. 그러던 2014년, Ruby on Rails(*풀 스택 웹 프레임워크, 이후 등장한 거의 모든 풀 스택 웹 프레임워크에 영향을 미쳤다)를 만든 DHH(David Heinemeier Hansson)가 자신의 블로그에 ‘TDD는 죽었다. 테스트여 영원하라’4란 도발적 제목의 글을 올리며, TDD 논쟁이 시작되었어요. 

DHH가 주장한 주요 내용은 다음과 같습니다.5

  • TDD는 나름의 효용성이 있지만 교조주의적 종교로 변했다. 
  • TDD는 단위 테스트에 집중한 나머지 불필요한 복잡성(ex. 목)을 가져올 때도 있다.

        ◦ 레이어간 중계 객체들이 얽힌 복잡한 구조

         ◦ 테스트에 DB나 I/O를 직접 사용하는 것을 피함 → 시스템 테스트는 나쁜 것으로 치부

         ◦ 서비스 객체와 커맨드 패턴, 그리고 더 나쁜 것들이 얽힌 정글이다. → 오로지 테스트만을 위한 패턴 남용과 객체 설계

  • TDD가 설계를 주도할 수도 있겠지만, 나의 설계 방식과는 잘 맞지 않았다.
  • 모든 의존 관계를 목으로 만들고 수천 개의 테스트가 몇 초 안에 끝나는 등, 전통적인 의미에서의 단위 테스트를 나는 거의 하지 않는다.

         ◦ Rails에서는 좋은 테스트로 보지 않는다.

         ◦ 나는 ActiveRecord 모델을 직접 테스트한다. fixture를 만들고 DB에 직접 접근한다.

         ◦ 탑 레이어에는 컨트롤러 테스트가 있지만, 나의 경우 Capybara 같은 도구를 사용해서 높은 수준의 시스템 테스트로 대체하는 것을 선호한다.

  • 전부 나처럼 해야 한다는 뜻은 아니지만, TDD를 해야만 좋은 개발이라는 분위기는 멈추자.

(3) 켄트 백 vs. DHH, TDD에 대한 그들의 생각은

해당 글은 당시 TDD를 지지하는 그룹의 반감을 일으키며 온라인상에서 큰 논란으로 번져갔는데요. 

한동안 논란이 이어지다 결국 마틴 파울러(*「Refactoring」으로 유명한 소프트웨어 개발자)의 주도로 켄트 벡과 DHH의 온라인 토론이 열렸고, 당시 토론에서 그들이 나눈 내용을 요약하면 다음과 같습니다. 6 7

TDD가 말하는 단위 테스트의 정의
  • DHH: TDD에서 단위 테스트의 정의는 무엇인가? 꼭 목을 사용해서 단위 테스트를 해야 하는가? 또한 Red-Green-Refactor 사이클은 내게 맞는 부분도 있었지만, 전반적으로 맞지 않았다.
  • 켄트: 오래전부터 프로그래머가 (어떤 방식이든) 테스트하는 것은 당연했다. Smalltalk로 프로그래밍하면서 나는 이를 자동화했고, Test-First를 시도했는데 나에게는 아주 잘 맞았다.
  • 공통: 물론 TDD를 적용하는 것은 나름의 효용이 있지만, 취향이나 프로젝트의 특성에 따라 다를 수 있겠다.

  • DHH: 많은 사람이 무거운 목을 사용하면서 나쁜 트레이드-오프(Trade-off)를 한다. 왜 이렇게까지 목을 써가며 손해를 보는 걸까?
  • 켄트: 목을 사용하는 것은 트레이드-오프고 그 점에 나도 동의한다. 만약 실제 객체로 빠른 피드백 루프를 만들 수 있다면 실제 객체를 쓰겠다. 나도 TDD를 할 때 목을 쓴 적이 거의 없다(Smalltalk는 Java와 달리 레이어를 나누지 않아 목을 쓰지 않고 바로 테스트하기 때문).

    또한 테스트를 만들고 실제 코드를 수정할 때 테스트 코드도 함께 고쳐야 하는데, 실제 코드가 많이 변하면 그에 해당하는 목도 모두 바꿔야 한다. 테스트는 리팩토링을 더 쉽게 해주는 반면, 목은 리팩토링을 더 어렵게 한다는 점이 조금 우려된다.
  • DHH: 수많은 목을 사용하는 레이어드 아키텍처는 간접 참조가 많고, 복잡도가 지나치다.
  • 켄트: 이는 TDD 때문에 발생하는 문제는 아니다. 
  • ‍‍마틴: 그렇다. 이건 헥사고날(Hexagonal) 아키텍처의 핵심인 ‘환경으로부터의 철저한 분리’에 기인한다.
  • DHH: 동의한다. 그런데 분리 자체를 목적으로 강조하는 사람들을 종종 보게 된다. TDD에서 단위 테스트를 하기 때문이다.

    헥사고날 아키텍처를 적용하는 이들은 이를(도메인 모델을) 터미널 애플리케이션, 또는 커맨드 라인으로 쓸 수 있다고 이야기한다. 그들에게 묻고 싶다. 과연 그럴 일이 있을까?

    리포지토리(repository)도 마찬가지다. DB에 접근하던 구현을 인메모리 DB 또는 웹서비스에 접근하는 걸로 바꿀 수 있다고 하는데, 과연 그럴 일이 있을지 의문이다. 
  • 켄트: 이 역시 TDD에서 비롯된 문제가 아니라 설계나 피드백을 얼마나 빨리 받을 수 있는가의 문제이다. 설계상 문제는 말 그대로 설계를 잘 고려하는 것이 중요하다.
  • 공통: 빠른 피드백은 중요하고, TDD가 QA를 대체할 순 없다.

모든 코드에 TDD를 하는 것이 옳은가? 테스트가 너무 많거나 불필요한 테스트는 아닐까?
  • 켄트: 나도 항상 TDD를 하진 않는다. 사람들은 테스트 코드를 작성하기 위한 비용을 지불하는 것이 아니라, 충분한 확신을 얻기 위해 작성하는 것이다.
  • ‍마틴: 과도한 테스트는 분명 존재한다. 내가 이 코드 라인을 주석 처리하면 테스트가 깨질 것인가? 중요한 건 깨질만한 케이스를 테스트로 만드는 것이다. getter 같은 곳은 믿을 수 있기 때문에 테스트를 붙일 필요 없다.

・ 코드를 자신 있게 변경할 수 없다면? → 테스트가 충분하지 않거나 좋은 테스트가 아니란 신호
・ 코드 변경 시 테스트 변경이 더 힘든 거 같다. → 테스트가 너무 많다는 신호

TDD는 어떤 점에서 유용할까?
  • 켄트: TDD는 문제를 작은 단위로 쪼개주며, 자신감을 준다.
  • 마틴: TDD를 사용하기에 적합한 분야와 그렇지 않은 분야가 있다.
  • DHH: TDD에 적합한 분야가 있다는 말에 동의한다. 개인적인 경험상 MVC 웹 애플리케이션에는 맞지 않는 경우가 많았다. 하지만 TDD가 적합하지 않은 분야라도 자동화된 셀프 테스트는 꼭 필요하다. 그것이 TDD에 있어 중요한 가치라고 생각한다.

결과적으로 TDD는 좋은 툴이지만 어느 정도 취향의 문제이며, 모든 분야에 적합한 툴이 아닌 것으로 결론지어졌습니다. 

💡TDD가 취향의 문제란 점에 관해 Clojure를 만든 리치 히키(Rich Hickey)의 발언도 함께 살펴볼까요?

리치는 이를 ‘가드레일 프로그래밍’이라 부르며 “테스트가 있기 때문에 수정을 할 수 있다고 하는데, 누가 그렇게 해요? 누가 차를 가드레일에 부딪혀 가면서 운전하나요?”라며 TDD의 Red-Green-Refactor 사이클을 비판했습니다. 그는 TDD 자체를 비판하는 것이 아닌, 프로그램을 논리적으로 분석할 수 있는 능력이 중요하다(숙고에 의한 분석이 프로그램 설계에 적용되어야 한다)는 주장을 펼쳤는데요.

즉, TDD로 개발하거나 테스트가 모두 통과했다는 이유로 좋은 프로그램은 아니라는 뜻이죠. 이는 TDD보다 앞서 이야기한 타입 체커를 비꼬는 부분에서도 알 수 있습니다.8

리치 역시 자동화된 자가 테스트의 필요성은 인정하면서도, (해당 발언 이후로 TDD 지지자들의 공격이 이어지자) ‘만약 자신이 사용하는 툴이나 방법론이 특정 목적에 맞지 않는다고 비판받는 것이 공격처럼 느껴진다면 릴렉스할 필요가 있다'며 방법론은 도구일 뿐이라고 말하기도 했습니다.

(4) 여전한 TDD 논쟁에서 알 수 있는 진실

이전처럼 활발하지는 않지만, 여전히 TDD 논쟁이 계속되는 이유는 무엇일까요?

논쟁을 자세히 들여다보면, TDD에서 비롯된 이익이 아님에도 TDD를 하면서 발생하는 이익으로 간주하는 경우가 많습니다. 예를 들면 다음과 같이 말이죠.

  • TDD를 해야만 자동화된 셀프 테스트를 얻을 수 있는 것인가? 아니다.
  • TDD를 해야만 회귀 테스트를 얻을 수 있는가? 아니다.
  • TDD를 해야만 좋은 설계(*좋은 설계가 무엇이냐는 또 다른 이야기이긴 하다)가 나오는가? 아니다.
  • TDD는 스펙을 코드로 명확히 표현하나, TDD를 해야만 가능한가? 아니다.

TDD는 Red-Green-Refactor 사이클로 진행되는 설계 방법이라 분명 효용이 있지만 누구에게나, 혹은 모든 프로젝트에 맞지 않을 수도 있습니다. 또한, 논쟁에서 켄트가 이야기했듯이 목은 TDD와 관계가 없죠. 따라서, 본질적으로 설계 기법인 ‘TDD’의 효용과 ‘테스트’의 효용을 명확히 분리할 필요가 있습니다. 

3. 개발자에게 좋은 테스트는?

좋은 테스트가 무엇일지 고민하기에 앞서 테스트도 트레이드 오프라는 사실을 전제로 해야 합니다. 테스트를 만들고 유지 보수하는 작업에 개발자의 리소스가 들어가기 때문이죠.

또 테스트는 분명 개발에 도움을 주지만 마틴 파울러가 대담에서 얘기했듯 부족함과 지나침이 존재합니다. 그렇다면 개발자에게 있어 좋은 테스트는 무엇일까요?

(1) 테스트할 코드 vs. 테스트하지 않을 코드

좋은 테스트는 깨지기 쉬운, 즉 잘못되기 쉬운 부분을 테스트하는 코드입니다. 

깨지기 쉬운 코드
  • 여러 곳에서 참조하는 코드
  • 핵심 로직 코드
  • 앞으로 변경될 가능성이 큰 코드
  • QA 또는 실제 서비스 시 문제가 발생한 코드

위와 같은 코드를 판단하는 건 단위 테스트를 작성하는 것만으로는 알기 어려운 측면이 있습니다. 이때 BDD를 활용하면 좋은데요. Given/When/Then 컨텍스트에서 테스트 케이스를 생각하면 자연스럽게 해당 객체가 다른 객체와 어떻게 연관되는지 알 수 있습니다.

반대로, 어떤 영역은 믿을 수 있는 부분이라 판단하고 테스트하지 않기로 결정하는 것도 중요합니다. 

깨지기 어려운 코드
  • 사용되는 곳이 많지 않은 코드
  • 명확하고 단순한 코드
  • 라이브러리에 있는 기능

다만 테스트하지 않을 코드를 선택하는 데 있어 절대적인 기준은 없습니다. 예를 들어 라이브러리 코드는 일반적으로 테스트하지 않는 개발팀이 있다고 가정해 볼까요? 특정 라이브러리가 자주 업데이트되면서 인터페이스나 동작이 자주 변경된다면 해당 라이브러리를 사용하는 코드에는 테스트를 붙여주는 것이 좋습니다. 

즉 테스트하지 않을 코드로 판단하는 건 개발자가 처한 상황에 따라 충분히 고려하여 판단해야 하는 거죠.

(2) 목(mock), 꼭 사용해야 할까?

테스트 관련 논쟁을 살펴보면 유난히 목에 대한 언급이 많습니다. 

TDD 같은 방법론에서는 기본적으로 단위 테스트를 이야기하고, 단위 테스트는 독립적인 환경에서 테스트 대상 단위의 행동을 빠르게 검증할 수 있어야 하는데요. 단위 테스트 정의에 따르면 단위를 테스트하기 위해 필요한 레이어는 목을 통해 사용하는 것이 권장됩니다.

물론 타당한 이야기지만, TDD 논쟁에서 DHH는 목의 폐해를 강조했고 켄트 역시 TDD를 하면서 거의 목을 사용하지 않았다고 밝혔습니다. 더 나아가 켄트는 목으로 인해 리팩토링이 어려워질 수 있다는 우려를 표하기도 했죠.

근본적으로 목을 사용하는 이유는 “독립된 환경에서의 빠른 테스트 시도”에 있습니다.

그런데 반드시 목을 사용해야 정답이라는 규칙은 없습니다. 그저 테스트를 구성하는 하나의 도구이죠.

  • 독립된 환경: 여러 객체 간 상호작용으로 일하는 객체 지향 시스템에서 목을 통한 독립된 유닛 테스트는 과연 테스트 대상을 제대로 테스트하는 좋은 테스트일까?
  • 속도: 목을 쓰지 않고 실제 레이어 객체를 사용하더라도 속도가 느리지 않다면 실제 객체 사용이 문제가 될까?

(3) 자동화된 자가 회귀 테스트

또 다른 ‘좋은 테스트’의 특징으로 자동화된 자가 회귀 테스트를 꼽을 수 있습니다. 각 단어의 의미를 하나씩 살펴보겠습니다.

  • 자동화(automated): 너무 당연해서 언급하지 않았지만 매우 중요한 요소입니다. 만약 테스트를 실행하기 위해 별도 작업이 필요하다면 테스트를 실행하는데 부담이 될 수밖에 없습니다.
  • 자가(self-checking): 기능이 스스로 잘 동작하는지 테스트할 수 있어야 합니다. 테스트 결과를 사람이 판단해야 한다면 (당연하지만) 그것 또한 일이 되니까요.
  • 회귀(regression): 기존에 잘 동작하던 기능들이 지금도 잘 동작하는지 확인할 수 있어야 합니다.

자동화된 자가 회귀 테스트가 모든 코드의 안전을 보장할 수는 없습니다. 하지만 숙고 끝에 테스트가 필요하다고 판단한 부분을 테스트가 커버한다면 그 부분에 대해서는 자신감을 가질 수 있죠. 

한 가지 더 덧붙이자면 TDD를 하면서 자연스럽게 자동화된 자가 회귀 테스트가 만들어질 수 있지만, 그게 TDD가 추구하는 목표는 아닙니다. 자동화된 자가 회귀 테스트는 TDD와 관계없이 만들 수 있는데요. 

즉 TDD는 유용한 설계 방법의 하나이며, 무엇보다 중요한 것은 깨지기 쉬운 부분을 판단하고 자동화된 자가 테스트를 정교하게 작성하는 개발자의 능력입니다.

사실 개발자들은 이런 논의가 있기 오래전부터 테스트를 해왔습니다. 로그나 화면, 프린터에 찍힌 값을 눈으로 보고 기댓값과 맞는지 확인했고, 때로는 프로그램을 실행하는 쉘 스크립트를 만들어 해당 프로그램이 원하는 값을 내는지 눈으로 또는 쉘 스크립트 명령어로 확인했죠. 켄트는 TDD와 SUnit을 통해 이를 정형화된 영역으로 가져왔고, 이에 따른 많은 논란도 가져왔습니다. 하지만 논란 속에서도 자동화된 자가 테스트의 효용성엔 아무도 이견이 없었습니다. 뛰어난 개발자라면 이미 오래전부터 어떤 방식으로든 테스트를 해왔기 때문이죠. 


참고

1. 번역서: http://www.yes24.com/Product/Goods/12246033, 원서: Test Driven Development: By Example: Beck, Kent: 8601400403228: Amazon.com: Books

2. Chrysler Comprehensive Compensation System - Wikipedia

3. JUnit Was Born on a Plane! – Tesla Tales (wordpress.com)

4. TDD is dead. Long live testing. (DHH)

5. "TDD는 죽었다" - Rails를 만든 DHH의 글 (sangwook.github.io)

6. TDD 공부 중. Is TDD dead? (junho85.pe.kr)

7. [한글화 프로젝트] TDD는 죽었는가? (tistory.com)

8. "Simple Made Easy" - Rich Hickey (2011) (youtube) 

숙박산업 최신 동향, 숙소 운영 상식 등 숙박업의 유용한 정보들을
이메일로 짧고 간편하게 만나보세요!
위클리온 구독신청
Gunner
Backend Developer

웹서비스, 디지털 광고 분야에서 백엔드 개발 업무를 했습니다. 현재는 온다에서 호텔 관리 솔루션을 개발하고 있습니다.