[책 리뷰] 테스트 주도 개발

[책 리뷰] 테스트 주도 개발

본 포스트에서는 켄트 벡(Kent Beck)의 테스트 주도 개발(Test-Driven Development) 에 대해 리뷰한다.

TL;DR;

  • 3개의 파트로 이루어져 있음
  • 파트1, 2는 각각 서로 다른 예제를 TDD 를 사용하여 바닥부터 개발하면서 TDD 가 어떤식으로 진행되는지 체험 시켜주고, TDD 에서 사용할 수 있는 기술들과 핵심 개념에 대해서 설명해 줌
  • 파트3은 코드 보다는 TDD 와 관련하여 전반적인 개념 설명을 하고 팁을 알려 줌
  • 각 파트를 개인적으로 도움이 된 정도로 비교해보자면 1 >> 3 >> 2
  • 그러므로 시간이 없다면 파트1만 이라도 읽어보길 권하고 싶음(TDD가 뭔지 제대로 느낄 수 있게 해 줌)
  • 파트1은 코드를 따라치는게 중요하다고 생각
    • TDD 에 대한 전반적인 개념을 알게된다고 해도 이 방법론을 실제 개발에 사용하는 것은 다른 이야기
    • 실제로 코드를 작성하며 TDD 장점을 몸소 체득하지 않으면 왜 이것을 사용해야되는지 와 닿지 않을 것이고, 그럼 결과적으로 사용하지 않게 될 가능성이 크기 때문

파트1

파트1은 TDD 를 사용하여 다중 통화 시스템을 구현한다. 여기서 TDD 의 가장 핵심적인 내용을 거의 다 다룬다.

다음은 기억이 나는 내용들에 대해 정리해봤다. 내가 가장 중요하다고 생각이 되는 부분을 생각나는 대로 나열했기 때문에 조금 두서가 없을 수도 있다.

일반적인 소프트웨어 개발에서는 보통 먼저 전체적으로 윤곽을 설계하고나서 세부적인 구현을 하는 탑다운 방식을 사용하게 된다. 하지만 TDD 는 일반적으로 이와 반대로[1] 전체적인 설계를 전혀 하지 않은 채, 그때 그때 필요한 기능들을 할일 목록에 적어가면서 지금 당장 필요한 구체적인 구현을 한다. 그럼에도 불구하고 우리의 소프트웨어는 저절로 좋은 설계를 향해 가는데, 그 이유는 TDD 의 개발 사이클 내에서 중간 중간 중복을 없애는 리팩토링의 과정을 거치기 때문이다. 참고로, 저자는 파트1의 예제를 TDD 로 여러번 개발해 봤지만 최종 결과물이 매번 다른 설계를 갖게 되었다고 한다. 즉, 구현 이전에 설계가 선행되지 않기 때문에 최종적으로 어떤 설계가 나올지 알 수가 없는 것이다.

소프트웨어 개발은 전통적으로 건물을 짓는 행위에 비유되곤 한다. 하지만 실제로는 건물을 짓는 행위와는 엄연히 다르다. 건물은 처음 설계된대로 지어지고 나면 잘 바뀌지 않는다. 그렇기 때문에 처음 설계를 하고, 그대로 지으면 웬만해선 그냥 끝이다. 하지만 소프트웨어는 성격이 조금 다르다. 예를 들어, 개발 도중이나 이후에 요구사항이 바뀌는 경우가 많으며, 개발이 완료된 이후에 발견되는 버그들이 많다. 즉, 처음에 설계한대로 깔끔하게 개발이되고 끝나는 경우는 드물다. 그렇기 때문에 건물을 짓는 것보다는 정원을 가꾸는 것에 비유하는것이 좀 더 정확하다.

