-
테스트 데이터 빌더Object & Test 2014. 1. 13. 17:26
자바 기반으로 애플리케이션을 작성 할 때 좋은 점은 셀 수 없을 정도의 다양한 프레임워크, 라이브러리, 도구등의 도움을 받을 수 있다는 점입니다. 현시점을 기준으로 다른 어떤 개발 언어도 자바만큼 많은 지원 도구들을 가지고 있지 않을 겁니다. 사실 너무 많아서 골라 쓰기도 쉽지 않을 정도죠… 그렇게 많은 프레임워크 중에서도 제가 가장 좋아하는 프레임워크가 있는데 바로 jUnit 테스트 프레임워크입니다. 존경해 마지 않는 켄트 백 횽님, 에릭 감마 횽님(두 분 모두 61년생)의 역작이면서 제가 본 가장 직관적이고 우아한 프레임워크 중에 하나 이기도 합니다. 게다가 배우기도 쉬어서 jUnit만 놓고 본다면 배워서 사용하는 데 하루도 걸리지 않을 겁니다.
하지만 보통 아무리 좋은 프레임워크라도 그 자체만으로 좋은 설계나 코딩을 보장해 주지는 않습니다. 프레임워크를 도입한다고 저절로 좋은 품질의 애플리케이션이 만들어지지 않는 다는 이야기입니다. 프레임워크가 해결 하려는 문제를 잘 이해하고 사용하는 것이 중요하기 때문입니다. 그런 이해 없이 남들이 좋다고 해서 사용한다면 제대로 사용하기도 어려울 뿐더러 설령 좋아지는 부분이 있더라고 그 범위가 제한적일 확률이 큽니다.
jUnit도 마찬가지 입니다. 프레임워크가 제공하는 기능은 단순하고 사용하기도 쉽습니다만, 정말 제대로 사용하려면 단위 테스트, TDD(Test Driven Development), CI(Continuous Integration)를 비롯한 다양한 테스트에 대한 개념에서부터 올바른 단위 테스트 작성법까지 정말 많은 것을 알아야 합니다.
근데 위에 나열한 개념을 다 모른다고 할지라도 jUnit이 주는 특별한 장점이라면 일단 jUnit을 이용하여 단위 테스트를 작성을 시작하는 것 만으로도 어느 정도 품질 향상을 기대할 수 있다는 점입니다. 단위 테스트가 전혀 존재하지 않는 코드와 단 하나라도 테스트가 존재하는 코드는 큰 차이가 있기 때문입니다. 테스트를 작성함으로써 코드에서 이상한 부분을 찾아내고 고치는 주기가 빨라지기도 하고 하나 하나씩 쌓여나간 테스트 코드가 축적되면 나중에 기능을 추가하거나 코드를 리팩토링 하는 데도 아주 많은 도움을 받을 수 있기도 합니다.
제 경우에는 2004년부터 jUnit을 이용해 단위 테스트를 만들고 있으니 꽤 오랫동안 썼다고 할 수 있습니다. 단위 테스트를 작성하는 습관이 든 후로는 단위 테스트를 작성하지 않으면 왠지 불안하기까지 합니다. 하지만 단위 테스트를 열심히 작성하면서부터 자연스레 몇 가지 고민 거리가 생기기도 했습니다.
그 중 하나가 단위 테스트 코드의 유지 보수였습니다. 열심히 작성한 테스트코드였지만 시간이 흘러서 보면 너무 난해한 경우가 많았거든요. 이해하기도 어렵고 고치기도 어려운 테스트 코드… 여러분들이라면 어떻게 하시겠습니까? 가장 쉬우면서도 어리석은 방법은 삭제입니다. 기존 로직에 무언가 새로운 기능을 추가합니다. 테스트를 돌려 봅니다. 테스트 코드가 깨집니다. 여러 테스트 케이스 중에 유독 한 테스트 케이스만 깨집니다. 근데 테스트 코드를 도통 이해하지 못하겠고 시간도 별로 없습니다. 잠시 고민합니다. ‘어쩌지…’
테스트 코드를 삭제하면 금방은 모든 게 편해집니다. 삭제한 테스트 코드를 제외한 모든 테스트 코드가 잘 돌아가니까요. 하지만 그러면서 빚이 쌓입니다. 그리고 그 빚은 며칠 혹은 몇 주 내로 이자까지 합쳐진 채로 내게 돌아오곤 했습니다.
그렇게 테스트 코드를 어떻게 하면 잘 작성할 수 있을까 고민하다가 한 권의 책을 만납니다. ‘제라드 메스자로스’께서 쓰신 xUnit Test Patterns이라는 책이었습니다. 보신 분들은 아시겠지만 엄청난 두께를 자랑하는 책입니다. 게다가 번역서가 나오기 한참 전이라… 잠시 고민하다가 ‘마틴 파울러’ 도장이 찍혀 있는 책은 읽어서 손해 볼게 없다는 생각에 원서로 읽기 시작합니다. 근데 막상 읽어보니 너무 재미있어서 정말 한 장도 빼지 않고 모두 읽었습니다. 그리고 그전까지 고민하던 많은 부분들이 이 책을 읽으며 해소 됐습니다.
xUnit Test Pattern (출처 : 아마존)
이 책을 통해 읽기 쉬운 테스트 코드를 작성하는 법에 대해 배웠고 관리 용이한 테스트 픽스처를 만드는 다양한 패턴에 대해 알게 됐습니다. 이 책을 읽기 전과 후에 테스트 코드가 정말 많이 달라졌다고 할 만큼 많은 걸 배우게 된 책입니다. 아직 안 읽어 보신 분께는 꼬~옥 “강추”합니다. 지금은 번역서도 나왔습니다.
그렇게 시간이 흘렀고 2012년부터 일년 반 동안 네비게이션 관련 프로젝트를 하게 됩니다. 그런데 이 프로젝트에서 테스트 코드를 작성하면서 또 다시 고민거리가 생깁니다. 대상 시스템이 네비게이션 시스템이다 보니 테스트 픽스처를 대부분 복잡한 그래프 형태로 구성해야 했는데 이게 쉽지 않았습니다.
그래프 (출처 : 위키피디아)
이런걸 그래프라고 합니다. 자세한 내용은 위키피디아 참조
그래프를 구성하는 각 요소(Link, Node…)가 다음 그림과 같이 컴포지트 구조로 구성되어 있어 픽스처 작성시 최대 3~4단계 이상의 계층을 구성해야 하는 경우도 많았습니다.
결국, 픽스처 작성시 보통 이런 식의 코드들이 많이 나타나게 됩니다.
Path p = new Path();
p.setInOutWeight(100);
p.setId(1);
p.setTurnControlType(1);
p.setTurnType(1);
DirectionLink d1 = new DirectionLink();
p.setNext(d1);
DirectionLink d2 = new DirectionLink();
p.setCurrent(d2);
Link link = new Link();
d1.setLink(link);
d2.setLink(link);
구조를 강조하기 위해 세부사항은 대부분 생략하고 단순히 한 단계의 코드만 작성했는데도 객체 생성 구조가 복잡함을 알 수 있습니다. 여기서 객체 생성의 복잡도를 낮추기 위해 각 객체 생성 부분을 다음과 같이 Creation 메소드로 분리했습니다.
Path p = createPath(1,100);
Link l1 = createLink(...);
l1.setPositive(createDirectionLink(...));
l1.setNegative(createDirectionLink(...))
p.setCurrent(l1.getPostive());
p.setNext(l1.getNegatvie());
하지만 그래도 여전히 그래프 구조를 엮는 코드들이 그대로 남았습니다. 마음에 들지 않았습니다. 게다가 더 큰 문제는 테스트 케이스마다 필요한 Creation메서드의 파라미터 유형이 달라지는 경우가 많았다는 것입니다.
예를 들어 Link라는 객체가 15개의 속성을 갖고 있는데 테스트 케이스마다 테스트하려는 속성의 조합이 조금 달라지는 경우 입니다. 조합이 몇 가지 없을 경우에는 조합의 수 만큼 creation 메서드를 만들고 재활용하면 되겠지만 우리 경우에는 조합의 수가 너무 많아 적합하지 않았습니다. 경우에 따라서 파리미터 조합의 개수가 유동적으로 변했기 때문에 대응하는 creation 메서드를 모두 만드는 건 원래의 의도와는 다르게 결국 복잡도를 더 높이는 결
과를 초리했습니다.
createPathByA(…)
createPathByB(…)
createPathByC(…)
createPathByD(…)
createPathByE(…)
…
수 많은 CreationMethod들 중에 입맛에 맞는 메서드를 고르는 것부터 해서 파라미터가 하나만 달라져도 기존 Creation 메서드를 사용할 수 없어 새롭게 만들어야 하는 문제까지... 게다가 주요 도메인 객체에 속성값 명이 변경되기라도 하면 고쳐야 할 코드가 엄청나게 많아지기까지 했습니다.
‘아무리 봐도 이건 좀 아닌데…’ 라는 생각을 할 무렵 또 한 권의 책을 만납니다. 바로 이 책 Growing Object-Oriented Software, Guided by Tests(이하: GOOSGT로 표기)입니다. 이건 켄트 백님 도장이 찍혀 있네요. 그리고 저자인 넷 프라이스는 jMock 프레임워크를 만든 분이기도 합니다.
Growing Object-Oriented Software Guided by Tests (출처: 아마존)
회사 동료 분 책상에 방치되어 있는 이 책을 우연히 보고 허락도 없이 가져다가 읽었는데 “이런 훌륭한 책을 이제서야 읽다니!!!” 하는 생각이 들 정도로 좋았습니다.
회사 동료 분 책상에 방치되어 있는 이 책을 우연히 보고 허락도 없이 가져다가 읽었는데 “이런 책을 이제서야 읽다니!!!” 라는 생각이…ㄷ ㄷㄷ 아직 번역서도 없어 출판사에 연락을 했더니 다른 분께서 이미 번역 중.. OTZ 그래서 리뷰라도 하겠다고 자청해서 근 6개월에 걸쳐 리뷰를 했드랬죠. 리뷰는 이 때 처음 해봤는데…리뷰하는 것도 쉬운 일은 아니더군요. 그 때 약속한 리뷰 일정을 못 지켜서 담당자 분께 무척 죄송했다능…근데 리뷰를 다 해드리고 나서도 거의 1년이 지나서 책이 나왔습니다. 오래 기간이 걸린 책 출간으로 보면서 책 한 권 출판하는게 참 쉬운 일은 아니구나 하는 생각이 들더군요. 이 책의 한글판은 “테스트 주도 개발로 배우는 객체 지향 설계와 실천”입니다. 이런 책들은 유행을 타지 않는 책이기에 꼭 읽어 보시길 추천합니다.
.
아무튼 이 책에서 고민하던 문제의 해결책을 발견합니다. 바로 테스트 데이터 빌더 (Test Data Builder)이었습니다. 테스트 데이터 빌더는 디자인 패턴에 나오는 빌더 패턴의 일종이라고 보면 됩니다. 위키를 찾아보면 빌더 패턴에 대해 다음과 같이 설명하는데요.
The intent of the Builder design pattern is to separate the construction of a complex object from its representation. By doing so, the same construction process can create different representations.
복잡한 객체의 생성법과 표현법을 분리해서 똑같은 생성 절차를 통해 다른 표현이 만들어질 수 있게 한다
GOOSGT 책에서도 복잡한 테스트 데이터를 만들 때 효과적인 패턴으로 테스트 데이터 빌더를 이야기 하고 있습니다. 복잡한 픽스처 생성 과정을 테스트 데이터 빌더를 통해서 읽기 쉽게 만들 수 있다는 점과 테스트 코드와 복잡한 도메인 객체 생성 로직간의 커플링을 막을 수 있다는 점, 그리고 Case별로 Creation메소드를 만들지 않고도 딱 원하는 크기의 픽스처를 만들어 낼 수 있다는 점에서 테스트 데이터 빌더는 정확히 우리가 찾고 있던 솔루션이었습니다.
데이트 데이트 빌더의 구조는 비교적 간단한데요. 만약 Order라는 테스트 대상 객체에 대한 픽스처를 만들어야 한다면 OrderBuilder라는 테스트 데이터 빌더 객체를 만들면 됩니다. (GOOSGT책 내용 인용)
public class OrderBuilder {
private Customer customer = new CustomerBuilder().build();
private List<OrderLine> lines = new ArrayList<OrderLine>();
private BigDecimal discountRate = BigDecimal.ZERO;
public static OrderBuilder anOrder() {
return new OrderBuilder();
}
public OrderBuilder withCustomer(Customer customer) {
this.customer = customer;
return this;
}
public OrderBuilder withOrderLines(OrderLines lines) {
this.lines = lines;
return this;
}
public OrderBuilder withDiscount(BigDecimal discountRate) {
this.discountRate = discountRate;
return this;
}
public Order build() {
Order order = new Order(customer);
for (OrderLine line : lines) order.addLine(line);
order.setDiscountRate(discountRate);
}
}
}
그리고 사용은 이런식으로 할 수 있습니다. 원하는 테스트 객체를 만들기 위해 최후에 build() 메서드 호출합니다.
Order order = new OrderBuilder().build();
여기서 눈여겨 보아야 할 것은 빌더 객체가 제공하는 모든 공용 설정 메서드들이 모두 자신의 객체 타입(여기서는 OrderBuilder)을 리턴한다는 것입니다. 이를 통해 빌더 내의 메서드를 “.”지시자를 통해 연속적으로 연결 가능하도록 만들 수 있습니다. 이렇게 하면 객체 생성 로직 작성이 한결 유연해 지고 해당 객체가 어떤 속성을 갖고 생성되는지 명확히 알 수 있습니다.
결과적으로는 계층 구조에 맞춰 적절한 테스트 빌더를 만들고 빌더를 조합함으로써 다음처럼 계층적인 테스트 데이터 구조를 쉽게 만들 수 있게 됩니다.
new OrderBuilder()
.withCustomer(
new CustomerBuilder()
.withAddress(new AddressBuilder().withNoPostcode().build())
.build())
.build();
테스트 빌더를 이용하면 내용이 조금씩 달라지는 다수의 비슷한 테스트 객체를 쉽게 만들어 내는 것도 무척 쉽습니다. 다음의 예처럼 말이죠.
Order orderWithSmallDiscount = new OrderBuilder()
.withLine("Deerstalker Hat", 1)
.withLine("Tweed Cape", 1)
.withDiscount(0.10)
.build();
Order orderWithLargeDiscount = new OrderBuilder()
.withLine("Deerstalker Hat", 1)
.withLine("Tweed Cape", 1)
.withDiscount(0.25)
.build();
마지막으로 빌더를 생성하는 부분을 Factory 메서드로 만들면 우리가 관심 없는 테스트 빌더 객체 생성 로직을 숨기고, 테스트 데이터 생성 부분을 더욱 강조할 수 있습니다.
Order order =
anOrder().withCustomer(
aCustomer().withAddress(anAddress().withNoPostcode())).build();
다음은 실제 우리가 테스트 데이터 빌더 패턴을 테스트케이스에서 사용한 코드의 일부 입니다.
코드를 통해 무슨 용도인지는 정확히 알 수 없지만 테스트를 위해 LinkComposite이라는 객체를 만든다는 사실과 이 객체가 목적지와 터미널종류, 안내 이지미 해상도 속성을 갖으며 두 개의 링크로 구성된 LinkContainer를 갖는 다는 사실을 알 수 있습니다.
위와 같이 테스트 데이터 빌더 패턴을 도입한 후로 복잡한 테스트 픽스처를 만드는 작업이 한결 쉬워졌으며, 도메인 객체 와 테스트코드가 분리됨으로써 변경에 따른 테스트 수정 여파도 최소화할 수 있었습니다. 또, 테스트 코드를 읽는 것도 한결 편해졌습니다. 더불어 한동안의 고민거리도 해결되었습니다.
지금까지 본 바와 같이 여러 단계의 계층구조를 갖는 복잡한 픽스처를 만들어야 한다면 테스트 데이터 빌더를 도입하는 것이 무척 효과적일 수 있습니다. 하지만 테스트 데이터 빌더 패턴을 아무때나 무조건 사용하는 것은 바람직하지 않습니다. 빌더 패턴을 도입하려면 테스트대상 클래스에 대해 각각 빌더를 만들어야 하기 때문에 대상 클래스가 많을 경우에는 빌더를 작성하는데 오버헤드가 있으며, 작성된 빌더 코드에 대한 유지보수 또한 분명히 필요합니다. 따라서, 픽스처 생성 과정이 복잡하지 않고 Creation 메서드 정도로 충분하다면 굳이 이 패턴을 도입할 이유가 없습니다. 앞 부분에도 이야기 했지만 이 패턴이 필요한 정확한 상황을 이해하고 사용하는 것이 중요합니다.
'Object & Test' 카테고리의 다른 글
DBUnit 사용시 테스트 데이터 INSERT 문제 (0) 2011.10.12 소프트웨어의 품질 (0) 2011.08.31 디버그 코드를 SVN에 체크인하지 마세요. (0) 2011.07.22 단위 테스트용 테스트 데이터베이스 싱크하기 (2) 2011.07.07 Mock Object 탄생 비화(1) (0) 2011.06.27