박종훈 기술블로그

테스트 주도 개발 (TDD) 사용법

원문 : Test-Driven Development
James Shored의 Art of Agile Development의 TDD장

마틴 파울러의 블로그 글에 Kent Beck의 Test-Driven Development 책과 함께 소개되어 있는 글이다.

Kent Beck의 Test-Driven Development 도 조만간 읽어나가기 시작할 예정이지만 이 글도 소개되어 있어 번역하며 읽어 나가고자 한다.

원문은 TDD를 적용하고 싶은 사람들을 위해 어떤 방식으로 작업을 해나가야 하는지에 대한 방법과 예시를 제공해준다.

이 글에서는 방법에 대해서 다루고 다음 글(예시로 알아보는 테스트 주도 개발 (TDD) 사용법)에서 구체적인 예시에 대해서 다룬다.


테스트 주도 개발

우리는 작고 검증 가능한 단계들을 통해 잘 설계되고 잘 테스트되고 잘 구성된 코드를 생성합니다.

“프로그래밍 언어에 필요한 명령어는 ‘DWIM (do what I mean)’이라는 농담도 있습니다, ‘내가 작성 코드 대로가 아니라 내가 의도한대로 동작 해줘’ 라는 뜻입니다.”

프로그래밍을 하는 것은 까다롭습니다. 꾸준하게 완벽함을 유지해야 합니다. 몇달이든 몇년이든 끝없이 노력해야 합니다. 실수를 하면 적어도 컴파일 에러를 일으킵니다. 최악의 경우에는 알지 못하고 있다가 어느 순간 큰 문제를 일으키는 버그가 되기도 합니다.

사람은 완벽하지 않습니다. 따라서 소프트웨어에 버그가 있는 것은 놀라운것이 아닙니다.

프로그램을 작성하는 중에 실수를 저질렀을 때 알려줄 수 있는 도구가 있다면 좋지 않을까요? 그런 도구가 있다면 디버깅 시간을 거의 없애줄 텐데 말이죠.

사실 그러한 도구 (기술) 가 있습니다. 바로 테스트 주도 개발 입니다. 실제로 이러한 결과(실수를 알려주고 디버깅 시간을 줄여주는)를 제공합니다.

테스트 주도 개발( TDD )는 테스트, 코딩, 리팩터링의 빠른 주기로 이뤄집니다. 기능을 추가할 때, 이러한 주기를 수십번 수행하여, 더이상 추가하거나 뺄 것이 없을때까지 소프트웨어를 구현하고 개선하는 것을 반복합니다. 연구에 따르면 TDD는 결함 발생률을 줄여줍니다. (janzen & Saiedian) 적절하게 사용하면 디자인을 개선하고 공개 인터페이스를 문서화하며 향후 실수를 방지하는 데에도 도움이 됩니다.

물론, TDD가 만능은 아닙니다. (세상에 만능인 것이 있을까요?) TDD는 레거시 코드베이스에서 사용하기 어렵습니다. 그린필드 시스템을 사용하더라도 학습 곡선을 극복하려면 몇 달을 꾸준히 사용해야 합니다. 어쨌든 시도해 보세요. TDD는 다른 XP방식의 이점을 누리지만 반드시 필요한 것은 아닙니다. TDD는 거의 모든 프로젝트에서 사용할 수 있습니다.

TDD가 효과적인 이유

펀치카드 시대에 프로그래머들은 코드가 컴파일되는지 확인하기 위해 힘들게 직접 코드를 검사했습니다. 컴파일 오류이 발생되면 배치 작업(batch job)이 실패하게 되고 그걸 해결하기 위해 열심히 디버깅을 해야합니다.

이제 코드 컴파일을 하는 것은 큰 문제가 아닙니다. 대부분의 IDE는 사용자가 입력하는 것에 따라 구문을 확인하고, 저장할 때마다 컴파일을 하기도 합니다. 피드백 순환(loop)가 빠르기 때문에 오류를 쉽게 찾고 수정할 수 있습니다. 컴파일이 되지 않았다면 컴파일 할만한 최소한의 코드가 없기 때문입니다.