TDD 로 개발을 할 때는 테스트를 작성하기 전에 절대로 기능 코드를 작성하지 않으며, 이미 널리 알려진 것처럼 Red-Green-Refactor 이렇게 3가지 단계로 이루어진 사이클을 반복하게 된다. 여기서 중요한 점 몇가지에 대해 말해보겠다. 우선, Red 에서 Green 으로 갈 때는 가능하면 가장 빨리 가야한다. 우선 컴파일조차 되지 않는 상황이 있을 것이다. 예를 들어, 특정 타입의 객체를 반환하는 특정 이름의 메서드가 존재하지 않는다는 컴파일 에러가 발생하면 해당 이름을 가진 null 을 반환하는 메서드를 생성하여 일단 컴파일이 되게 한다. 이제는 컴파일은 되지만 NullPointerException 이 발생할 것이다. 이번엔 테스트에서 원하는 바로 그 객체를 반환하도록 변경한다. 이것을 가짜 구현이라고 한다. 가짜 구현을 통해 가능한한 빨리 Green(초록 막대라고도 부른다)을 본다. 이것이 핵심이다. 일단 초록 막대를 보고 난 후에 리팩토링 단계에서 명확한 코드를 작성하거나 삼각측량법 등을 사용하여 가짜 구현을 일반화한다. 만약 확신이 있다면 즉시 명확한 코드를 원하는 만큼 작성해도 된다. 하지만 그렇게 했더니 테스트가 실패한다면? 다시 작성한 코드를 지우고 전 단계로 돌아가 더 적은 기능을 구현해 보거나 삼각측량법을 사용하던 한다. 만약 중간 중간에 새로 개발해야하거나 수정해야하는 부분이 생각나면 일단 할 일 목록(To-Do List)에 적어놓기만 한다.
이러한 TDD 의 개발 방식은 다음과같은 장점이 있다.

  • 개발자가 자유자재로 개발의 보폭을 조절할 수 있다.
  • 현재 프로젝트 내에서 자신이 위치한 곳을 정확히 인지할 수 있다.
  • 매 단계 마다 지금 해야할 일을 정확히 알고 그것에만 온전히 집중할 수 있다.
  • 지금까지 작성한 코드에 대한 확신을 가지게 된다.
  • 그렇기 때문에 오히려 전체적인 개발 속도를 향상시키는 결과를 가져온다.
  • TDD 의 라이프사이클 특성상 개발 도중 반복적으로 리팩토링을 수행하기 때문에 개발 도중 어떠한 순간에도 코드의 품질은 좋은 상태로 유지된다.
  • 내가 다음에 구현할 기능에 대한 테스트만 작성하며, 테스트를 통과하기 위한 만큼의 코드만 작성하기 때문에 오버 엔지니어링의 가능성이 줄어들게 된다.
    • (파트3 의 내용) 꼭 필요한 기능만 구현하기 때문에 지금 당장은 미래에 발생할지도 모르는 변화에 대해 유연한 코드는 아닐 수 있다. 이를테면 일시적으로 OCP 를 위반하게 될 수도 있으나, 이미 작성해 둔 많은 테스트가 서포트 해주기 때문에 미래에 변화가 발생하는 그 시점에 코드를 변경하더라도 비용이 그리 크지 않게 된다.

파트2

  • 파트1에서와 비슷하게 TDD 를 사용하여 또다른 앱인 xUnit 을 직접 작성하는 예제를 담았다.
  • 특별히 기억에 남는 내용은 없고, 파트1에서 배운 TDD 개발 과정을 다른 예제와 언어를 통해 다시 한 번 연습하기위한 파트라고 생각이 든다.
  • 이 파트에서는 스스로 뇌 수술을 하는 것에 비유하기도 하는, 자기참조 프로그래밍이라는 것을 하는데(테스트 케이스를 작성하기 위해 사용할 프레임워크를 테스트하기 위한 테스트 케이스를 작성해야 하기 때문..) 굉장히 신기한 작업이라 중간 중간 이해를 위해 생각해야하는 시간이 조금 있었다.
  • 여담으로, 저자는 새로운 언어를 학습할 때 TDD 로 xUnit 을 작성해본다고 한다. xUnit 을 작성하기 위해 사용되는 여러 문법과 함수 등을 익히다보면 그 언어의 핵심 기능들을 대부분 익힐 수 있게 되기 때문이라고.

파트3

