Eternity's Chit-Chat

aeternum.egloos.com



의존성 끊기와 단위 테스트 – 5부[完] Software Quality

객체 지향의 이러한 특성을 사용해서 jobConfiguration이 가리키는 객체를 변경할 수 있다면 해당 호출 부분은 OBJECT SEAM이 될 수 있다. 그러나, 모든 메소드 호출 부분이 OBJECT SEAM인 것은 아니다. <리스트 2>의 경우 jobConfiguration이 가리키는 객체를 변경하는 것이 불가능하기 때문에 SEAM이라고 할 수 없다. 소스 코드 구조와 실행 구조가 동일할 경우 SEAM의 개념을 이용하는 것이 불가능하다.

<리스트 2> SEAM이 존재하지 않는 경우
public void resolvePathes() {
  ...
  JobConfiguration jobConfiguration = new DailyJobConfiguration();
  ...
  String [] inputPathes = jobConfiguration.resolveInputPath();
  ...
}

코드를 수정하지 않고도 해당 코드의 행위를 변경하기 위해서는 행위를 선택할 수 있는 수단을 제공해야 한다. 이를 ENABLING POINT라고 한다.


모든 SEAM에는 특정 행위를 선택할 수 있는 코드 상의 지점인 ENABLING POINT가 존재한다.
- Michael Feathers, Working Effectively with Legacy Code
<리스트 2>에는 행위를 선택할 수 있는 ENABLING POINT가 존재하지 않는다. 따라서 만약 DailyJobConfiguration이 인프라스트럭처에 대해 의존하고 있을 경우 해당 클래스에 대한 단위 테스트를 실행하기가 쉽지 않다. 의존성을 제어하기 위해서는 <리스트 3>과 같이 ENABLING POINT를 제공해야 한다. 

<리스트 3> ENABLING POINT가 존재하도록 리팩토링
public void setJobConfiguration(JobConfiguration jobConfiguration) {
  this.jobConfiguration = jobConfiguration;
}

public
void resolvePathes() {
  ...
  String [] inputPathes = jobConfiguration.resolveInputPath();
  ...
}

<리스트 3>을 테스트하기 위해서는 JobConfiguration 인터페이스를 상속받는 새로운 TEST STUB을 만들고 이를 setJobConfiguration에 전달하면 된다. ENABLING POINT를 통해 코드 상에 OBJECT SEAM을 추가함으로써 단위 테스트 가능한 코드를 얻게 되었다. 그리고 OBJECT SEAM이 존재하는 코드는 일반적으로 유연하고 응집도가 높은 설계를 가지게 된다.

SEAM과 유연한 설계, 그리고 단위 테스트

앞의 예제를 통해 OBJECT SEAM을 사용할 경우 코드가 유연해 진다는 사실을 알게 되었다. OBJECT SEAM은 런 타임 시에 객체를 변경할 수 있도록 하기 때문에 객체 간에 낮은 결합도를 유지할 수 있도록 해 준다. 단위 테스트는 실제로 OBJECT SEAM이 필요한 위치에 대한 정보를 제공한다.

단위 테스트와 동시에 코드를 작성할 경우 코드가 유연해지고 설계가 향상된다. 따라서 테스트 주도 개발(Test-Driven Development, TDD) 방식으로 개발된 코드가 그렇지 않은 코드보다 더 훌륭한 방식으로 설계될 확률이 높다. 의존성 끊기 게임의 목적은 단위 테스트를 가능하게 함으로써 설계를 향상시키는 것이다.

TDD는 테스트에 관한 것이 아니다. 프로그래밍과 설계에관한 것이다. TDD는 더 단순하고, 더 깔끔하고, 더 견고한 코드를 작성하는 일과 관련된 것이다!(물론, 부수 효과로 작성된 단위 테스트 케이스는 매우 중요하다.)
- Jimmy Nilsson, Applying Domain-Driven Design and Patterns