테스트 주도 개발은 동일한 원칙을 프로그래머의 의도(intent)에 적용합니다. 최신 컴파일러가 코드 구문에 대해 더 많은 피드백을 제공하는 것처럼. TDD는 코드 실행에 대한 피드백을 강화합니다. 몇 분마다(20~30 초 마다) TDD는 코드가 사용자가 생각하는 대로 작동하는지 확인합니다. 문제가 발생하면 코드 몇 줄만 확인하면 됩니다. 실수를 쉽게 발견하고 수정하기 쉽도록합니다.

TDD는 복식 부기와 비슷한 접근 방식을 사용합니다. 동일한 아이디어를 다른 방식으로 두 번 명시하고 전달합니다. 처음에는 테스트 코드를, 다음에는 프로덕션 코드를 사용합니다. 만약 두가지가 일치한다면 올바르게 코딩이 되었을 가능성이 높습니다. 그렇지 않다면 어딘가 실수가 있는 것입니다.

[Note]
이론적으로는 테스트와 코드가 모두 똑같은 방식으로 잘못되어 모든 것이 괜찮은 것처럼 보일 수 있습니다. 하지만 실제로 테스트 코드와 프로덕션 코드 사이에 잘라내어 붙여넣지 않는 한 이러한 일은 매우 드물기 때문에 걱정할 필요는 없습니다.

TDD에서 테스트는 클래스의 공개 인터페이스 관점에서 작성됩니다. 세부 구현이 아닌 클래스의 동작에 중점을 둡니다. 프로그래머는 프로덕션 코드를 작성하기 전에 테스트를 작성합니다. 이는 구현하기 쉬운 인터페이스보다는 사용하기 쉬운 인터페이스를 만드는데 집중하도록 하여 인터페이스 설계를 개선합니다.

TDD가 완료된 후에도 테스트는 남아 있습니다. 코드와 함께 남아 코드에 대한 살아있는 문서 역할을 해줍니다. 더 중요한 부분은 프로그래머가 (거의) 모든 빌드에서 모든 테스트를 실행하면서, 기존에 작성한 코드가 여전히 원래 의도한 대로 동작하는지 확인해준다는 것입니다. 누군가 실수로 코드 동작을 변경했다면 (예: 잘못된 리팩터링으로 인해) 테스트는 실패하고 실수를 알아차리게 합니다.

TDD를 사용하는 방법

당장 오늘부터라도 TDD를 사용할 수 있습니다. 배우는 것은 금방이지만 마스터 하기 위해서는 평생 노력해야 합니다.

[Note]
TDD의 기본 단계는 배우기 쉽지만 사고방식을 익히는 데는 시간이 걸립니다. 그렇게 될 때 까지 TDD는 서투르고 느리고 어색해 보입니다. 익숙해지기 위해 2~3개월 동안 TDD를 사용해보세요.

TDD는 마치 빠르게 도는 작은 모터같습니다. 계속해서 반복되는 매우 짧은 주기로 작동합니다. 이 주기를 거칠수록, 코드가 한 단계씩 발전됩니다. 완성에는 아직 이르지 못한 상태더라도 코드는 테스트, 설계, 코딩을 거쳐 반영될 준비(ready to check in)가 된 코드를 제공합니다.

TDD는 지속적으로 테스트, 설계, 코딩되어 입증된 코드를 제공합니다.

TDD를 사용하려면 아래 그림의 “red, green, refactor” 주기(사이클, cycle)을 따라야 합니다. red-green-refactor-cycle

경험상 많은 리팩터링이 필요하지 않은 경우 한 주기는 5분 미만이 소요됩니다. 작업이 완료될 때까지 주기를 반복합니다. 모든 테스트가 통과했을 때 진행하던 사이클을 멈추고 통합합니다. 이 사이클이 몇 분마다 수행되어야 합니다.

1 단계 : 생각하기

