Eternity's Chit-Chat

aeternum.egloos.com



유연한 설계를 위한 패턴과 원리 - 4.잃어버린 시간을 찾아서 1부 Supple Design

소프트웨어와 시간
소프트웨어의 관점에서 시간은 스칼라다. 즉, 시간은 크기를 가지며 비교 가능한 어떤 것이다. 시간의 상대적인 크기에 따라 과거, 현재, 미래가 결정된다. 컴퓨터에서 현재 시간을 표현하는 전통적인 방법은 에폭(epoch)이라고 불리는 GMT 1970년 1월 1일 0시부터 현재까지 경과된 밀리초를 사용하는 것이다. Java에서는 System.cuurentTimeMills() 메소드를 이용하면 이 값을 얻을 수 있다.
java.util.Date나 java.util.Calendar 클래스 역시 내부적으로는 에폭으로부터의 경과 시간을 사용하여 날짜를 표현한다.

그러나 Java의 표준 라이브러리만을 사용해서 다양한 시간 개념을 표현하는 데에는 한계가 있다. Java의 Calendar를 사용해서 시간을 표현하기 위해서는 복잡한 단계의 코드를 작성해야 한다. 또한 시와 분 단위만 필요한 경우에도 표준 라이브러에서는 모든 시간 단위를 조작하는 클래스를 제공하기 때문에 불필요한 로직이 많아 진다. 우리에게 필요한 것은 선언적으로 시간을 표현하는 방법이다. 즉 시간을 생성하는 방법(how)이 아니라 원하는 시간(what)을 기술하는 방식으로 프로그램을 작성할 수 있어야 한다.

이번 호에는 앞에서 설명한 유연한 설계를 위한 패턴을 적용하여 선언적으로 프로그래밍이 가능한 시간 라이브러리를 작성해 볼 것이다. 


표준 시간대
앞에서 살펴본 바와 같이 컴퓨터에서의 시간은 에폭으로부터 경과된 밀리초를 사용해서 표현한다. 불행하게도 지구는 둥글고 자전하기 때문에 국제화 요구사항을 가진 어플리케이션에서 시간을 다루기 위해서는 지역에 따른 시차 문제를 고려해야 한다.

지구 상의 모든 지역은 영국의 그리니치 천문대를 기준으로 경도 15도마다 1시간씩의 시차를 두고 있는데 이처럼 지리적 위치에 따라 적용되는 기준 시간을 표준 시간대(Time Zone)라고 한다. 이 때 영국의 그리니치 천문대에서 측정한 기준 시간을 GMT라고 한다. 한국의 표준 시간대는 KST(Korean Standard Time)이며 GMT에 9시간을 더한 값이다.

핸드폰 과금 시스템의 경우 해외 로밍 서비스를 이용한 가입자의 통화 정보를 처리하기 위해서는 현지의 통화 시간을 한국 표준시로 변환하여 처리해야 한다. 따라서 표준 시간대의 개념과 시차라는 도메인 개념을 코드에 녹여야 한다.

가장 먼저 GMT 1970년 1월 1일부터 현재까지 경과된 밀리초 값을 표현하는 TimePoint 클래스를 추가하도록 하자. 지금부터는 시간을 구성하는 클래스들의 전체적인 구조와 관계에 초점을 맞추기 위해 특별한 경우가 아니면 테스트 코드를 생략하기로 한다. 테스트 용이성을 위해 밀리초 값을 반환하는 getMilliseconds() 메소드를 추가했으며 테스트 케이스에서만 사용할 것이므로 이 메소드에는 기본 가시성을 부여한다.

 

TimePoint.java

private long miliseconds;

 

TimePoint(long miliseconds) {

  this.miliseconds = miliseconds;

}

 

long getMilliseconds() {

  return miliseconds;

}

 

