Java 날짜/시간 API
Java 8부터 새로운 날짜/시간 API가 추가되었다. 날짜/시간 관련 기능을 사용할 일이 생기면 어떤 클래스를 어떻게 사용해야 할지 생각나지 않는 경우가 많아 여기 각 클래스의 기본 사용법을 간단히 정리하려 한다.
기능을 간단히 테스트할 때는 jshell
을 사용하면 편리하다. jshell
을 실행한 후, 날짜/시간 관련 클래스를 쉽게 사용할 수 있도록 java.time
과 java.time.temporal
패키지를 import 해둔다.
$ jshell
| Welcome to JShell -- Version 14.0.1
| For an introduction type: /help intro
jshell> import java.time.*
jshell> import java.time.temporal.*
Instant
Instant
는 타임라인의 특정 시점을 나타낸다. 사건이 발생한 시점을 기록하는 데 사용할 수 있다. Instant
객체는 정적 팩터리 메서드로 생성할 수 있다.
jshell> var now = Instant.now()
now ==> 2020-07-01T16:15:13.342880Z
지금이 아닌 다른 시점, 예를 들어 1시간 전 또는 하루 전 Instant
는 어떻게 만들 수 있을까? epoch milliseconds를 안다면 Instant.ofEpochSecond
메서드를 사용할 수도 있겠지만, 다음과 같이 하는 게 더 직관적이다.
jshell> var oneHourAgo = now.minus(1, ChronoUnit.HOURS)
oneHourAgo ==> 2020-07-01T15:15:13.342880Z
jshell> var oneDayAgo = now.minus(1, ChronoUnit.DAYS)
oneDayAgo ==> 2020-06-30T16:15:13.342880Z
즉, Instant
객체에 plus
또는 minus
메서드를 사용해 다른 시점을 구할 수 있다. 또한 isAfter
, isBefore
메서드로 두 시점의 선후 관계를 알 수 있다.
jshell> now.isAfter(oneHourAgo)
$6 ==> true
jshell> now.isBefore(oneDayAgo)
$7 ==> false
Instant
에서는 연도, 월, 일과 같은 날짜 기본 정보를 얻을 수 없다. get
메서드로 이런 정보를 얻으려 하면 예외가 발생한다.
jshell> now.get(ChronoField.YEAR)
| Exception java.time.temporal.UnsupportedTemporalTypeException: Unsupported field: Year
| at Instant.get (Instant.java:566)
| at (#6:1)
LocalDate / LocalTime / LocalDateTime
LocalDate
는 시간 정보 없이 날짜만 저장한다. LocalTime
은 날짜 정보 없이 시간만 저장한다. LocalDateTime
은 날짜와 시간을 함께 저장한다. 세 클래스 모두 시간대(Time-zone) 정보가 없다.
LocalDate
일반 날짜를 저장할 때 사용할 수 있다. 시간 및 시간대 정보가 없기 때문에 타임라인의 특정 시점을 나타낼 수 없다. 오늘 날짜를 구하려면 now()
메서드를 사용하면 되며, of(year, month, dayOfMonth)
메서드로 원하는 날짜 객체를 만들 수 있다.
jshell> var today = LocalDate.now()
today ==> 2020-07-01
jshell> var d = LocalDate.of(2020, 1, 1)
d ==> 2020-01-01
LocalDate
에는 날짜 기본 정보를 얻을 수 있는 여러 메서드가 있다. 예를 들어 연도는 getYear()
메서드로 알 수 있고, 월은 getMonth()
나 getMonthValue()
메서드로 알 수 있다. getMonth()
는 Month
enum을 리턴하고, getMonthValue()
는 월을 숫자로 리턴한다. getDayOfWeek()
도 DayOfWeek
enum을 리턴한다. get(ChronoField.DAY_OF_WEEK)
는 요일을 숫자로 리턴한다.
jshell> today.getYear()
$11 ==> 2020
jshell> today.getMonth()
$12 ==> JULY
jshell> today.getMonthValue()
$13 ==> 7
jshell> today.getDayOfWeek()
$14 ==> WEDNESDAY
jshell> today.get(ChronoField.DAY_OF_WEEK)
$15 ==> 3
오늘 날짜는 getDayOfMonth()
로 알 수 있다.
jshell> today.getDayOfMonth()
$16 ==> 1
Instant
와 마찬가지로 plus
, minus
메서드로 이전 또는 이후 날짜를 구할 수 있다. LocalDate
는 {plus|minus}Days
, {plus|minus}Weeks
, {plus|minus}Months
, {plus|minus}Years
를 이용해 며칠, 몇 주, 몇 달, 몇 년 전/후 날짜를 구할 수 있다.
jshell> d.plus(10, ChronoUnit.DAYS)
$17 ==> 2020-01-11
jshell> d.minusMonths(2)
$18 ==> 2019-11-01
한가지 재미있는 점은 {plus|minus}Months
동작 방식이다. 2020-01-31
을 생성한 다음 한 달 후를 구하면 2020-02-29
가, 두 달 후를 구하면 2020-03-31
, 세 달 후를 구하면 2020-04-30
이 된다.
jshell> var d31 = LocalDate.of(2020, 1, 31)
d31 ==> 2020-01-31
jshell> d31.plusMonths(1)
$20 ==> 2020-02-29
jshell> d31.plusMonths(2)
$21 ==> 2020-03-31
jshell> d31.plusMonths(3)
$22 ==> 2020-04-30
2020-01-30
을 생성한 다음 한 달 후를 구하면 2020-02-29
가, 두 달 후를 구하면 2020-03-30
, 세 달 후를 구하면 2020-04-30
이 된다.
jshell> var d30 = LocalDate.of(2020, 1, 30)
d30 ==> 2020-01-30
jshell> d30.plusMonths(1)
$24 ==> 2020-02-29
jshell> d30.plusMonths(2)
$25 ==> 2020-03-30
jshell> d30.plusMonths(3)
$26 ==> 2020-04-30
날짜 수는 매 달마다 다르기 때문에 이렇게 동작하는 것 같다. 달을 더하거나 빼는 데는 약간의 모호함이 있다.
atTime
메서드를 이용해 LocalDate
에 시간 정도를 더해 LocalDateTime
객체를 만들 수 있다.
jshell> d.atTime(12, 10, 25)
$27 ==> 2020-01-01T12:10:25
LocalDate
에는 datesUntil
메서드가 있어 날짜 스트림을 만들 수 있다. Period
로 스텝을 지정할 수도 있다.
jshell> d
d ==> 2020-01-01
jshell> d.datesUntil(d.plusDays(7)).collect(Collectors.toList())
$29 ==> [2020-01-01, 2020-01-02, 2020-01-03, 2020-01-04,
2020-01-05, 2020-01-06, 2020-01-07]
jshell> d.datesUntil(d.plusDays(31), Period.ofDays(7)).collect(Collectors.toList())
$30 ==> [2020-01-01, 2020-01-08, 2020-01-15, 2020-01-22, 2020-01-29]
LocalDate
또한 isAfter
, isBefore
메서드로 두 날짜의 선후 관계를 알 수 있다.
LocalTime
시간을 저장할 때 사용할 수 있다. 날짜 및 시간대 정보가 없기 때문에 타임라인의 특정 시점을 나타낼 수 없다. 시간은 나노 초 정밀도로 저장한다.
jshell> var t0 = LocalTime.now()
t0 ==> 16:41:19.327766
jshell> var t1 = LocalTime.of(9, 10, 25)
t1 ==> 09:10:25
plus
, minus
메서드로 이전 또는 이후 시간을 구할 수 있다. LocalDate
와 마찬가지로 {plus|minus}Hours
, {plus|minus}Minutes
, {plus|minus}Seconds
, {plus|minus}Nanos
로 몇 시간, 몇 분, 몇 초, 몇 나노초 전/후 시간을 구할 수 있다.
LocalDate
와 비슷하게 atDate
메서드로 날짜를 지정해 LocalDateTime
객체를 만들 수 있고, isBefore
, isAfter
메서드로 시간의 선후 관계를 확인할 수 있다.
LocalDateTime
시간과 날짜를 함께 저장할 때 사용할 수 있다. 시간대 정보는 없다. 시간은 LocalTime
과 마찬가지로 나노 초 정밀도로 저장한다. 인터페이스는 LocalDate
나 LocalTime
과 비슷하다. 여러 개의 of
정적 팩터리 메서드가 있고, 다양한 plus*
, minus
메서드가 있다.
atOffset
메서드로 오프셋 정보를 추가해 OffsetDateTime
객체를 만들 수 있고, atZone
메서드로 시간대 정보를 추가해 ZonedDateTime
객체를 만들 수 있다.
jshell> LocalDateTime.now()
$33 ==> 2020-07-01T17:33:45.298881
jshell> LocalDateTime.now().atOffset(ZoneOffset.ofHours(1))
$34 ==> 2020-07-01T17:34:08.290028+01:00
jshell> LocalDateTime.now().atZone(ZoneId.systemDefault())
$35 ==> 2020-07-01T17:34:32.793507+01:00[Europe/London]
OffsetDateTime / ZonedDateTime
OffsetDateTime
은 날짜 시간에 UTC 오프셋이 함께 저장되며, ZonedDateTime
은 날짜 시간에 시간대 정보가 함께 저장된다. Instant
, OffsetDateTime
, ZonedDatetime
모두 타임라인의 특정 시점을 저장하는데, Instant
가 가장 단순한 형태고, OffsetDatetime
은 인스턴트에 UTC 오프셋이 함께 있으므로 지역 시간을 얻을 수 있게 된다. ZonedDatetime
은 가장 복잡한 형태로 일광 절약 시간제(daylight saving time, DST) 같은 시간대 규칙이 모두 저장된다.
Period / Duration
Period
는 1년 2개월 3일과 같은 기간을 나타내고, Duration
은 30분 21초와 같은 시간 기간을 나타낸다. 두 클래스 모두 TemporalAmount
인터페이스를 구현한다. 정적 팩터리 메서드 of
로 기간을 직접 지정해 만들 수도 있고, between
메서드로 두 시점 사이의 기간을 구할 수 있다. Peroid
의 between
메서드는 두 날 사이의 기간을 구하며 두 인자 모두 LocalDate
로 받는다
jshell> var d1 = LocalDate.of(2018, 1, 1)
d1 ==> 2018-01-01
jshell> var d2 = LocalDate.now()
d2 ==> 2020-06-14
jshell> var p = Period.between(d1, d2)
p ==> P2Y5M13D
d1
과 d2
사이의 Peroid
를 구하면 P2Y5M13D
로 표시되는데 이는 2년(2Y
) 5개월(5M
) 13일(13D
)을 뜻한다. 각 값은 다음과 같은 구할 수 있다.
jshell> p.getYears()
$39 ==> 2
jshell> p.getMonths()
$40 ==> 5
jshell> p.getDays()
$41 ==> 13
Duration
의 between
메서드도 비슷하지만, 파라미터 타입이 Temporal
이다. LocalDate
, LocalTime
, OffsetDateTime
, OffsetTime
, Zoneddatetime
모두 Temporal
인터페이스를 구현하므로 Duration.between
메서드를 호출하는 데 사용할 수 있지만, LocalDate
와 같이 시간 정보가 없는 객체로 호출하면 예외가 발생한다.
jshell> Duration.between(d1, d2)
| Exception java.time.temporal.UnsupportedTemporalTypeException: Unsupported unit: Seconds
| at LocalDate.until (LocalDate.java:1657)
| at Duration.between (Duration.java:491)
| at (#25:1)
jshell> var dt1 = LocalDateTime.of(2020, 6, 10, 10, 20)
dt1 ==> 2020-06-10T10:20
jshell> var dt2 = LocalDateTime.of(2020, 6, 11, 12, 45)
dt2 ==> 2020-06-11T12:45
jshell> var d = Duration.between(dt1, dt2)
d ==> PT26H25M20S
PT26H25M20S
은 26시간 25분 20초를 뜻한다. getSeconds
메서드로 이 기간이 몇 초인지 알 수 있다.
jshell> d.getSeconds()
$46 ==> 95120
Peroid
에는 getYears
, getMonths
, getDays
메서드가 있고 각각 해당 기간의 연, 개월, 날자 수를 리턴한다. Duration
에서도 이에 대응하는 getHours
, getSeconds
등의 메서드가 있어야 할 것 같은데, getHours
메서드는 없고, getSeconds
메서드는 해당 기간의 초 부분만 리턴하는 게 아니라 전체 기간을 초로 환산한 값을 리턴한다. 인터페이스에 일관성이 없다.
Peroid
에도 해당 기간이 전체 며칠이나 되는지 알려주는 메서드가 있으면 좋을텐데 아쉽게 그런 메서드를 찾을 수 없었다. 대신 두 날짜 사이의 날수를 계산하려면 다음과 같이 해야 한다.
jshell> ChronoUnit.DAYS.between(d1, d2)
$47 ==> 895