TDD에 대한 기본적인 내용은 인터넷 검색을 통해 충분히 알 수 있다. 그렇게해서 나오는 내용에는 언뜻 특별한 점이 전혀 없는 것 같이 보인다. 처음에는 그저, 구현 후 테스트를 미흡하게 하지 않도록 테스트를 먼저 작성하여 프로그램 결함을 보다 줄이기 위한 방법 쯤으로 생각되었고, 그냥 이런게 있구나 정도로, 교양 지식 정도로 알고 넘어갔었다.

 

그러다 읽은 켄트 백의 TDD는 어쩐지 개발자를 위한 자기계발서를 읽는 듯한 느낌이었다. 개발 과정에서 멘탈을 관리하는 방법 등 직접적인 개발 행위를 벗어난 조언이 섞여 있는 점도 그렇고, 이건 마치 무술 수련서와도 같이, 지식적인 측면보다는 개발 수련법을 제시하는 것 같다. 책에서 어떤 정답을 제시하고 그대로만 하면 모든 개발에 적용할 수 있는 그런 과학적인 종류의 지식을 소개하지 않는다. 책의 내용은 "앞으로 이렇게 이렇게 연습하세요, 그럼 이러이러한 장점이 있습니다" 정도에서 끝이고, 정말 여기서 무언가를 얻으려면 꾸준히 실천해야만 한다.

 

지금까지 TDD를 적용해보며 느낀 점들을 나열해보면,

 

1. 구현 먼저? 테스트 먼저? 테스트도 구현인가?

- 정석적인 OO설계와 배치된다고 느껴지는 부분이 있어, 어디에 중심을 두어야할지 판단이 서지 않는 기분이 간혹 느껴졌다. 내가 아는 OO의 정석? 이라고 한다면, 도메인에서 필요한 주요 오퍼레이션(메시지)을 도출하고 그 메시지를 수신하기 적당한 객체를 선정한다(책임의 할당). 그리고 책임을 수행하기 위해 필요한 정보를 그 객체가 가지는데, 책임의 일부가 다른 객체에게 있다고 판단되는 경우 책임을 나눌 다른 객체를 정의하여 책임을 위임한다. 위임은 역시 메시지를 전달하는 방식으로 이루어지며, 이렇게 객체 간 협력이 완성된다... 이런 식인데, TDD는 일단 다짜고짜 테스트를 작성한다. 심지어 객체 클래스를 정의하기도 전에 테스트를 작성해서 컴파일 에러를 일으키라 한다. OO에서는 바로 구현에 뛰어드는 행위는 금물이라 했다. 왜냐하면 모든걸 구현 중심으로 생각하게 되기 쉽고, 이는 메시지와 책임, 협력을 우선시하는 객체 지향 모델링에 방해가 될 수 있어서이다. 그런데 TDD는 간단히 눈에 보이는 구현을 일단 중시하는 것 같아 내가 알고 있는 OO 설계와는 배치되는게 아닌가 생각했다.

 

여기서 혼란을 느껴 구글링해보니 스택오버플로에서 누군가 정확히 나와 같은 혼란을 느껴 질문했다. 인터페이스를 먼저 작성해야 하느냐, 테스트를 먼저 작성해야 하느냐고. 그리고 여기의 답변이 내 혼란을 잠재워줬다. 그 내용은, 테스트를 작성하는 일과 인터페이스를 작성하는 일은 같은 일이지, 나눌 수 있는 일이 아니라는 것이다. 답변에서 든 단순한 예로, 계산기 어플리케이션을 작성할 때 먼저 떠오르는 기능은 어떤 것인가? 그것이 덧셈이라고 하면, 간단히 다음과 같은 테스트를 먼저 작성할 수 있을 것이다.

Calcurator calc = new Calcurator();
int sum = calc.add(3, 4);
assertTrue(sum == 7);

add의 명세와 의도는 명확하다. 두 숫자값을 취하여 그 합을 반환한다. 이 add가 바로 퍼플릭 인터페이스의 오퍼레이션, 즉 메시지가 된다. 이렇게 출발한다. 사용자가 계산기에 기대하는 다른 오퍼레이션들을 테스트로 하나 하나 추가해가며 하나의 인터페이스가 완성되고, 각 테스트들은 어플리케이션에 기대하는 하나의 작고 구체적인 실행 가능한 기능이 된다. 퍼블릭 인터페이스는 사용자가 기대하는 동작(책임)을 수행하는 것이어야 하며, 테스트는 인터페이스의 상호작용을 정의하는 첫 번째 스텝이다. 이렇게 하면 인터페이스에 불필요한 기능이 추가되거나 자신의 책임이 아닌 코드가 들어가는 일이 잘 발생하지 않는다. 구현에 앞서 메시지를 생각한 덕분이다.

 

 

2. 테스트를 아주 작게 나누는 일은 OO에서 책임을 분리하는 일과 같은 맥락이다

- 한 번에 한 기능에 대한 짧은 테스트를 작성하고 테스트 간 의존성을 두지 않게 함으로써(테스트 격리), 테스트 대상 기능이 자연스럽게 단일 책임 원칙을 준수하게 만드는 효과가 있음을 느꼈다.

 

 

