안녕하세요, ONDA에서 호텔 운영관리 솔루션을 개발하고 있는 백엔드 개발자 Gunner(거너, 정재훈)입니다.
지난 1편에서 소프트웨어 테스트의 종류에 대해 다양한 관점으로 살펴봤습니다. BDD와 TDD 같은 테스트 도구에 대해서도 다뤄봤는데요.
간혹 TDD와 테스트를 혼용하여 그 의미가 불분명해지는 경우가 있습니다. TDD의 개념과 개발자 사이에 일어났던 논쟁의 쟁점을 이해하고, 개발자에게 좋은 소프트웨어 테스트는 무엇일지 차근차근 다뤄보겠습니다.
먼저 TDD에 대한 개념을 알아보겠습니다. TDD는 켄트 벡(Kent Beck)의 ⌜Test-Driven Development By Example⌟ 책1 (이하 TDDBE)에 소개된 소프트웨어 설계 기법으로 테스트를 통해 개발을 주도해가는 방법입니다.
*TDD를 테스트 방법으로 보는 경우도 많지만, 개인적으로는 설계 방법에 더 가깝다고 생각합니다.
TDDBE에서 주장하는 TDD 방법은 간단해요.
TDDBE 책에서는 Money 클래스를 개발하면서 통화 단위 등을 구현해 나가는데요. 예를 들어 달러($) 기준으로 개발하여 테스트가 성공하지만, 다른 통화 단위를 적용했을 때 실패하며 이를 이어 나가기 위한 클래스를 만들어 가는 과정을 보여준 것이지요.
실패하는 테스트를 먼저 작성하면서 Red-Green-Refactor 사이클을 보여주고, Test-First로 만들어진 자동화된 테스트들은 Money 클래스의 설계 방향을 주도(drive)할 뿐 아니라 새로운 기능이 추가될 때 기존 기능이 잘 동작하는지 체크하는 안전망 역할도 합니다.
또 하나 주목할 점은 TDDBE에서 켄트는 아주 작은 스텝을 밟아나간다는 것인데요. 후에 켄트는 한 인터뷰를 통해 ‘반드시 작은 스텝을 밟으라는 뜻은 아니며, 좋은 설계 아이디어가 떠오르지 않을 때 작은 스텝을 밟아 나가다가 확신이 생기면 큰 스텝을 밟을 수 있도록 감각을 기르게 하는 것이 목적’이라고 밝혔습니다.
결론적으로 TDDBE에서 말하는 TDD의 핵심은 Red-Green-Refactor 사이클이며, 이를 통해 좋은 설계와 자동화된 테스트를 만들어 나가는 것이죠.
TDD가 등장한 뒤 개발자들 사이에서는 많은 논쟁이 오고 갔습니다. 주로 ‘TDD를 이용하여 개발해야 한다’와 ‘TDD는 실무에 맞지 않는다’는 의견이 대부분이었는데요.
‘TDD를 이용하여 개발해야 한다’는 입장을 보인 개발자들은 TDD를 통해 발생하는 유용한 점, 즉 Test-First를 통해 진행되는 좋은 설계, 개발 진행과 함께 만들어지는 자동화된 테스트 등을 주장했지만, ‘TDD는 실무에 맞지 않는다’는 입장을 보인 개발자들은 Test-First로 디자인(설계)하는 방법에 공감하지 않는다고 주장했죠.
자세한 이야기를 하기에 앞서 켄트가 TDD를 주장하게 된 배경에 대해 잠깐 알아볼까요?
켄트가 주로 사용했던 언어는 ‘Smalltalk’이고, 실제로 이 언어를 이용하여 유명한 프로젝트인 C32 를 진행하면서 XP(eXtreme Programming)도 적용했죠. Smalltalk은 VM(Virtual Machine, 가상 머신)에서 실행되는 언어로서 개발 환경 자체를 하나의 IDE(Integrated Development Environment, 통합 개발 환경)로 볼 수 있으며, 소스 코드가 하나의 통합 이미지로 저장되는 것이 특징입니다.
Smalltalk는 객체 지향적으로 만들어진 언어로, 아래와 같이 TDD를 적용하기에 좋은 환경과 요건을 갖추고 있었습니다.
허나 보통의 Java Spring 프로젝트는 그렇지 않죠.
Smalltalk는 Java처럼 '파일 - 클래스' 순으로 찾을 수 없고, 객체 간 상호 작용이 많아 이를 자동화된 테스트로 확인하는 과정이 필요했다고 볼 수 있는데요. 또 다르게 말하면 Smalltalk는 단위 테스트를 빠르게 실행하고 즉각적인 피드백을 통해 개발하기 매우 좋은 환경이기도 했던 것이죠.
2000년대 초반 TDD 개념이 등장하면서 개발자들 사이에서 많은 논의가 있었고, 앞서 말한 것과 같이 이를 적극적으로 수용하는 그룹과 그렇지 않은 그룹으로 나뉘었습니다. 그러던 2014년, Ruby on Rails(*풀 스택 웹 프레임워크, 이후 등장한 거의 모든 풀 스택 웹 프레임워크에 영향을 미쳤다)를 만든 DHH(David Heinemeier Hansson)가 자신의 블로그에 ‘TDD는 죽었다. 테스트여 영원하라’4란 도발적 제목의 글을 올리며, TDD 논쟁이 시작되었어요.
DHH가 주장한 주요 내용은 다음과 같습니다.5
◦ 레이어간 중계 객체들이 얽힌 복잡한 구조
◦ 테스트에 DB나 I/O를 직접 사용하는 것을 피함 → 시스템 테스트는 나쁜 것으로 치부
◦ 서비스 객체와 커맨드 패턴, 그리고 더 나쁜 것들이 얽힌 정글이다. → 오로지 테스트만을 위한 패턴 남용과 객체 설계
◦ Rails에서는 좋은 테스트로 보지 않는다.
◦ 나는 ActiveRecord 모델을 직접 테스트한다. fixture를 만들고 DB에 직접 접근한다.
◦ 탑 레이어에는 컨트롤러 테스트가 있지만, 나의 경우 Capybara 같은 도구를 사용해서 높은 수준의 시스템 테스트로 대체하는 것을 선호한다.
해당 글은 당시 TDD를 지지하는 그룹의 반감을 일으키며 온라인상에서 큰 논란으로 번져갔는데요.
한동안 논란이 이어지다 결국 마틴 파울러(*「Refactoring」으로 유명한 소프트웨어 개발자)의 주도로 켄트 벡과 DHH의 온라인 토론이 열렸고, 당시 토론에서 그들이 나눈 내용을 요약하면 다음과 같습니다. 6 7
TDD가 말하는 단위 테스트의 정의
모든 코드에 TDD를 하는 것이 옳은가? 테스트가 너무 많거나 불필요한 테스트는 아닐까?
TDD는 어떤 점에서 유용할까?
결과적으로 TDD는 좋은 툴이지만 어느 정도 취향의 문제이며, 모든 분야에 적합한 툴이 아닌 것으로 결론지어졌습니다.
💡TDD가 취향의 문제란 점에 관해 Clojure를 만든 리치 히키(Rich Hickey)의 발언도 함께 살펴볼까요?
리치는 이를 ‘가드레일 프로그래밍’이라 부르며 “테스트가 있기 때문에 수정을 할 수 있다고 하는데, 누가 그렇게 해요? 누가 차를 가드레일에 부딪혀 가면서 운전하나요?”라며 TDD의 Red-Green-Refactor 사이클을 비판했습니다. 그는 TDD 자체를 비판하는 것이 아닌, 프로그램을 논리적으로 분석할 수 있는 능력이 중요하다(숙고에 의한 분석이 프로그램 설계에 적용되어야 한다)는 주장을 펼쳤는데요.
즉, TDD로 개발하거나 테스트가 모두 통과했다는 이유로 좋은 프로그램은 아니라는 뜻이죠. 이는 TDD보다 앞서 이야기한 타입 체커를 비꼬는 부분에서도 알 수 있습니다.8
리치 역시 자동화된 자가 테스트의 필요성은 인정하면서도, (해당 발언 이후로 TDD 지지자들의 공격이 이어지자) ‘만약 자신이 사용하는 툴이나 방법론이 특정 목적에 맞지 않는다고 비판받는 것이 공격처럼 느껴진다면 릴렉스할 필요가 있다'며 방법론은 도구일 뿐이라고 말하기도 했습니다.
이전처럼 활발하지는 않지만, 여전히 TDD 논쟁이 계속되는 이유는 무엇일까요?
논쟁을 자세히 들여다보면, TDD에서 비롯된 이익이 아님에도 TDD를 하면서 발생하는 이익으로 간주하는 경우가 많습니다. 예를 들면 다음과 같이 말이죠.
TDD는 Red-Green-Refactor 사이클로 진행되는 설계 방법이라 분명 효용이 있지만 누구에게나, 혹은 모든 프로젝트에 맞지 않을 수도 있습니다. 또한, 논쟁에서 켄트가 이야기했듯이 목은 TDD와 관계가 없죠. 따라서, 본질적으로 설계 기법인 ‘TDD’의 효용과 ‘테스트’의 효용을 명확히 분리할 필요가 있습니다.
좋은 테스트가 무엇일지 고민하기에 앞서 테스트도 트레이드 오프라는 사실을 전제로 해야 합니다. 테스트를 만들고 유지 보수하는 작업에 개발자의 리소스가 들어가기 때문이죠.
또 테스트는 분명 개발에 도움을 주지만 마틴 파울러가 대담에서 얘기했듯 부족함과 지나침이 존재합니다. 그렇다면 개발자에게 있어 좋은 테스트는 무엇일까요?
좋은 테스트는 깨지기 쉬운, 즉 잘못되기 쉬운 부분을 테스트하는 코드입니다.
깨지기 쉬운 코드
위와 같은 코드를 판단하는 건 단위 테스트를 작성하는 것만으로는 알기 어려운 측면이 있습니다. 이때 BDD를 활용하면 좋은데요. Given/When/Then 컨텍스트에서 테스트 케이스를 생각하면 자연스럽게 해당 객체가 다른 객체와 어떻게 연관되는지 알 수 있습니다.
반대로, 어떤 영역은 믿을 수 있는 부분이라 판단하고 테스트하지 않기로 결정하는 것도 중요합니다.
깨지기 어려운 코드
다만 테스트하지 않을 코드를 선택하는 데 있어 절대적인 기준은 없습니다. 예를 들어 라이브러리 코드는 일반적으로 테스트하지 않는 개발팀이 있다고 가정해 볼까요? 특정 라이브러리가 자주 업데이트되면서 인터페이스나 동작이 자주 변경된다면 해당 라이브러리를 사용하는 코드에는 테스트를 붙여주는 것이 좋습니다.
즉 테스트하지 않을 코드로 판단하는 건 개발자가 처한 상황에 따라 충분히 고려하여 판단해야 하는 거죠.
테스트 관련 논쟁을 살펴보면 유난히 목에 대한 언급이 많습니다.
TDD 같은 방법론에서는 기본적으로 단위 테스트를 이야기하고, 단위 테스트는 독립적인 환경에서 테스트 대상 단위의 행동을 빠르게 검증할 수 있어야 하는데요. 단위 테스트 정의에 따르면 단위를 테스트하기 위해 필요한 레이어는 목을 통해 사용하는 것이 권장됩니다.
물론 타당한 이야기지만, TDD 논쟁에서 DHH는 목의 폐해를 강조했고 켄트 역시 TDD를 하면서 거의 목을 사용하지 않았다고 밝혔습니다. 더 나아가 켄트는 목으로 인해 리팩토링이 어려워질 수 있다는 우려를 표하기도 했죠.
근본적으로 목을 사용하는 이유는 “독립된 환경에서의 빠른 테스트 시도”에 있습니다.
그런데 반드시 목을 사용해야 정답이라는 규칙은 없습니다. 그저 테스트를 구성하는 하나의 도구이죠.
또 다른 ‘좋은 테스트’의 특징으로 자동화된 자가 회귀 테스트를 꼽을 수 있습니다. 각 단어의 의미를 하나씩 살펴보겠습니다.
자동화된 자가 회귀 테스트가 모든 코드의 안전을 보장할 수는 없습니다. 하지만 숙고 끝에 테스트가 필요하다고 판단한 부분을 테스트가 커버한다면 그 부분에 대해서는 자신감을 가질 수 있죠.
한 가지 더 덧붙이자면 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)