테스트 용이성(Testability) 향상을 위한 DI(Dependency Injection)
이전 포스팅 BDD(Behaviour-Driven Development)에 대한 간략한 정리에서 같이 다루려고 했던 내용이다.
하나의 클래스를 테스트하기 위해서는 그 클래스가 사용하는 다른 클래스의 실제 구현이 필요하거나 테스트 대상 클래스가 사용하는 데이터베이스 등에 대한 외부 환경이 구성되어 있어야 하는 경우가 있다. 하지만 테스트 환경에서 이를 모두 구성하는 것은 현실적으로 쉽지 않고, 테스트 실패에 대한 원인이 해당 클래스만의 문제인지, 아니면 관련된 다른 클래스의 문제인지를 찾기가 무척 어렵다. 즉, 테스트 대상 클래스의 기능만 한정 지어 테스트를 수행하기 어려운 경우가 많다.
테스트 용이성이란 말 그대로 테스트 대상을 얼마나 테스트하기 쉬운가에 대한 척도이다. 테스트 대상이 얼마나 복잡한가? 얼마나 결합도가 높은가? 등 몇 가지 내용이 있지만, 이번 글에서는 테스트 용이성에서 결합도와 Mocking 에 대한 내용만을 간단히, 이를 개선하기 위해 Dependency Injection 이 어떻게 사용되는지에 대해 설명한다.
이 글에서 Mocking 은 Mocking 객체를 의미하며 이를 간단히 말하면 실제 객체를 흉내내는 가짜 객체를 하나 만드는 것이다. 실제 클래스와 동일한 인터페이스를 가지고 있어서 외부에서 볼 때는 가짜인지 모르고 사용할 수 있도록 설계된 클래스를 인스턴스화 한 것을 말한다.
이야기 진행을 위해서 테스트 대상 클래스와 몇 개의 주변 클래스들이 필요한데 아래와 같이 있다고 하자.
자 이제 위와 같이 개발이 완료되었다고 하자. 여기서 테스트 대상은 SpecialThing 클래스인데 해당 클래스는 SomeFilter, ConfigDB, ConfigForFilterDB, OperationDB 에 방향성 있는 연관(Directed Association)을 가지고 있고 UserInfo에 대해서 의존(Dependency)을 가지고 있는 설계이다. UserInfo 클래스는 UserDB, VipInfo, AccountInfo 등을 방향성 있는 연관(Directed Association)으로 설계되어 있고 연관된 클래스들도 DB 에 방향성 있는 연관을 가지고 있다.
위 내용을 간단히 의사 코드로 보자.
1 2 3 4 5 6 7 8 9 10 11 12
class UserInfo { AccountInfo accountInfo; // 내부적으로 AccountDB 를 직접 생성/사용 UserDB userDB; // 내부적으로 UserDB 를 직접 생성/사용 VipInfo vipInfo; // 내부적으로 VipDB 를 직접 생성/사용 }; class SpecialThing { doSomething(UserInfo* userInfo); SomeFilter someFilter; ConfigDB configDB; ConfigForFilterDB configForFilterDB; OperationDB operationDB; };
설계자는 나름 책임을 분리했고 모듈화했다고 생각할 수 있다. 하지만 테스트는 전혀 고려하지 않은 설계이다. SpecialThing 을 테스트 하려면 SpecialThing 을 사용하는 어떤 모듈을 실행해서 열심히 로그를 찍어으면서 결과를 직접 보거나 디버깅 모드에서 브레이크 포인트 찍어가면서 보는 방법이 있다.
어느 날 갑자기 심심해서 SpecialThing 에 대한 단위 테스트를 구성하려고 보니 SpecialThing 를 인스턴스화 하려면 내부적으로 사용하는 DB들 때문에 실제 DB 가 구축되어 있어야 하고 UserInfo 클래스도 같이 인스턴스화해야 한다는 것을 깨닫게 된다. UserInfo 클래스를 보니 거대하다. 모든 클래스와 직접 연관으로 단단하게 묶여있다. 지금 상황에서 SpecialThing 과 UserInfo 는 모두 결합도가 매우 높은 클래스다.
SpecialThing 를 테스트하기 위해서 UserInfo 까지 Mocking 하기전에 SpecialThing 의 doSomething 메소드가 UserInfo 의 어떤 정보가 필요해서 UserInfo 를 받는지 확인해볼 필요가 있다. 확인해보니 UserDB 에 있는 UserNo 와 VipInfo 에 있는 VipGrade 2개의 정보만 필요한데 개발 편의성으로 UserInfo 를 통으로 받아서 사용하고 있었다고 하자. 그러면 doSomething 메소드를 호출할 때 위 2개의 정보만 추출해서 넘겨주는 것으로 수정하면 SpecialThing 과 UserInfo 의 연관은 끊어 버릴 수 있다.
1 2 3 4
class SpecialThing { doSomething(int userNo, int vipGrade); ... };
약간의 수정으로 연관 하나를 끊었다. 이제 나머지 ConfigDB, ConfigForFilterDB, OperationDB 에 대한 연관을 끊어야 한다. 이것도 어렵지 않다.
1 2 3 4 5 6 7 8 9
class SpecialThing { SpecialThing(ConfigDB* configDB, ConfigForFilterDB* configForFilterDB, OperationDB* operationDB) : someFilter(someFilter), configDB(configDB), configForFilterDB(configForFilterDB), operationDB(operationDB); doSomething(int userNo, int vipGrade); SomeFilter someFilter; ConfigDB* configDB; ConfigForFilterDB* configForFilterDB; OperationDB* operationDB; };
이번에도 어려운 내용 없이 수정했다. SpecialThing 클래스가 내부적으로 직접 인스턴스를 생성하여 사용하던 4개의 클래스들 중에서 3개를 생성자를 통해서 받도록 수정한 것이 전부다. 이 단순한 설계 변경으로 SpecialThing 은 의존성 주입이 가능해졌다. 이런 형태의 의존성 주입을 생성자 주입이라고 한다.
의존성 주입 : DI(Dependency Injection)가 이름처럼 거창한 게 아니다.
클래스 A가 클래스 B, C 에 의존적일 때 B, C 를 외부에서 생성하고 A 에게 B, C 를 사용하라고 전달하는 것이 의존성 주입이다.
참고로 보통 DI 와 IoC 가 같이 언급되지만 여기서 IoC 는 필요 없다.
DI 가 왜 좋은지 알아보자. 이제 SpecialThing 를 생성할 때 3개의 객체를 전달해줘야 하는데 인터페이스만 맞는다면 뭘 줘도 상관이 없다. 즉, SpecialThing 을 상대로 사기를 칠 수가 있다.
다시 테스트 이야기로 돌아와서 SpecialThing 클래스 하나 테스트하자고 DB 가 필요하다면 SpecialThing 클래스가 과연 테스트하기 편리한가? 아니다. DB 를 구축해야 하고 그나마 구축한 DB 의 테이블의 정보는 테스트를 진행할 때마다 변한다.
이제 위에서 언급한 Mocking 이 필요한 순간이다. 참고로 Mocking 하는 방법은 사용하는 언어마다 다르겠지만 흔한 방법은 클래스 상속과 인터페이스 구현이다. DB 에 직접적인 연관을 가지고 있는 ConfigDB, ConfigForFilterDB, OperationDB 클래스를 Mocking 해서 MockConfigDB, MockConfigForFilterDB, MockOperationDB 클래스를 구현하자. 당연히 이 Mock 클래스들은 DB 에 연관을 가지지 말아야 하고 인터페이스만 구현하거나 컨테이너를 사용해서 약간의 기능 구현을 통해 데이터 입/출력을 구현해도 된다. 그리고 이렇게 구현된 클래스의 인스턴스를 SpecialThing 객체를 생성할 때 넘겨주면 된다.
1 2 3 4 5 6 7
MockConfigDB mockConfigDB; MockConfigForFilterDB mockConfigForFilterDB; MockOperationDB mockOperationDB; SpecialThing specialThing(&mockConfigDB, &mockConfigForFilterDB, &mockOperationDB); int userNo = 1000; int vipGrade = 3; specialThing.doSomething(userNo, vipGrade);
위와 같이 외부 DB 에 종속이 없이 specialThing 객체를 생성할 수 있다. 또한 doSomething 메소드를 호출할 때 UserInfo 객체도 필요 없고, Mock 객체에 테스트에 필요한 값들을 테스트 시나리오에 따라서 설정할 수 있다. 몇 번을 실행해도 항상 같은 데이터 기반에서 테스트가 실행될 수 있는 상태가 되었다. 그렇다, SpecialThing 클래스는 매우 테스트하기 편리한 상태가 되었다.
마무리 정리를 하자.
간단한 설계 수정으로 SpecialThing 의 결합도는 매우 낮아졌으며 단위 테스트에서 SpecialThing 클래스를 쉽게 생성하고 사용할 수 있게 되었다. 여기서 오해하면 안 되는 것이 있는데 어떤 클래스에 DI 를 적용할 때 모든 연관에 대해서 DI 를 적용해야만 하는 것은 아니라는 것이다. 그래서 위에서 SpecialThing 클래스가 가지고 있는 연관 모두를 DI 로 변경하지는 않았다. SomeFilter 클래스에 대한 연관은 그대로 두었다. SomeFilter 클래스는 다른 외부 연관을 가지고 있지도 않고 비즈니스 로직만 가지고 있다고 가정했기 때문이다. 만약, SpecialThing 클래스의 테스트에만 집중해야 해서 SomeFilter 클래스의 로직도 모두 테스트에서 배제하고 싶다면 이 클래스도 Mocking 해서 DI에 포함해야 한다.
이로써 이전 포스팅 BDD(Behaviour-Driven Development)에 대한 간략한 정리에서 시작한 단위 테스트에 대한 글을 마친다.
제가 잘못 이해하고 있는 내용은 언제든지 알려주시면 감사하겠습니다.
(원글 : http://prostars.net/228)