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