모던 자바 인 액션 20. 새로운 날짜와 시간 API
JAVA/모던 자바 인 액션

모던 자바 인 액션 20. 새로운 날짜와 시간 API

자바는 Date 클래스 하나로 날짜와 시간 관련 기능을 제공해왔었습니다. 하지만 단위가 밀리초 단위였고, 0에서 시작하는 달 인덱스, 오프셋의 기준은 1900년인 등 설계상의 많은 불편함이 있었습니다.

 

이들을 해결하기 위해 생긴 클래스들이 java.time 패키지 안의 LocalDate, LocalTime, LocalDateTime, Instant, Duration, Period 등등입니다!

 

1. LocalDate, LocalTime

이 클래스들의 인스턴스를 생성하기 위해 팩토리 메서드를 사용하는데, 여기서 실제 날짜를 입력하거나, TemporalField를 이용할 수 있습니다.

TemporalField는 시간 관련 객체에서 어떤 필드의 값에 접근할지 정의하는 인터페이스입니다.

열거자인 ChronoField는 TemporalField인터페이스를 정의하므로 적절하게 이용하셔야 합니다.

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

 

 

2. LocalDateTime

LocalDateTime은 LocalDate,와 LocalTime을 쌍으로 갖는 복합 클래스입니다.

그렇기 때문에 LocalDate의 날짜와 LocalTime의 시간 모두 표현이 가능합니다.

 

1) 데이터 입력

// 기본적인 파라미터들을 인스턴스 생성
LocalDateTime dt1 = LocalDateTime.of(2021, Month.SEPTEMBER, 17, 12, 10, 30);
LocalDateTime dt2 = LocalDateTime.of(date, time);

// build 형식으로 날짜에 시간 추가, 시간에 날짜 추가
LocalDateTime dt3 = date.atTime(12, 10, 30);
LocalDateTime dt4 = date.atTime(time);
LocalDateTime dt5 = time.atDate(date);

 

2) 데이터 출력

LocalDateTime을 그대로 쓸 수 있지만, LocalDate와 LocalTime으로 추출 역시 가능합니다.

LocalDate date = dt1.toLocalDate();
LocalTime time = dt1.toLocalTime();

 

3. Instant

컴퓨터의 관점에서는 특정 날짜를 입력하면 그게 뭔지 잘 모릅니다.

하지만 지정된 기준으로부터 지나간 시간을 입력하면 어떤 뜻인지 잘 알죠.

이것이 바로 Instant 개념입니다!

 

그림1. Instant

Instant클래스는 Epoch(1970.01.01:00:00:00)를 기준(포인터)으로 두어 특정 지점까지의 시간을 초로 표시합니다.

 

1) 데이터 입력

팩토리 메서드 ofEpochSecond에 초를 넘겨주면, 나노초의 정밀도를 가지는 Instant 인스턴스를 만들 수 있습니다.

또한 오버 로드된메서드 버전에서는 두 번째 파라미터에 시간 보정(nanoAdjustment)이 가능합니다.

Instant instant = Instant.ofEpochSecond(3);	// 1970-01-01T00:00:03Z

// 1970-01-01T00:00:03Z + 01z 보정 => 1970-01-01T00:00:04Z
Instant instant = Instant.ofEpochSecond(3, 1_000_000_000);

// 1970-01-01T00:00:04Z - 01z 보정 => 1970-01-01T00:00:03Z
Instant instant = Instant.ofEpochSecond(4, -1_000_000_000);

 

 

2) 데이터 출력

인스턴스를 그대로 이용해도 되고, get( )으로 다른 형식으로 매핑이 가능합니다.

하지만, 사람이 읽을 수 있는 시간 정보는 제공하지 않습니다!!

// Exception in thread "main" java.time.temporal.UnsupportedTemporalTypeException: 
// Unsupported field: DayOfMonth

int i = Instant.now().get(ChronoField.DAY_OF_MONTH);

 

 

