테스트 얼마나 만들어야 할까?

에자일한 소프트웨어를 만드는 방법의 하나로 TDD에서는 test를 먼저 작성하는 것을 권장? 혹은 강요한다. 스펙에 맞춰 테스트를 하나하나 만들어가고, 그 테스트 케이스들을 초록색으로 만드는 것을 반복해 나가다 보면 에러 없는 코드가 완성된다는 것이다. 이 방법론에 대해 비난할 생각은 없다. 매우 좋은 방법이다. 근데 정말로 언제나 해야 하는 걸까? 부작용은 없을까?

내가 이것들에 대해 고민하게 된 것은 TDD를 처음 배우고 Rails를 사용하고 있던 3년 전 일이다. 그전에 했던 프로젝트가 C++로 서버를 만드는 일이었고 사소한 예외처리 몇 개를 실수해서 몇 일 밤을 새웠던 직후라서 더욱더 테스트에 매달리게 되었다. 다행히도 rails는 유닛테스트에 특화된 framework이었고 (당시에 DB까지 unit test가 가능하도록 지원해주는 framework이 얼마 없었다.), TDD의 원칙에 따라서 많은 테스트와 함께 코드를 작성해 나갔다. 그렇게 가끔 테스트가 없었으면 크게 삽질했을 버그를 잡아가면서, 그 프로젝는 내가 질릴 때까지 별문제 없이 진행되었다.

여기까지만 보면 TDD의 긍정적인 사례에 추가될 수 있을 것 같지만, 실상은 전혀 아니었다. 문제는 두 가지가 있었다. 첫 번째는 테스트 작성에 심취해서 개발에 너무 많은 시간이 들었다는 것이고, 두 번째는 너무 빨리 질렸다는 것이다. 당시에는 아직 TDD에 적응이 안 돼서라고 생각했는데, 정도의 차이가 있었지만 2번째 프로젝트에서도 비슷한 현상이 나타났다. 왜 그렇게 된 것일까?

이것에 대해 고민하다가 RubyOnRails의 제작자인 David Heinemeier Hansson의 글을 보게 되었다. 논쟁이 많이 되었던 글이지만 David가 말하고자 하는 바는 단순하다. 테스트를 작성하는데 들어가는 비용도 기능 구현이나 디버깅에 들어가는 것과 같은 비용이니 overtest하지 않도록 노력해야 한다는 것이다. Unittest라는 도구를 처음 접해 아무 생각 없이 심취해버렸던 나는, test를 작성하는데 열정과 시간이라는 비용을 너무 많이 지불했던 것이다.

아직 계속 발전시켜 나가는 중이지만 요새는 아래와 같은 기준을 정하고 테스트를 작성한다.

  1. 프로토타입을 만들 때는 절대 테스트를 만들지 않는다.
  2. 테스트를 먼저 만드는 것을 강제하지 않는다.
  3. 디버깅은 테스트 작성으로 하는 것을 원칙으로 한다.
  4. 단순히 다른 함수를 호출하기만 하는 래퍼 함수는 테스트를 만들지 않는다.
  5. 단, third party 라이브러리의 래퍼인 경우, 해당 함수가 하는 일이 직관적이지 않은 경우(문서를 찾아봐야 동작이 이해되는 경우)는 만든다.
  6. Refactoring 할 기능에 대한 test는 반드시 만든다.

프로토타입을 만들 때는 절대 테스트를 만들지 않는다.

내 경험으로 봤을 때, 프로토타입은 엉성하게 만들어야 한다. 다시 말하면 엄밀하게 짜인 프로토타입은 비용의 낭비다. 프로토타입을 만들다가 디버깅을 해야 하는 단계까지 갔다면, 그건 이미 프로토타입의 단계를 넘어갔다고 본다. 그런 의미에서 프로토타입을 만드는 중에는 테스트를 만들지 않는다.

테스트를 먼저 만드는 것을 강제하지 않는다.

