글로벌 서비스를 개발함에 있어서 고려해야할 여러가지 사항들 중에 빠질수 없는 것이 바로 시간에 대한 개념이다.
글로벌 서비스를 클라우드 환경에서 제공하게 될텐데 각 서버들은 세계 각국에 흩어져 있고 그에 따라 서비스 시간에 대한 차이가 발생하게 된다.
또한 UI, 서버, DB 간의 날짜 포맷이 다르게 되면 이를 동기화 하기 위한 작업이 추가로 들어가게 된다.
따라서 이 글에서는 java에서 지원하는 여러 time 포맷들을 알아보고 어떤 식으로 구성하는 것이 좋을까 고찰해볼 것이다.
Date와 Calendar API는 Mutable 하다.
즉 변경이 가능하기 때문에 Thread-safety하지 않다.
Calendar를 이용하여 월을 가져오면 현재 월 - 1 값이 나온다.
월이 0부터 시작되기 때문이다. 이처럼 직관적이지 않다.
타임존: 동일한 로컬 시간을 따르는 지역을 의미하며, 일반적으로 국가별로 고유한 타임존을 사용한다. 하지만 미국이나 캐나다 처럼 면적이 넓은 경우 지역별로 타임존을 가지기도 한다. (반면 중국은 넓은 지역에도 불구하고 하나의 타임존을 사용한다.)
GMT: 16세기 후반에 나온 개념으로 한국의 타임존은 GMT+09:00 으로 표현된다. GMT는 Greenwich Mean Time의 약자로서 경도 0에 위치한 영국 그리니치 천문대를 기준으로 하는 태양 시간을 의미한다.
UTC: 20세기 후반에 등장한 개념으로 한국은 UTC+9:00라고 표현한다. 협정 세계시는 1972년부터 시행된 국제 표준시이다. UTC는 태양 대신 원자 시계를 기준으로 하며 전 세계 400여개의 원자시계가 데이터를 비교하며 GMT 오차를 보정해 나가므로 정확도가 더욱 높다. 오차범위가 30만년에 1초?
ISO-8601 캘린더 시스템의 타임존 개념이 없는, 날짜-시간 시스템
즉 Java Time에서 Local이 들어간다는 것은 시간대(Zone Offset / Zone Region)에 대한 정보가 없다는 의미이다.
예) 1994-06-13T11:15:30
그 지역의 현재 시간을 나타낸다. (생일이나 현지 시간 등)
다른 나라에서의 시간 값을 맞추기 위해서는 OffsetDateTime 또는 ZonedDateTime을 사용해야 한다.
LocalDateTime + ZoneOffset의 개념으로 OffsetDateTime은 UTC보다 몇 시/분/초 앞 또는 뒤의 컨텍스트를 사용하여 순간을 날짜 및 시간으로 나타낸다.
예) 1994-06-13T:11:15:30+09:00
Offset의 크기(시/분/초)는 ZoneOffset 클래스로 표시되며 시/분/초가 0이면 OffsetDateTime은 Instant와 동일한 UTC의 순간을 나타낸다.
그렇지 않다. 현재 +09:00의 시차를 사용하고 있는 나라는 한국도 있지만 일본, 인도네시아 등 많은 지역에서 사용하고 있기에 시차와 타임존은 1:N 관계이다.
OffsetDateTime + ZoneRegion에 대한 정보까지 포함한 객체이다.
OffsetDateTime과의 차이점은 DST(Daylight Saving Time)와 같은 Time Transition Rule을 포함하는 ZoneRegion의 유무 차이이다. DST를 ZoneRules를 통해 알아서 계산해준다.
예) 1994-06-13T11:15:30+09:00 Asia/Seoul
Instant는 1970년 부터 현재 시간까지를 계산한 nano초 동안의 시간이며 하나의 순간을 나타낸다.
대부분의 로직과 데이터들은 UTC와 같은 정확한 시간으로 계산되어야 하므로 자주 사용하는 클래스이다.
UTC 기준시인 1970년 1월 1일 0시 0분 0초에 해당하는 Instant 객체는 Instant.EPOCH라는 정적 필드에 저장되어 있다.
Instant epoch = Instant.EPOCH; // Instant.ofEpochSecond(0); 와 동일
//epoch = 1970-01-01T00:00:00Z
System.out.println("epoch = " + epoch);
Instant epochInFuture = Instant.ofEpochSecond(1_000_000_000);
//epochInFuture = 2001-09-09T01:46:40Z
System.out.println("epochInFuture = " + epochInFuture);
Instant epochInPast = Instant.ofEpochSecond(-1_000_000_000);
//epochInPast = 1938-04-24T22:13:20Z
System.out.println("epochInPast = " + epochInPast);
Instant.now() 정적 메소드를 호출하면 UTC 기준의 ISO 포멧의 현재 시간의 Instant 객체를 얻을 수 있다.
Instant current = Instant.now();
//Current Instant = 2017-12-22T08:30:18.870Z
System.out.println("Current Instant = "+ current);
long epochSecond = current.getEpochSecond();
//Current Timestamp in seconds = 1513931481
System.out.println("Current Timestamp in seconds = " + epochSecond);
long epochMilli = current.toEpochMilli();
//Current Timestamp in milli seconds = 1513931418870
System.out.println("Current Timestamp in milli seconds = " + epochMilli);
Instant 클래스의 atZone() 메서드와 ZonedDateTime 클래스의 toInstnat() 메소드를 통해서 두 타입의 객체는 서로 변환이 가능하다.
// 서울 ZonedDateTime
ZonedDateTime zdtSeoul = Year.of(2002).atMonth(6).atDay(18).atTime(20, 30).atZone(ZoneId.of("Asia/Seoul"));
System.out.println("Time in Seoul = " + zdtSeoul);
// Timestamp
Instant instant = zdtSeoul.toInstant();
System.out.println("Instant = " + instant + ", Timestamp = " + instant.getEpochSecond());
// 캐나다 ZonedDateTime
ZonedDateTime zdtVancouver = instant.atZone(ZoneId.of("America/Vancouver"));
// ZonedDateTime zdtVancouver = ZonedDateTime.ofInstant(instant, ZoneId.of("America/Vancouver")); 와 동일
System.out.println("Tine in Vancouver = " + zdtVancouver);
크리스마스는 전세계 12월 25일 자정에 시작된다.
내년 1월 23일 오후 3시 라는 일정을 예약했을 때, 해당 지역이 DST를 적용 여부에 따라 오후 2시가 될 수도 오후 4시가 될 수도 있다.
따라서 약속의 경우 LocalDateTime 및 ZoneId를 별도로 저장하고 필요시 LocalDateTime::atZone(ZoneId)를 호출하여 ZonedDateTime을 생성하여 순간을 결정하여 사용한다.
ZonedDateTime zdt = ldt.atZone(zoneId);
//필요한 경우 UTC
Instant instant = zdt.toInstant() ;
시간대 또는 오프셋을 알 수 없는 상황에서 LDT를 사용할 수 있다.
하지만 이것은 부적절하며 오프셋이 결정되지 않았거나 모르는 경우 해당 데이터는 잘못된 데이터인 것이다.
마치 통화(달러, 파운드, 원화 등)를 모른채 제품의 가격을 저장하는 것과 같다.
java.time은 JDK8 이상에 포함되어 있으며 기존의 java.util.Date, Calendar 와 같은 레거시 날짜-시간 클래스를 대체한다.
또한 java.time은 데이터베이스와 직접 교환할 수 있으며 더이상 java.sql.* 필요하지 않다.
JPA2.2 및 Hibernate 5.3 부터 java.time을 지원한다.
시간 불일치 문제를 해결하기 위해서 크게 아래 두가지 방법이 존재할 것 같다.
2번의 경우 비즈니스 로직단에서 시간 데이터를 조작하는 작업(년/월/시/분/초 등의 세부적인 시간 값을 추출하고 계산 등)이 많은 경우 적합할 것 같고 시간 데이터의 조작없이 단순히 글로벌 환경에서의 정확한 저장이 그 목적이라면 1번의 방식으로 보다 단순히 구현할 수 있을것같다.
참고 자료
https://stackoverflow.com/questions/32437550/whats-the-difference-between-instant-and-localdatetime