💡 목표
1. Java 8에서 새로운 시간 패키지가 생긴 이유를 이해한다.
2. Java 8에서 생긴 날짜/시간 클래스들의 각각의 특징을 이해한다.
왜 새로운 클래스가 생겼을까?
Java8이 되면서 시간/날짜와 관련된 패키지가 추가되고, 새로운 클래스들이 많이 추가되었습니다. 우리가 흔히 사용하는 `LocalDateTime`과 같은 것들이 이때 추가가 된 것입니다. 본래 Java에서 사용하던 시간과 관련된 클래스는 `java.util.Date`와 `java.util.Calendar`였습니다.
왜 이 두 개의 클래스를 놔두고 새로운 패키지와 클래스를 만들었어야 했을까요?
Practical 모던 자바(장윤기)에 따르면 `Date`와 `Calendar`에는 3가지 치명적인 문제가 있다고 합니다.
1. Thread Safe 하지 못하다.
2. API가 복잡하게 설계되어 있다.
3. 불변성이 없다.
먼저 API가 이해하기 어렵게 설계되어 있습니다.
`Calendar`을 먼저 살펴보면 내부에 아주 많은 상수들이 선언되어 있습니다.
하지만 상수를 선언한 규칙이 일관되지 않습니다.
예를 들면 ` Calendar.SUNDAY`의 값은 1입니다. 그렇다면 1부터 시작한다는 의미를 예상하게 될 것입니다.
하지만 달을 표현하는 상수 `JANUARY`는 값이 0입니다.
일관되지 못한 상수 선언으로 인해 개발자들은 매번 값을 확인하면서 개발을 해야 했습니다.
`Date` 역시 이상한 점은 존재합니다. 현재는 Deprecated가 되었지만, Date의 생성자의 파라미터 설명을 보면 다음과 같습니다.
Params:
year – the year minus 1900.
month – the month between 0-11.
date – the day of the month between 1-31.
year를 표기하려면 표기하려는 연도에서 1900을 빼서 사용해야 합니다.
1900년도 이전을 표기하려면 음수를 입력해야 하고, 2000년대 이후를 입력하기 위해선 100을 더한 값을 입력해줘야 합니다.
Date before1900 = new Date(-20, 10, 2);
System.out.println(before1900); // Tue Nov 02 00:00:00 KST 1880
Date after2000 = new Date(101, 10, 2);
System.out.println(after2000); // Tue Nov 02 00:00:00 KST 2001
불변성이 없는 것 역시 문제가 됩니다.
`Date`와 `Calendar` 모두 setter 메서드가 존재하기 때문에, 인스턴스가 생성된 이후, 필드의 값이 변경될 수 있습니다. 이로 인해 어디서든 값이 변경될 수 있고, 인스턴스가 가진 값에 대한 신뢰성이 떨어지게 됩니다.
어디서든 값이 변경될 수 있기 때문에, Thread-Safe하지 못하게 됩니다. 객체가 불변객체가 아니기 때문에, 발생하는 문제이기도 합니다.
다양한 시간 클래스
이런 문제들을 해결하기 위해 Java8에서 새로 생긴 클래스들은 모두 불변성을 가지고 있고, 이해하기 쉽고, 사용하기 쉬운 API를 제공합니다.
새로 생긴 클래스 중 자주 사용되는 `Instant`와 `LocalOOO` 클래스, 그리고 `LocalOOO`클래스에서 타임존에 관한 내용을 추가한 `ZonedDateTime`에 대해서 소개를 하며, 이 클래스들과 밀접한 관련이 있는 `Clock`에 대해서 소개하겠습니다.
Clock
`Clock`이라는 존재는 약간 낯설 수 있을 것입니다. 사전에 조사를 해본 결과 많은 분들이 아래에 있는 클래스들을 위주로 소개를 해주고, 저도 이번에 처음 들어봤습니다.
`Clock`에 대한 내용을 요약하면 다음과 같습니다.
1. `Clock`은 TimeZone을 기반으로 현재의 `instant`, `date`, `time`에 접근할 수 있다.
2. date 혹은 time과 관련된 클래스들은 `now()` 팩토리 메서드에서 `Clock`의 정보를 이용한다.
3. `Clock`을 추상화한 이유는 다른 `Clock`을 이용할 수 있게 하기 위해서고, 이는 테스트를 용이하게 하기 위함이다. 테스트를 할 때엔 `FixedClock` 혹은 `OffsetClock`을 이용한다.
자세한 내용은 Clock Docs에서 확인할 수 있습니다.
코드를 기반으로 위에서 설명한 `Clock`의 세 가지 내용을 살펴보겠습니다.
첫 번째 내용을 보면 `Clock`은 TimeZone을 기반으로 생성됩니다. 아래 메서드를 보면 팩토리 메서드를 보면 UTC 혹은 시스템의 타임존을 이용해서 `Clock`을 생성하는 것을 확인할 수 있습니다.
public static Clock systemUTC() {
return SystemClock.UTC;
}
public static Clock systemDefaultZone() {
return new SystemClock(ZoneId.systemDefault());
}
TimeZone을 기반으로 생성된 `Clock`은 `instant()` 메서드를 통해 `Instant`를 생성할 수 있고, 이후 살펴보겠지만, `Instant`를 통해 시간, 날짜를 얻을 수 있습니다.
Instant instant = Clock.systemUTC().instant();
System.out.println(instant); // 2023-12-08T08:35:03.154339600Z
두 번째 내용인 `Clock`을 기반으로 date/time 클래스들이 생성되는 것입니다.
아래 코드는 `LocalDate`의 팩토리 메서드입니다. `Clock`을 사용해서 인스턴스가 생성되는 것을 확인할 수 있습니다.
public static LocalDate now() {
return now(Clock.systemDefaultZone());
}
public static LocalDate now(ZoneId zone) {
return now(Clock.system(zone));
}
public static LocalDate now(Clock clock) {
Objects.requireNonNull(clock, "clock");
final Instant now = clock.instant(); // called once
return ofInstant(now, clock.getZone());
}
세 번째 내용은 `Clock`의 추상화입니다.
`Clock`은 추상클래스입니다. 그리고 `Clock`의 하위 클래스는 5개가 있습니다.
이 중에서 Docs에 언급된 두 클래스 `FixedClock`과 `OffsetClock`에 대해서 짧게 알아보겠습니다.
우선 `FixedClock`과 `OffsetClock`은 `Clock` 클래스에 있는 `fixed()`와 `offset()`메서드를 통해 생성할 수 있습니다.
`fixed()`를 통해 생성되는 `FixedClock`은 항상 같은 시간을 리턴합니다.
`offset()`을 통해 생성되는 `OffsetClock`은 기준이 되는 `Clock`과 `Duration` 를 넣습니다. `Duration`의 값을 기준으로 양수면, 기준 `Clock`보다 이후 시간을, 음수면 이전 시간을 가진 `Clock` 리턴합니다.
이 두 메서드를 통해 `Clock`을 이용하는 객체에게 의존성 주입을 하여 사용한다면 테스트를 쉽게 진행할 수 있을 것입니다.
아래 코드를 보면 기본으로 사용하는 `SystemClock`의 경우는 `instant()`를 사용할 때, 그 시점을 정확하게 집어내는 것을 볼 수 있습니다. 그와 달리 `FixedClock`인 경우는 `Thread.sleep(5000)`을 해도 제가 지정한 시간을 리턴하는 것을 확인할 수 있습니다.
@Test
void systemClockTest() throws Exception{
Clock clock = Clock.systemDefaultZone(); // SystemClock
System.out.println(clock.instant()); // 2023-12-11T00:13:06.215199200Z
System.out.println(clock.instant()); // 2023-12-11T00:13:06.217188Z
Thread.sleep(5000);
System.out.println(clock.instant()); // 2023-12-11T00:13:11.227792400Z
}
@Test
void fixedClockTest () throws Exception{
//given FixedClock
Clock clock = Clock.fixed(Instant.ofEpochMilli(1000), ZoneId.of("Asia/Tokyo"));
System.out.println(clock.instant()); // 1970-01-01T00:00:01Z
System.out.println(clock.instant()); // 1970-01-01T00:00:01Z
Thread.sleep(5000);
System.out.println(clock.instant()); // 1970-01-01T00:00:01Z
}
Instant
Java Docs에 따르면 `Instant`는 타임라인의 한 순간을 정의합니다. 그리고 애플리케이션에서 특정 이벤트가 발생했을 때, 그 시간을 저장할 때 주로 사용된다고 합니다.
`Instant`는 1970년 1월 1일 00시 00분 00초 이후 해당 시점까지 몇 초가 흘렀는지에 대해서 저장합니다. 따로 Zone과 관련된 정보가 없기 때문에, Instant에 ZoneId를 부여해서 `ZonedDateTime`으로 변환할 수 있습니다. 그렇다면, 운영하는 서비스 지역에 따라서 다른 값을 보여줄 수 있어서 이용하기 편하지 않을까 생각이 듭니다.
아래 테스트코드는 같은 시점의 `Instant`로부터, ZoneId을 입력받아서 ZonedDateTime으로 변환한 결과입니다. Asia/Seoul과 America/Kentucky/Monticello 지역은 14시간 정도 차이가 있는 듯싶습니다.
@Test
void instantTest () throws Exception{
Instant now = Instant.now();
ZonedDateTime seoulDateTime = now.atZone(ZoneId.systemDefault());
System.out.println(seoulDateTime); // 2023-12-11T10:13:21.099280+09:00[Asia/Seoul]
ZonedDateTime americaDateTime = now.atZone(ZoneId.of("America/Kentucky/Monticello"));
System.out.println(americaDateTime); // 2023-12-10T20:13:21.099280-05:00[America/Kentucky/Monticello]
}
LocalDate/LocalTime/LocalDateTime
`LocalOOO` 클래스는 위에서 살펴봤듯이, 주어진 TimeZone을 기반으로 날짜/시간을 설정합니다.
클래스의 이름에서 알 수 있듯이, `LocalDate`는 연-월-일, `LocalTime`은 시-분-초에 대한 정보를 저장하고, `LocalDateTime`은 이 모든 정보를 저장합니다.
하지만 이 클래스들은 TimeZone과 관련한 정보를 저장하지 않기 때문에, 서로 다른 TimeZone에서 시간이 저장된다면, 주의를 해서 이용해야 할 것 같습니다.
ZonedDateTime
`ZonedDateTime` 역시 위에서 `Instant`를 이야기할 때 살펴보았습니다. 위에서 등장한 `LocalDateTime`에서 TimeZone에 대한 정보를 함께 저장합니다. 그 외엔 특별히 다른 내용은 없을 것 같습니다.
마무리
지금까지 자바 8에서 날짜/시간과 관련된 새로운 클래스가 생기게 된 이유와 어떤 클래스들이 생겼는지에 대해서 살펴보았습니다. 각 클래스에 대한 사용법보단, 클래스가 어떤 내용을 담고 있는지에 대해서 이야기해서 크게 넣을 내용이 없었네요. 다음 글에서는 지금까지 언급한 클래스들이 JDBC와 DB에서는 어떤 형태로 저장이 되는지에 대해서 알아보겠습니다.
참고자료
Java Instant Docs
Java Clock Docs
장윤기. 『Practical 모던 자바 어려워진 자바, 실무에 자신 있게 적용하기』. 인사이트, 2020.