개요
채팅방에 24 시간 내에 입장 기록이 있으면 (redis 캐시에 해당 key 값이 있는지 여부)
사용자에게 주어지는 질문 리스트의 알고리즘을 달리 해야 하는 로직에서 사용된 isWithin24Hours 메소드 입니다.
저는 현재
System.currentTimeMillis()
를 사용하고 있지만
LocalDateTime.now().minusDays(1)
LocalDateTime 은 minusDays 로 측정 가능하기 때문에 Java 날짜 시간 유형에 각각 어떤 차이가 있는지
어떤 class 를 사용하는 것이 적합할지 궁금해졌습니다.
타임존이란?
타임존은 동일한 로컬 시간을 따르는 지역을 의미하며, 주로 해당 국가에 의해 법적으로 지정된다. 보통 국가별로 각자의 고유한 타임존을 사용하고 있으며, 미국이나 캐나다처럼 면적이 넓은 나라인 경우 지역별로 각기 다른 타임존을 사용하기도 한다. (반면 중국은 그 넓은 면적에도 불구하고 하나의 타임존을 이용하고 있는데, 이로 인해 중국 서쪽 지역에서는 오전 10시는 되어야 해를 볼 수 있다)
GMT, UTC 그리고 오프셋(Offset)
GMT
한국의 타임존은 보통 GMT+09:00 으로 표현된다. 여기서 GMT는 Greenwich Mean Time의 약자로서 경도 0도에 위치한 영국 그리니치 천문대를 기준으로 하는 태양 시간을 의미한다. GMT 시간은 1925년 2월 5일부터 사용하기 시작하였으며, 1972년 1월 1일까지 세계 표준시로 사용되었다.
UTC
GMT는 또한 UTC라고도 불리는데, 두 용어가 혼용되어서 사용되고 있기는 하지만 엄밀히 구분하자면 둘은 다른 의미를 가진다. UTC는 지구 자전주기의 흐름이 늦어지고 있는 문제를 해결하기 위해 1972년에 세슘 원자의 진동수에 기반한 국제 원자시를 기준으로 다시 지정된 시간대이다. 즉, UTC는 좀더 정확한 시간측정을 위해서 GMT를 대체하기 위해 제정된 새로운 표준이며, 시간적으로는 둘 사이에 아주 미세한 차이밖에 없지만, 소프트웨어에서 사용할 때는 UTC라고 하는 것이 더 정확한 표현일 것이다.
UTC라는 약자가 탄생한 배경은 좀 재미있는데, 처음에 영어권에서는 CUT(Coordinated Universal Time)를, 프랑스어권에서는 TUC(Temps Universel Coordonn)를 사용하기를 원했다고 한다. 하지만 결국 한쪽이 이기지는 못하고, 두 언어 모두 C, T, U로 되어 있으니 중재안으로 UTC가 채택되었다고 한다.
오프셋
UTC+09:00 에서 +09:00 의 의미는 UTC의 기준시간보다 9시간이 빠르다는 의미이다. 즉 UTC 기준으로 현재 낮 12시라면 한국시간으로는 오후 9시가 될 것이다. 이렇게 UTC와의 차이를 나타낸 것을 오프셋이라고 하며, +09:00 혹은 -03:00 등과 같이 표현된다.
보통 국가나 지역들마다 자신들이 사용하는 타임존에 대해 고유의 이름을 부여한다. 예를 들어 대한민국의 타임존은 KST(Korea Standard Time)이라고도 불리는데, 이는 앞서 설명했듯이 특정 오프셋을 지칭하므로 KST = UTC+09:00 이라고 이해하면 된다. 하지만 +09:00 오프셋은 한국 뿐만 아니라 일본, 인도네시아 등 여러 지역에서 사용하고 있으므로, 오프셋과 타임존 이름들의 관계는 1:1 관계가 아닌 1:N 관계이다. +09:00를 사용하는 국가 혹은 지역의 목록은 UTC+09:00에서 확인할 수 있다.
오프셋은 한시간 단위가 아닌 경우도 있다. 예를 들어 북한은 +08:30을 기준시로 사용하고 있고, 호주의 경우 지역에 따라 +08:45, 혹은 +09:30을 기준시로 이용하고 있다.
Instant vs LocalDateTime
Instant 클래스와 LocalDateTime 클래스는 비슷해 보이지만 사실 완전히 다릅니다.
하나는 순간(moment)을 나타내고, 다른 하나는 순간(moment)을 나타내지 않습니다.
Instant 클래스는 타임라인의 특정 지점을 나타냅니다.
LocalDateTime 클래스는 타임존을 가지고 있지 않습니다. 해당 클래스의 문서를 인용하자면 아래와 같습니다.
이 클래스는 시간대를 저장하거나 나타내지 않습니다.
대신, 이것은 생일과 같은 날짜와 시계에 보이는 현지 시간을 결합한 것입니다.
오프셋과 표준시와 같은 추가 정보가 제공되지 않는 한 타임라인의 특정 지점(Instant)을 나타낼 수 없습니다.
따라서, Local은 < 타임존, 그리고 오프셋이 없음을 의미 > 합니다.
Instant

