[DEV] 기록

Java 날짜 시간 유형에 대한 고찰

꾸준함. 2021. 1. 25. 01:49

개요

현재 다중 서버 환경에서 프로젝트를 개발하고 있는데 인프라팀에서는 분명 서버 간 시간을 동기화하였다고 했는데도 불구하고 LocalDateTime.now()를 로그로 찍어보면 서버 간의 시간이 조금씩 다른 것을 확인할 수 있었습니다.

이에 따라, LocalDateTime.now() 대신 어떤 클래스의 메서드를 사용해야 할지 고민하는 와중 stackoverflow에 Java의 날짜 시간 유형을 잘 정리한 글이 있어 의역을 해보고자 합니다.

 

Instant vs LocalDateTime

Instant 클래스와 LocalDateTime 클래스는 비슷해 보이지만 사실 완전히 다릅니다.

하나는 순간(moment)을 나타내고, 다른 하나는 순간(moment)을 나타내지 않습니다.

  • Instant 클래스는 타임라인의 특정 지점을 나타냅니다.

* 오프셋을 이해하기 위해서는 타임존, GMT, 그리고 UTC에 대한 이해도가 있어야 합니다. 해당 내용은 여기에 잘 정리되어있으니 꼭 한번 읽어보시길 바랍니다.

 

LocalDateTime

LocalDateTime 클래스는 타임존을 가지고 있지 않습니다.해당 클래스의 문서를 인용하자면 아래와 같습니다.

이 클래스는 시간대를 저장하거나 나타내지 않습니다. 대신, 이것은 생일과 같은 날짜시계에 보이는 현지 시간을 결합한 것입니다. 오프셋과 표준시와 같은 추가 정보가 제공되지 않는 한 타임라인의 특정 지점(Instant)을 나타낼 수 없습니다.

따라서, Local은 타임존, 그리고 오프셋이 없음을 의미합니다.

 

Instant

 

Instant를 설명해주는 사진

 

Instant는 UTC의 타임라인에 있는 한 순간(moment)으로, 1970년 UTC의 첫 번째 모멘트의 발생 이후 nano초 동안의 시간입니다.

대부분의 비즈니스 로직, 데이터 스토리지 및 데이터 교환은 UTC여야 하므로 Instant는 자주 사용할 수 있는 편리한 클래스입니다.

 

Instant instant = Instant.now(); // UTC의 현재 순간을 나타냄

 

LocalDate, LocalTime, LocalDateTime

 

LocalDate를 설명해주는 사진
LocalTime을 설명해주는 사진
LocalDateTime을 설명해주는 사진

 

LocalDateTime, LocalDate, LocalTime은 Instant와 성격이 다릅니다. 해당 클래스들은 한 지역이나 시간대에 묶이지 않으며 어느 한 지역 혹은 타임존에 얽매이지 않습니다. 즉, 지역성을 부여하기 전에는 의미를 지니지 않은 클래스들이라고 할 수 있습니다.


클래스에 공통적으로 있는 "Local"이라는 단어는 어떤 지역, 혹은 모든 지역을 의미하지만, 특정한 지역을 의미하지는 않습니다.

따라서 실제 런칭된 서비스에서는 위에서 언급된 세 가지 클래스가 타임라인의 특정 순간이 아닌 대략적인 날짜 혹은 시간을 나타내기 때문에 자주 사용되지 않습니다. 상용에 런칭된 서비스는 송장 도착 시간, 운송을 위해 제품을 발송한 시간, 직원을 고용한 시점, 택시가 차고에서 출발하는 정확한 시간 등을 정확히 저장해야 합니다. 따라서 비즈니스 앱 개발자들은 Instant 및 ZonedDateTime 클래스를 가장 많이 사용합니다.

그렇다면 LocalDateTime은 언제 사용해야 될까요?

LocalDateTime을 사용해야 하는 세 가지 상황의 예시는 아래와 같습니다:

  • 특정 날짜와 시간을 여러 위치에서 적용하려는 경우
  • 예약을 하는 경우
  • 타임존이 정해지지 않은 경우

Notice that none of these three cases involve a single certain specific point on the timeline, none of these are a moment. (이 부분은 어떻게 의역해야 할지 모르겠습니다.)

 

첫 번째 사례

때때로 우리는 특정 날짜의 특정 시간을 나타내려고 하지만 서로 다른 타임존을 사용하는 여러 지역에 동시에 적용하려고 합니다.
예를 들어, "크리스마스는 2021년 12월 25일 자정에 시작합니다"와 같은 경우에는 LocalDateTime 클래스를 사용합니다. 서울과 파리는 서로 다른 시간대를 사용하기 때문에 위와 같은 문구를 코드로 적용시키기 위해서는 아래와 같이 코드를 작성해야 합니다.