코드를 단순히 평면적인 텍스트의 집합으로 보지 않고 그 안에서 적절한 SEAM을 식별하는 방법을 익히는 것이 코드의 테스트 용이성을 향상시키는 최선의 방법이다. 그리고 테스트 용이성을 목표로 할 때 훌륭한 설계를 얻을 수 있다. 코드를 작성하기 전에 테스트를 작성하는 TDD 방식은 다양한 방식으로 OBJECT SEAM을 강요하기 때문에 자연스럽게 훌륭한 설계에 도달할 수 있도록 한다.


대부분의 경우 테스트 가능한 애플리케이션을 목표로 할 경우 훌륭한 애플리케이션 코드를 작성하게 된다.
- Rod Johnson, Expert One-On-One J2EE without EJB
앞서 설명한 것처럼 테스트 가능한 애플리케이션을 작성하는 방법은 테스트와 코드를 함께 작성하는 것이다. 그러나 테스트와 함께 코드를 작성하더라도 훌륭한 설계에 대한 감각을 기르지 않는다면 최적의 설계에 이를 수 없다. 테스트 용이성을 향상시킬 수 있는 다음과 같은 지침이 설계를 향상시키기 위해서도 도움이 될 것이라 생각한다.

  • 구현이 아닌 인터페이스에 따라 프로그래밍하라
    인터페이스에 대해 프로그래밍을 하면 할수록, 실행 시간이나 테스트 시점에 서로 다른 구현체를 좀 더 자유롭게 플러그 인할 수 있다. 이것은 구현 세부 사항을 변경할 수 있는 OBJECT SEAM을 손쉽게 추가할 수 있는 아키텍처적인 유연성을 제공한다. 인터페이스에 따라 프로그래밍함으로써 테스트에 적합한 TEST STUB이나 MOCK OBJECT를 구현할 수 있으며 인프라스트럭처에 대한 의존성을 보다 쉽게 제어할 수 있다.
  • 상속보다는 합성을 사용하라
    객체 지향 프로그래밍 환경에서 기능을 재사용하는 방법은 상속(Inheritance)과 합성(Composition)의 두 가지 방식이 존재한다. 상속은 클래스와 클래스 간의 정적인 관계를 통해 기능을 재사용하며 두 클래스 간의 관계가 컴파일 시점에 고정된다. 합성은 객체와 객체 간의 동적인 관계를 통해 기능을 재사용하며 두 객체 간의 관계가 고정적이지 않고 실행 시간에 변경 가능하다.
    일반적으로 상속은 캡슐화를 저해한다. 부모 클래스를 수정할 경우 모든 서브 클래스가 영향을 받게 된다. 따라서 변경에 대한 파급효과를 줄이기 위해서는 상속보다는 합성을 사용하는 것이 유리하다.
    단위 테스트 관점에서 상속보다 합성이 선호되는 이유는 무엇인가? 상속 계층을 다룰 경우 테스트 하니스 상에서 객체를 생성하기가 어려워질 수 있기 때문이다. 부모 클래스가 생성자의 파라미터로 거대한 객체 그래프를 취하는 경우를 생각해 보자. 서브 클래스의 일부 기능을 테스트하기 위해 전체 객체 그래프가 필요하지 않음에도 불구하고 테스트를 위해서는 해당 객체 그래프를 부모 클래스의 생성자로 전달해야 한다. 또한 부모 클래스가 인프라스트럭처와 강하게 결합되어 있을 경우에도 하위 클래스를 단위 테스트하는 것이 어렵다.
    상속의 경우 컴파일 타임에 관계가 고정된다는 사실을 기억하자. 따라서 상속을 사용하기 위해서는 실행 시점이 아닌 컴파일 시점에 ENABLING POINT를 제공해야 한다는 제약이 따른다. 따라서 OBJECT SEAM을 추가하고자 한다면 가능하면 상속 보다는 합성을 선택하고 객체와 객체를 조합하는 위치에 ENABLING POINT를 제공하도록 하자. 이를 위한 가장 훌륭한 방법은 STRATEGY 패턴을 이용하는 것이다.
  • static 메소드와 SINGLETON의 사용을 피하라
    static 메소드와 SINGLETON 패턴은 클라이언트 코드를 구체적인 클래스와 강하게 결합시킨다. static 메소드와 SINGLETON이 단위 테스트 하니스 상에서 인스턴스를 생성하거나 실행시키기 어려운 대상에 의존할 경우 인스턴스를 변경할 수 있는 static setter 를 이용하거나 최악의 경우 바이트 코드를 갱신시키는 기법을 사용하지 않는 한 이를 대체하기가 거의 불가능하다.
    물론 static 메소드와 SINGLETON을 사용하는 것이 무조건 잘못 된 것은 아니다. 클래스 계층 구조에 섞여 있는 유틸리티성 메소드를 외부로 빼내거나 Thread Local을 사용하기 위해서는 static 메소드가 유용하다. 하나의 인스턴스가 필요한 경우에는 SINGLETON을 사용하는 것이 유용하다. 그러나 전역 접근이나 DEPENDENCY LOOKUP을 위해 SINGLETON을 사용하는 것은 시스템 전체적인 결합도를 높이는 것이다. 가능하면 구체적인 클래스에 강하게 얽매이는 코드를 작성하지 마라. 잘못 사용된 static 메소드와 SINGLETON은 SEAM에 대한 안티 패턴이다.
    결론적으로 static 메소드와 SINGLETON의 경우 SEAM을 식별하는 것이 어렵기 때문에 이를 인스턴스 메소드와 인터페이스 기반의 클래스로 변경하는 것이 좋다. 
     
  • 의존성을 고립시켜라
    이전 컬럼에서는 예제로 사용한 통계 애플리케이션을 테스트 가능하도록 만들기 위해 Apache Hadoop에 관련된 코드를 다른 코드로부터 분리시켰다. 이렇게 함으로써 Apache Hadoop에 의존하지 않는 코드에 대해 단위 테스트를 실행할 수 있었다.
    일반적으로 테스트하기 어려운 프레임워크, 표준 라이브러리, 인프라스트럭처에 관련된 코드를 개별적인 클래스로 고립시키는 것이 좋다. 이것은 높은 응집성과 낮은 결합도를 달성할 수 있는 가장 간단한 방법인 동시에 기본적인 캡슐화 원칙을 지키는 설계 방법이다. 별도로 고립시킨 클래스에 대한 인터페이스를 제공함으로써 이를 사용하는 클라이언트에 대해 SEAM을 제공할 수 있다. SEAM을 지원하기 위해 ENABLING POINT를 작성하는 가장 간단한 방법은 DEPENDENCY INJECTION을 사용하는 것이다.
     
  • 의존성을 요청하지 말고 주입하라
    static 메소드와 SINGLETON을 통해 알아본 것처럼 의존성이 하드코딩되어 있으면 SEAM을 제공하기 어렵다. 구체적인 클래스가 아닌 인터페이스에 대해 프로그래밍하고 구체적인 클래스는 이를 사용하는 클래스가 아닌 외부에서 설정하도록 하라. DEPENDENCY LOOKUP의 경우에는 사용하려는 객체에 대한 의존성은 제거하지만 객체 탐색 메커니즘에 대한 의존성은 여전히 존재한다. DEPENDENCY INJECTION이 OBJECT SEAM에 대한 ENABLING POINT를 제공하기 위한 가장 간단하면서도 최적의 방법이다.
    현재 Spring을 위시로 한 많은 프레임워크들이 DEPENDENCY INJECTION을 제공하고 있으므로 이를 적극 활용하도록 한다. DEPENDENCY INJECTION을 사용할 경우 자연스럽게 STRATEGY 패턴, 인터페이스에 대한 프로그래밍, 상속보다는 합성의 원칙을 준수하게 된다.