TimePoint는 Money와 마찬가지로 VALUE OBJECT이며 GMT에서의 시간을 생성하는 CREATION METHOD인 atMidnightGMT() 메소드를 제공한다. atMidnightGMT() 메소드는 인자로 전달되는 연, 일, 월 정보를 사용하여 GMT 표준 시간대의 자정 시간을 계산한다. 좀 더 범용적으로 사용할 수 있도록 표준 시간대를 인자로 받는 CREATION METHOD at()을 추가하고 atMidnightGMT() 메소드가 내부적으로 at()을 호출하도록 구현하자. at()과 atMidnightGMT() 메소드는 표준 시간대와 관련된 생성 의도를 적절하게 표현하는 INTENTION REVEALING INTERFACE의 예이다.

 

TimePoint.java

public static final TimeZone GMT = TimeZone.getTimeZone("Universal");

public static final TimeZone KST = TimeZone.getTimeZone("Asia/Seoul");

 

public static TimePoint atMidnightGMT(int year, int month, int day) {

  return at(year, month, day, 0, 0, 0, 0, TimePoint.GMT);

}

 

public static TimePoint atMidnight(int year, int month, int day, TimeZone zone) {

  return at(year, month, day, 0, 0, 0, 0, zone);

}

 

public static TimePoint at(int year, int month, int day,

  int hour, int minute, TimeZone zone) {

  return at(year, month, day, hour, minute, 0, 0, zone);

}

 

public static TimePoint at(int year, int month, int day,

  int hour, int minute, int second, int milisecond, TimeZone zone) {

  Calendar calendar = Calendar.getInstance(zone);

  calendar.set(Calendar.YEAR, year);

  calendar.set(Calendar.MONTH, month-1);

  calendar.set(Calendar.DAY_OF_MONTH, day);

  calendar.set(Calendar.HOUR_OF_DAY, hour);

  calendar.set(Calendar.MINUTE, minute);

  calendar.set(Calendar.SECOND, second);

  calendar.set(Calendar.MILLISECOND, milisecond);

  return new TimePoint(calendar.getTime().getTime());

}

 

TimePoint의 밀리초 값이 현재 TimePoint의 밀리초 값보다 작으면 과거를 나타내고 현재 TimePoint의 밀리초 값보다 크면 미래를 나타낸다. 따라서 시간적인 선후 관계를 명확하게 판단할 수 있도록 isBefore()와 isAfter() 메소드를 추가한다. Comparable 인터페이스를 구현하도록 하여 TimePoint가 비교 가능한 개념임을 코드 상에 명확히 표현하고 isBefore()와 isAfter() 메소드에서 Comparable 인터페이스의 comapreTo() 메소드를 호출하도록 한다. TimePoint는 VALUE OBJECT이므로 두 객체의 비교를 위해 equals()와 hashCode()를 오버라이딩하는 것도 잊지 말자.

 

TimePoint.java

public class TimePoint implements Comparable<TimePoint> {

  ......

 

  public boolean isBefore(TimePoint other) {

    return compareTo(other) < 0 ;

  }

 

  public boolean isAfter(TimePoint other) {

    return compareTo(other) > 0 ;

  }

 

  public int compareTo(TimePoint other) {

    long difference = getMilliseconds() – other.getMilliseconds();

 

    if (difference < 0) {

      return -1;

    }

 

    if (difference > 0) {

      return 1;

    }

 

    return 0;

  }

 

이제 시간이라는 도메인을 표현하기 위해 필요한 가장 기본적인 TimePoint 클래스를 추가했다. 잠시 여유를 두고 지금까지 작성된 테스트 코드를 음미해보자. 그리고 Calendar와 Date를 사용해서 동일한 의미의 코드를 작성한다고 가정하고 두 코드를 비교해 보자. TimePoint가 좀 더 직관적이고 사람의 언어에 더 가깝다는 느낌이 들지는 않는가? TimePoint는 인간이 인식하고 있는 보편적인 시간 개념에 좀 더 근접한 코드를 개발할 수 있도록 한다. 지금부터 이 작은 발걸음이 프로그램을 얼마나 깔끔하고 아름답게 만드는지를 살펴볼 것이다.