LocalDate localDate = LocalDate.of( 2021 , Month.DECEMBER , 25 ) ;
LocalTime localTime = LocalTime.MIN ;   // 00:00:00
LocalDateTime localDateTime = LocalDateTime.of( localDate , localTime ) ;

 

두 번째 사례

LocalDateTime 클래스를 사용해야 하는 또 다른 상황은 향후 이벤트를 예약할 때입니다.(예: 치과 예약). 이러한 약속은 정치인들이 타임존을 재정의해도 무관할 만큼 충분히 먼 장래에 있을 수 있습니다. 

 

* 우리나라는 해당사항이 없지만 외국의 경우 정치인들이 시간대를 변경하는 경우가 잦은 것 같습니다.

예약의 경우 LocalDateTime과 ZoneId를 각각 별도로 저장한다면 나중에 예약을 생성할 때 즉시 LocalDateTime::atZone(ZoneId)을 호출하여 ZonedDateTime 개체를 생성하여 예약 날짜 및 시간(moment)을 결정할 수 있습니다.

ZoneId zoneId = ZoneId.of("Asia/Seoul");

LocalDate localDate = LocalDate.of( 2021 , Month.DECEMBER , 25 ) ;
LocalTime localTime = LocalTime.MIN ;   // 00:00:00
LocalDateTime localDateTime = LocalDateTime.of( localDate , localTime ) ;

ZonedDateTime zonedDateTime = localDateTime.atZone( zoneId ) ;

 

세 번째 사례

일부 개발자는 표준 시간대 또는 오프셋을 알 수 없는 상황에서 LocalDateTime을 사용하는 경우가 있습니다.

하지만 필자는 이러한 행위를 부적절하고 현명하지 못하다고 생각합니다. 타임존 또는 오프셋이 결정되지 않은 경우 올바른 데이터가 아닙니다. 이는 의도된 통화(달러, 파운드, 유로 등)를 알지 못한 채 제품의 가격을 저장하는 것과 같습니다. 따라서, 세 번째 사례와 같이 사용을 하지 않았으면 좋겠습니다.

 

OffsetDateTime

 

OffsetDateTime을 설명해주는 사진

 

OffsetDateTime 클래스는 UTC의 앞이나 뒤에 있는 시간-분-초의 컨텍스트를 가진 순간을 날짜 및 시간으로 나타냅니다.

오프셋의 양(시간, 분, 초)은 ZoneOffset 클래스로 표시됩니다. (오프셋의 양의 예: UTC+09:00)

시간, 분, 초가 모두 0인 경우 오프셋 날짜 시간은 UTC에서의 Instant와 동일한 순간(moment)을 나타냅니다.

 

ZoneOffSet

 

ZoneOffset을 설명해주는 사진

 

ZoneOffset 클래스는 UTC에서 오프셋을 나타내며, UTC보다 몇 시간 앞이나 뒤에 있는 시간을 나타냅니다.

ZoneOffset은 단지 시간, 분, 초를 나타낼 뿐 그 이상 그 이하도 아닙니다.

Zone은 ZoneOffset보다 훨씬 큰 개념인데, 오프셋의 이름 및 변경 이력을 가지고 있습니다.

따라서, 단순 offset을 사용하는 것보다는 zone을 사용하는 것이 권장됩니다.

 

ZoneId

 

ZoneId를 설명해주는 사진

 

표준 시간대는 ZoneId 클래스로 표시됩니다.

예를 들어, 파리는 몬트리올보다 시간이 빠릅니다. 그래서 우리는 주어진 지역에 대해 정오를 더 잘 나타내기 위해 시곗바늘을 움직일 필요가 있다. 서유럽/아프리카의 UTC 라인에서 동쪽/서쪽으로 멀어질수록 오프셋은 커집니다.

표준 시간대는 지역 커뮤니티 또는 지역에서 시행되는 조정 및 이상 징후를 처리하기 위한 규칙 집합입니다. 가장 흔한 이상 징후는 일광 절약 시간(DST)입니다.

표준 시간대는 가까운 미래에 대해 확인된 과거의 규칙, 현재 규칙 및 규칙의 기록을 가지고 있습니다.

이러한 규칙은 예상보다 자주 변경됩니다. 따라서, 날짜/시간 라이브러리의 규칙(일반적으로 'tz' 데이터베이스 복사본)을 최신 상태로 유지해야 합니다. 다행히도 Oracle이 시간대 업데이트 도구를 출시하여 Java 8에서 최신 상태를 유지하기가 그 어느 때보다 쉬워졌습니다.

시간대 이름은 아메리카/몬트리올, 아프리카/카사블랑카 또는 태평양/오크랜드와 같이 대륙/지역 형태로 적절한 시간대 이름을 지정합니다. EST 또는 IST와 같은 2~4자 약어는 실제 표준 시간대가 아닐뿐더러 독단적인 이름이 아니므로 절대 사용하면 안 됩니다.

