어플을 개발할 때 많은 상황에서 시간을 활용해야 하는 순간이 발생한다. 이 때 우리는 DateTime API를 사용할 수 있다. DateTime 이전에 사용했던 Date, Calendar 라이브러리의 문제점과 DateTime 라이브러리를 소개한다.
Date, Calendar vs DateTime
- Immutable Type
- 기존 Date, Calendar 라이브러리는 가변 클래스였다. 가변성은 날짜와 시간 같은 요소에서 좋지 않으며, 유지 보수를 어렵게 한다.
- DateTime은 불변 타입을 제공함으로써, 안전하게 사용할 수 있도록 설계되었다.
- Thread Safety
- Date, Calendar 라이브러리는 동시성 문제를 가지고 있었고, 이를 위해 추가적인 코드를 작성해야만 했다.
- DateTime은 불변 타입이며, 스레드로부터 동시성 문제를 해결하였다.
- Zoned Date and Time
- Date, Calendar 라이브러리는 타임존(지역에 따라 시간이 다른 것) 이슈를 처리하기 위해 추가적인 코드를 작성해야만 했다.
- DateTime API는 따로 ZoneDate, ZoneTime를 제공하여, 타임존 이슈를 간편하게 처리할 수 있다.
- Consistent Type
- DateTime API는 Duration, Period 모두 같은 도메인 모델을 가르키고 있다. 문제가 발생하는 경우 컴파일 타임에서 확인할 수 있다.
이러한 많은 문제 때문에, 사용자들은 추가적인 코드를 작성하거나, 오픈소스 라이브러리를 사용하여 시간 작업을 처리했었다. 오라클에서는 Java 8 부터 개선되고, 다양한 기능을 포함하는 DateTime API를 제공하기 시작했다.
LocalDate, LocalTime, LocalDateTime
가장 일반적으로 사용되는 클래스는 LocalDate , LocalTime, LocalDateTime이다. 이름에서 알 수 있듯이 관찰자의 컨텍스트에서 로컬 날짜/시간을 나타낸다. 컨텍스트에서 시간대를 명시적으로 지정할 필요가 없을 때 주로 이러한 클래스를 사용한다.
Local Date
Local Date는 시간이 포함되지 않는 ISO TimeZone 규칙을 적용한 날짜를 제공한다. 아래를 통해 사용법을 확인할 수 있다.
fun main(){
// LocalDate 선언
val currentDay = LocalDate.now() // > 2023-08-05
val currentDayWithOf = LocalDate.of(2023, 8, 5) // > 2023-08-05
val currentDayWithParse = LocalDate.parse("2022-08-05") // > 2023-08-05
}
now() 메소드를 통해 현재 날짜를 불러오거나, of() 메소드를 통해 지정한 날짜를 입력하여 LocalDate 객체를 생성할 수 있다. 또한, parse() 메소드를 통해 ISO 형식에 맞는 문자열을 LocalDate 객체로 변환할 수 있다.
LocalDate의 정보를 가져오기 위해 다양한 메소드를 제공한다. 아래의 예시를 통해 일부를 확인할 수 있다.
fun main(){
// LocalDate 선언
val currentDay = LocalDate.now() // > 2023-08-05
currentDay.year // > 2023
currentDay.month // > 8
currentDay.dayOfMonth // > 5
currentDay.get(ChronoField.DAY_OF_MONTH) // > 5
currentDay.plusDays(30) // > 날짜에서 30일 추가한 LocalDate 반환
currentDay.isLeapYear // > 현재 윤년인가?
currentDay.lengthOfMonth() // > 현재 달이 몇일까지 있는가?
}
getter(year, month ... ) 메소드 및 다양한 날짜 연산 메소드를 통해, 코드의 가독성이 개선되고, 편리하게 연산을 활용할 수 있다.
ChoronoField는 TemproalField의 구현체로 enum 클래스의 값을 입력함으로써, 원하는 정보를 얻을 수 있다.
LocalDate는 따로 시간을 필요하지 않는 생일 리마인더 어플리케이션과 같은 곳에서 사용할 수 있다.
Local Time
LocalTime은 LocalDate와 다르게 날짜가 없고 시간만 표시한다.
LocalDate와 같이 parse() 및 of() 메서드를 사용하여 LocalTime 인스턴스를 만들 수 있다.
fun main(){
// LocalTime 선언
val currentTime = LocalTime.now() // > 17:21:05.861459
val currentTimeWithOf = LocalTime.of(5, 20) // 05:20
val currentTimeWithOfToNano = LocalTime.of(5, 20, 30, 30) // > 05:20:30.000000030
val currentTimeWithParse = LocalTime.parse("05:23:03") // > 05:23:03
}
of() 메소드를 통해, 시간과 분만 표기할 수 도 있고, 초와 나노초도 표기할 수 있다.
LocalTime의 정보를 가져오기 위해 다양한 메소드를 제공한다. 아래의 예시를 통해 일부를 확인할 수 있다.
// LocalDate 선언
val currentTime = LocalTime.now() // > 17:18:31.169584000
val hour = currentTime.hour // > 17
val minute = currentTime.minute // > 28
val second = currentTime.second // > 31
val nanoSecond = currentTime.nano // > 169584000
currentTime.plusHours(30) // > 23:18:31.169584000
currentTime.minusSeconds(20) // > 17:18:11.169584000
val maxTime = LocalTime.MAX // > 하루의 마지막 시간 23:59:59.999999999
val minTime = LocalTime.MIN // > 하루의 처음 시간 00:00
LocalDate와 같이, getter 및 연산 메소드를 사용할 수 있다. LocalTime의 멤버 프로퍼티를 통해서 하루의 최대, 최소, 정오 시간을 얻을 수 있다.
LocalTime은 날짜가 필요없는 일정이나 리마인더 앱에서 사용이 가능하다.
LocalDateTime
LocalDate과 LocalTime 함께 사용하는 LocalDateTime이 있다. LcoalDateTime은 날짜와 시간을 함께 표기할 때 사용한다.
LocalDate과 LocalTime과 동일하게, now(), of(), parse() 메소드를 통해 구현할 수 있으며, LocalDate에 atTime()을 사용하거나, LocalTime에 atDate() 메소드를 사용하여 구현할 수 있다.
fun main(){
// LocalDateTime 선언
val currentDateTime = LocalDateTime.now() // > 2023-08-05T19:44:40.384472
val currentDateTimeWithOf = LocalDateTime.of(2023, Month.AUGUST, 8, 7, 42)
val currentDateAtTime = LocalDate.now().atTime(7, 42) // > Date.atTime()
val currentTimeAtDay = LocalTime.now().atDate(LocalDate.now()) // > Time.atDate()
}
LocalDateTime에서는 LocalDate와 LocalTime에서 사용한 메소드 대부분 사용할 수 있다.
fun main(){
val currentDateTime = LocalDateTime.now() // > 2023-08-05T20:44:40.384472
currentDateTime.year // > 2023
currentDateTime.hour // > 20
currentDateTime.get(ChronoField.DAY_OF_WEEK) // > 6 토요일
currentDateTime.toLocalDate() // LocalDate 으로 변환
currentDateTime.toLocalTime() // LocalTime 으로 변환
currentDateTime.plusHours(10) // > 10시간 후 연산
currentDateTime.minusMonths(3) // > 10달 전 연산
println(LocalDateTime.MAX) // > +999999999-12-31T23:59:59.999999999
println(LocalDateTime.MIN) // > -999999999-01-01T00:00
}
LocalDateTime에 toLocalDate(), toLocalTime() 메소드를 통해, 다른 객체로 변환할 수 있다.
LocalDateTime은 Calendar 어플리케이션과 같이, 날짜와 시간 모두 사용되는 기능에서 사용할 수 있다.
TimeZone
타임존은 동일한 로컬 시간을 따르는 지역을 의미한다. 보통 국가별로 고유 타임존을 사용하며, 면적이 넓은 나라는 지역별로 다른 타임존을 사용한다.
TimeZone 표기 방식
- GMT(GreenWich Mean Time)
- 영국 그리니치 천문대 기준으로 하는 태양 시간이다.
- UTC
- GMT를 대체하기 위한 새로운 표준이다.
- 970년 1월 1일 자정을 0 밀리초로 설정하여 기준을 삼아 그 후로 시간의 흐름을 밀리초로 계산한다.
- Offset
- UTC+09:00에서 +09:00의 의미는 UTC 기준 시간보다 9시간 빠르다는 의미이다. 즉, UTC 12시이면, 한국시간으로 9시를 의미한다. 이처럼, UTC와의 차이를 나타낸 것을 Offset이라고 한다.
- 한 지역의 타임존은 하나 이상의 오프셋을 갖는다.
- ISO 8610
- 날짜와 시간 관련된 데이터 교환을 다루는 국제 표준. 국제 표준화 기구(ISO)가 지정한 표준이다.
- LocalDateTime의 기본 형식이다.
ZonedDateTime
어플을 개발하다, 다른 나라의 사람과 일정을 잡는 기능을 구현한다고 가정하자. 우리나라에서 LocalDateTime을 통해 설정한 일정을 다른나라 사람과 공유하면, 절대로 만날 수 없을 것이다. 앞서 설명한 TimeZone(시간대)때문인데, 이 때 우리는 ZonedDateTime을 활용할 수 있다.
ZonedDateTime은 세 가지, Date, Time, ZoneId으로 구성된다.
ZonedDateTime을 확인하기 전에, ZoneId를 먼저 확인하자. TimeZone은 아래와 같이 구현할 수 있다.
fun main(){
// ZoneId 생성
val koreaZoneId = ZoneId.of("Asia/Seoul") // 지정한 시간 대 설정
val defaultZoneId = ZoneId.systemDefault() // > 기기가 속한 시간 대 설정
ZoneId.getAvailableZoneIds().forEach{ zoneId ->
println("$zoneId, ") // > Asia/Seoul, Australia/Sydney, America/Lima, Australia/LHI, Europe/Madrid ...
} // 총 603개
}
ZoneId는 ZoneId 클래스의 of(), systemDefault() 메소드를 통해 구현할 수 있다.
다른 나라의 TimeZone은 ZoneId는 getAvailableZoneIds() 메소드를 통해 확인할 수 있다.
이렇게 구현한 ZoneId를 통해 ZonedDateTime을 만들 수 있다.
fun main(){
val currentDateTime = LocalDateTime.now() // > 2023-08-05T20:00:42.963540
val koreaZoneId = ZoneId.systemDefault()
val parisZoneId = ZoneId.of("Europe/Paris")
currentDateTime.atZone(koreaZoneId) // > 2023-08-05T20:00:42.963540+09:00[Asia/Seoul]
currentDateTime.atZone(parisZoneId) // > 2023-08-05T20:00:42.963540+02:00[Europe/Paris]
}
atZone 메소드를 통해, ZonedDateTime을 생성하였다. TimeZone을 설정함으로써, 위에서 설명했던 offset과 timeZone이 새로 생긴 것을 확인할 수 있다. 이를 통해, 설정한 시간이 어느 시간대를 기준으로 설정되었는 지 표시할 수 있다.
TimeZone을 고려할 수 있는 방법 중, OffsetDateTime도 존재한다. OffsetDateTime은 offset이 추가된 DateTime 표기이다.
fun main(){
// offset을 통해서 생성
val offset = ZoneOffset.of("+09:00")
OffsetDateTime.of(currentDateTime, offset) // > 2023-08-05T20:00:42.963540+09:00
}
따로 ZoneId를 생성하는 것이 아닌, ZoneOffset을 설정하여, TimeZone에 맞는 OffsetDateTime을 생성할 수 있다.
내용이 많아, 2편에서 추가로 소개할 예정이다!
2편에서는 Instant, DateTimeFormatter, Kotlinx-dateTime을 소개한다.