Eternity's Chit-Chat

aeternum.egloos.com



유연한 설계를 위한 패턴과 원리 - 5.시간, 돈, 그리고 분석 패턴 1부 Supple Design

지금 몇시인가요?
시간과 관련된 기능 중 가장 일반적이고 빈번하게 사용되는 기능은 아마 현재 시간이나 날짜를 구하는 기능일 것이다. 대부분의 비즈니스 요구사항 안에는 특정 이벤트가 발생한 시점의 시간을 저장하거나 특정 시간과 현재 시간과의 차이를 구하는 기능이 포함된다.

현재 시간을 얻어오는 기능을 테스트하는데 있어 가장 큰 난관은 시간이 계속 변한다는 점이다. 현재 시간을 얻는 코드의 반환값은 코드가 호출되는 시간에 따라 달라지며, 이것은 테스트 실행 결과 역시 실행 시간에 의존한다는 것을 의미한다. 따라서 테스트의 성공 여부가 테스트 시간에 따라, 정확하게는 테스트를 실행하는 시스템이 반환하는 현재 시간에 따라 변경될 수 있다는 문제점이 있다.

이처럼 테스트 대상 클래스가 의존하는 외부 환경의 변화에 따라 테스트 결과가 변경된다면 외부에 대한 의존성을 끊어주어야 한다. 현재 시간을 구하는 일반적인 방법은 System.currentTimeMillis() 메소드를 호출하거나 java.util.Date 클래스를 사용하는 것이다. 그러나 코드 내에서 이런 식으로 유틸리티 클래스나 메소드를 직접 사용할 경우 테스트를 위해 외부 환경과의 의존성을 끊을 수 있는 방법이 존재하지 않는다. System.currentTimeMillis()는 정적 메소드이기 때문에 대체가 불가능하며, java.util.Date의 경우 구체적인 클래스를 직접 생성해야 하기 때문에 실행 시에 대체가 불가능하다. 따라서 현재 시간과 관련한 테스트를 쉽게 하기 위해서는 시간을 제공하는 도메인 객체를 추가하고 인터페이스와 클래스를 분리하여 실행 시간에 테스트용 오브젝트를 쉽게 DEPENDENCY INJECTION 할 수 있는 구조로 개발해야 한다.

지금까지 작성된 시간 관련 클래스의 중심에는 밀리초 단위로 시간을 관리하는 TimePoint 객체가 존재한다. 일단 현재 시간의 TimePoint를 얻으면 DayOfYear나 TimeOfDay와 같은 낮은 정밀도의 객체들을 쉽게 얻을 수 있다. 따라서 현재 시간에 대한 TimePoint를 반환하도록 클래스를 추가한 후, 반환된 TimePoint를 DayOfYear나 TimeOfDay 객체로 변환하면 쉽게 현재 시간을 관리할 수 있다.

DayOfYear.now()는 각 표준 시간대에 맞는 현재 일자를 반환하기 위해 TimeZone을 파라미터로 받는다.

DayOfYear.java
public
static DayOfYear now(TimeZone timeZone) {
  return TimePoint.now().asDayOfYear(timeZone);
}

현재 일자를 계산하기 위해서는 TimePoint의 현재 값을 얻어야 하므로 TimePoint에도 현재 시간을 반환하는 now() 메소드를 추가하도록 한다.

TimePoint.java
public
static TimePoint now() {
  return new TimePoint(new Date().getTime());
}

TimePoint의 now() 메소드에는 한가지 약점이 존재한다. 바로 내일이 되면 이 테스트가 실패한다는 사실이다. 매일 해당 테스트의 날짜를 변경해주지 않는 이상 테스트는 실패하게 될 것이며 일관성 없는 테스트 결과를 제공하는 테스트 케이스는 지속적인 통합과 같은 자동화된 빌드 기법을 사용할 수 없도록 막는 장애물이 된다. 이처럼 클래스가 외부 환경에 대한 의존성을 가질 경우 테스트가 어려워 진다.

이 모든 것이 의존성 문제다. 클래스가 테스트 내부에서 사용하기 어려운 어떤 것에 의존할 때, 그 클래스를 수정하고 사용하기가 어려워진다. ... 의존성은 흔히 테스트의 가장 명백한 장애물이다. 이 문제가 명백해지는 때는 테스트 하네스 내에서 인스턴스를 생성할 수 없거나 메소드를 실행하는데 어려움을 겪는 경우이다. 흔히 레거시 코드의 경우 테스트가 적절하게 동작하기 위해서는 의존성을 끊어야 한다.