4. Duration과 Period

앞의 클래스들은 모두 Temporal 인터페이스를 구현하고 있습니다.

Temporal 인터페이스는 특정 시간을 모델링하는 객체의 값을 어떻게 읽고 조작할지를 정의하고 있습니다.

이번에는 두 시간 객체 사이의 지속시간을 정의하는 TemporalAmount 인터페이스를 구현하는 Duration과 Period에 대해서 알아봅시다.

 

1) 데이터 입력

Duration.between( )

Duration은 정적 팩토리 메서드 between으로 두 시간 객체 사이의 지속시간을 구할 수 있습니다.

파라미터로는 시간을 포함하고 있는 LocalTime, LocalDateTime, Instant 모두 가능합니다. 시간을 포함하지 않은 LocalDate는 불가능합니다.

 

 

Duration.of( )

of( ) 팩토리 메서드는 두 객체를 사용할 필요가 없는 메서드입니다.

Duration duration = Duration.ofMinutes(3);	// PT3M
Duration duration1 = Duration.ofDays(3);	// PT72H
Duration duration2 = Duration.ofHours(3);	// PT3H
Duration duration3 = Duration.ofSeconds(10); 	// PT10S

초(S)에서 시간(H)까지 단위에 맞게 표현이 가능합니다.

 

 

Period.between( )

Duration에서는 시간만을 구할 수 있다고 했었습니다. 그럼 날짜는 어떻게 구해야 할까요?

바로 Period를 이용해야 합니다!

Period between = Period.between(LocalDate.of(2021, 9, 1), LocalDate.of(2021, 9, 10));	// P9D
Period between1 = Period.between(LocalDate.of(2021, 9, 1), LocalDate.of(2024, 9, 10));	// P3Y9D
Period between2 = Period.between(LocalDate.of(2021, 9, 1), LocalDate.of(2021, 10, 10));	// P1M9D

 

Period.of( )

Duration.of( )와 같은 방법입니다.

 

 

2) 공통 메서드

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

 

 

4. 날짜 조정 / 파싱 / 포매팅

위에서 살펴본 클래스들은 모두 불변 클래스들입니다. 덕분에 스레드 안전성과 도메인 모델의일관성을 유지하는데 좋았습니다. 하지만, 날짜를 변경을 해야 하는 경우도 생기는데 이는 어떻게 해야 할까요? 이를 위한 새로운 API들이 존재합니다!

 

1) 지정된 날짜로 변경

년도, 월, 일을 파라미터로 두어 날짜를 변경할 수 있습니다.

LocalDate date = LocalDate.of(2021, 9, 17);			// 2021-09-17
LocalDate newYear = date.withYear(2020);			// 2020-09-17
LocalDate newDay = date.withDayOfMonth(25);			// 2021-09-25
LocalDate newMonth1 = date.withMonth(10);			// 2021-10-17
LocalDate newMonth2 = date.with(ChronoField.MONTH_OF_YEAR, 2);	// 2021-02-17

 

2) 파라미터 값과의 연산을 통해 날짜 변경

여러 연산을 통해 날짜를 변경할 수도 있습니다!

LocalDate date = LocalDate.of(2021, 9, 17);		// 2021-09-17
LocalDate newYear = date.plusYears(2);			// 2023-09-17
LocalDate newDay = date.plusDays(10);			// 2021-09-27
LocalDate newMonth1 = date.plusMonths(10);		// 2022-07-17
LocalDate newMonth2 = date.plus(6, ChronoUnit.MONTHS);	// 2022-03-17

 

 

5. TemporalAdjusters

이렇게 기본적인 날짜 말고, 다음주, 돌아오는 월요일의 날짜, 이번달의 마지막 날짜 등의 복잡한 날짜는 어떻게 알 수 있을까요? 이럴 때 TemporalAdjusters를 사용합니다.