정리를 하자면, 타임존은 오프셋과 조정 규칙의 집합체입니다.

ZoneId zoneId = ZoneId.of("Asia/Seoul");

 

ZonedDateTime

 

ZoneDateTime를 설명해주는 사진

 

ZonedDateTime 클래스는 ZoneId가 할당된 Instant라는 개념으로 생각하시면 됩니다. (ZoneDateTime = ZoneId + Instant)

특정 지역의 현재 순간(moment)을 포착하기 위해서는 아래와 같이 코드를 작성하시면 됩니다.

ZoneId zoneId = ZoneId.of("Asia/Seoul");
ZonedDateTime zonedDateTime = ZonedDateTime.now( zoneId ) ;


거의 모든 백엔드, 데이터베이스, 비즈니스 로직, 데이터 지속성, 그리고 데이터 교환은 UTC 형식이어야 합니다. 그러나 사용자에게 보일 때는 사용자가 예상하는 표준 시간대로 조정해야 합니다. 해당 조정 작업을 시행하기 위해 ZonedDateTime 클래스와 formatter 클래스가 존재합니다.

ZoneId zoneId = ZoneId.of("Asia/Seoul");
ZonedDateTime zonedDateTime = ZonedDateTime.now( zoneId );
String output = zonedDateTime.toString();

formatter 클래스 중 DateTimeFormatter 클래스를 사용하면 해당 지역에서 사용하는 형식으로 시간을 출력할 수 있습니다.

ZoneId zoneId = ZoneId.of("Asia/Seoul");
ZonedDateTime zonedDateTime = ZonedDateTime.now( zoneId );

DateTimeFormatter formatter = DateTimeFormatter
	.ofLocalizedDateTime( FormatStyle.FULL )
    .withLocale( Locale.Korea ) ; 
String outputFormatted = zonedDateTime.format( formatter ) ;

 

정리

위 내용들을 정리하면 아래와 같이 하나의 표로 요약할 수 있습니다.

 

 

 

결론

정리를 하자면, 로그를 남기거나 현재 시간을 불러올 때 LocalDateTime 클래스를 사용하는 것은 추천되지 않는 방법인 것 같습니다.

stackoverflow에서 추천을 많이 받은 답변에 의하면 위와 같은 경우 UTC를 사용하는 것을 권장하기 때문에 LocalDateTime이 아닌 Instant 클래스를 사용하는 것이 맞는 방법이라고 합니다. 또한, java.time.* 클래스들이 문자열을 파싱 하거나 생성할 때 ISO 8601 포맷을 디폴트로 사용하기 때문에 로그를 남기기 위해 텍스트 형태로 직렬화할 때 해당 포맷을 사용하는 것을 권장한다고 합니다. 

저 같은 경우 서버 간의 시간 차를 해결하기 위해 LocalDateTime.now()에서 System.currentTimeMillis()로 변경했었는데 이를 다시 Instant.now()로 변경해야 할 것 같습니다.

만약 위와 같은 방법으로도 해결이 되지 않는다면 공통 NTP 서버를 구축하여 모든 서버가 해당 서버의 시간대를 사용하는 방법으로 문제를 해결해야 할 것 같습니다.

 

** 업데이트 사항 **

선배님과 확인 결과, Instant 객체와 LocalDateTime 모두 systemUTC() 메서드를 쓰고 있기 때문에 고정된 clock이 아닌 best available system clock 즉, 상황에 따라서 다른 clock을 사용하고 있다고 정의되어있습니다.

따라서, 현재로서는 ntp 서버를 구축하기 전까지 기존대로 clock 중 하나인 System.currentTimeMillis()를 사용하는 것이 최선의 선택인 것 같습니다.

 

[출처]

stackoverflow.com/questions/32437550/whats-the-difference-between-instant-and-localdatetime

 

What's the difference between Instant and LocalDateTime?

I know that: Instant is rather a "technical" timestamp representation (nanoseconds) for computing. LocalDateTime is rather date/clock representation including time-zones for humans. Still in the ...

stackoverflow.com

stackoverflow.com/questions/39586311/java-8-localdatetime-now-only-giving-precision-of-milliseconds

 

Java 8 LocalDateTime.now() only giving precision of milliseconds

Is it possible to get microseconds in Java 8? The Java 8 LocalDateTime class has a .getNano() method which is meant to return nanoseconds, but on both Linux(Ubuntu) and OS X (10.11.5) it only returns

stackoverflow.com

meetup.toast.com/posts/125

 

자바스크립트에서 타임존 다루기 (1) : TOAST Meetup

자바스크립트에서 타임존 다루기(1)

meetup.toast.com

 

* 의역을 했기 때문에 많은 오역이 있을 수 있습니다. 혹시 오역이 있을 경우 지적해주시면 즉각 반영하여 수정하도록 하겠습니다!

반응형