아마 많은 사람이 동의 못 할 부분이 이 부분이라고 생각한다. 나도 예전에는 이런 주장을 하는 사람을 보고 TDD를 이해 못 해서 그러는 거로 생각했다. 그런데 TDD라는 방법론을 쓰는 이유는 무엇일까를 생각해보자. 옆 사람한테 잘 보이기 위해서일 리는 없다. 테스트를 잘 만들기 위해서라면 주객이 전도된 것이다. TDD라는 방법론을 쓰는 이유는 한 문장으로 말하면 "삽질하는 시간을 줄이고, 코드의 퀄리티를 보장하자." 정도가 될 것이다. 그렇다면 이제 가장 최근에 작업했던 파일을 열어보자. 모든 함수가 테스트가 필요할 만큼 복잡한 코드인가? 정말로 모두 다 그렇게 중요한가? 모든 함수가 잠재적 버그의 위험성을 가지고 있다고 보는가? 내 경험상으로는 아니었고, 테스트 작성에도 80/20 법칙이 적용된다고 본다. 실제로 테스트를 작성하면서까지 엄밀하게 보장해줘야 하는 코드는 20% 정도밖에 안 된다고 본다. 그 외의 부분에도 테스트를 먼저 작성할 것을 강제하면, 결국 뻔한 테스트 코드밖에 안 나온다.

디버깅은 테스트 작성으로 하는 것을 원칙으로 한다.

그렇다면 테스트는 언제 작성되어야 할까? 난 디버깅 중에 작성한다. 상황에 따라서 printf를 찍어가며 디버깅 하는 경우도 있지만, 난 보통 다음과 같은 과정을 따라 디버깅한다.

  1. 해당 증상을 보고 의심되는 함수를 추측한다.
  2. 의심되는 함수들에 대해 테스트 케이스들을 만들어 돌린다.
  3. 실패하는 테스트 케이스가 있으면 성공하도록 바꾼다.
  4. 버그가 없어졌는지 본다.
  5. 안 없어졌으면 1번부터 다시 한다.

이런 과정을 거쳐서 테스트를 만들면, 쓸모 없는 테스트 때문에 코드의 크기를 늘리는 일 없이 테스트를 만들 수 있다. 난 이걸 테스트의 밀도를 높인다고 표현한다.

단순히 다른 함수를 호출하기만 하는 래퍼 함수는 테스트를 만들지 않는다.

그렇다면 절대 테스트를 만들지 말아야 하는 경우도 있을까? 난 특별히 예외처리나 validation이 추가되지 않는 한 래퍼 함수에 대해서는 테스트를 만들지 않는다. 래퍼 함수가 부르는 함수는 이미 테스트 된 코드일 것이다. 래퍼 함수에 대해 다시 테스트를 만드는 것은 테스트의 밀도를 낮춘다고 생각한다.

단, third party 라이브러리의 래퍼인 경우, 해당 함수가 하는 일이 직관적이지 않은 경우(문서를 찾아봐야 동작이 이해되는 경우)는 만든다.

래퍼 함수라도 서드파티 라이브러리의 래퍼 함수인 경우에는 어떻게 해야 할까? 보통 TDD를 하는 사람들은 "서드파티의 무결성은 서드파티 내에서 테스트해서 보장했을 것이니 믿고 써야 한다. (만약 못 믿을 거면 써서는 안 된다.)"라고 주장한다. 하지만 나는 라이브러리가 예외처리를 어떻게 했는지를 대해 스펙 문서를 읽어봐야 했을 때 테스트를 작성한다. 이때 작성하는 테스트는 무결성을 보장하기 위한 테스트와는 목적이 다르다. 문서를 찾아봐야 한다는 것은 함수 이름이 직관적이지 않았다는 것이다, 이런 함수는 다음에 봤을 때, 또 헷갈릴 여지가 있다. 다음번에 문서 찾는 비용을 줄이기 위해서 테스트를 작성해둔다.

Refactoring 할 기능에 대한 test는 반드시 만든다.

그렇다면 반드시 테스트를 만들어야 할 경우는 뭐가 있을까? 많은 사람이 말하듯이 리팩토링 하기 전에도 반드시 테스트를 만들어야 한다. 리팩토링이라는 것 자체가 test가 선행되지 않았다면, 그냥 놔두는 게 좋다고 생각한다. 그렇지 않으면 토발즈가 일부 커널개발자들에게 불평했듯이 "일단 고치고 될 때까지 뜯어고치는" 서부 영화에 나오는 총잡이 같은 개발이 될 것이다.

댓글

이 블로그의 인기 게시물

[C++] enum class - 안전하고 쓰기 쉬운 enum

Log Aggregator 비교 - Scribe, Flume, Fluentd, logstash

RAII는 무엇인가

[Python] cache 데코레이터로 최적화하기

[Web] SpeechSynthesis - TTS API