LocalDate date = LocalDate.of(2021, 9, 17);			// 2021-09-17 FRI
LocalDate day1 = date.with(nextOrSame(DayOfWeek.SUNDAY));	// 이번주 일요일 날짜 2021-09-19-SUN
LocalDate day2 = date.with(nextOrSame(DayOfWeek.MONDAY));	// 만약 지난 요일이면 다음주 2021-09-20 MON
LocalDate day3 = date.with(lastDayOfMonth());			// 9월의 마지막 날짜 2021-09-30
메서드 설명
dayOfWeekInMonth 요일에 해당하는 날짜를 반환하는 TemporalAdjuster 반환
(파라미터가 음수 -> 월의 끝에서부터 계산)
firstDayOfMonth 현재 달의 첫 번째 날짜를 반환하는 TemporalAdjuster 반환
firstDayOfNextMonth 다음 달의 첫 번째 날짜를 반환하는 TemporalAdjuster 반환
firstDayOfNextYear 내년의 첫 번째 날짜를 반환하는 TemporalAdjuster 반환
firstDayOfYear 올해의 첫 번째 날짜를 반환하는 TemporalAdjuster 반환
firstInMonth 현재 달의 첫 번째 요일에 해당하는 날짜를 반환
lastDayOfMonth 현재 달의 마지막 날짜를 반환하는 TemporalAdjuster 반환
lastDayOfNextMonth 다음 달의 마지막 날짜를 반환하는 TemporalAdjuster 반환
lastDayOfNextYear 내년의 마지막 날짜를 반환하는 TemporalAdjuster 반환
lastDayOfYear 올해의 마지막 날짜를 반환하는 TemporalAdjuster 반환
lastInMonth(요일) 현재 달의 제시된 요일에 해당하는 마지막 날짜를 반환하는 TemporalAdjuster 반환
nextOrSame 현재 날짜 이후로 지정한 요일이 처음으로 나타나는 날짜를 반환하는 TemporalAdjuster 반환
previousOrSame 현재 날짜 이전으로 지정한 요일이 처음으로 나타나는 날짜를 반환하는 TemporalAdjuster 반환

이 기능 외에 필요한 것이 있다면, TemporalAdjusters구현을 통해 직접 만드시면 됩니다!

 

 

6. DateTimeFormatter

포매팅과 파싱은 실과 바늘같은 존재입니다. 이들을 이용하면 기본적인 ISO_LOCAL_DATE상수를 통한 형식 부터, 커스텀된 형식의 문자열도 만들 수 있습니다.

LocalDate date = LocalDate.of(2021, 9, 17);
String format1 = date.format(DateTimeFormatter.BASIC_ISO_DATE);	// 20210917
String format2 = date.format(DateTimeFormatter.ISO_LOCAL_DATE); // 2021-09-17

 

이런 이미 정의된 상수 말고도 ofPattern 팩토리 메서드를 이용하면 커스텀 된 형식을 구현할 수 있습니다!

LocalDate date = LocalDate.of(2021, 9, 17);
DateTimeFormatter formatter1 = DateTimeFormatter.ofPattern("dd/MM/yyyy");
DateTimeFormatter formatter2 = DateTimeFormatter.ofPattern("yyyy년 MM월 dd일");

String format1 = date.format(formatter1);	// 17/09/2021
String format2 = date.format(formatter2);	// 2021년 09월 17일

 

추가적으로 한번에 많은 작업을 실시하려면 Builder 형식인 DateTimeFormatterBuilder을 사용하는 것이 좋습니다.

DateTimeFormatter dateTimeFormatter = new DateTimeFormatterBuilder()
        .appendText(ChronoField.DAY_OF_MONTH)
        .appendLiteral(". ")
        .appendText(ChronoField.MONTH_OF_YEAR)
        .appendLiteral(" ")
        .appendText(ChronoField.YEAR)
        .parseCaseInsensitive()
        .toFormatter(Locale.KOREA);