<그림 2>는 이전 연재에서 의존성 끊기 게임의 결과로 얻게 된 코드의 구조를 나타낸 것이다. 구체적인 클래스 대신 JobConfiguration 인터페이스를 사용하고, LogAnalyzeJob과 JobConfiguration을 합성 관계로 구성하며, Apache Hadoop에 대한 의존성은 모두 LogAnalyzeJob 내부로 고립시키고, LogAnalyzeJob에서 JobConfiguration으로의 의존성은 DEPENDENCY INJECTION에 의해 외부에서 주입된다. 리팩토링된 코드는 단위 테스트 가능하며, 확장 가능한 동시에 유연한 설계를 가지게 되었다.

<그림 2> 테스트 가능한 코드를 목표로 할 경우 설계가 개선된다.

의존성 끊기와 단위 테스
시스템은 상호 관계를 맺고 있는 수 많은 객체로 구성되어 있다. 단위 테스트의 목적은 이렇게 얽히고 설킨 객체를 고립시켜 예상한 대로 동작하는 지를 테스트하는 것이다. 단위 테스트를 실행하기 위해서는 적절한 위치에서 의존성을 끊어야 한다.

평면적인 텍스트의 목록이 아닌 SEAM의 관점에서 프로그램 코드를 살펴 보는 것은 효과적으로 단위 테스트를 수행할 수 있는 코드가 무엇인지에 관한 통찰을 제공한다. SEAM을 고려해서 작성된 코드는 높은 응집도와 낮은 결합도를 가지고 있으며 적절한 책임을 지닌 작은 객체들로 구성된 캡슐화가 잘 된 코드다. 

