LocalDateTime.now() 테스트 방법

이번 글에서는 LocalDateTime.now() 테스트 방법과 현재 시간에 특정 타임존을 적용하는 방법에 대해서 알아보겠습니다. Java8 부터 제공되는 유틸 클래스인 LocalDateTime이 제공하는 LocalDateTime.now() 메서드는 현재 시간을 반환합니다. 즉, 고정된 값이 아니라 매 초 다른 값이 반환 됩니다. 현재 시간과 관련된 테스트 코드를 작성하려면 고정된 시간 값을 반환할 수 있어야 합니다. 어떻게 고정된 시간 값을 지정하여 테스트 코드를 작성했는지 공유하려 합니다.

저는 멀티테넌시 구조의 프로젝트를 진행하면서 테넌트의 타임존에 따라 변환된 현재 시간을 반환하는 공통 메서드를 설계하고 구현해야 했습니다. 먼저 특정 타임존이 적용된 현재 시간을 제대로 가져오는지 확인하는 일이 우선이었습니다. 시스템의 시간을 변경해 두고 Application을 실행해서 직접 확인하는 등의 수동적인 테스트 방법은 매우 비효율적이라고 생각했고, 충분히 테스트 코드를 작성해서 자동화 할 수 있다고 생각했습니다. 그래서 특정 타임존에 따라 올바른 오프셋이 더해진 시간을 반환하는지, 그리고 서머타임 적용 기간, 적용 해제된 기간에도 올바른 시간을 반환하는지 확인할 수 있는 테스트 코드를 작성해서 검증해보기로 했습니다.

이를 확인하기 위해서는 특정 타임존에서 서머타임이 적용중일 때, 그렇지 않을 때, 서머 타임이 시작될 때, 서머타임이 종료될 때 기대하는 시간 값을 반환하는지 확인해야 합니다. 예를 들어, 타임존 아이디가 Asia/Seoul 이라면 UTC+9가 적용되기 때문에 현재 서버의 UTC 기준 시간에 9 시간이 더해져야 합니다. 즉, UTC 기준 시간이 9시 30분 이라면 서울은 18시 30분이 됩니다. 그리고 타임존 아이디가 America/New_York이고, 2024년에 서머 타임 적용이 막 시작된다면, 3월 둘째 주 일요일 새벽 1시 59분에서 2시가 되어야 하는 시점에 2시가 되는 것이 아니라 3시로 한 시간을 건너뛰게 됩니다. 서머 타임 적용이 종료된다면, 11월 첫째 주 일요일 새벽 1시 59분에서 2시가 되어야 하는 시점에 2시가 되는 것이 아니라 1시로 되돌아 가게 됩니다. 우리는 이를 검증할 수 있는 코드를 작성할 수 있어야 합니다.

서머타임 알아보기 👈click !!

테스트 가능한 메서드 만들기

그래서 저는 처음에 LocalDateTime (UTC 기준 현재 시간)과 ZoneId (현재 테넌트의 타임존 아이디) 객체를 파라미터로 받는 메서드를 정의했습니다. 그러면 클라이언트가 메서드를 호출할 때 타임존 아이디와 함께 LocalDateTime.of() 메서드를 사용해서 고정된 시간 값을 직접 넣어줄 수 있습니다. 그러면 테스트 할 시간 정보와 타임존 아이디를 마음대로 지정해서 테스트 해볼 수 있게 됩니다. 타임존이 적용된 현재 시간을 반환하기 위한 로직은 UTC 기준 시간에 타임존 오프셋을 더해서 반환하도록 구현하면 되었습니다.

그런데 이게 과연 최선일까요? 테스트 코드를 작성하기 위해서 메서드에 불필요한 파라미터를 추가했다는 느낌을 지울 수가 없었습니다.

기본 제공되는 메서드로 테스트하기

우리가 한번쯤 고민했던 문제는 누군가 이미 해결해 놓았을 가능성이 매우 높죠. 조금 찾아보니 역시 저와 같은 고민을 하는 사람들이 있었고 이미 해결된 문제였습니다. 제가 했던 것처럼 불필요한 파라미터를 추가하게 되는 문제를 해결하기 위해서 LocalDateTime 클래스는 이를 고려한 정적 메서드를 이미 제공하고 있었습니다.

ZonedDateTime zonedDateTime = ZonedDateTime.of(2024, 3, 10, 1, 59, 59, 0,
    ZoneId.of("America/New_York"));
Clock fixedClock = Clock.fixed(zonedDateTime.toInstant(),
    ZoneId.of("America/New_York"));

LocalDateTime.now(fixedClock); //고정된 시간으로 LocalDateTime.now(ZoneId.of("America/New_York")) 를 호출한 것과 같다

이렇게 하면 LocalDateTime.now가 특정 타임존에 대한 고정 시간을 반환하게 만들 수 있습니다. LocalDateTime.now(Clock) 메서드는 특정 시각을 나타내는 Clock 객체를 파라미터로 받습니다. Clock 객체는 고정된 시간 값과 타임존을 설정할 수 있습니다. 덕분에 개발자가 테스트 코드로 기능을 검증하기 위해서 메서드에 불필요한 매개변수를 추가할 필요가 없어집니다. LocalDateTime.now(Clock) 메서드의 주석을 보면 아래와 같이 작성되어 있습니다.

    /**
     * Obtains the current date-time from the specified clock.
     * <p>
     * This will query the specified clock to obtain the current date-time.
     * Using this method allows the use of an alternate clock for testing.
     * The alternate clock may be introduced using {@link Clock dependency injection}.
     *
     * @param clock  the clock to use, not null
     * @return the current date-time, not null
     */
    public static LocalDateTime now(Clock clock) {
        ...
    }