Instant는 UTC의 타임라인에 있는 한 순간(moment)으로, 1970년 UTC의 첫 번째 모멘트의 발생 이후 nano초 동안의 시간입니다.
대부분의 비즈니스 로직, 데이터 스토리지 및 데이터 교환은 UTC여야 하므로 Instant는 자주 사용할 수 있는 편리한 클래스입니다.
Instant now = Instant.now(); // UTC의 현재 순간을 나타냄
println(now);
// now 2024-04-19T03:44:29.766163Z -- 2024년 04월 19일 12시 44분 29초
- 나노초까지 포현이 가능하다는 것을 볼 수 있다. (getNanoTimeAdjustment(localOffset))
- 어떻게 UTC 기반으로 타임을 찍을까?
public static Instant now() {
return Clock.currentInstant();
}
public abstract class Clock {
private static final long OFFSET_SEED = System.currentTimeMillis() / 1000 - 1024;
private static long offset = OFFSET_SEED;
// 시스템의 현재 시간을 가져온다 나노초로 가져오는 것을 밀리초로 표현해준다.
static Instant currentInstant() {
long localOffset = offset; long adjustment = VM.getNanoTimeAdjustment(localOffset);
// 밀리초를 나노초로 변환해서 가져온다
...
return Instant.ofEpochSecond(localOffset, adjustment);
}
}
LocalDate, LocalTime, LocalDateTime



- LocalDateTime 은 날짜 + 시간 정보를 가지고 있다. Timezone이 없는 것을 볼 수 있다.
- LocalDateTime은 인간에게 친화적인 타입이다.
- Timezone이 없는데 Instant 와 같이 UTC로 넣는게 아닌가?라는 생각을 할 수 있다.
- LocalDateTime 은 현재 로컬 시간에 맞춰서 시간을 표현하고 있다.
LocalDateTime now = LocalDateTime.now();
println(now);
// 2024-04-19T12:46:06.670820100 2024년 04월 19일 12시 16분 06초
public final class LocalDateTime {
public static LocalDateTime now() {
return now(Clock.systemDefaultZone());
}
}
public static Clock systemDefaultZone() {
// 시스템의 ZoneId를 가져온다
return new SystemClock(ZoneId.systemDefault());
}
public static LocalDateTime now(Clock clock) {
Objects.requireNonNull(clock, "clock");
final Instant now = clock.instant();
// ZoneId를 기반으로 Offset 정보를 가져온다.
ZoneOffset offset = clock.getZone().getRules().getOffset(now);
return ofEpochSecond(now.getEpochSecond(), now.getNano(), offset);
}
public static LocalDateTime ofEpochSecond(long epochSecond, int nanoOfSecond, ZoneOffset offset) {
Objects.requireNonNull(offset, "offset");
NANO_OF_SECOND.checkValidValue(nanoOfSecond);
long localSecond = epochSecond + offset.getTotalSeconds();
long localEpochDay = Math.floorDiv(localSecond, SECONDS_PER_DAY);
int secsOfDay = Math.floorMod(localSecond, SECONDS_PER_DAY);
LocalDate date = LocalDate.ofEpochDay(localEpochDay);
LocalTime time = LocalTime.ofNanoOfDay(secsOfDay * NANOS_PER_SECOND + nanoOfSecond);
return new LocalDateTime(date, time);
}
- 인스턴스의 Timezone 기반으로 Offset을 생성한 후 현재 시점 기준으로 LocalDate,LocalTime객체를 생성 후LocalDateTime을 생성 해주는 것을 볼 수 있다.
LocalDateTime 을 사용 했을 때 문제점?
LocalDateTime 코드를 살펴보았으니 한 가지 추론을 해보자.
글로벌 서비스를 Seoul(Asia/Seoul), LA(America/Los_Angeles) , Tokyo(Asia/Tokyo) 에 운영하고 있다고 가정하면 각각 Region마다 서버를 둘 것이고 타임존이 각각 다를 것이다.
각 인스턴스에서 LocalDateTime.now()로 시간을 입력한다고 해보자.
DB에는 어떻게 들어갈 것인가?
인스턴스의 Timezone을 UTC로 설정해 주지 않는 이상 각각 Timezone에 맞춰 데이터가 들어갈 것이다.
LocalDateTime, LocalDate, LocalTime은 Instant와 성격이 다릅니다. 해당 클래스들은 한 지역이나 시간대에 묶이지 않으며 어느 한 지역 혹은 타임존에 얽매이지 않습니다. 즉, 지역성을 부여하기 전에는 의미를 지니지 않은 클래스들이라고 할 수 있습니다.
클래스에 공통적으로 있는 "Local"이라는 단어는 어떤 지역, 혹은 모든 지역을 의미하지만, 특정한 지역을 의미하지는 않습니다.
따라서 실제 런칭된 서비스에서는 위에서 언급된 세 가지 클래스가 타임라인의 특정 순간이 아닌 대략적인 날짜 혹은 시간을 나타내기 때문에 자주 사용되지 않습니다. 상용에 런칭된 서비스는 송장 도착 시간, 운송을 위해 제품을 발송한 시간, 직원을 고용한 시점, 택시가 차고에서 출발하는 정확한 시간 등을 정확히 저장해야 합니다. 따라서 비즈니스 앱 개발자들은 Instant 및 ZonedDateTime 클래스를 가장 많이 사용합니다.
그렇다면 LocalDateTime은 언제 사용해야 될까요?
LocalDateTime을 사용해야 하는 세 가지 상황의 예시는 아래와 같습니다
1. 특정 날짜와 시간을 여러 위치에서 적용하려는 경우
2. 예약을 하는 경우
3. 타임존이 정해지지 않은 경우
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 클래스는 UTC의 앞이나 뒤에 있는 시간-분-초의 컨텍스트를 가진 순간을 날짜 및 시간으로 나타냅니다.
오프셋의 양(시간, 분, 초)은 ZoneOffset 클래스로 표시됩니다. (오프셋의 양의 예: UTC+09:00)
시간, 분, 초가 모두 0인 경우 오프셋 날짜 시간은 UTC에서의 Instant와 동일한 순간(moment)을 나타냅니다.
ZoneOffSet