SEAM을 고려한 코드는 단위 테스트가 가능한 코드이며, 단위 테스트가 가능한 코드는 훌륭하게 설계된 코드다. 훌륭한 코드를 작성하는 개발자로 성장하고 싶은가? 그렇다면 테스트 가능한 코드를 작성하기 위해 노력하라. 테스트 가능한 코드가 곧 훌륭한 코드일 가능성이 높다.


덧글

  • 민달 2010/11/01 09:30 # 삭제

    5편의글잘보고갑니다 ~~
  • 이터너티 2010/11/01 19:18 #

    옙 감사합니다. ^^
    닉네임이 민달이라서 혹시 민창 과장님인가 생각했는데 맞는지 모르겠네요.
  • 민달 2010/11/03 01:05 # 삭제

    네 맞아요~^^
  • 이터너티 2010/11/04 15:14 #

    ㅎㅎ 맞군요 ^^
    저도 민창 과장님 블로그 종종 들어가는데 이렇게라도 자주 뵈요~ ㅋ
  • ologist 2010/11/16 13:17 # 삭제

    글 잘보고 갑니다. 통계가 예제로 있으니 내용이 쏙쏙 들어오네요...^^
  • 이터너티 2010/11/16 18:32 #

    블로그쪽도 통계때문에 많이 힘드셨죠?
    카페는 통계 안정화시키느라 고생이 좀 심했어요.ㅠㅠ
    고생한만큼 배운 것도 많았던것 같아요. ^^
  • pistos 2010/12/21 22:44 # 삭제

    아앗.... 부끄럽사와요.
    저때문에 고생 많으셨습니다 (__)
  • 이터너티 2010/12/22 18:05 #

    아앗... 그런게 아닌데... ^^;;
    카페 통계 나름 재미있었어요. ^^
  • 2015/08/12 00:28 # 삭제 비공개

    비공개 덧글입니다.
  • 이터너티 2015/08/12 01:17 #

    두번째 예제에서 setJobConfiguration() 메서드 안의 this는 인스턴스 변수와 파라미터의 이름이 같아서 구분을 위해서 필수적으로 붙여야 하지만 resolvePathes() 메서드 안에서는 혼동의 여지가 없으므로 this를 생략할 수 있습니다. :-)
※ 로그인 사용자만 덧글을 남길 수 있습니다.