3. 완성 형태를 먼저 그리게 된다

- 테스트 대상 기능이 최종적으로 어떤 형태로 완성될 것이고, 어떤 형태로 사용될 것인지를 먼저 테스트 작성 시 그려놓게 된다. 이는 내가 늘 생각해온 올바른 개발 방식에 거의 정확히 부합하는 느낌이다. 난 늘 코드는 그 코드의 최종 사용자의 입장에 서서, 사용자가 이 완성본을 볼 때, 그리고 실제 사용할 때 어떤 느낌일지를 유념하며 작성해야 한다고 생각한다. 그 과정의 어려움을 먼저 생각해서는 안 된다. 그런 것은 일단 저 멀리 던져두고, 내가 개발하는 프로그램이 궁극적으로 어떤 형태를 띄고, 그 기능의 아웃풋이 어떤 모양이기를 원하는지 생각해두고 개발하는 것이 바람직하다고 생각한다. 그리고 그 품질 기준이 높아야한다. 개발자가 개발 과정의 힘듦을 먼저 생각하면 그 고통을 두려워하여 그건 하지 못하는 일로 단정짓게 됨을 피하기 위해서이다. 그래서는 안 된다.

 

 

4. 내부 값에 의존하는 테스트는 언제나 틀린가?

- 어떤 객체의 인스턴스가 정확히 작동하는지 여부를 인스턴스의 내부 상태로 확인하는 것은 일반적으로 bad practice라고 배웠다. 테스트는 객체의 행동을 확인해야 하지, 그 내부 상태에 의존해서는 안 된다는 것이다. 테스트를 위해 런타임에 인스턴스의 내부 상태를 직접 확인해야만 한다면 이는 내부 구현에 의존하는 일이고, 이런 경우 설계가 옳지 않았을 확률이 높다고 한다. 그런데 여기에 한 가지 예외 케이스를 두고 싶다. 테스트하려는 기능이 인스턴스를 생성하는 것이라면? 생성된 인스턴스가 의도한 값을 정확히 가지고 있는지를 확인해야만 한다면 어떨까? 한번은 커스텀 xsd를 작성하고 스프링 커스텀 빈 데피니션 파서를 작성하여 사용자가 xml에 정의한대로 원하는 빈을 생성해주는 기능을 개발하고 있었다. 생성된 빈은 설정 xml에 정의된 값을 내부에 가져야만 하며, 이 변수는 역시 private field였다. 이것 자체가 이미 하나의 기능이기 때문에, 테스트 대상이 되어야 한다고 생각되었다. 그런데 생성된 인스턴스의 값 확인은 리플렉션을 사용하여 private field에 접근하는 방법 외에는 떠오르지 않았다. 이처럼 테스트하려는 기능 자체가 private field 값을 세팅하는 것이라면, 이 테스트는 그 값을 확인해야 옳지 않을까? 이 private field를 사용하는 메서드를 호출하여 그 결과를 통해 필드값이 정확히 설정되었음을 간접적으로 확인하는 방법도 있기는 했다. 허나 이 방법은 그 과정에 다른 기능 테스트를 포함하는 것이고, 결국 테스트 격리를 깨는 행위로 보여 결국 리플렉션을 사용했다. 결과적으로 의도한 테스트는 잘 되었지만, 정말 이 방법 뿐이었을지, 애초에 인터페이스 설계가 잘못된 것은 아니었을지 두고 두고 나를 괴롭혔다.

 

 

5. TDD는 설계를 포함한다

- 하면 할수록, TDD는 설계에 관련 것이라는 느낌에 확신이 들었다. 사실 Test Driven Developement 라는 이름이 말하듯, TDD는 구현에 국한되는 것이 아닌, 개발이라는 행위 전체를 아우르는 방법론이다. 하지만 TDD는 언뜻 보면 '기능을 작은 단위로 나누어 테스트를 먼저 작성하여 빠르게 피드백을 받아 어플리케이션을 구축하는', 즉 빠르게 테스트를 수행하여 안정적인 기능을 구축하기 위한 방법쯤으로 보인다. 처음에는 나도, 개발 다 해놓고 테스트를 작성하려면 번거롭고 고통스러우니, 그냥 그 때 그 때 부지런히 테스트해서 개발하자는 뜻으로 해석했다. 하지만 직접 실천해보니 테스트를 작성하며 자연히 오퍼레이션이 정의되고, 퍼블릭 인터페이스가 구성되는 과정을 보았고, TDD는 어플리케이션을 설계하는 또다른 경로(path)라고 느꼈다. OO를 처음 제대로 공부하기 시작하면서는 실제 구현에서 첫 발을 어떻게 떼야 할지 더 어려워지는 느낌을 받았었는데, TDD가 그것을 아주 가볍게 만들어주었다. 아직 한참 부족하지만 적어도 TDD에서 이것 하나는 확실히 취한 수 있는 이점이라고 생각되었다.

 

 

 

+ Recent posts