20 min to read
자바 8 인 액션
12. 새로운 날짜와 시간 API
기존 java 1.0에서 제공하던 날짜/시간 API였던 java.util.Date와 java.util.Calendar로 날짜를 다뤘으나 여러 문제가 많아 java 8에서는 새로운 시간 API를 사용한다.
LocalDate, LocalTime, Instant, Duration, Period
쟈바 8에서 java.time 패키지는 LocalDate, LocalTime, LocalDateTime, Instant, Duration, Period 등 새로운 클래스를 제공한다.
LocalDate와 LocalTime 사용
LocalDate는 시간을 제외한 날짜를 표현하는 불변 객체다. 특히, LocatDate 객체는 어떤 시간대 정보도 포함하지 않는다.
정적 팩토리 메서드 of로 LocalDate 인스턴스를 만들 수 있다.
- LocalDate 만들고 값 읽기
LocalDate date = LocalDate.of(2014, 3, 18); // 2014-03-18
int year = date.getYear(); // 2014
Month month = date.getMonth(); // MARCH
int day = date.getDayOfMonth(); // 18
DayOfWeek dow = date.getDayOfWeek(); // TUESDAY
int len = date.lengthOfMonth(); // 31(3월의 일수)
boolean leap = date.isLeapYear(); // false(윤년이 아님)
- 팩토리 메서드 now는 시스템 시계의 정보를 이용해서 현재 날짜 정보를 얻는다.
LocalDate today = LocalDate.now();
- TemporalField를 이용해 LocalDate값 읽기
LocalDate date = LocalDate.of(2014, 3, 18);
int year = date.get(ChronoField.YEAR);
int month = date.get(ChronoField.MONTH_OF_YEAR);
int day = date.get(ChronoField.DAY_OF_MONTH);
System.out.println("결과 : " + year + " " + month + " " + day); // 결과 : 2014 3 18
TemporalField는 시간 관련 객체에서 어떤 필드의 값에 접근할지 정의하는 인터페이스다.
열거자 ChronoField는 TemporalField 인터페이스를 정의하므로 위 코드처럼 ChronoField의 열거자 요소를 이용해서 원하는 정보를 쉽게 얻을 수 있다.
- LocalTime 만들고 값 읽기
LocalTime time = LocalTime.of(13, 45, 20); // 13:45:20
int hour = time.getHour(); // 13
int minute j= time.getMinute(); // 45
int second = time.getSecond(); // 20
- 날짜와 시간 문자열로 LocalDate, LocalTime 인스턴스 만드는 법
LocalDate date = LocalDate.parse("2014-03-18");
LocalTime time = LocalTime.parse("13:45:20");
System.out.println("date = " + date + " time = " + time); // 결과 : date = 2014-03-20 time = 13:45:20
parse 메서드에 DateTimeFormatte를 전달한다. 문자열을 LocalDate나 LocalTime으로 파싱할 수 없을 때 parse 메서드는 DateTimeParseException(RuntimeException을 상속받은 예외)을 일으킨다.
날짜와 시간 조합
LocalDateTime은 LocalDate와 LocalTime을 쌍으로 갖는 복합 클래스다. 날짜와 시간을 모두 표현할 수 있으며 직접 LocalDateTime을 만드는 방법도 있고 날짜외 시간을 조합할 수도 있다.
- LocalDateTime 조합 예제
// 2020-09-05T19:40:30
LocalDate date = LocalDate.parse("2020-09-05");
LocalTime time = LocalTime.parse("19:40:30");
LocalDateTime dt1 = LocalDateTime.of(2020, Month.SEPTEMBER, 5, 19, 40, 30);
LocalDateTime dt2 = LocalDateTime.of(date, time);
LocalDateTime dt3 = date.atTime(13, 45, 20);
LocalDateTime dt4 = date.atTime(time);
LocalDateTime dt5 = time.atDate(data);
LocalDate의 atTime 메서드에 시간을 제공하거나 LocalTime의 atDate 메서드에 날짜를 제공해서 LocalDateTime을 만들 수도 있다.
LocalDateTime의 toLocalDate나 toLocalTime 메서드로 LocalDate나 LocalTime 인스턴스를 추출할 수 있다.
LocalDate date1 = dt1.toLocalDate(); // 2020-09-05
LocalTime time2 = dt1.toLocalTime(); // 19:40:30
Instant: 기계의 날짜와 시간
java.time.Instant 클래스에서는 기계적인 관점에서 시간을 표현한다. 즉, Instant 클래스는 유닉스 에포크 시간(Unix epoch time)(1970년 1월 1일 0시 0분 0초 UTC)을 기준으로 특정 지점까지의 시간을 초로 표현한다.
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초)
팩토리 메서드 ofEpochSecond에 초를 넘겨줘서 Instant 클래스 인스턴스를 만들 수 있다. Instant 클래스는 나노초(10억 분의 1초)의 정밀도를 제공한다.
Instant 클래스도 사람이 확인할 수 있도록 시간을 표시해주는 정적 팩토리 메서드인 now메서드를 제공한다. 하지만 Instant 는 기계 전용의 유틸리티기 때문에 초와 나노초 정보도 포함된다.
- ex
int day = Instant.now().get(ChronoField.DAY_OF_MONTH);
System.out.println(day);
Instant 클래스는 사람이 읽을 수 있는 시간 정보는 제공하지 않기 때문에 위 결과는 java.time.temporal.UnsupportedTemporalTypeException: Unsupported field: DayOfMonth 예외를 발생시킨다.
따라서 Instant 클래스는 Duration과 Period 클래스와 함께 활용할 수 있다.
Duration과 Period 정의
지금 까지 살펴본 클래스는 Temporal 인터페이스를 구현한다. Temporal 인터페이스는 특정 시간을 모델링하는 객체의 값을 어떻게 읽고 조작할지 정의한다.
반면, Duration 클래스는 정적 팩토리 메서드 between를 통해 두 시간 객체 사이의 지속시간을 만들 수 있다.
Duration d1 = Duration.between(time1, time2);
Duration d1 = Duration.between(dateTime1, dateTime2);
Duration d2 = Duration.between(instant12, instant2);
두 개의 LocalTime, 두 개의 LocalDateTime, 또는 두 개의 Instant로 Duration을 만들 수 있다.
LocalDateTime과 Instant 클래스가 혼합되어 사용될 수는 없다.
또한 Duration 클래스는 초와 나노초로 시간 단위를 표현하므로 between 메서드에 LocalDate를 전달할 수 없다. 년, 월, 일로 시간을 표현할 때는 Period 클래스를 사용해야 한다.
Period tenDays = Period.between(LocalDate.of(2010, 8, 5), LocalDate.of(2020, 9, 9));
System.out.println(tenDays); // P10Y1M4D
- Duration, Period 인스턴스를 만드는 법
Duration threeMinutes = Duration.ofMinutes(3);
Duration threeMinutes1 = Duration.of(3, ChronoUnit.MINUTES);
System.out.println(threeMinutes + " " +threeMinutes1); // PT3M PT3M
Period tenDays = Period.ofDays(10);
Period threeWeeks = Period.ofWeeks(3);
Period twoYearsSixMonthsOneDay = Period.of(2, 6, 1);
System.out.println(tenDays + " " + threeWeeks + " " + twoYearsSixMonthsOneDay); // P10D P21D P2Y6M1D
- 간격을 표현하는 날짜와 시간 클래스의 공통 메서드
| 메서드 | 정적 | 설명 |
|---|---|---|
| between | O | 두 시간 사이의 간격을 생성함 |
| from | O | 시간 단위로 간격을 생성함 |
| of | O | 주어진 구성 요소에서 간격 인스턴스를 생성함 |
| parse | O | 문자열을 파싱해서 간격 인스턴스를 생성함 |
| addTo | X | 현재값의 복사본을 생성한 다음에 지정된 Temporal 객체에 추가함 |
| get | X | 현재 간격 정보값을 읽음 |
| isNegative | X | 간격이 음수 인지 확인함 |
| isZero | X | 간격이 0인지 확인함 |
| minus | X | 현재값에서 주어진 시간을 뺀 복사본을 생성함 |
| multipliedBy | X | 현재값에 주어진 값을 곱한 복사본을 생성함 |
| negated | X | 주어진 값의 부호를 반전한 복사본을 생성함 |
| plus | X | 현재값에 주어진 시간을 더한 복사본을 생성함 |
| subtractFrom | X | 지정된 Temporal 객체에서 간격을 뺌 |
지긤까지 살펴본 모든 클래스는 불변이다. 불변 클래스는 함수형 프로그래밍 그리고 스레드 안전성과 도메인 모델의 일관성을 유지하는데 좋은 특징이다.
하지만, 새로운 날짜와 시간 API에서는 변경된 객체 버전을 만들 수 있는 메서드를 제공한다.
날짜 조정, 파싱, 포매팅
- 절대적인 방식으로 LocalDate의 속성 바꾸기
LocalDate date1 = LocalDate.of(2020, 9, 9); // 2020-09-09
LocalDate date2 = date1.withYear(2011); // 2011-09-09
LocalDate date3 = date2.withDayOfMonth(25); // 2011-09-25
LocalDate date4 = date3.with(ChronoField.MONTH_OF_YEAR, 8); // 2011-08-25
위에서 사용된 메서드들은 모두 기존 객체를 바꾸진 않는다.
- 상대적인 방식으로 LocalDate 속성 바꾸기
LocalDate date1 = LocalDate.of(2020-09-09); // 2020-09-09
LocalDate date2 = date1.plusWeek(1); // 2020-09-16
LocalDate date3 = date2.minusYear(3); // 2017-09-16
LocalDate date4 = date3.plus(3, ChronoUnit.MONTHS); // 2017-12-16
절대적인 방식에서 본 with 메서드와 상대적인 방식에서 본 plus, minus 메서드 모두 Temporal 인터페이스에 정의되어 있다.
Temporal 인터페이스 모두 LocalDate, LocalTime, LocalDateTime, Instant 처럼 특정 시간을 정의한다.
- 특정 시점을 표현하는 날짜 시간 클래스의 공통 메서드
| 메서드 | 정적 | 설명 |
|---|---|---|
| from | O | 주어진 Temporal 객체를 이용해서 클래스의 인스턴스를 생성함 |
| now | O | 시스템 시계로 Temporal 객체를 생성함 |
| of | O | 주어진 구성 요소에서 Temporal 객체의 인스턴스를 생성함 |
| parse | O | 문자열을 파싱해서 Temporal 객체를 생성함 |
| atOffSet | X | 시간대 오프셋과 Temporal 객체를 합침 |
| atZone | X | 시간대와 Temporal 객체를 합침 |
| format | X | 지정된 포매터를 이용해서 Temporal 객체를 문자열로 변환함(Instant는 지원 X) |
| get | X | Temporal 객체의 상태를 읽음 |
| minus | X | 특정 시간을 뺀 Temporal 객체의 복사본을 생성함 |
| plus | X | 특정 시간을 더한 Temporal 객체의 복사본을 생성함 |
| with | X | 일부 상태를 바꾼 Temporal 객체의 복사본을 생성함 |
TemporalAdjusters 사용하기
위에서 본 것 보다 좀 더 복잡한 날짜 조정이 필요한 경우에는 TemporalAdjuster를 전달하는 방법으로 문제를 해결할 수 있다.
- 미리 정의된 TemporalAdjusters의 기능 활용
import static java.time.temporal.TemporalAdjusters.*;
LocalDate date1 = LocalDate.of(2020, 9, 9);
LocalDate date2 = date1.with(nextOrSame(DayOfWeek.SUNDAY)); // 2020-09-13(다음주 일요일)
LocalDate date3 = date2.with(lastDayOfMonth()); // 2020-09-30(이 달의 마지막 날)
- TemporalAdjusters 클래스의 팩토리 메서드
| 메서드 | 설명 |
|---|---|
| dayOfWeekInMonth | '3월의 둘째 화요일'처럼 서수 요일에 해당하는 날짜를 반환하는 TemporalAdjuster를 반환함 |
| firstDayOfMonth | 현재 달의 첫 번째 날짜를 반환하는 TemporalAdjusters를 반환함 |
| firstDayOfNextMonth | 다음 달의 첫 번째 날짜를 반환하는 TemporalAdjusters를 반환함 |
| firstDayOfNextYear | 내년의 첫 번째 날짜를 반환하는 TemporalAdjusters를 반환함 |
| firstDayOfYear | 올해의 첫 번째 날짜를 반환하는 TemporalAdjusters를 반환함 |
| firstInMonth | '3월의 첫 번째 화요일'처럼 현재 달의 첫 번째 요일에 해당하는 날짜를 반환하는 TemporalAdjusters를 반환함 |
| lastDayOfMonth | 현재 달의 마지막 날짜를 반환하는 TemporalAdjusters를 반환함 |
| lastDayOfNextMonth | 다음 달의 마지막 날짜를 반환하는 TemporalAdjusters를 반환함 |
| lastDayOfYear | 올해의 마지막 날짜를 반환하는 TemporalAdjusters를 반환함 |
| lastInMonth | '3월의 마지막 화요일'처럼 현재 달의 마지막 요일에 해당하는 날짜를 반환하는 TemporalAdjusters를 반환함 |
| next | 현재 날짜 이후로 지정한 요일이 처음으로 나타나는 날짜를 반환하는 TemporalAdjusters를 반환함(현재 날짜는 포함 X) |
| previous | 현재 날짜 이후로 역으로 날짜를 거슬러 올라가며 지정한 요일이 처음으로 나타나는 날짜를 반환하는 TemporalAdjusters를 반환함(현재 날짜는 포함 X) |
| nextOrSame | 현재 날짜 이후로 지정한 요일이 처음으로 나타나는 날짜를 반환하는 TemporalAdjusters를 반환함(현재 날짜도 포함 O) |
| previousOrSame | 현재 날짜 이후로 역으로 날짜를 거슬러 올라가며 지정한 요일이 처음으로 나타나는 날짜를 반환하는 TemporalAdjusters를 반환함(현재 날짜도 포함 O) |
TemporalAdjusters 인터페이스는 하나의 메서드만 정의하는 함수형 인터페이스로, 필요한 기능은 커스텀 TemporalAdjusters를 구현하여 만들 수도 있다.
@FunctionalInterface
public interface TemporalAdjuster {
Temporal adjustInto(Temporal temporal);
}
TemporalAdjuster 인터페이스 구현은 Temporal 객체를 어떻게 다른 Temporal 객체로 변환할지 정의한다. 결국 TemporalAdjuster 인터페이스를 UnaryOperator
커스텀 TemporalAdjuster 구현하기
영업일만 체크하는 NextWorkingDay 클래스를 구현해본다.
date = date.with(new NextWorkingDay());
위와 같이 사용하려면 아래와 같이 NextWorkingDay 클래스를 구현한다.
- TemporalAdjuster 구현
package com.company;
import java.time.DayOfWeek;
import java.time.temporal.ChronoField;
import java.time.temporal.ChronoUnit;
import java.time.temporal.Temporal;
import java.time.temporal.TemporalAdjuster;
public class NextWorkingDay implements TemporalAdjuster {
@Override
public Temporal adjustInto(Temporal temporal) {
DayOfWeek dow = DayOfWeek.of(temporal.get(ChronoField.DAY_OF_WEEK));
int dayToAdd = 1;
if (dow == DayOfWeek.FRIDAY) {
dayToAdd = 3;
} else if (dow == DayOfWeek.SATURDAY) {
dayToAdd = 2;
}
return temporal.plus(dayToAdd, ChronoUnit.DAYS);
}
}
- 람다 표현식 활용
public static void main(String[] args) {
LocalDateTime date = LocalDateTime.now();
date = date.with(temporal -> {
DayOfWeek dow = DayOfWeek.of(temporal.get(ChronoField.DAY_OF_WEEK));
int dayToAdd = 1;
if (dow == DayOfWeek.FRIDAY) {
dayToAdd = 3;
} else if (dow == DayOfWeek.SATURDAY) {
dayToAdd = 2;
}
return temporal.plus(dayToAdd, ChronoUnit.DAYS);
});
System.out.println(date); // 2020-09-07T00:16:22.861463 (09-05에 적용)
}
- 캡슐화 적용
TemporalAdjuster nextWorkingDay = TemporalAdjusters.ofDateAdjuster(
temporal -> {
DayOfWeek dow = DayOfWeek.of(temporal.get(ChronoField.DAY_OF_WEEK));
int dayToAdd = 1;
if (dow == DayOfWeek.FRIDAY) {
dayToAdd = 3;
} else if (dow == DayOfWeek.SATURDAY) {
dayToAdd = 2;
}
return temporal.plus(dayToAdd, ChronoUnit.DAYS);
});
LocalDate date = LocalDate.now();
System.out.println(date.with(nextWorkingDay)); // 2020-09-07
캡슐화하게 되면 팀 전체가 재사용할 수 있는 나ㅣㄹ짜를 조정하는 작은 라이브러리를 만들수 있다.
날짜와 시간 객체 출력과 파싱
포매팅과 파싱 전용 패키지인 java.time.format을 사용할 수 있다. 이 패키지에서 DateTimeFormatter를 사용하여 날짜나 시간을 특정 형식의 문자열로 만들 수 있다.
- ex
LocalDate date = LocalDate.of(2020, 9, 9);
String s1 = date.format(DateTimeFormatter.BASIC_ISO_DATE); // 20200909
String s2 = date.format(DateTimeFormatter.ISO_LOCAL_DATE); // 2020-09-09
LocalDate date1 = LocalDate.parse("20200909", DateTimeFormatter.BASIC_ISO_DATE);
LocalDate date2 = LocalDate.parse("2020-09-09", DateTimeFormatter.ISO_LOCAL_DATE);
System.out.println(date1 + " " +date2); // 2020-09-09 2020-09-09
기존 java.util.DateFormat 클래스와 달리 모든 DateTimeFormatter는 스레드에서 안전하게 사용할 수 있는 클래스다.
- 패턴으로 DateTimeFormatter 만들기
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd/MM/yyyy");
LocalDate date1 = LocalDate.of(2020, 9, 9);
String formattedDate = date1.format(formatter);
LocalDate date2 = LocalDate.parse(formattedDate, formatter);
System.out.println(date1 + " " + date2); // 2020-09-09 2020-09-09
- 지역화된 DateTimeFormatter 만들기
DateTimeFormatter italianFormatter = DateTimeFormatter.ofPattern("d. MMMM yyyy", Locale.ITALIAN);
LocalDaate date1 = LocalDate.of(2020, 3, 9);
String formattedDate = date.format(italianFormatter); // 9. marzo 2020
LocalDate date2 = LocalDate.parse(formattedDate, italianFormatter);
- DateTimeFormatter 만들기
DateTimeFormatter italianFormatter = new DateTimeFormatterBuilder()
.appendText(ChronoField.DAY_OF_MONTH)
.appendLiteral(". ")
.appendText(ChronoField.MONTH_OF_YEAR)
.appendLiteral(" ")
.appendText(ChronoField.YEAR)
.parseCaseInsensitive()
.toFormatter(Locale.ITALIAN);
System.out.println(italianFormatter); // Text(DayOfMonth)'. 'Text(MonthOfYear)' 'Text(Year)ParseCaseSensitive(false)
DateTimeFormatterBuilder 클래스로 복합적인 포매터를 정의해서 좀 더 세부적으로 포매터를 제어할 수 있다.
즉, DateTimeFormatterBuilder 클래스로 대소문자를 구분하는 파싱, 관대한 규칙을 적용하는 파싱(정해진 형식과 정확하게 일치하지 않는 입력을 해석할 수 있도록 체험적 방식의 파서 사용), 패딩, 포매터의 선택사항 등을 활용할 수 있다.
다양한 시간대와 캘린더 활용 방법
기존에 사용하던 java.util.TimeZone을 대체하는 java.time.ZoneId 클래스가 새롭게 등장했다. 이를 활용함으로써 서머타임(DST)와 같은 복잡한 사항이 자동으로 처리된다.
- 시간대 규정
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.Month;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.TimeZone;
ZoneId romeZone = ZoneId.of("Europe/Rome");
ZoneId zoneId = TimeZone.getDefault().toZoneId();
LocalDate date = LocalDate.of(2020, Month.SEPTEMBER, 6);
ZonedDateTime zdt1 = date.atStartOfDay(romeZone);
LocalDateTime dateTime = LocalDateTime.of(2020, Month.SEPTEMBER, 6, 22, 42);
ZonedDateTime zdt2 = dateTime.atZone(romeZone);
Instant instant = Instant.now();
ZonedDateTime zdt3 = instant.atZone(romeZone);
System.out.println(zdt1 + " " + zdt2 + " " + zdt3); // 2020-09-06T00:00+02:00[Europe/Rome] 2020-09-06T22:42+02:00[Europe/Rome] 2020-09-06T15:46:26.366728+02:00[Europe/Rome]
ZonedDateTime은 지정한 시간대에 상대적인 시점을 표현한다.
UTC/GMT 기준의 고정 오프셋
예를 들어 ‘뉴욕은 런던보다 5시간 느리다’라는 것을 표현해야 할 때, ZoneId의 서브클래스인 ZoneOffset 클래스로 런던의 그리니치 0도 자오선과 시간값의 차이를 표현할 수 있다.
ZoneOffset newYorkOffset = ZoneOffset.of("-05:00");
요약
- 자바 8 이전 버전에서 제공하는 기존의 java.util.Date 클래스와 관련 클래스에서는 여러 불일치점들과 가변성, 어설픈 오프셋, 기본 값, 잘못된 이름 결정 등의 설계 결함이 존재했다. - LocalDate, LocalDateTime 등으로 해결
- 새로운 날짜와 시간 API(LocalDate, LocalTime, LocalDateTime)에서 날짜와 시간 객체는 모두 불변이다.
- 새로운 API는 각각 사람과 기계가 편리하게 날짜와 시간 정보를 관리할 수 있도록 두 가지 표현 방식을 제공한다. - Duration, Period
- 날짜와 시간 객체를 절대적/상대적 방법으로 처리할 수 있으며, 기존 인스턴스를 변환하지 않도록 처리 결과로 새로운 인스턴스가 생성된다.
- TemporalAdjuster를 이용하면 단순히 값을 바꾸는 것 이상의 복잡한 동작을 수행할 수 있으며(ex. 돌아오는 평일, 다음주 일요일 계산) 자신만의 커스텀 날짜 변환 기능을 정의(ex. 영업일 계산)할 수 있다.
- 날짜와 시간 객체를 특정 포맷으로 출력하고 파싱하는 포매터를 정의할 수 있다. 패턴을 이용하거나 프로그램으로 포매터를 만들 수 있으며 포매터는 thread safe 하다.
- 특정 지역/장소에 상대적인 시간대 또는 UTC/GMT 기준의 오프셋을 이용해서 시간대를 정의할 수 있으며 이 시간대를 날짜와 시간 객체에 적용해서 지역화할 수 있다.
- ISO-8601 표준 시스템을 준수하지 않는 캘린더 시스템도 사용할 수 있다.
Comments