[Modern Java in Action] Chapter12. 새로운 날짜와 시간 API

July 15, 2022 - 10 minute read -
book moder java in action

모던 자바 인 액션 12장에서는 날짜와 시간 API 에 대해 소개한다.
자바 8에서 개선된 날짜와 시간 API 기능과 사용 방법에 대해 자세히 알아본다.

기존에 존재하던 Date, DateFormat 기능들은 사용도 불편하고 문제가 있었다.
그래서 많은 자바 개발자는 Joda-Time 같은 서드파티 라이브러리를 많이 사용했다.
결국 자바 8에서는 Joda-Time의 많은 기능들을 java.time 패키지로 추가했다.


12.1 LocalDate, LocalTime, Instant, Duration, Period 클래스

LocalDate 와 LocalTime 사용

LocalDateof 정적 팩터리 메서드로 인스턴스를 만들 수 있고
인스턴스는 연도, 달, 요일 등을 반환하는 메서드를 제공한다. LocalDate.now() 팩토리 메서드로 시스템 시계의 정보로 현재 날짜 정보를 가져올 수 있다.

LocalDate date = LocalDate.of(2022, 7, 15)
int year      = date.getYear();        // 2022
Month month   = date.getMonth();       // JULY
int day       = date.getDayOfMonth();  // 15
DayOfWeek dow = date.getDayOfWeek();   // FRIDAY
int len       = date.lengthOfMonth();  // 31    (7월의 일 수)
boolean leap  = date.isLeapYear();     // false (윤년 아님)

TemporalField 는 시간 관련 객체에서 어떤 필드의 값에 접근할지 정의하는 인터페이스다.
ChronoFiledTemporalField 인퍼에이스를 정의하는 열거 타입으로 원하는 정보를 쉽게 가져올 수 있다.

int year = date.get(ChronoField.YEAR);
int month = date.get(ChronoField.MONTH_OF_YEAR);
int day = date.get(ChronoField.DAY_OF_MONTH);

LocalTime 클래스로 시간을 표현할 수 잇다.

LocalTime time = LocalTime.of(13, 45, 20);
int hour   = time.getHour();    // 13
int minute = time.getMinute();  // 45
int second = time.getSecond();  // 20

parse 정적 메서드를 이용하여 문자열로 인스턴스를 만드는 방법도 있다.
문자열을 파싱할 수 없을 때는 DateTimeParseException 이 발생된다.

LocalDate date = LocalDate.parse("2021-07-15");
LocalTime time = LocalTime.parse("13:45:20");

날짜와 시간 조합

LocalDateTimeLocalDateLocalTime 을 쌍으로 갖는 복합 클래스다.

LocalDate date = LocalDate.now();
LocalTime time = LocalTime.now();
LocalDateTime dt1 = LocalDateTime.of(2022, Month.JULY, 15, 13, 45, 20);
LocalDateTime dt2 = LocalDateTime.of(date, time);
LocalDateTime dt3 = date.atTime(13, 45, 20);
LocalDateTime dt4 = date.atTime(time);
LocalDateTime dt5 = time.atDate(date);

// date 또는 time 인스턴스 추출
LocalDate date1 = dt1.toLocalDate();
LocalTime time1 = dt1.toLocalTime();

Instant 클래스: 기계의 날짜와 시간

기계는 주, 날짜, 시간, 분으로 날짜와 시간을 계산하지 않고 연속된 시간에서 특정 지점을 하나의 큰수로 표현한다.
java.time.Instant 클래스에서는 이와 같은 기계적인 관점에서 시간을 표현한다. (유닉스 에포크 시간, unix epoch time, 1970년 1월 1일 0시 0분 0초 UTC)

Instant 는 나노초(10억분의 1초)의 정밀도를 제공하며, ofEpochSecond 팩토리 메서드에 초를 넘겨서 인스턴스를 만들 수 있다.
두번째 인수를 이용하면 나노초 단위로 시간 보정도 가능하다.

Instant.ofEpochSecond(3);
Instant.ofEpochSecond(3, 0);
Instant.ofEpochSecond(2, 1_000_000_000);   // 2초 이후의 1억 나노초(1초)
Instant.ofEpochSecond(4, -1_000_000_000);  // 4초 이전의 1억 나노초(1초)

기계 전용이기 때문에 사람이 읽을 수 있는 시간 정보는 제공하지 않는다.

// java.time.temporal.UnsupportedTemporalTypeException: Unsupported field: DayOfMonth 발생 
int day = Instant.now().get(ChronoField.DAY_OF_MONTH);

Duration 과 Period 정의

Duration 은 두 시간 객체 사이의 지속시간을 의미한다. between 정적 팩토리 메서드로 두 시간 객체 사이의 지속시간을 만들 수 있다.
하지만 Duration은 초와 나노초로 시간 단위를 표현하므로 LocalDate 는 전달할 수 없다.

Duration d1 = Duration.between(time1, time2);
Duration d2 = Duration.between(dateTime1, dateTime2);
Duration d3 = Duration.between(instant1, instant2);