지정된 Clock에서 현재 시간을 가져오며, 테스트를 위해 사용될 수 있다고 주석이 남겨져 있습니다. 테스트 코드에서 어떻게 활용할 수 있는지는 아래에서 확인해 보세요.

서머타임 적용 검증을 위한 테스트 코드

2024년 뉴욕에서 서머타임 적용이 시작될 때와 종료될 때 offset이 어떻게 변하는지, LocalDateTime(ZoneId) 메서드 호출 시 현재 시간이 어떻게 반환 되는지 테스트 코드를 통해 확인해 보겠습니다. 테스트 코드에서는 LocalDateTime.now(Clock) 메서드를 호출하는데, Clock 객체에 America/New_York 타임존을 세팅해두었기 때문에 프로덕션 코드에서 LocalDateTime.now(ZoneId.of("America/New_York"))를 호출했을 때의 결과를 테스트 하는 것과 같습니다.

LocalDateTime.now(ZoneId)ZoneId 객체를 받아서 타임존이 적용된 현재 시간을 반환하는 기능을 제공합니다. 특정 시간에 타임존이 제대로 적용되어 동작하는지 확인하려면 현재 시간을 마음대로 설정해서 테스트할 수 있어야 하는데, LocalDateTime.now(ZoneId) 는 현재 시간을 반환하므로 매 초 다른 시간을 반환합니다. 매번 다른 값을 반환하기 때문에 테스트 코드에서 LocalDateTime.now(ZoneId)를 호출해서는 테스트 할 수 없습니다. 그래서 ZoneId 대신 Clock 객체와 협력해서 과거나 미래로 시간을 고정하여 테스트할 수 있도록 만들어져 있습니다.

@DisplayName("뉴욕에서 서머타임 적용이 시작될 때 한 시간을 건너뛴다. 2024-03-10T01:59:59 → 2024-03-10T03:00")
@Test
void New_York_서머타임_적용_시작() {
  //뉴욕 서머타임 2024년 3월 둘째주 일요일 2시 부터 적용 시작
  ZonedDateTime inactiveDSTZonedDateTime = ZonedDateTime.of(2024, 3, 10, 1, 59, 59, 0,
      ZoneId.of("America/New_York"));
  Clock fixedBeforeDST = Clock.fixed(inactiveDSTZonedDateTime.toInstant(),
      ZoneId.of("America/New_York"));
  ZonedDateTime activeDSTZonedDateTime = ZonedDateTime.of(2024, 3, 10, 2, 0, 0, 0,
      ZoneId.of("America/New_York"));
  Clock fixedAfterDST = Clock.fixed(activeDSTZonedDateTime.toInstant(),
      ZoneId.of("America/New_York"));

  LocalDateTime resultInactiveDST = LocalDateTime.now(fixedBeforeDST);
  LocalDateTime resultActiveDST = LocalDateTime.now(fixedAfterDST);

  Assertions.assertThat(inactiveDSTZonedDateTime.getOffset().toString()).isEqualTo("-05:00");
  Assertions.assertThat(resultInactiveDST).isEqualTo("2024-03-10T01:59:59");
  Assertions.assertThat(activeDSTZonedDateTime.getOffset().toString()).isEqualTo("-04:00");
  Assertions.assertThat(resultActiveDST).isEqualTo("2024-03-10T03:00");
}

@DisplayName("뉴욕에서 서머타임 적용이 종료될 때 한 시간을 되돌린다. 2024-11-03T01:59:59 → 2024-11-03T01:00")
@Test
void New_York_서머타임_적용_종료() {
  //뉴욕 서머타임 2024년 11월 첫째주 일요일 2시에 적용 종료
  ZonedDateTime activeDSTZonedDateTime = ZonedDateTime.of(2024, 11, 3, 1, 59, 59, 0,
      ZoneId.of("America/New_York"));
  Clock fixedBeforeDST = Clock.fixed(activeDSTZonedDateTime.toInstant(),
      ZoneId.of("America/New_York"));
  ZonedDateTime inActiveDSTZonedDateTime = ZonedDateTime.of(2024, 11, 3, 1, 0, 0, 0,
      ZoneId.of("America/New_York"));
  Clock fixedAfterDST = Clock.fixed(inActiveDSTZonedDateTime.toInstant(),
      ZoneId.of("America/New_York"));

  LocalDateTime resultActiveDST = LocalDateTime.now(fixedBeforeDST);
  LocalDateTime resultInactiveDST = LocalDateTime.now(fixedAfterDST);

  Assertions.assertThat(activeDSTZonedDateTime.getOffset().toString()).isEqualTo("-05:00");
  Assertions.assertThat(resultActiveDST).isEqualTo("2024-11-03T01:59:59");
  Assertions.assertThat(inActiveDSTZonedDateTime.getOffset().toString()).isEqualTo("-04:00");
  Assertions.assertThat(resultInactiveDST).isEqualTo("2024-11-03T01:00");
}
스스로 경험하며 얻은 깨달음을 공유하기 좋아하며, 세상이 필요로 하는 코드를 작성하기 위해 노력하는 개발자입니다.

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다