실무에서 유용하게 사용할 수 있는 테스트의 종류들, 실제 개발을 할 때 마주칠 수 있는 다양한 경우에 대한 저자의 견해, 여러 디자인 패턴들을 TDD 에서 어떤 식으로 활용할 수 있는지 등에 대한 이야기를 한다. 파트3의 마지막에는 자문자답을 통해 TDD 에 대해 깊이 알아본다. 다음은 내가 가장 기억에 남는 내용이다.

  • 훌륭한 엔지니어링이 프로젝트의 성공의 필수적인 부분이 아니기 때문에 적당한 수준의 엔지니어링 만으로도 프로젝트를 성공적으로 이끌 수도 있다는 점에서 TDD 는 오버액션이라고 볼 수도 있다. 그렇기 때문에 TDD 가 항상 가장 좋은 방법은 아니다. 업계에서 통용하는 수준보다 훨씬 더 적은 결함과 훨씬 더 깨끗한 설계의 코드를 작성하게 해주기 때문이다.
  • 보통 프로젝트가 진행됨에 따라 코드의 품질은 떨어지게 되고, 다른 프로젝트를 하고싶어 지는데, TDD 는 프로젝트를 처음 시작할 때의 흥미와 설렘을 유지할 수 있게 해준다.

개인적으로 파트1만 읽고 실제 개발에 TDD 를 적용해보면서 들었던 고민이 일부 해소가 되기도 했고, 읽어도 잘 이해가 잘 가지 않은 부분도 있었던 파트였다.

마지막 쯤에 있는 마틴 파울러의 추천사가 TDD 의 장점을 굉장히 잘 정리한것 같다. TDD 는 여러가지 고민을 한 번에 하는 것이 아니라 한 번에 한 가지 기능에만 집중할 수 있게 해준다. 그리고 그 기능에 있어서도, 기능을 구현하거나 리팩토링을 하거나, 둘 중 하나에만 집중할수 있게 해준다.

느낀점

실무에서 TDD 로 코드를 작성해보니 잦은 리팩토링과 테스트 코드의 실행을 통해 나중에 꽤나 힘겹게 발견했을 구현 실수를 조기에 발견할 수 있게되는 경우가 꽤 많았다. 그래서 테스트를 작성하는데 시간이 든 만큼 앞선 경우로 인해 절감된 시간도 있기 때문에 단기적인 개발 시간만 보더라도 그렇게 손해보는 장사는 아니라고 생각했다. 게다가 사실 테스트는 TDD 와는 별개로 당연히 작성해야되는 것이기 때문에 대부분의 경우 TDD 로 개발하는 것이 이득이라고 볼 수도 있을것 같다.
또한, 어떻게보면 내가 추가하고자 하는 모든 기능에 대해 강제로 테스트를 작성하게 되기 때문에 확실히 나의 코드에 자신감을 가지게 된다. 그렇기 때문에 리팩토링을 하는데 부담이 별로 없게 되어 리팩토링을 자주하게 되고, 자연스레 코드 품질은 올라간다.
일부 빨간색(혹은 노란색)인 테스트 결과를 모두 초록색으로 바꾸는 단순한 행위 자체가 더욱 성취감을 줘서 프로그래밍을 더 재미있게 만들어주기도 한다.