Period 클래스를 이용하면 년, 월, 일로 시간 표현을 할 수 있다.
Periodbetween 정적 팩토리 메서드를 이용하면 두 LocalDate 의 차이를 구할 수 있다.

Period tenDays = Period.between(
        LocalDate.of(2022, 7, 1),
        LocalDate.of(2022, 7, 15)
);

간격을 표현하는 날자와 시간 클래스의 공통 메서드

메서드 정적 설명
betewwen 두 시간 사이의 간격을 생성
from 시간 단위로 간격을 생성
of 주어진 구성 요소에서 간격 인스턴스 생성
parse 문자열을 파싱해서 간격 인스턴스 생성
addTo 아니오 현재값의 복사본을 생성하고 지정된 Temporal 객체에 추가
get 아니오 현재 간격 정보값을 읽음
isNegative 아니오 간격이 음수인지 확인
isZero 아니오 간격이 0인이 확인
minus 아니오 현재값에 주어진 시간을 뺀 복사본 생성
multipliedBy 아니오 현재값에 주어진 값을 곱한 복사본 생성
negated 아니오 주어진 값의 부호를 반전한 복사본 생성
plus 아니오 현재값에 주어진 시간을 더한 복사본 생성
subtractFrom 아니오 지정된 Temporal 객체에서 간격을 뺌


12.2 날짜 조정, 파싱, 포매팅

withAttribute 메서드를 이용하면 절대적으로, plus 또는 minus 메서드를 이용하면 상대적으로 기존 객체를 변경하지 않고 변경된 속성을 포함한 객체를 생성할 수 있다.
첫번째 인수에 TemporalField 메서드를 추가하면 더 범용적으로 메서드를 활용할 수 있다. 만약 해당 필드를 지원하지 않으면 UnsupportedTemporalTypeException 이 발생된다.

// 절대적인 방식
LocalDate date1 = LocalDate.of(2022, 7, 16);                 // 2022-07-16
LocalDate date2 = date1.withYear(2021);                      // 2021-07-16
LocalDate date3 = date2.withDayOfMonth(25);                  // 2021-07-25
LocalDate date4 = date3.with(ChronoField.MONTH_OF_YEAR, 2);  // 2021-02-25

// 상대적인 방식
LocalDate date1 = LocalDate.of(2022, 7, 16);                 // 2022-07-16
LocalDate date2 = date1.plusWeeks(1);                        // 2022-07-23
LocalDate date3 = date2.minusYears(1);                       // 2021-07-23
LocalDate date4 = date3.plus(3, ChronoUnit.MONTHS);          // 2021-10-23

특정 시점을 표현하는 날짜 시간 클래스의 공통 메서드

메서드 정적 설명
from 주어진 Temporal 객체로 인스턴스 생성
now 시스템 시계로 Temporal 객체 생성
of 주어진 구성요소에서 Temporal 객체 생성
parse 문자열을 파싱해서 Temporal 객체 생성
atOffset 아니오 시간대 오프셋과 Temporal 객체 합침
atZone 아니오 시간대 오프셋과 Temporal 객체 합침
format 아니오 지정된 포매터를 이용해서
Temporal 객체를 문자열로 변환 (Instant 지원 x)
get 아니오 Temporal 객체의 상태를 읽음
minus 아니오 특정 시간을 뺀 Temporal 객체의 복사본 생성
plus 아니오 특정 시간을 더한 Temporal 객체의 복사본 생성
with 아니오 일부 상태를 바꾼 Temporal 객체의 복사본 생성

TemporalAdjusters 사용하기

TemporalAdjusters 을 전달하여 with 메서드에 다양한 동작을 수행할 수 있다.

import static java.time.temporal.TemporalAdjusters.*;
LocalDate date1 = LocalDate.of(2022, 7, 16);                // 2022-07-16
LocalDate date2 = date1.with(nextOrSame(DayOfWeek.SUNDAY)); // 2022-07-17
LocalDate date3 = date2.with(lastDayOfMonth());             // 2022-07-31

팩토리 메서드

메서드 설명
dayOfWeekInMonth 서수 요일에 해당하는 날짜를 반환하는
TemporalAdjuster 반환(음수면 월의 끝에서 거꾸로 계산)
firstDayOfMonth 현재 달의 첫 번째 날짜를 반환하는 TemporalAdjuster 반환
firstDayOfNextMonth 다음 달의 첫 번째 날짜를 반환하는 TemporalAdjuster 반환
firstDayOfNextYear 내년의 첫 번째 날짜를 반환하는 TemporalAdjuster 반환
firstDayOfYear 올해의 첫 번째 날짜를 반환하는 TemporalAdjuster 반환
firstInMonth 현재 달의 첫 번째 요일에 해당하는 날짜를 반환하는 TemporalAdjuster 반환
lastDayOfMonth 현재 달의 마지막 날짜를 반환하는 TemporalAdjuster 반환
lastDayOfNextMonth 다음 달의 마지막 날짜를 반환하는 TemporalAdjuster 반환
lastDayOfNextYear 내년의 마지막 날짜를 반환하는 TemporalAdjuster 반환
lastDayOfYear 올해의 마지막 날짜를 반환하는 TemporalAdjuster 반환
lastInMonth 현재 달의 마지막 요일에 해당하는 날짜를 반환하는 TemporalAdjuster 반환
next, previous 지정한 요일이 처음/이전으로 나타나는 날짜를 반환하는 TemporalAdjuster 반환
nextOrSame 지정한 요일이 같거나 처음으로 나타나는 날짜를 반환하는 TemporalAdjuster 반환
previousOrSame 지정한 요일이 같거나 이전으로 나타나는 날짜를 반환하는 TemporalAdjuster 반환

