본문 바로가기

TDD

TDD를 잘하는 방법

최근 개발 관련 이야기를 하다가 "TDD를 잘하려면 어떻게 하는 게 좋을까?"라는 질문이 나왔다. 당시에 만족스러운 답변을 못 했고 며칠을 고민해 봤다. 사람마다 의견이 다르겠지만, TDD 사이클로 설명하는 게 제일 쉽겠다는 생각이 들었다.
 
TDD 사이클은 빨강(테스트 작성) → 초록(테스트 통과) → 리팩토링(중복제거) 순으로, 반복적으로 진행한다. 2000년대 TDD가 소개된 이후로 TDD 사이클은 변경된 게 없다. 어느 정도 안정화된 작업 순서 같다. 그런데, TDD 사이클은 왜 이런 순서로 구성되었을까? TDD 사이클의 단계별 목표는 무엇이고, 작업자는 어떤 이득을 얻을 수 있을까? 아래에서는 TDD 사이클의 단계별 목표를 설명한다.
 

1. 먼저 테스트를 작성한다

이 단계는 '인터페이스'를 설계하는 단계이다. 기능의 '사용법'을 결정하고, '작동 여부'를 확인하는 방식을 결정해야 한다. 구체적인 예를 들어 생각하는 것이 도움이 된다. 기능의 사용 예제 코드(또는 에러 예제 코드)를 작성한다고 생각하자.
 
클래스 이름, 메서드 이름, 파라미터 개수 등을 정하고, 테스트에서 기능을 사용할 때 불편한 부분은 없는지 확인한다. 테스트 데이터는 읽기 쉽고 사용하기 쉬운 것을 사용한다. 작동 여부 확인을 위해서 객체의 내부 상태를 사용하지는 말자. 테스트 코드에서 객체 내부에 대한 의존성이 생기면, 리팩토링 시 테스트 코드도 변경해야 한다.
 
왜곡된 기억일지 모르지만, 개인적으로 이 습관을 고치기가 제일 힘들었던 거 같다. TDD를 알기 전에는 '구현 코드'를 먼저 고민하는 습관이 있었다. "이런 데이터와 저런 데이터의 검증 로직이 있어야 할 거 같고, A 객체와 B 객체는 호출 순서가 중요하겠구나" 같은 고민을 했었다. 이후 습관을 고치기 위해 노력했다. IDE 에디터를 절반으로 갈라서 왼쪽에는 테스트 코드를, 오른쪽에는 구현 코드를 띄워서 작업하기도 했다. (부끄럽지만, 물리적인 두 개의 모자를 준비해서 번갈아 가며 써보기도 했다🫣) 의도적으로 '인터페이스' 설계와 '구현 코드' 설계를 분리해서 생각하고, '인터페이스' 설계를 먼저 고민하는 습관을 들였다.
 

2. 테스트를 실행하고 의도대로 실패하는지 확인한다

이 단계는 실제로 테스트를 실행하는 게 중요하다. (컴파일 에러가 없다는 가정하에) 실패할 것으로 예상한 테스트가 통과하는 경우가 있다. 이 경우 원인을 찾아야 한다. 기존에 작성한 테스트와 중복되는 테스트일 수도 있고, 구현할 필요가 없는 기능일 수도 있다.
 

3. 빠르게 테스트를 통과시킨다

이 단계는 테스트 통과 속도가 핵심이다. 무슨 방법을 사용하든 가능한 한 빨리 테스트를 통과하는 것이 중요하다. 제한 시간을 두는 것도 방법일 수 있다.
 
테스트를 통과하는 방법은 '가짜로 구현하기', '명백한 구현 사용하기', '테스트 삭제하기' 등이 있다. 이 단계에서는 구현 코드를 더럽게(?) 작성해도 상관없다. 다음 단계에서 코드를 정리하기 때문이다. 통과하는 방법을 찾는 데 오랜 시간이 걸리는 테스트도 있다. 이런 테스트는 삭제 후 더 작은 테스트로 다시 작성하는 게 방법일 수 있다.
 
이 부분도 습관을 고치기 힘들었던 거 같다. '가짜로 구현하기'를 적극적으로 사용하면 좋다고 스스로를 세뇌했지만, 무의식적으로 "너무 멍청해 보이는 방법이다"라고 생각한 거 같다. 다른 우아한 방식(예: 디자인 패턴)에 대한 동경이 있었던 거 같기도 하다.
 

4. 느긋하게 코드 구조를 변경한다

이 단계는 '구현 코드'를 설계하는 단계이다. 여러 가지 '설계 기법'을 적용해서 실험하고, '설계 검토'를 통해 추가 작업이 없는지 확인한다.
 
설계 기법을 적용해서 중복코드를 제거하거나 코드 구조를 변경한다. 리팩토링 단위는 작업 리듬에 따라 조절한다. 잘 풀리면 큼직하게 시도하고, 그게 아니면 작은 단위로 시도한다. 코드를 변경할 때마다 전체 단위 테스트 실행해야 한다. 내가 당연하다고 생각하는 코드 변경이 실제로 작동하는지 확인해야 한다. 어느 정도 리팩토링이 끝나면 검토하는 시간을 갖는다. 적용한 설계가 실제로 좋은지 확인하고, 설계에 대한 다른 아이디어(다른 입/출력 데이터의 지원 여부, 공통 인터페이스 추출 등)가 생각나면 할 일 목록이나 주석에 추가한다.
 
TDD를 하면서 제일 좋아하는 단계이다. 이 단계를 하다 보면 설계를 '발견'한다는 느낌을 많이 받는다. TDD에 익숙해지기 전까지는, 최대한 작은 단위의 리팩토링을 한 번에 하나씩 적용하는 것을 추천한다.
 

마치며

프로그래밍 문화에서 '분할정복'은 상식으로 통한다. 분할정복은 크고 복잡한 문제를 작고 단순한 문제로 쪼개서, 한 번에 하나씩 집중해서 해결할 수 있다는 점이 매력이다. TDD 사이클도 비슷하다. 인터페이스 설계와 구현 코드 설계를 분리해서 해결하고, 설계에 대한 작업자의 추측이 맞는지 확인하는 과정을 밟는다.
 
새로운 기술을 배울 때는 '마음가짐'과 '주변 상황'이 중요할 수 있다. "켄트 벡이 책을 팔아먹으려고 사기 쳤구나"라고 생각할 수 있고, "이거 괜찮아 보이네. 한 번 배워볼까?"라고 생각할 수도 있다. 또, 주변 사람들의 'TDD에 대한 평가'에 영향을 받을 때도 많다. (나는 몇 년 단위로 반복해서 'TDD 변절자'의 삶을 살았다)
 
TDD는 사고방식을 바꿔야 하므로 익숙해지기까지 시간이 필요하다. 당장 다음 주부터 잘할 수 없다. 조급한 마음보다 여유를 갖고 시도하는 편이 좋다. TDD 사이클의 단계별 목표를 의식해서 사용한다면, 익숙해지는 시간을 단축할 수 있다고 생각한다.