- Michael C. Feathers,Working Effectively with Legacy Code


의존성은 전이된다. Date는 현재 일자를 구하기 위해 운영체제의 시스템 클럭을 사용한다. 따라서 Date는 JVM이 구동 중인 운영체제에 대한 의존성을 갖는다. 따라서 TimePoint 역시 운영체제에 의존성을 갖게 되며 DayOfYear 역시 운영체제에 의존성을 갖게 된다. 해결 방법은 TimePoint에서 Date로의 직접적인 의존성을 끊고 테스트가 실행될 때 원하는 시간을 설정할 수 있도록 TimePoint의 구조를 변경하는 것이다. 의존성을 끊고 결합도를 낮추는 가장 간단한 방법은 TimePoint와 Date 간에 추상 계층을 도입해서 의존성을 역전 시키는 것이다.

현재 시간을 반환하는 기능을 제공하는 Clock 인터페이스를 추가하자.
Clock.java
public
interface Clock {
  TimePoint now();
}

현재 시간을 계산하는 작업은 Clock 인터페이스를 구현하는 WorldClock 클래스에서 담당한다.

WorldClock.java
public
class WorldClock implements Clock {
  public TimePoint now() {
    return new TimePoint(new Date().getTime());
  }
}

TimePoint.now()는 Date를 직접 사용하는 대신 Clock 인터페이스를 사용한다. WorldClock을 사용하기 위해 클래스 변수로 clock을 추가하고 클래스 로딩 시에 WorldClock을 생성해서 clock에 대입한다. 기존의 DayOfYear에서 현재 시간을 가져오는 테스트를 실행시켜 테스트가 깨지지 않았는지 확인한다.

TimePoint.java

private
static Clock clock = new WorldClock();

public
static TimePoint now() {
  return clock.now();
}

이제 TimePoint가 Clock 인터페이스에 의존하도록 코드를 리팩토링했다. 다음 작업은 Clock이 반환하는 현재 시간을 원하는 값으로 수정할 수 있도록 Clcok의 스텁(Stub) 클래스를 추가하는 것이다. DayOfYear와 TimeOfDay, TimeZone을 사용해서 TimePoint를 설정하고 이를 반환할 수 있도록 Clock 인터페이스에는 존재하지 않는 setter 메소드를 추가한다.

StubClock.java
public
class StubClock implements Clock {
  private TimePoint timePoint;

  public
void setTimePoint(DayOfYear dayOfYear,
TimeZone zone) {
    timePoint = TimePoint.atMidnight(dayOfYear, zone);
  }

  public void setTimePoint(DayOfYear dayOfYear, TimeOfDay timeOfHour,
    TimeZone zone) {
    timePoint = TimePoint.at(dayOfYear, timeOfHour, zone);
  }

 
public TimePoint now() {
    return timePoint;
  }
}

StubClock 클래스는 테스트에서만 사용되므로 프로덕션 코드의 소스 폴더가 아니라 테스트 코드의 소스 폴더에 위치시켜야 한다. 일반적으로 테스트 용 클래스의 경우 클래스 사용자들을 위해 Javadoc을 생성하지 않으며, 테스트 케이스를 포함시키지 않고 손쉽게 어플리케이션 jar 파일로 묶을 수 있어야 하기 때문이다.

이제 Clock 인터페이스를 구현하는 두 개의 클래스인 WorldClock과 StubClock이 완성되었다. WorldClock은 운영 환경에서 실제 시각을 얻기 위해 사용되고, StubClock은 테스트 환경에서 우리가 원하는 시각을 TimePoint가 반환하도록 하기 위해 사용된다. TimePoint에 Clock을 대체할 수 있도록 setter 메소드를 추가하자.

TimePoint.java

public static
void setClock(Clock clock) {
  TimePoint.clock = clock;
}

TimePoint의 static 변수인 clock을 외부에서 설정 가능하도록 수정했다. 현재 일자를 얻어 오는 DayOfYear 테스트에서는 StubClock에 원하는 시간을 설정하고 이를 TimePoint에 설정함으로써 DayOfYear.now()가 원하는 날짜를 반환하는지를 테스트할 수 있다. 테스트에 사용할 일자를 설정해야 하므로 stubClock 객체의 타입으로 Clock 인터페이스가 아닌 setter 메소드를 가진 StubClock 클래스를 사용했다는 사실에 주목하자.