날짜와 시간 객체 출력과 파싱

날짜와 시간 관련 포매팅과 파싱 전용 패키지 java.time.format이 추가되었다.
그 중 DateTimeFormatter 클래스는 정적 팩토리 메서드와 상수(BASIC_ISO_DATE, ISO_LOCAL_DATE)를 이용해 쉽게 포매터를 만들 수 있는 가장 중요한 클래스다.

// 포매팅 
LocalDate date = LocalDate.of(2022, 7, 16)
date.format(DateTimeFormatter.BASIC_ISO_DATE);  // 20220716
date.format(DateTimeFormatter.ISO_LOCAL_DATE);  // 2022-07-16

// 파싱
LocalDate.parse("20220716", DateTimeFormatter.BASIC_ISO_DATE);
LocalDate.parse("2022-07-16", DateTimeFormatter.ISO_LOCAL_DATE);

// 특정 패턴 생성
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd/MM/yyyy");
LocalDate date1 = LocalDate.of(2022, 7, 16);
String formattedDate = date1.format(formatter);        // 16/07/2022
LocalDate date2 = LocalDate.parse(formattedDate, formatter);

// 지역화된 DateTimeFormmater
DateTimeFormatter italianFormatter = DateTimeFormatter.ofPattern("d. MMMM yyyy", Locale.ITALIAN);
LocalDate date1 = LocalDate.of(2022, 7, 16);
String formattedDate = date1.format(italianFormatter);        // 16. luglio 2022
LocalDate date2 = LocalDate.parse(formattedDate, italianFormatter);

// DateTimeFormatterBuilder 로 포매터 생성하기
DateTimeFormatter italianFormatter = new DateTimeFormatterBuilder()
        .appendText(ChronoField.DAY_OF_MONTH)
        .appendLiteral(". ")
        .appendText(ChronoField.MONTH_OF_YEAR)
        .appendLiteral(" ")
        .appendText(ChronoField.YEAR)
        .parseCaseInsensitive()
        .toFormatter(Locale.ITALIAN);


12.3 다양한 시간대와 캘린더 활용 방법

시간대를 간단하게 처리하기 위해 java.time.ZoneId 불변 클래스가 추가되었다.
이 클래스를 이용하면 서머타임(Daylight Saving Time, DST) 같은 사항이 자동으로 처리된다.

시간대 사용하기

표준 시간이 같은 지역을 묶어서 시간대(time zone) 규칙 집합을 정의한다. ZoneRules 클래스에는 약 40개 정도의 시간대가 존재하며, getRules 로 시간대의 규정을 가져올 수 있다. 지역 ID 는 ‘{지역}/{도시}’ 형태로 이루어지며 IANA Time Zone Database 에서 제공하는 지역 집합 정보를 이용한다.

ZoneId romeZone = ZoneId.of("Europe/Rome");

LocalDate date = LocaDate.of(2022, Month.JULY, 17);
ZonedDateTime zdt1 = date.atStartOfDay(romeZone);
LocalDateTime dateTime = LocalDateTime.of(2022, Month.JULY, 17, 13, 45);
ZonedDateTime zdt2 = dateTime.atZone(romeZone);
Instant instant = Instant.now();
ZonedDateTime zdt3 = instant.atZone(romeZone);
// LocalDateTime 으로 변경
LocalDateTime timeFromInstant = LocalDateTime.ofInstant(instant, romeZone);

UTC/Greenwich 기준의 고정 오프셋

가끔 UTC(Universal Time Coordinated, 협정 세계시) / GMT(Greenwich Mean Time, 그리니치 표준시) 기준으로 시간대를 표현하기도 한다.

// 서머 타임을 제대로 처리할 수 없어서 권장하지 않는 방식
ZoneOffset newYorkOffset = ZoneOffset.of("-05:00");

// OffsetDateTime으로 ISO-8601 캘린더 시스템에서 정의하는 UTC/GMT와 오프셋으로 표현 가능
LocalDateTime dateTime = LocalDateTime.of(2022, Month.JULY, 17, 13, 45);
OffsetDateTime dateTimeInNewYork = OffsetDateTime.of(date, newYorkOffset);

대안 캘린더 시스템 사용하기

자바 8에서는 ISO-8601 표준 시스템을 준수하지 않는 4개의 캘린더 시스템을 추가로 제공한다.
LocalDate 와 함께 이 클래스들은 ChronoLocalDate 인터페이스를 구현한다.

  • ThaiBuddhistDate
  • MinguoDate
  • JapaneseDate
  • HijrahDate