일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | ||||
4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 |
- QueryDSL
- bean
- offsetdatetime
- consumer
- producer
- mysql
- entity graph
- git
- spring
- AWS
- Spring Data JPA
- Streams
- ECS
- Entity
- CodePipeline
- topic생성
- Kubernetes
- PAGING
- Spring JPA
- K8s
- API
- transactionaleventlistener
- kafka
- centos7
- CI
- Kotlin
- cd
- JPA
- spring kafka
- mirror maker2
- Today
- Total
Yebali
OffsetDateTime의 시간 비교 본문
현재 일하는 회사의 시설 예약서버에서는 날짜와 시간을 한 번에 다루기 위해 'OffsetDateTime' 타입을 사용한다.
OffsetDateTime은 연월일+시분초 데이터뿐만 아니라 Timezone 정보를 함께 포함하는 데이터이다.
위 타입을 사용하면서 시간을 비교할 때 유의해야 하는 몇 가지가 있다.
동등성 비교
동등성이란 두 객체가 가지는 값이 서로 같다는 의미이다.
@Test
fun `동등성`() {
val `UTC 1월 1일 0시` = OffsetDateTime.parse("2023-01-01T00:00:00Z")
val `Seoul 1월 1일 9시` = OffsetDateTime.parse("2023-01-01T09:00:00+09:00")
// Equals(Object obj)은 offset까지 같아야 한다.
assertThat(`UTC 1월 1일 0시` == `Seoul 1월 1일 9시`).isFalse
// isEquals(OffsetDateTime other)은 offset이 달라도 같다.
assertThat(`UTC 1월 1일 0시`.isEqual(`Seoul 1월 1일 9시`)).isTrue
// toInstant()은 offset이 달라도 같다.
assertThat(`UTC 1월 1일 0시`.toInstant() == `Seoul 1월 1일 9시`.toInstant()).isTrue
}
먼저 동일한 시간을 각각 UTC, Asia/Seoul(+09:00)의 타임존으로 설정하고 각각 '==', 'isEqual()', 'toInstant'를 사용해 비교했다.
'=='을 사용한 동등성 비교
'=='는 두 객체가 서로 동등하지 않다고 판단한다.
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
return (obj instanceof OffsetDateTime other)
&& dateTime.equals(other.dateTime)
&& offset.equals(other.offset);
}
OffsetDateTime.java 내부의 구현을 보면 위처럼 dateTime(LocalDateTime)과 offset이 모두 같아야 두 객체가 동등하다고 판단한다.
예시에서는 두 객체가 시간과 offset이 모두 다르기 때문에 'false'를 반환한다.
isEqual()을 사용한 동등성 비교
'isEqual()'은 두 객체가 서로 동등하다고 판단한다.
// OffsetDateTime.java
public boolean isEqual(OffsetDateTime other) {
return toEpochSecond() == other.toEpochSecond() &&
toLocalTime().getNano() == other.toLocalTime().getNano();
}
public long toEpochSecond() {
return dateTime.toEpochSecond(offset);
}
// ChronoLocalDateTime.java
default long toEpochSecond(ZoneOffset offset) {
Objects.requireNonNull(offset, "offset");
long epochDay = toLocalDate().toEpochDay();
long secs = epochDay * 86400 + toLocalTime().toSecondOfDay();
secs -= offset.getTotalSeconds();
return secs;
}
'==' 과는 다르게 각 객체의 값을 offset을 감안하여 'dateTime'과 'dateTime'을 LocalTime으로 변환한 값들을 각각 epoch second로 변환하여 서로 비교한다.
toInstant()를 사용한 동등성 비교
'toInstant()'은 두 객체가 서로 동등하다고 판단한다.
// OffsetDateTime.java
public Instant toInstant() {
return dateTime.toInstant(offset);
}
// ChronoLocalDateTime.java
default Instant toInstant(ZoneOffset offset) {
return Instant.ofEpochSecond(toEpochSecond(offset), toLocalTime().getNano());
}
'toInstant()'역시 내부의 'dateTime'을 offset을 반영하여 epoch second로 변환하여 비교하기 때문에 'isEqual()'과 동일한 결과를 반환한다.
대소 비교
@Test
fun `대소비교`() {
val `UTC 1월 1일 0시` = OffsetDateTime.parse("2023-01-01T00:00:00Z")
val `Seoul 1월 1일 9시` = OffsetDateTime.parse("2023-01-01T09:00:00+09:00")
// compareTo(OffsetDateTime other)은 Offset을 고려하지 않기 때문에 Asia/seoul이 더 크다고 판단한다.
assertThat(`UTC 1월 1일 0시` < `Seoul 1월 1일 9시`).isTrue
assertThat(`UTC 1월 1일 0시` > `Seoul 1월 1일 9시`).isFalse
// isBefore(OffsetDateTime other)/isAfter(OffsetDateTime other)은
// epoch second로 변환하여 비교하기 때문에 두 객체가 동일하다고 판단한다.
assertThat(`UTC 1월 1일 0시`.isBefore(`Seoul 1월 1일 9시`)).isFalse
assertThat(`UTC 1월 1일 0시`.isAfter(`Seoul 1월 1일 9시`)).isFalse
}
compareTo()을 사용한 대소비교
'compareTo()'은 `Seoul 1월 1일 9시`을 더 큰 값으로 판단한다.
// OffsetDateTime.java
@Override
public int compareTo(OffsetDateTime other) {
int cmp = compareInstant(this, other);
if (cmp == 0) {
cmp = toLocalDateTime().compareTo(other.toLocalDateTime());
}
return cmp;
}
private static int compareInstant(OffsetDateTime datetime1, OffsetDateTime datetime2) {
if (datetime1.getOffset().equals(datetime2.getOffset())) {
return datetime1.toLocalDateTime().compareTo(datetime2.toLocalDateTime());
}
int cmp = Long.compare(datetime1.toEpochSecond(), datetime2.toEpochSecond());
if (cmp == 0) {
cmp = datetime1.toLocalTime().getNano() - datetime2.toLocalTime().getNano();
}
return cmp;
}
'compareTo()'의 구현은 위와 같다.
offset이 같다면 두 객체의 LocalDate값을 비교한다.
offset이 다르다면 epoch second로 변환하여 비교하고, 변환하여 비교한 결과가 같다면 시간 값을 비교한다.
시간 값까지 같다면 'LocalDateTime.compareTo()'를 사용해 또 한 번 비교한다.
위 예시에서는 'compareInstant()'에서 비교한 결과가 0(같다)이기 때문에 마지막 'LocalDateTime.compareTo()'비교에서
`Seoul 1월 1일 9시`가 더 크다고 판단한다.
isBefore()/isAfter()을 사용한 대소비교
isBefore()/isAfter은 두 값이 서로 같다고 판단한다.
// OffsetDateTime.java
public boolean isBefore(OffsetDateTime other) {
long thisEpochSec = toEpochSecond();
long otherEpochSec = other.toEpochSecond();
return thisEpochSec < otherEpochSec ||
(thisEpochSec == otherEpochSec && toLocalTime().getNano() < other.toLocalTime().getNano());
}
public boolean isAfter(OffsetDateTime other) {
long thisEpochSec = toEpochSecond();
long otherEpochSec = other.toEpochSecond();
return thisEpochSec > otherEpochSec ||
(thisEpochSec == otherEpochSec && toLocalTime().getNano() > other.toLocalTime().getNano());
}
isBefore()/isAfter()은 두 객체의 'dateTime'값을 epoch second로 변환하여 비교한다.
결론
Timezone을 고려한 동등성, 대소비교를 하고 싶다면 isEqual(), isBefore(), isAfter()을 적절히 사용하자.
'Kotlin' 카테고리의 다른 글
Coroutine 톺아보기 (0) | 2024.04.27 |
---|---|
Java, Kotlin의 날짜와 시간 (0) | 2022.11.28 |
Spring JPA Entity에 Data Class를 사용해도 될까? (0) | 2021.10.31 |
Kotlin의 Collections (0) | 2021.10.31 |
Kotlin의 Data/Enum 클래스 (0) | 2021.09.22 |