몇년 전 자주 가던 커뮤니티에서 누군가가 부과된 과제에 대한 질문을 올렸는데 TDD에 관한 것이었다.


내용을 간단하게 설명하자면 아래와 같은 추상클래스가 있고 요구사항이 주저리주저리 적혀있다. 주어진 요구사항을 구현하는데, "TDD를 통해서 자기 기술적인 테스트 코드를 작성하라"는게 과제 중 한 부분이었다.
합집합, 교집합, 차집합 등의 연산을 구현하는 것인데 당시로서는 TDD가 막 알려지던 때였고 나도 TDD를 손가락에 익히려고 하던 때여서 나름 진지하게 테스트 코드를 짜본 기억이 난다.

첫번째로 배열 구현체를 작성하는데 기본적인 코드는 아래와같이 시작한다.
하나의 클래스 내에서 메소드들간에 의존 관계를 맺을 수 있는데 가장 의존성이 낮은 메소드부터 차례차례 공략해 나가는 것이 중요하다. 위의 경우 합집합, 교집합, 차집합 등에서 isIn() 메소드로  집합의 원소를 빈번하게 조회하므로 isIn과 같이 의존성이 낮은(독립성이 높은) 메소드가 제대로 구현이 안된다면 여기에 의존하는 다른 메소드들도 사실상 테스트가 불가능해진다.(테스트가 실패했을때 문제 발생 지점이 넓어지므로 수정하기가 더 어려워진다.)

isIn(..)을 테스트하는 코드는 아래와 같이 간단하다.
위 테스트 코드는 다음과 같은 의미를 담고 있다.

집합에 "10"이라는 값을 넣은 후 isIn()으로 확인하면 true가 나올 것이다.


TDD 방식에서 중요한 것은 테스트 코드가 그 자체로서 설명서가 되게끔 하는 것이다. 제품 메뉴얼처럼 간결하고 부드럽게 쓰여진 테스트 코드를 보면서 자연스럽게 그 메소드의 기능을 알 수 있다.

JDK의 자바 API 문서를 봐도 클래스와 메소드의 기능을 제대로 짐작하기 어려운 것은 그 메소드와 클래스가 실제 코드 안에서 누구와 어떤 협력관계를 맺으며 기능하는지 나오지 않기 때문이다.

테스트 코드를 돌려보면 Error 가 던져지는데 기본 구현때문에 그렇다.
이제 저 안에 파라미터로 전달된 값이 현재 배열에 들어있는지 확인하는 코드를 만들면 된다.
만일 위 코드가 실패한다면  Value의 구현에 문제가 있는 것인데, 의미상 똑같은 "10"의 값이기 때문에 서로 다른 인스턴스라도 equals 메소드는 true를 반환해야 한다. 여기서 중요한 점은 isIn의 구현이 Value 클래스의 + boolean equals(Object obj) 메소드에 의존한다는 점이다. 따라서 지금 테스트하는 isIn(..) 이전에 equals(...) 메소드를 먼저 테스트하고 기능을 완전히 구현한 후에 다시 isIn(..)으로 돌아와서 나머지 테스트를 마저 진행한다.

이렇게 isIn(..)을 어느정도 마무리짓고 "합집합"을 구현한다.
위의 테스트 코드는

집합에 10, 30, 12를 넣으면 집합의 크기는 3이 된다. 그리고 여기에 다시 10을 넣으면 이미 존재하는 값이므로 무시하고 여전히 집합의 크기는 3이 된다.

를 말하고 있다.

테스트 코드가 성공하도록 일단 기능을 구현해보면 아래와 같은데...
중복된 10을 그대로 포함시켜서 test-2 에서 에러가 발생한다.

        junit.framework.AssertionFailedError: expected:<3> but was:<4>
                ... <생략>
                at ynseo.setadt.test.Test_ArrayAlgSet.testAdd(Test_ArrayAlgSet.java:36)

3을 예상했는데 테스트 해보니 4가 나왔다는 뜻이다. 즉, 구현 내용이 테스트를 통고하지 못했으니 다시 구현으로 돌아가서 이 부분을 해결지어야 한다.
이렇게 해서 어느정도 테스트가 통과된다.

마지막으로 "교집합" 테스트를 어떻게 할까 생각해보면... 두개의 집합을 교집합 했을때 나온 결과 집합이 맞는지를 확인하면 될 것이다.
augend와 addend 를 를 교집합하면 {"8", "12", "22"}의 집합이 나와야 함을 의미한다.
그리고 위와같은 코드 구현이 나오게 된다. 물론 한 번에 나오지는 않는다. 코드를 만들고 테스트에서 걸리고, 그러면 다시 그 부분을 해결하고 또 테스트를 돌리고 또 실패하고 다시 코드를 고치고...

이렇게 여러번 과정을 반복해서 위와같은 메소드 하나가 나온다. 그리고나서 추가적으로 테스트 코드를 좀 더 다양하게 구성해서 한 번 더 돌려보는 것도 좋다. 테스트 자체가 너무 많은 경우의 수를 담는 경우에 메소드가 복잡해지므로 아주 최소한의 기능을 검증하는 간단한 테스트를 해보고 통과하면 테스트 내용을 더 확장해서 기능을 보강해 나간다.

TDD 방식의 가장 큰 장점이라면 "확신"을 가질 수 있다는 점이다. 학교에 입학 후 맨 처음 프로그램밍을 할 때 TDD라는 개념도 모를때니까 죽어라 System.out.println("...");으로 코드를 도배해 가면서 코딩을 했었는데 지금 생각해도 참 무식했다.(하지만 이런 과정은 꼭 필요하다고 생각함)

당연히 메소드 하나를 구현해도 이에 대한 확신이 없다보니 다른 메소드에서 에러가 발생하면 코드 전체를 다 훑어보는 일도 있었다. 에러를 유발하는 범위가 전혀 관리되지 않았기 때문에 예외 하나 터지면 메소드 호출 경로를 따라서 이잡듯이 뒤지는 일이 허다했다.(생산성같은걸 논할 때가 아니었다. 그저 돌아가기만 바랄뿐...)

TDD를 통해서 의존성이 낮은 메소드부터 차근차근 정복해나가면 예외가 발생해도 테스트를 통과한 부분들은 대상에서 제외함으로써 살펴볼 범위가 크게 줄어든다. 이제는 뭘 하나 짜더라도 "테스트를 어떻게 할까"를 생각하는 정도에 와 있다.

자연히 파일 입출력이나 네트워크 입출력, 데이터베이스 입출력과 같은 이기종의 시스템간의 테스트 방식에도 더 고민하게 되고 이 과정에서 자신만의 노하우가 쌓이게된다.

TDD를 다룬 책 중에는

테스트 주도 개발 - 켄트벡

이 책이 가장 많이 읽히지 않았나 싶다. 의미있는 예제로 TDD에 대한 이야기를 풀어가다보니 소설책 읽듯이 봐도 전혀 무리가 없다.
Posted by yeori
,