TDD는 작은 테스트를 사용하여 코드를 작성하도록 강제합니다. 테스트를 통과할 수 있을 만큼만 코드를 작성하면 됩니다. XP에서는 “실패한 테스트가 없으면 프로덕션 코드를 작성하지 말라” 고 이야기 합니다.

“실패한 테스트가 없으면 프로덕션 코드를 작성하지 말라”
실제 코드를 작성하기 전에 테스트 케이스를 작성하면 최초에는 실패하게 됨.
테스트 코드를 먼저 작성하라는 뜻.

따라서 첫 번째 단계에서는 상당히 이상한 사고 과정을 거칩니다.

  1. 코드에 어떤 동작을 필요한지 상상한 다음
  2. 5줄 미만의 코드로 작성할 수 있는 작은 부분(increment)으로 나눠보세요.
  3. 다음으로 이 동작이 없다면 실패할 (마찬가지로 적은 줄의 코드로 작성할 수 있는) 테스트를 생각해보세요.

즉, 프로덕션 코드에 몇 줄을 추가하도록 강제하는 테스트를 생각해보세요.

이게 TDD의 어려운 부분입니다. 테스트가 코드를 작성하도록 주도하는 것이 반대로 하는 것처럼 느껴집니다.
그리고 작은 부분으로 나누는 것은 쉽지 않을 수 있습니다.

페어 프로그래밍이 도움이 됩니다. 드라이버(driver)가 현재 테스트를 통과하려고 시도하는 동안 내비게이터(navigator)는 다음으로 할 테스트를 생각하면서 몇 단계 앞서 있어야 합니다.

2 단계: 빨간 막대 (Red Bar)

이제 테스트를 작성해봅시다. 현재 부분(increment)에 해당하는 테스트만 작성하면 됩니다. (일반적으로 코드 5줄 미만) 시간이 많이 걸리더라도 괜찮습니다. 다음 번에는 더 작은 단위로 시도 해보세요.

클래스의 내부 구현 방식이 아닌, 클래스의 동작과 공개 인터페이스 에 집중해서 코딩해보세요. 캡슐화 원칙을 지키세요. 이 말은 처음 진행하는 몇 테스트들에서는 아직 존재하지 않는 메서드 및 클래스 이름을 사용하도록 테스트를 작성하는 것을 의미합니다. 일부로 그렇게 하는 것입니다. 이렇게 하면 클래스의 구현자가 아닌, 클래스의 사용자 관점에서 인터페이스를 설계할 수 있습니다.

테스트 코드를 작성한 후 전체 테스트 모음을 실행하고 새 테스트가 실패하는지 확인하세요. 대부분의 테스트 도구에서 빨간색으로 실패했다고 나올 것입니다.

이 과정을 통해 실제로 일어나는 일과 의도를 비교할 수 있는 첫 번째 기회입니다. 테스트가 실패하지 않거나 예상과 다른 방식으로 실패하면 뭔가 잘못된 것입니다. 테스트가 깨졌을 수도 있고, 테스트햇다고 생각했던 것을 테스트하지 못한 것일수도 있습니다. 문제가 있다면 해결하십시오. 코드에서 무슨 일이 일어나고 있는지 항상 예측할 수 있어야 합니다.

[Note]
예상치 못한 실패 뿐 아니라 예상치 못한 성공 문제를 해결하는 것도 중요합니다. 단순히 작동하는 테스트를 갖는 것이 중요한 것이 아닙니다. 코드를 계속 제어(control)하여 코드가 수행하는 작업과 이유를 항상 파악해야 합니다.

3단계: 녹색 막대

다음으로, 테스트를 통과할 수 있는 프로덕션 코드를 작성합니다. 다시 말하지만 이 단계에서는 일반적으로 5줄 미만의 코드를 작성합니다. 코드 디자인이나 우아함은 걱정하지 마세요. 테스트를 통과하기 위해 해야 할 일만 하면 됩니다. 때로는 하드코딩을 할 수도 있습니다. 하지만 잠시 후에 리팩터링을 하게 될 것이므로 괜찮습니다.