DayOfYearTest.java
@Test
public void getCurrentDateUsingTimeZone () {
  StubClock stubClock = new StubClock();
  stubClock.setTimePoint(DayOfYear.at(2008, 11, 2),
    TimeOfDay.at(10, 30), TimePoint.KST);
  TimePoint.setClock(stubClock);

  DayOfYear dayOfYear = DayOfYear.now(TimePoint.
KST);
  assertEquals(DayOfYear.at(2008, 11, 2), dayOfYear);
}

현재 시간을 얻어올 때마다 매번 표준 시간대를 전달하는 방식은 불편히다. 표준 시간대를 파라미터로 받지 않는 now() 메소드는 테스트가 실행되는 기본 시간대를 사용해서 현재 날짜를 반환하도록 하자. StubClock 역시 표준 시간대를 받지 않을 경우 기본 시간대를 사용해서 내부의 TimePoint를 설정한다.

표준 시간대를 파라미터로 받지 않는 now() 메소드는 기본 시간대를 전달해서 DayOfYear를 계산한다.

DayOfYear.java
public
static DayOfYear now() {
  return TimePoint.now().asDayOfYear(TimeZone.getDefault());
}

이제 테스트에서 우리가 원하는 값으로 현재 시간을 설정할 수 있게 되었다. 현재 시간을 설정할 수 있는 StubClock이 정말 유용할까? 아마 무엇을 상상하든 그 이상을 보게 될 것이다.

PATTERNS & PRINCIPLES
DEPENDENCY INJECTION

소프트웨어에서 발생하는 모든 문제의 원인을 파헤쳐 가다 보면 결국 의존성이라는 근원에 도달하게 된다. 어떤 코드를 수정했더니 전혀 예상하지 못 했던 곳에서 버그가 발생했다면 두 코드 간에는 어떤 식으로든 의존성이 존재하게 된다. 정상적으로 실행되던 시스템이 특정 시간만 되면 이상한 결과를 쏟아 낸다면 시간에 대한 의존성이 문제인 경우가 대부분이다.
소프트웨어 개발자로서 가장 빈번하게 마주치는 의존성은 클래스와 클래스 간의 의존성이다. 두 클래스가 너무 단단하게 결합되어 유연성이 결여되어 있다면 수정하기가 어려워진다. 결합도가 높은 시스템은 한 지점에서의 오류가 시스템 구석 구석으로 빠른 속도록 전파된다. 진원으로부터 수천 킬로미터 밖에 떨어진 도시가 영향을 받는 것 처럼, 문제가 발생하기 시작한 코드와 개념적으로 거리가 먼 코드가 붕괴되기 시작한다.
의존성을 낮추는 가장 효과적인 구현 방법은 테스트를 먼저 작성하는 것이다. 테스트 케이스에서 해당 클래스를 실행시키기 위해서는 불필요한 의존성을 끊고 결합도를 낮출 수 밖에 없다. 테스트 가능한 설계를 하기 위해 고민할 필요 없이 테스트를 먼저 만들기 시작하면 자연스럽게 테스트 가능한 코드를 만들 수 있다.
만약 테스트할 클래스 코드가 외부 환경에 의존한다면 어떻게 해야 할까? 클래스 내에 외부 환경에 의존하는 부분을 하드 코딩하지 말고 이를 STRATEGY로 만들어 클래스에 전달 한다. 그리고 테스트 케이스에서 대체가 가능하도록 구체적인 클래스가 아니라 인터페이스를 사용한다. 인터페이스는 추상화의 개념을 극한으로 사용하기 때문에 구현에 의한 변경의 파급도를 최소화할 수 있다.
DEPENDENCY INJECTION을 적용함으로써 시스템 전체적인 결합도를 낮출 수 있고, 의존성을 쉽게 관리할 수 있으며, 변경에 의한 파급 효과를 제어할 수 있다. 그리고 DEPENDENCY INJECTION으로 얻을 수 있는 가장 커다란 효과는 테스트가 용이해 진다는 것이다. 이 말을 명심하라. 좋은 설계란 테스트 하기 쉬운 설계다.