아쉬운 점도 있었다. 개인적으로는 예제와 실무 코드 사이에 약간의 괴리가 있다고 느꼈다. 책에서 나오는 예제들은 주로 굉장히 단순한 도메인 룰을 작성하는 경우가 대부분이다. 하지만, 실무에서 스프링 프레임워크를 사용하여 개발을 하다보면 하나의 Service 클래스가 여러 서비스와 리포지토리, mapper 등의 의존성을 주입받는 경우가 많은데, mocking 을 해야하는 의존성이 많은 경우 AAA(Arrange, Action, Assert)의 Arrange 와 Assert 에 해당하는 부분에서 해야할 일이 많아진다. 도메인 엔티티가 가진 필드의 수나 메서드의 인자의 수가 많으면 더미 데이터를 선언하는 부분이 장황해 지기도 한다. 책에 나오는 모든 예제는, 물론 설명을 위해 단순화한 형태의 코드를 사용한 것이겠지만서도, 테스트 코드가 굉장히 짧기 때문에 하나의 기능에 대해 여러개의 테스트 코드를 작성하는것에 대해 아무런 부담감이 들지 않는다. 하지만 의존성이 많아지면 때때로 하나의 기능에 대해 여러개의 테스트 코드를 작성하는 것을 주저하게 되는 자신을 발견했다. 예를 들어 삼각측량법을 통해 중복을 제거하고 일반화를 하기 위해서는 사실상 거의 같은 코드를 한 번 더 작성해야 되는데 하나의 테스트가 너무 길다보니 주저하게 되는 것이다. 그래서 개인적으로 삼각측량법은 자연스레 안쓰게 되었다. 물론 어느정도의 확신이 있다면 굳이 굉장히 짧은 호흡인 삼각측량법같은 방법을 사용하지 않아도 된다. 하지만 빠르고 간단하게 사용할 수 있지만 불필요해서 사용하지 않는 것과, 필요성이 모호한 상황에서 약간의 귀찮음이 더해져 사용하지 않는건 엄연히 다르다. 그리고 테스트 코드에서 발생하는 중복도 리팩토링을 통해 적절히 줄일 수가 있다. 하지만 나는 하나의 테스트 클래스 내 존재하는 여러 테스트들이 가진 중복을 private 메서드로 분리하여 호출하는 것을 웬만해선 하고싶지 않아하는 편이다. 왜냐하면 테스트 코드는 위에서 아래로 쭉 읽기만 해도 스토리가 자연스럽게 읽히도록 작성하는것이 베스트라고 생각한다. 하지만 메서드를 분리하게 되면 이것에 방해가 되어 다소 가독성이 떨어지기 때문이다. 중복이 많아지면 어쩔 수 없이 메서드를 분리하는 편이긴 하다.

TDD 로 코드를 구현하면 IDE 의 자동완성 기능을 조금은 포기해야 한다는 것을 느끼기도 했다. 아직 존재하지 않는 클래스, 메서드, 변수 등을 사용하는 코드를 작성하기 때문이다. 물론, 존재하지 않는 클래스, 메서드, 변수를 사용하는 코드를 먼저 작성하고나서 IDE 가 제공하는 자동생성 기능을 활용할 수도 있다. 하지만 존재하지 않는 변수 등을 테스트 내에서 여러번 사용해야 하는 경우에는 매번 직접 타이핑하거나 복붙을 하기 위해 마우스를 사용하든, 커서를 위로 올렸다 내려야 해야하는 귀찮음이 있기 때문에 결국 일반적인 개발 순서와 반대로 개발하는 것에 대한 아주 약간의 불편함이 존재한다고 느꼈다. 새로운 방식이라 적응을 위해 좀 더 시간이 필요할 것 같기도 하다.

물론 위와 같은 이유로 실무에 적용하기 어렵다고 느낀다기 보다는, 책에서 언급하지 않은 특정 상황에서는 어떻게 적용하는 것이 맞는지에 대한 일부 물음이 해소가 되지 않았다는 표현이 정확할 것같다. 단순히 내가 아직 TDD 에 익숙하지 않아서 그런 것일 수도 있다. 그래서 앞으로 TDD 의 강력함을 실무에서 좀 더 느껴보아야 할 것 같다. 다른 사람들의 TDD 에 대한 경험은 어땠는지 알아보며 실무의 다양한 상황에서의 구체적인 적용방법에 대한 사례도 들어 보아야 할 것 같다.


  1. TDD 에서 일반적으로는 전체 시스템의 작은 조각을 나타내는 테스트부터 조금씩 붙여나가는 바텀업 빙식을 사용하겠지만, 사실 탑다운과 바텀업을 모두 사용할 수 있기 때문에 켄트 백은 TDD 의 개발 방향이 단순히 수직적인 메타포 보다는 '아는 것에서 모르는 것으로'(known-to-unknown)라는 표현이 정확할 것이라고 말하고 있긴 하다. 왜냐하면 TDD 에서는 확실히 아는 것부터 구현을 하게 되고 그러다 보면 계속 새로운 것을 알게되면서 할 일 목록에 적어나가기 때문이다. ↩︎

Comments