테스트를 다시 실행하고 모든 테스트가 통과하는지 확인하세요. 그러면 녹색 표시를 볼 수 있습니다.

이것은 당신의 의도와 현실을 비교할 수 있는 두 번째 기회입니다. 테스트가 실패하면 가능한 한 빨리 알려진 양호한 코드로 돌아가십시오. 페어링 파트너와 함께 방금 작성한 코드를 다시 살펴보면서 문제를 확인할 수 있습니다. 문제를 찾을 수 없으면 새 코드를 지우고 다시 시도해 보세요. 때로는 새 테스트를 삭제하고 (코드 몇 줄일 뿐이에요) 더 작은 부분으로 나누어 주기를 다시 시작하는 것이 좋을 수 있습니다.

[Note]
코드에 대한 제어(control)를 유지하는 것이 핵심입니다. 코드를 다시 제어할 수 있게 된다면 몇 단계를 돌아가도 괜찮습니다. 스스로 이전 상태로 돌리는게 익숙하지 않다면 타이머를 5분 또는 10분으로 설정하세요. 그리고 타이머가 꺼졌을 때까지 문제가 해결되지 않았다면 알려진 양호한 코드(known-good code)로 되돌리겠다고 페어링 파트너에게 이야기 해두세요.

4 단계: 리팩터링

모든 테스트를 다시 통과하면 이제 문제가 발생할 염려 없이 리팩터링할 수 있습니다. 코드를 검토하고 가능한 개선 사항을 찾으십시오. 네이게이터에게 메모를 한 적이 있는지 물어보세요.

나타나는 각 문제에 대해 코드를 리팩터링하여 문제를 해결하세요. 일련의 매우 작은 리팩터링(1~2분 정도, 5분을 넘기지 않도록)으로 작업하고 각 리팩터링 후에 테스트를 실행하세요. 통과 해야 합니다. 이전과 마찬가지로 테스트가 통과하지 못하고 답이 즉시 명확하지 않은 경우 리팩터링을 취소하고 알려진 양호한 코드로 돌아갑니다.

원하는 만큼 여러 번 리팩토링 해도 됩니다. 디자인을 최대한 좋게 하되 코드의 기존 동작으로 제한하세요. 미래의 요구 사항을 예상하지 말고 어떤 동작도 추가하지 마세요. 리팩토링은 동작을 변경해서는 안 된다는 점을 기억하세요. 새로운 동작에는 실패한 테스트가 필요합니다.

5단계: 반복

새로운 동작을 추가할 준비가 되었다면 다시 주기를 시작하세요.

TDD 주기를 마칠 때마다 잘 테스트 되고, 잘 설계된 코드를 추가해나가게 됩니다. 성공적인 TDD 의 핵심은 작은 부분으로 나누는 것입니다. 일반적으로 여러 주기를 매우 빠르게 실행한 다음 다음 한두 주기 동안 리팩터링에 더 많은 시간을 소비합니다. 그리고 다시 속도를 높입니다.

연습하면 한 시간에 20사이클 이상을 완료할 수 있습니다. 하지만 얼마나 빨리 가는지에 너무 집착할 필요는 없습니다. 그러면 리팩토링과 디자인을 건너뛰고 싶어지게 됩니다. 두 가지 모두 중요하기 때문에 생략하면 안됩니다. 대신 아주 작은 단계를 수행하고, 테스트를 자주 실행하고, 빨간색 막대에서 소요되는 시간을 최소화하세요.


다음 글(예시로 알아보는 테스트 주도 개발 (TDD) 사용법)에서는 위 방법을 준수하여 TDD 방식으로 개발을 진행하는 예시를 다룬다.


이 글을 번역하면서 리팩터링이 맞을까 리팩토링이 맞을까 궁금해서 찾아봤는데 아래와 같은 글을 찾을 수 있었다. https://ntalbs.github.io/2010/refactoring-korean/