새로운 날짜와 시간 API
자바 API는 복잡한 애플리케이션을 만드는 데 필요한 여러 가지 유용한 컴포넌트를 제공한다.
아쉽게도 자바 API가 항상 완벽한 것은 아니다. 대부분의 자바 개발자가 지금까지의 날짜와 시간 관련 기능에 만족하지 못했다.
Java1에서는 java.util.Date 클래스 하나로 날짜와 시간 관련 기능을 제공했다.
하지만 대부분의 자바 개발자가 지금까지의 날짜와 시간 관련 기능에 만족하지 못했다.
날짜를 의미하는 Date라는 클래스의 이름과 달리 Date 클래스는 특정 시점을 날짜가 아닌 밀리초 단위로 표현한다.
게다가 1900년을 기준으로 하는 오프셋, 0에서 시작하는 달 인덱스 등 모호한 설계로 유용성이 떨어졌다.
java1.1에서는 Date 클래스의 여러 메서드를 사장시키고 java.util.Calendar이라는 클래스를 대안으로 제공했다.
하지만 Calendar 클래스 역시 쉽게 에러를 일으키는 설계 문제를 갖고 있었고 많은 개발자들이 Date를 사용해야 할지 Calendar를 사용해야 할지 혼란스러워했다.
이러한 문제 때문에 많은 개발자들은 서드파티 날짜와 시간 라이브러리를 사용하다 java8에서 java.time 패키지를 추가했다.
LocalData와 LocalTime 사용
새로운 날짜와 시간 API를 사용할 때 처음 접하게 되는 것이 LocalDate다.
LocalDate 인스턴스는 시간을 제외한 날짜를 표현하는 불변 객체다.
특히 LocalDate 객체는 어떤 시간대 정보도 포함하지 않는다.
정적 팩토리 메서드 of로 LocalDate 인스턴스를 만들 수 있고 아래 메서드들을 통해 날짜와 관련된 정보를 얻을 수 있다.
LocalDate today = LocalDate.now(); // 현재 날짜 : 2022-03-17
LocalDate date = LocalDate.of(2022, 3, 17); // 특정 날짜 : 2022-03-17
int year = date.getYear(); // 년도 : 2022
Month month = date.getMonth(); // 월 : MARCH
int day = date.getDayOfMonth(); // 일 : 17
DayOfWeek yoil = date.getDayOfWeek(); // 요일 : THURSDAY
int len = date.lengthOfMonth(); // 해당 월의 일 수 : 31
boolean leap = date.isLeapYear(); // 해당 년도의 윤년 여부 : false
get 메서드에 TemporalField를 전달해서 정보를 얻는 방법도 있다.
TemporalField는 시간 관련 객체에서 어떤 필드의 값에 접근할지 정의하는 인터페이스다.
ChronoField는 TemporalField 인터페이스를 정의하므로 ChronoField 요소를 이용해서 원하는 정보를 쉽게 얻을 수 있다.
LocalDate date = LocalDate.of(2022, 3, 17);
int year = date.get(ChronoField.YEAR); // 2022
int month = date.get(ChronoField.MONTH_OF_YEAR); // 3
int day = date.get(ChronoField.DAY_OF_MONTH); // 17
다음 내장 메서드 getYear(), getMonthValue(), getDayOfMonth() 등을 이용해 가독성을 높일 수 있다.
LocalDate date = LocalDate.of(2022, 3, 17);
int year = date.getYear(); // 2022
int month = date.getMonthValue(); // 3
int day = date.getDayOfMonth(); // 17
마찬가지로 10:17:30 같은 시간은 LocalTime 클래스로 표현할 수 있다.
LocalDate와 마찬가지로 정적 팩토리 메서드 of를 사용하여 LocalTime 인스턴스를 만들 수 있다.
LocalTime time = LocalTime.of(10, 17, 30);
int hour = time.getHour(); // 10
int minute = time.getMinute(); // 17
int second = time.getSecond(); // 30
parse 메서드를 이용해 날짜와 시간 문자열로 LocalDate와 LocalTime의 인스턴스를 만드는 방법도 있다.
LocalDate date = LocalDate.parse("2020-03-17");
LocalTime time = LocalTime.parse("10:17:30");
만약 파싱이 불가능한 문자열일 경우 DateTimeParseException을 일으킨다.
LocalDateTime 클래스 : 날짜와 시간 조합
LocalDateTime은 LocalDate와 LocalTime을 쌍으로 갖는 복합 클래스다.
즉, LocalDateTime은 날짜와 시간을 모두 표현할 수 있으며 직접 LocalDateTime을 만드는 방법도 있고 날짜와 시간을 조합하는 방법도 있다.
LocalDate date = LocalDate.parse("2020-03-17");
LocalTime time = LocalTime.parse("10:17:30");
LocalDateTime dt1 = LocalDateTime.of(2022, Month.MARCH, 17, 10, 17, 30);
LocalDateTime dt2 = LocalDateTime.of(date, time);
LocalDateTime dt3 = date.atTime(10, 17, 30);
LocalDateTime dt4 = date.atTime(time);
LocalDateTime dt5 = time.atDate(date);
LocalDateTime의 toLocalDate나 toLocalTime 메서드로 LocalDate나 LocalTime 인스턴스를 추출할 수 있다.
LocalDateTime dt = LocalDateTime.of(2022, Month.MARCH, 17, 10, 17, 30);
LocalDate date = dt.toLocalDate();
LocalTime time = dt.toLocalTime();
Instant 클래스 : 기계의 날짜와 시간
사람은 보통 주, 날짜, 시간, 분으로 날짜와 시간을 계산하지만, 기계는 이와 같은 단위로 시간을 표현하기가 어렵다.
기계의 관점에서는 연속된 시간에서 특정 지점을 하나의 큰 수로 표현하는 것이 가장 자연스러운 시간 표현 방법이다.
Instant 클래스는 유닉스 에포크 시간(1970년 1월 1일 0시 0분 0초 UTC)을 기준으로 특정 지점까지의 시간을 초로 표현한다.
팩토리 메서드 ofEpochSecond에 초를 넘겨줘서 Instant 클래스 인스턴스를 만들 수 있다.
Instant 클래스는 나노초(10억 분의 1초)의 정밀도를 제공한다. 또한 overload 된 ofEpochSecond 메서드 버전에서는 두 번째 인수를 이용해서 나노초 단위로 시간을 보정할 수 있다.
Instant instant1 = Instant.ofEpochSecond(3); // 1970-01-01T00:00:03Z
Instant instant2 = Instant.ofEpochSecond(3, 0); // 1970-01-01T00:00:03Z
Instant instant3 = Instant.ofEpochSecond(2, 1_000_000_000); // 1970-01-01T00:00:03Z
Instant instant4 = Instant.ofEpochSecond(4, -1_000_000_000); // 1970-01-01T00:00:03Z
Instant는 기계 전용의 유틸리티라는 점을 기억하자. Instant는 사람이 읽을 수 있는 시간 정보를 제공하지 않으므로 Duration과 Period 클래스를 함께 활용할 수 있다.
Duration과 Period 정의
Duration 클래스의 정적 팩토리 메서드 between으로 두 시간 객체 사이의 지속시간을 만들 수 있다.
다음 코드에서 보여주는 것처럼 두 개의 LocalTime, 두 개의 LocalDateTime, 또는 두 개의 Instant로 Duration을 만들 수 있다.
LocalTime time1 = LocalTime.parse("10:17:30");
LocalTime time2 = LocalTime.parse("11:17:30");
Duration timeDuration = Duration.between(time1, time2);
LocalDateTime dt1 = LocalDateTime.of(2022, Month.MARCH, 17, 10, 17, 30);
LocalDateTime dt2 = LocalDateTime.of(2022, Month.MARCH, 18, 10, 17, 30);
Duration dateTimeDation = Duration.between(dt1, dt2);
Instant instant1 = Instant.ofEpochSecond(10);
Instant instant2 = Instant.ofEpochSecond(15);
Duration instantDuration = Duration.between(instant1, instant2);
LocalDateTime은 사람이 사용하도록, Instant는 기계가 사용하도록 만들어진 클래스로 두 인스턴스는 서로 혼합할 수 없다. 또한 Duration 클래스는 초와 나노초 시간 단위를 표현하므로 between 메서드에 LocalDate를 전달할 수 없다.
년, 월, 일로 시간을 표현할 때는 Period 클래스를 사용한다.
LocalDate date1 = LocalDate.parse("2020-03-17");
LocalDate date2 = LocalDate.parse("2020-03-18");
Period period = Period.between(date1, date2);
Duration과 Period 클래스는 자신의 인스턴스를 만들 수 있도록 다양한 팩토리 메서드를 제공한다.
Duration threeMinutes1 = Duration.ofMinutes(3); // 3분
Duration threeMinutes2 = Duration.of(3, ChronoUnit.MINUTES); // 3분
Period tenDays = Period.ofDays(10); // 10일
Period threeWeeks = Period.ofWeeks(3); // 3주
Period twoYearsSixMonthsOneDay = Period.of(2, 6, 1); // 2년 6개월 1일
위에서 살펴본 모든 클래스는 불변이다.
불변 클래스는 함수형 프로그래밍 그리고 스레드 안전성과 도메인 모델의 일관성을 유지하는데 좋은 특징이다.
그래서 새로운 날짜와 시간 API에서는 변경된 객체 버전을 만들 수 있는 메서드를 제공한다.
날짜 조정, 파싱, 포매팅
withAttribute 메서드로 기존의 LocalDate를 바꾼 버전을 직접 간단하게 만들 수 있다.
withAttribute 메서드는 기존 객체를 변경하지 않는다.
LocalDate date1 = LocalDate.of(2022, 3, 17);
LocalDate date2 = date1.withYear(2021); // 2021-03-17
LocalDate date3 = date1.withDayOfMonth(4); // 2022-03-04
LocalDate date4 = date1.with(ChronoField.MONTH_OF_YEAR, 5); // 2022-05-17
get과 with 메서드로 Temporal 인터페이스의 인스턴스 필드 값을 읽거나 고칠 수 있다.
만약 지원하지 필드를 읽거나 고치려고 한다면 UnsupportedTemporalTypeException이 발생한다.
ex) instant.with(ChronoField.MONTH_OF_YEAR), localDate.with(ChronoField.NANO_OF_SECOND)
선언형으로 LocalDate를 사용하는 방법도 있다.
LocalDate date1 = LocalDate.of(2022, 3, 17);
LocalDate date2 = date1.plusWeeks(1); // 2022-03-24
LocalDate date3 = date1.minusYears(2); // 2020-03-17
LocalDate date4 = date1.plus(2, ChronoUnit.MONTHS); // 2022-05-17
LocalDate, LocalTime, LocalDateTime, Instant 등 날짜와 시간을 표현하는 모든 클래스는 서로 비슷한 메서드를 제공한다.
메서드 | static | 설명 |
from | O | 주어진 Temporal 객체를 이용해서 클래스의 인스턴스를 생성 |
now | O | 시스템 시계로 Temporal 객체를 생성 |
of | O | 주어진 구성 요소에서 Temporal 객체의 인스턴스를 생성 |
parse | O | 문자열을 파싱해서 Temporal 객체를 생성 |
atOffset | X | 시간대 오프셋과 Temporal 객체를 생성 |
atZone | X | 시간대 오프셋과 Temporal 객체를 합침 |
format | X | 지정된 포매터를 이용해서 Temporal 객체를 문자열로 변환 (Instant는 지원하지 않음) |
get | X | Temporal 객체의 상태를 읽음 |
minus | X | 특정 시간을 뺀 Temporal 객체의 복사본을 생성 |
plus | X | 특정 시간을 더한 Temporal 객체의 복사본을 생성 |
with | X | 일부 상태를 바꾼 Temporal 객체의 복사본을 생성 |
TemporalAdjuster 사용하기
때로는 다음 주 일요일, 돌아오는 평일, 어떤 달의 마지막 날 등 좀 더 복잡한 날짜 조정 기능이 필요할 것이다.
이때는 overload 된 버전의 with 메서드에 좀 더 다양한 동작을 수행할 수 있도록 하는 기능을 제공하는 TemporalAdjuster를 전달하는 방법으로 문제를 해결할 수 있다.
LocalDate date1 = LocalDate.of(2022, 3, 17);
LocalDate date2 = date1.with(TemporalAdjusters.nextOrSame(DayOfWeek.SUNDAY)); // 2022-03-20
LocalDate date3 = date1.with(TemporalAdjusters.lastDayOfMonth()); // 2022-03-31
TemporalAdjuster 클래스가 제공하는 팩토리 메서드는 다음과 같다.
메서드 | 설명 |
dayOfWeekInMonth | 해당 요일에 해당하는 날짜를 반환하는 TemporalAdjuster를 반환함 (음수를 사용하면 월의 끝에서 거꾸로 계산) |
firstDayOfMonth | 현재 달의 첫 번째 날짜를 반환하는 TemporalAdjuster를 반환 |
firstDayOfNextMonth | 다음 달의 첫 번째 날짜를 반환하는 TemporalAdjuster를 반환 |
firstDayOfNextYear | 내년의 첫 번째 날짜를 반환하는 TemporalAdjuster를 반환 |
firstInMonth | 현재 달의 첫 번째 요일에 해당하는 날짜를 반환하는 TemporalAdjuster를 반환 |
lastInMonth | 현재 달의 마지막 요일에 해당하는 날짜를 반환하는 TemporalAdjuster를 반환 |
firstDayOfMonth | 현재 달의 첫 번째 날짜를 반환하는 TemporalAdjuster를 반환 |
lastDayOfMonth | 현재 달의 마지말 날짜를 반환하는 TemporalAdjuster를 반환 |
firstDayOfYear | 현재 년도의 첫 번째 날짜를 반환하는 TemporalAdjuster를 반환 |
lastDayOfYear | 현재 년도의 마지막 날짜를 반환하는 TemporalAdjuster를 반환 |
next | 현재 날짜 이후로 지정한 요일이 처음으로 나타나는 날짜를 반환하는 TemporalAdjuster를 반환 |
nextOrSame | 현재 날짜 이후로 지정한 요일이 처음으로 나타나는 날짜를 반환하는 TemporalAdjuster를 반환 (오늘 요일 포함) |
previous | 현재 날짜 이전에 지정한 요일이 처음으로 나타나는 날짜를 반환하는 TemporalAdjuster를 반환 |
previousOrSname | 현재 날짜 이전에 지정한 요일이 처음으로 나타나는 날짜를 반환하는 TemporalAdjuster를 반환 (오늘 요일 포함) |
만약 필요한 기능이 정의되어 있지 않을 때는 TemporalAdjuster 인터페이스를 구현해 직접 만들 수도 있다.
public class NextWorkingDay implements TemporalAdjuster {
@Override
public Temporal adjustInto(Temporal temporal) {
// 현재 날짜
DayOfWeek dayOfWeek = DayOfWeek.of(temporal.get(ChronoField.DAY_OF_WEEK));
// 증가할 요일
int dayToAdd = 1;
// 금요일일땐 +3일
if (dayOfWeek == DayOfWeek.FRIDAY) dayToAdd = 3;
// 토요일일땐 +2일
else if (dayOfWeek == DayOfWeek.SATURDAY) dayToAdd = 2;
return temporal.plus(dayToAdd, ChronoUnit.DAYS);
}
}
TemporalAdjuster 인터페이스는 함수형 인터페이스이기 때문에 람다 표현식도 사용이 가능하다.
LocalDate nextWorkingDay = LocalDate.now().with(temporal -> {
// 현재 날짜
DayOfWeek dayOfWeek = DayOfWeek.of(temporal.get(ChronoField.DAY_OF_WEEK));
// 증가할 요일
int dayToAdd = 1;
// 금요일일땐 +3일
if (dayOfWeek == DayOfWeek.FRIDAY) dayToAdd = 3;
// 토요일일땐 +2일
else if (dayOfWeek == DayOfWeek.SATURDAY) dayToAdd = 2;
return temporal.plus(dayToAdd, ChronoUnit.DAYS);
});
날짜와 시간 객체 출력과 파싱
java.time 패키지에 포매팅과 파싱 전용 패키지인 java.time.format이 추가되었다.
기존의 java.util.DateFormat 클래스와 달리 모든 DateTimeFormatter는 스레드에서 안전하게 사용할 수 있는 클래스다.
DateTimeFormatter 클래스는 BASIC_ISO_DATE와 ISO_LOCAL_DATE 등의 상수가 미리 정의되어 있고, 정적 메서드와 정의된 상수를 이용해서 손쉽게 포매터를 만들 수 있다.
DateTimeFormatter를 이용해서 날짜나 시간을 특정 형식의 문자열로 만들 수 있고, 문자열을 객체로 만들 수 있다.
LocalDate date = LocalDate.of(2022, 3, 17);
// 문자열로 파싱
String date1 = date.format(DateTimeFormatter.BASIC_ISO_DATE);
String date2 = date.format(DateTimeFormatter.ISO_LOCAL_DATE);
// 객체로 파싱
LocalDate date3 = LocalDate.parse("20220317", DateTimeFormatter.BASIC_ISO_DATE);
LocalDate date4 = LocalDate.parse("2022-03-17", DateTimeFormatter.ISO_LOCAL_DATE);
DateTimeFormatter 클래스는 특정 패턴으로 포매터를 만들 수 있는 정적 팩토리 메서드도 제공한다.
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd/MM/yyyy");
LocalDate date = LocalDate.of(2022, 3, 17);
String formattedDateString = date.format(formatter);
LocalDate formattedDate = LocalDate.parse(formattedDateString, formatter);
ofPattern 메서드도 Locale로 포매터를 만들 수 있도록 overload 된 메서드를 제공한다.
DateTimeFormatter usFormatter = DateTimeFormatter.ofPattern("dd/MM/yyyy", Locale.US);
LocalDate date = LocalDate.of(2022, 3, 17);
String formattedDateString = date.format(usFormatter);
LocalDate formattedDate = LocalDate.parse(formattedDateString, usFormatter);
DateTimeFormatterBuilder 클래스로 복합적인 포매터를 정의해서 좀 더 세부적으로 포매터를 제어할 수 있다.
DateTimeFormatter formatter = new DateTimeFormatterBuilder()
.appendText(ChronoField.YEAR)
.appendLiteral(" . ")
.appendText(ChronoField.MONTH_OF_YEAR)
.appendLiteral(" . ")
.appendText(ChronoField.DAY_OF_MONTH)
.appendLiteral(" / ")
.appendText(ChronoField.DAY_OF_WEEK)
.parseCaseInsensitive()
.toFormatter(Locale.KOREA);
String formattedDate = LocalDate.now().format(formatter); // 2022 . 3월 . 17 / 목요일
다양한 시간대와 캘린더 활용 방법
기존의 java.util.TimeZone을 대체할 수 있는 java.time.ZoneId 클래스가 새롭게 등장했다.
ZoneId 클래스를 이용하면 서머타임(DST) 같은 복잡한 사항이 자동으로 처리된다.
날짜와 시간 API에서 제공하는 다른 클래스와 마찬가지로 ZoneId 클래스도 불변 클래스다.
시간대 사용하기
표준 시간이 같은 지역을 묶어서 시간대(time zone) 규칙 집합을 정의한다.
ZoneRules 클래스에는 약 40개 정도의 시간대가 있다.
ZoneId의 getRules()를 이용해서 해당 시간대의 규정을 획득할 수 있다.
ZoneId romeZone = ZoneId.of("Europe/Rome");
지역 ID는 "{지역}/{도시}" 형식으로 이루어지며 IANA Time Zone Database에서 제공하는 지역 집합 정보를 사용한다.
ZoneId의 새로운 메서드인 toZoneId로 기존의 TimeZone 객체를 ZoneId 객체로 변환할 수 있다.
ZoneId romeZone = TimeZone.getDefault().toZoneId();
ZoneId 객체를 얻은 다음에는 LocalDate, LocalDateTime, Instant를 이용해서 ZonedDateTime 인스턴스로 변환할 수 있다.
LocalDate date = LocalDate.of(2022, Month.MARCH, 17);
ZonedDateTime zdt1 = date.atStartOfDay(ZoneId.of("Europe/Rome"));
// 2022-03-17T00:00+01:00[Europe/Rome]
LocalDateTime dateTime = date.atTime(LocalTime.of(14, 43, 15));
ZonedDateTime zdt2 = dateTime.atZone(ZoneId.of("America/New_York"));
// 2022-03-17T14:43:15-04:00[America/New_York]
Instant instant = Instant.now();
ZonedDateTime zdt3 = instant.atZone(ZoneId.of("Asia/Seoul"));
// 2022-03-17T14:46:08.177579+09:00[Asia/Seoul]
ZonedDateTime은 지정한 시간대에 상대적인 시점을 표현한다.
UTC/Greenwich 기준의 고정 오프셋
때로는 UTC(협정 세계 시간)/GMT(그리니치 표준 시간)를 기준으로 시간대를 표현하기도 한다.
ZoneId의 서브클래스인 ZoneOffset 클래스로 런던의 그리니치 0도 자오선과 시간 값의 차이를 표현할 수 있다.
ZoneOffset newYorkOffset = ZoneOffset.of("-05:00");
하지만 위 예제에서 정의한 ZoneOffset으로는 서머타임을 제대로 처리할 수 없으므로 권장하지 않는 방식이다.
ISO-8601 캘린더 시스템에서 정의하는 UTC/GMT와 오프셋으로 날짜와 시간을 표현하는 OffsetDateTime을 만드는 방법도 있다.
ZoneOffset newYorkOffset = ZoneOffset.of("-05:00");
LocalDateTime dateTime = LocalDateTime.of(2020, Month.MARCH, 17, 14, 54, 0);
OffsetDateTime dateTimeInNewYork = OffsetDateTime.of(dateTime, newYorkOffset);
// 2020-03-17T14:54-05:00
대안 캘린더 시스템 사용하기
ISO-8601 캘린더 시스템은 실질적으로 전 세계에서 통용된다.
하지만 Java8에서는 ThaiBuddhistDate, MinguoDate, JapaneseDate, HijrahDate 4개의 캘린더 시스템을 추가로 제공한다.
위 4개의 클래스와 LocalDate 클래스는 ChronoLocalDate 인터페이스를 구현하는데, ChronoLocalDate는 임의의 연대기에서 특정 날짜를 표현할 수 있는 기능을 제공하는 인터페이스다.
LocalDate를 이용해서 이들 4개의 클래스 중 하나의 인스턴스를 만들 수 있다.
LocalDate date = LocalDate.now();
JapaneseDate japaneseDate = JapaneseDate.from(date);
또는 특정 Locale과 Locale에 대한 날짜 인스턴스로 캘린더 시스템을 만드는 방법도 있다.
Chronology japaneseChronology = Chronology.ofLocale(Locale.JAPAN);
ChronoLocalDate now = japaneseChronology.dateNow();
Chronology는 캘린더 시스템을 의미하며 정적 팩토리 메서드 tofLocale을 이용해서 Chronology의 인스턴스를 획득할 수 있다.
'Java' 카테고리의 다른 글
[모던 자바] Future 인터페이스를 활용한 비동기 계산 (0) | 2022.03.29 |
---|---|
[모던 자바] 디폴트 메서드(default method)란? (0) | 2022.03.18 |
[모던 자바] Optional 클래스란? (0) | 2022.03.16 |
[모던 자바] DSL(도메인 전용 언어)이란? (2) | 2022.03.15 |
[모던 자바] 람다 표현식과 스트림 테스팅과 디버깅 (0) | 2022.03.14 |