ZoneOffset 클래스는 UTC에서 오프셋을 나타내며, UTC보다 몇 시간 앞이나 뒤에 있는 시간을 나타냅니다.
ZoneOffset은 단지 시간, 분, 초를 나타낼 뿐 그 이상 그 이하도 아닙니다.
Zone은 ZoneOffset보다 훨씬 큰 개념인데, 오프셋의 이름 및 변경 이력을 가지고 있습니다.
따라서, 단순 offset을 사용하는 것보다는 zone을 사용하는 것이 권장됩니다.
ZoneId

표준 시간대는 ZoneId 클래스로 표시됩니다.
예를 들어, 파리는 몬트리올보다 시간이 빠릅니다. 그래서 우리는 주어진 지역에 대해 정오를 더 잘 나타내기 위해 시곗바늘을 움직일 필요가 있다. 서유럽/아프리카의 UTC 라인에서 동쪽/서쪽으로 멀어질수록 오프셋은 커집니다.
표준 시간대는 지역 커뮤니티 또는 지역에서 시행되는 조정 및 이상 징후를 처리하기 위한 규칙 집합입니다. 가장 흔한 이상 징후는 일광 절약 시간(DST)입니다.
표준 시간대는 가까운 미래에 대해 확인된 과거의 규칙, 현재 규칙 및 규칙의 기록을 가지고 있습니다.
이러한 규칙은 예상보다 자주 변경됩니다. 따라서, 날짜/시간 라이브러리의 규칙(일반적으로 'tz' 데이터베이스 복사본)을 최신 상태로 유지해야 합니다. 다행히도 Oracle이 시간대 업데이트 도구를 출시하여 Java 8에서 최신 상태를 유지하기가 그 어느 때보다 쉬워졌습니다.
시간대 이름은 아메리카/몬트리올, 아프리카/카사블랑카 또는 태평양/오크랜드와 같이 대륙/지역 형태로 적절한 시간대 이름을 지정합니다. EST 또는 IST와 같은 2~4자 약어는 실제 표준 시간대가 아닐뿐더러 독단적인 이름이 아니므로 절대 사용하면 안 됩니다.
정리를 하자면, 타임존은 오프셋과 조정 규칙의 집합체입니다.
ZoneId zoneId = ZoneId.of("Asia/Seoul");
ZonedDateTime

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 포맷을 디폴트로 사용하기 때문에 로그를 남기기 위해 텍스트 형태로 직렬화할 때 해당 포맷을 사용하는 것을 권장한다고 합니다.
Instant 객체와 LocalDateTime 모두 systemUTC() 메서드를 쓰고 있기 때문에 고정된 clock이 아닌 best available system clock 즉, 상황에 따라서 다른 clock을 사용하고 있다고 정의되어있습니다.
따라서, 현재로서는 기존대로 clock 중 하나인 System.currentTimeMillis()를 isWithin24Hours 메서드에 그대로 사용하는 것이 적합하겠다는 결론을 내렸습니다.
시간 유형의 다양한 class 에 각각 어떤 차이점들이 있는지, 글로벌 서비스에는 이와 같은 점을 고려하는 것이 유의미하겠다는 점을 느낄 수 있었습니다.
< Reference : https://meetup.nhncloud.com/posts/125 , https://jaimemin.tistory.com/1537 , https://sujl95.tistory.com/85 >
'java' 카테고리의 다른 글
Java Arrays.sort(), Collection.sort() 정렬 알고리즘 (1) | 2024.10.19 |
---|---|
HashMap 의 동작 원리 (1) | 2024.02.27 |
method class instance object (0) | 2022.09.23 |
생성자 내부클래스 (1) | 2022.09.23 |
Linked List vs Array List (0) | 2022.09.23 |