이번 게시글에서는 이전 게시글의 DateTime API를 이어서 설명한다. 이번 포스팅에서는 Instant, DateTimeFormatter, Kotlinx-DateTime API를 소개한다.
Instant : 기계 관점에서 시간과 날짜
인간은 더 식별 가능하게, 더 기억하기 쉽게 날짜와 시간을 년도, 월일, 시분초와 같은 단위로 나누어 생각한다.
그럼 기계도 이렇게 저장할까? 당연히 아니다. 그럼 기계 관점에서 어떤 표기가 가장 효율적일까?
아마도 십진수를 이진법으로 변환하듯, 꾸준히 증가하는 연속된 시간에서 하나의 지점을 큰 수로 표현하는 것이 이상적일 것이다.
Java에서는 Instant를 제공하여, Instant를 통해 기계관점에서 친화적인 시간과 날짜를 표기할 수 있다.
자세하게 Instant는 에포크 시간을 기준으로 특정 시간까지 초로 표현한다.
- 에포크(Epoch) 시간은 UTC를 기준으로 1970년 1월 1일 0시 0분 0초를 의미한다.
그럼 Instant는 어떻게 생성할까?
Instant를 생성하는 방법은 시간을 이용한 방법과 시각을 이용한 방법이 있다.
시간을 이용한 방법 : 에포크 시간으로부터 지난 시간
에포크 시간, 1970년 1월 1일부터 지난 시간을 입력하여, Instant를 생성할 수 있다.
fun main(){
// 에포크 시간으로부터 지난 "초"를 입력하여 생성하는 방법
val instantTimeSecond = Instant.ofEpochSecond(10L)
println(instantTimeSecond) // > 1970-01-01T00:00:10Z
// 에포크 시간으로부터 지난 "밀리 초"를 입력하여 생성하는 방법
val instantTimeMilli = Instant.ofEpochMilli(10L)
println(instantTimeMilli) // > 1970-01-01T00:00:00.010Z
// 에포크 시간으로부터 지난 "초 와 나노 초"를 입력하여 생성하는 방법
val instantTimeSecondAndNano = Instant.ofEpochSecond(10, 10)
println(instantTimeSecondAndNano) // > 1970-01-01T00:00:10.000000010Z
}
ofEpochSecond, ofEpochMilli 메소드를 통해 Instant 객체를 생성할 수 있다. Instant의 맨 끝 Z는 UTC+0를 의미한다.
시각을 이용한 방법 : 특정 시점의 ZonedDateTime 변환하기
특정 한 시점을 알고 있다면, 정적 메소드를 통해 Instant로 변환할 수 있다. 특정 한 시점은 Date, Time, TimeZone을 모두 포함하는 시점을 의미한다.
fun main(){
val currentDateTime = LocalDateTime.now()
val koreaZoneId = ZoneId.of("Asia/Seoul")
val currentZonedDateTime = currentDateTime.atZone(koreaZoneId)
// toInstant()를 활용한 예제
val currentInstantWithToInstant = currentZonedDateTime.toInstant()
println(currentInstantWithToInstant) // > 2023-08-05T13:59:33.177535Z
// Instant.from()를 활용한 예제
val currentInstantWithFrom = Instant.from(currentZonedDateTime)
println(currentInstantWithFrom) // > 2023-08-05T13:59:33.177535Z
}
여기서도 Z는 UTC+0를 의미하며, 한국은 UTC+9이므로 9시간을 뺀 시각이 Instant가 된다.
이러한 기계 친화적인 Instant를 어디서 사용할 수 있을까?
Instant는 이벤트가 발생했을 때 TimeStamp를 기록하는 기능으로 사용할 수 있다.
DateTimeFormatter
날짜와 시간의 작업을 처리할 때, Formatting과 Parsing 작업은 빈번하게 발생한다.
- Formatting : 객체를 문자열 형태로 변환하는 작업
- Parsing : 문자열을 객체로 변환하는 작업
Formatting과 Parsing을 위해 java.time 패키지의 DateTimeFormatter 클래스를 사용할 수 있다.
아래의 예시는 LocalDateTime 객체를 format() 메소드를 통해 문자열로 변환하였다.
fun main() {
val currentDateTime = LocalDateTime.now()
val currentDateTimeString = currentDateTime.format(DateTimeFormatter.ISO_DATE_TIME)
println(currentDateTimeString) // > 2023-08-06T12:23:57.639641
}
해당 예시는 parse() 메소드를 통해, 문자열을 LocalDate 객체로 변환하였다.
fun main() {
val LocalDateObject = LocalDate.parse("2022-03-01", DateTimeFormatter.ISO_DATE)
println(LocalDateObject.year) // > 2022
println(LocalDateObject.month) // > MARCH
println(LocalDateObject.dayOfMonth) // > 1
}
이외에도, DateTimeFormatter는 다양한 커스텀 패턴을 적용할 수 있는 메소드도 제공한다.
fun main() {
val currentDateTime = LocalDateTime.now()
val format = DateTimeFormatter.ofPattern("yyyy년 MM월 EEE요일 dd일 HH시 mm분")
val formattedDateTime = currentDateTime.format(format)
println(formattedDateTime) // > 2023년 08월 일요일 06일 12시 34분
}
또한, 옵션을 통해 FormatStyle이나 Locale을 적용할 수 있다.
fun main() {
val currentDateTime = LocalDateTime.now()
val format = DateTimeFormatter
.ofLocalizedDateTime(FormatStyle.MEDIUM) // > 포맷 스타일 길이 설정
.withLocale(Locale.KOREA) // > 한국어 형식으로 출력
val formattedDateTime = currentDateTime.format(format)
println(formattedDateTime) // > 2023. 8. 6. 오후 12:36:34
}
Kotlinx-DateTime
이전까지는 Java DateTime API에 대해서 소개하였다. Kotlin에서도 dateTime 라이브러리를 만들어 배포하고 있다. 여러가지 상황에서 Kotlinx-dateTime을 사용했을 때 장점이 있다.
- Kotlin-Serialization에서 Kotlinx-dateTime을 지원하여, 디폴트 컨버터를 통해 자동으로 직렬화, 역직렬화를 수행한다.
- KMP를 지원한다. 따라서 android, ios모두 kotlinx-dateTime을 기반으로 코드를 작성할 수 있다.
- Java DateTime API와 마찬가지로 불변 타입, 컴파일 시 에러 처리, 스레드 안정성 모두 보장한다.
- 정적 타입을 통해, 컴파일 시점에서 에러를 검출할 수 있다.
아래의 코드를 통해 Kotlinx-dateTime 예시를 확인할 수 있다.
fun main(){
// Instant
val currentMoment: Instant = Clock.System.now()
// TimeZone
val dateTimeInUtc = currentMoment.toLocalDateTime(TimeZone.UTC)
val dateTimeInSystemZone = currentMoment.toLocalDateTime(TimeZone.currentSystemDefault())
val timeZoneKorea = TimeZone.of("Asia/Seoul")
// LocalDate
val shorterToday: LocalDate = Clock.System.todayIn(timeZoneKorea)
// LocalTime, LocalDate, LocalDateTime
val currentDateTime = currentMomonet.toLocalDateTime(timeZoneKorea)
val currentTime = currentDateTime.time
val currentDate = currentMomonet.toLocalDate(timeZoneKorea)
// 매개변수 네이밍 사용가능.
val timeWithNanos = LocalTime(hour = 23, minute = 59, second = 12, nanosecond = 999)
}
기본적인 클래스와 메소드 네이밍이 Java의 DateTime API와 유사하다. 또한, 코틀린으로 작성되어 코틀린 코드의 특징을 잘 살릴 수 있다는 점이 있다.
공식 GitHub를 통해, 더 자세한 사용법을 확인할 수 있다.
마지막으로
시간 라이브러리는 정말 놓치기 쉬운 부분중 하나인 것 같다. 필자 역시도 시간 라이브러리는 구글링해서 복붙하는게 일상이였고, 당연하게도 Date, Calendar, DateTime 이런 키워드를 알지 못했다. 이번 블로그 포스팅을 통해, 다양한 DateTIme 및 키워드에 대해서 알게 되었다.
구글에 "안드로이드 시간 확인하는 법, 포매팅하는 법"을 검색하면, 아직도 Java의 Date나 Calendar 라이브러리를 사용한 예제들이 자주 보인다. 이번 포스팅을 통해, 새롭게 개발하시거나 공부하시는 분들이 DateTime API를 정확하게 알지 못해도, 구 버전의 라이브러리를 도입하는 것을 막고, 개선된 라이브러리를 사용하도록 유도하는것에 목적이 있다.
참고 문헌
https://www.baeldung.com/java-8-date-time-intro
https://umanking.github.io/2020/05/09/java-date-time/