Eternity's Chit-Chat

aeternum.egloos.com



의존성 끊기와 단위 테스트 – 4부 Software Quality

프로그램과 의존성

프로그램을 바라보는 프로그래머의 관점은 프로그램을 작성하고 수정하는 행위에 영향을 미친다. 프로그램을 텍스트의 목록으로 바라보는 관점에서 프로그램 작성과 수정은 단순하게 텍스트를 편집하는 작업으로 요약할 수 있다. 새로운 행위가 필요하면 텍스트를 추가하고, 행위를 변경하기 위해서는 텍스트를 수정한다. 프로그램은 텍스트의 목록이기 때문에 전체적인 문맥 흐름에 문제만 없다면 텍스트를 추가하거나 수정하는데 아무런 제약도 없다.

불행하게도 프로그램에 대한 텍스트 메타포는 높은 응집도와 낮은 결합도, 캡슐화와 같은 훌륭한 설계가 갖추어야 하는 기본 덕목을 설명하기에는 적절하지 않다. 애플리케이션 설계를 적절한 추상화의 발견과 복잡한 의존성의 제어라고 요약할 때, 프로그램을 다른 각도에서 바라볼 경우 의존성을 제어하기 위한 체계적인 접근 방법을 적용할 가능성이 높아진다.

개인적으로 단위 테스트 관점에서 프로그램을 바라보게 되면서 훌륭한 설계란 어떠해야 하는지에 대해 심사숙고 하게 되었다. 어떤 클래스는 간단하게 단위 테스트 할 수 있기 때문에 작동하는 가장 단순한 방법으로 구현하면 된다. 어떤 클래스는 의존성의 사슬에 묶여 옴짝달싹하지 못하기 때문에 의존성을 끊을 수 있는 설계를 요구한다. 너무 많은 의존성을 가진 클래스는 SRP(Single Responsibility Principle)를 위반한다는 증거다. EXTRACT CLASS를 적용하라. 내부에서 직접 생성하는 클래스로 인해 단위 테스트를 실행할 수 없다면 DEPENDENCY INJECTION을 통해 외부에서 객체를 조립하도록 변경하거나 DEPENDENCY LOOKUP을 통해 생성되는 인스턴스를 변경할 수 있는 여지를 제공하라. 인프라스트럭처에 대한 의존성을 가진 코드가 여기저기에 흩어져 있어 단위 테스트를 수행하기가 어렵다면 인프라스트럭처에 의존하는 모든 코드를 단일 클래스나 패키지 내부로 캡슐화하고 이를 사용하는 클래스에서는 STRATEGY 패턴을 적용하라.

단위 테스트를 작성하다 보면 수많은 설계 이슈들의 아우성 소리가 들려온다. 단위 테스트를 작성하기 시작하면 프로그램은 더 이상 단순한 텍스트의 집합이 아니다. 프로그램은 이야기를 담고 있지만 그 이야기는 평면적이지 않다. 프로그램이 들려주는 이야기는 완결된 스토리를 가지고 있지만 그렇다고 해서 완전히 고정된 것은 아니다. 단위 테스트는 소프트웨어가 좀 더 소프트해지기를 요구한다. 결국 프로그램은 유연해져야 하며 유연한 설계는 훌륭한 프로그램의 필요 조건이다.

단위 테스트는 훌륭한 설계를 작성할 수 있도록 우리를 인도한다. 이것이 이번 컬럼의 주제다.

SEAM과 의존성 제어

의존성을 제어한다는 것은 의존하고 있는 코드를 다른 코드로 대체한다는 것을 의미한다. 그러나 의존성을 대체하기 위해 테스트 플래그를 이용하는 TEST HOOK을 적용할 필요는 없다.

전통적으로 PROCEDURAL TEST STUB은 필요한 코드가 준비되기 전까지 디버깅을 진행 하기 위해서 사용되었다. PROCEDURAL TEST STUB은 실행 시간에 교체할 수 없으며, 대부분의 절차적 프로그래밍 언어에서 이를 교체하는 것은 매우 어려운 작업이다. 프로덕션 코드에 테스트 코드를 추가하는 것에 대해 거부감을 느끼지만 않는다면 SUT(System Under Test) 내에 if testing then … else와 같은 TEST HOOK을 사용하는 PROCEDURAL TEST STUB을 구현할 수 있다.
- Gerard Meszaros, xUnit Test Patterns

TEST HOOK은 코드를 대체하기 어려운 절차형 언어에서 테스트를 용이하게 하기 위해 사용하는 방법이다. 현재 주류를 이루는 객체지향 언어에서는 TEST HOOK을 사용하지 않고도 의존성을 대체하는 것이 가능하다. 이를 위해서는 SEAM의 개념을 이해하는 것이 필요하다.

Michael Feathers는 그의 저서 “Working Effectively with Legacy Code”에서 SEAM을 다음과 같이 정의하고 있다.

SEAM이란 코드를 수정하지 않고도 프로그램의 행위를 변경할 수 있는 지점을 의미한다.

- Michael Feathers, Working Effectively with Legacy Code

단위 테스트 관점에서 SEAM이란 테스트 대상 코드를 수정하지 않고도 테스트 실행 중에 다른 코드로 대체할 수 있는 부분이라고 정의할 수 있다. SEAM은 코드를 대체하는 시점에 따라 컴파일 단계에서 행위를 대체하는 PREPROCESSING SEAM, 링크 단계에서 행위를 대체하는 LINK SEAM, 실행 시에 행위를 대체하는 OBJECT SEAM의 3가지로 분류할 수 있다. 대부분의 객체 지향 언어의 경우에는 OBJECT SEAM을 사용하는 것이 적절하다.

OBJECT SEAM은 객체 지향 프로그램 상에서 메소드 호출부가 존재할 때, 실제로 어떤 메소드가 호출되는 지는 실행 시간에 결정된다는 특성을 사용한다. <리스트 1>은 이전 컬럼에서 예제로 들었던 통계 코드에서 발췌한 것이다. 

<리스트 1> 통계 코드에서의 메소드 호출 예제
String [] inputPathes = jobConfiguration.resolveInputPath();

<리스트 1>에서 resolveInputPath() 메소드는 <그림 1>에 표시된 3개의 클래스 중 어떤 클래스의 메소드를 호출하는 것일까?

<그림 1> JobConfiguration 인터페이스의 계층도 

jobConfiguration이 어떤 객체를 가리키는 지 알지 못한 다면 어떤 메소드가 호출되는 지를 알 수는 없다. jobConfiguration은 DailyJobConfiguration의 인스턴스일 수도, WeeklyJobConfiguration의 인스턴스일 수도, MonthlyJobConfiguration의 인스턴스일 수도 있다.  실제로 어떤 객체를 가리키는 지는 코드의 전후 관계 또는 실행 시의 설정을 살펴보아야만 한다.

객체 지향 프로그램의 실행 구조는 소스 코드 구조와 일치하지 않는 경우가 종종 있다. 코드 구조는 컴파일 시점에 확정되는 것이고 이 구조에는 고정된 상속 클래스 관계들을 포함한다. 그러나 프로그램의 런타임 시 구조는 교류하는 객체들에 따라서 달라질 수 있다. 즉, 이 두 구조는 전혀 다른 별개의 독립성을 갖는다. 하나로부터 다른 하나를 이해하려는 것은 생태계의 동적인 성질을 식물과 동물과 같은 정적 분류 구조를 바탕으로 이해하려는 것과 똑같다.

- GOF, Design Patterns