LocalDate, LocalTime, Instant, Duration, Period 클래스 (java.time 패키지)
- Month의 인덱스는 0부터 시작, DateFormat같은 기능은 Date클래스에만 작동, 스레드가 동시에 하나의 formatter로 날짜를 파싱할 때 예기치 못한 결과가 일어나는 다양한 이슈들로 인해 java8에서 시간 라이브러리(Joda-Time)의 많은 기능을 java.time 패키지로 추가했다.
LocalDate와 LocalTime 사용
팩토리 메서드 of로 LocalDate 인스턴스를 만들 수 있으며 now는 시스템 시계의 정보를 이용해서 현재 날짜 정보를 얻는다.
LocalDate date = LocalDate.of(1999, 9, 27);
LocalDate now = LocalDate.now();
- LocalDate 인스턴스는 연도, 달, 요일, 해당 날짜가 윤년인지 등을 반환하는 메서드를 제공한다.
get 메서드에 TemporalField를 전달해서 정보 얻기
TemporalField는 시간 관련 객체에서 어떤 필드의 값에 접글할지 정의하는 인터페이스로 열거자 ChronoField는 이 인터페이스를 정의한다.
@Override // override for Javadoc and performance
public int get(TemporalField field) {
if (field instanceof ChronoField) {
return get0(field);
}
return ChronoLocalDate.super.get(field);
}
내장 메서드 getYear(), getMothValue(), getDayOfMoth()등을 이용해서 가독성을 높일 수 있다.
LocalDate date = LocalDate.of(1999, 9, 27);
int year = date.get(ChronoField.YEAR);
int year = date.getYear();
LocalDate 클래스처럼 LocalTime 클래스도 getter메서드와 정적 메서드 of로 LocalTime 인스턴스를 만들 수 있다. (오버로드)
- getHour(), getMinute(), getSecond(), getNano()
parse 정적 메서드를 이용해서 날짜와 시간 문자열로 LocalDate와 LocalTime의 인스턴스를 만들 수 있다.
LocalDate date = LocalDate.parse("1999-09-27");
LocalTime time = LocalTime.parse("11:22:33");
- parse 메서드에 DateTimeFormatter를 전달할 수 있으며 파싱할 수 없을 때 DateTimeParseException(Runtime 예외) 발생
날짜와 시간 조합
LocalDateTime은 LocalDate와 LocalTime을 쌍으로 갖는 복합 클래스이다. (of 정적 메서드)
LocalDate date = LocalDate.parse("1999-09-27");
LocalTime time = LocalTime.parse("11:22:33");
LocalDateTime dateTime1 = LocalDateTime.of(1999, Month.SEPTEMBER, 27, 11, 22, 33);
LocalDateTime dateTime2 = LocalDateTime.of(date, time);
System.out.println(dateTime1.equals(dateTime2)); //true
- toLocalDate(), toLocalTime으로 각각의 인스턴스 추출 가능하다.
Instant 클래스 : 기계의 날짜와 시간
기계의 관점에서 연속된 시간에서 특정 지점을 하나의 큰 수로 표현하는 것이 가장 자연스러운 시간을 표현한다.
- java.time.Instant 클래스에서는 Unix epoch time(1970년 1월 1일 0시 0분 0초 UTC)을 기준으로 특정 지점까지 시간을 초로 표현한다.
초를 넘겨서 Instant클래스 인스턴스를 만들 수 있다. 2번 째 인수에는 0~999,999,999사이의 값을 지정할 수 있다.
Instant.ofEpochSecond(3);
Instant.ofEpochSecond(2, 1_000_000_000); //2초 이후의 1억 나노초(1초)
Instant.ofEpochSecond(4, -1_000_000_000); //4초 이전의 1억 나노초(1초)
- Instant 클래스는 기계 전용의 유틸리티지만 사람이 확인할 수 있도록 시간을 표시해주는 now도 제공한다.
Instant.now().get(ChronoField.DAY_OF_MONTH);
- 사람이 읽을 수 있는 시간 정보를 제공하지 않기 때문에 UnsupportedTemporalTypeException 예외 발생
- Instant에서는 Duration과 Period 클래스를 함께 활용할 수 있다.
Duration과 Period 정의
Duration 클래스는 정적 패토리 메서드 between으로 두 시간 객체 사이의 지속시간을 만들 수 있다.
Duration.between(LocalDateTime.of(localDate, localTime), LocalDateTime.of(localDate, localTime));
Duration.between(localDate, localDate);
Duration.between(Instant.ofEpochSecond(1), Instant.ofEpochSecond(3));
- 2개의 LocalDateTime, 2개의 LocalTime, 2개의 Instant로 Duration을 만들 수 있다.
주의점
- Instant는 기계가 사용하도록 만들어진 클래스로 두 인스턴스를 혼합할 수 없다.
- Duration 클래스는 초와 나노초로 시간 단위를 표현하므로 between메서드에 LocalDate를 전달할 수 없다.
년, 월, 일로 시간을 표현할 때는 Period 클래스를 사용해야 한다.
Period.between(LocalDate.of(2023, 1, 1),
LocalDate.of(2023, 8, 16));
- 두 LocalDate의 차이를 확인할 수 있다.
두 시간 객체를 사용하지 않고 Duration과 Period 만들기
Duration threeMinutes1 = Duration.ofMinutes(3);
Duration threeMinutes2 = Duration.of(3, ChronoUnit.MINUTES);
Period tenDays = Period.ofDays(10);
Period threeWeeks = Period.ofWeeks(3);
Period twoYearsSixMonthOneDay = Period.of(2, 6, 1);
지금까지 살펴본 모든 클래스는 불변 클래스로 함수형 프로그래밍 그리고 스레드 안전성과 도메인 모델의 일관성을 유지하는데 좋다.
날짜 조정, 파싱 , 포매팅
withAttribute 메서드로 기존의 LocalDate를 바꾼 버전을 직접 간단하게 만들 수 있다.
- Temporal 인터페이스는 LocalDate, LocalTime, LocalDateTime, Instant처럼 특정 시간을 정의한다.
LocalDate date1 = LocalDate.of(2023, 8, 16); //2023-8-16
LocalDate date2 = date1.withYear(2022); //2022-8-16
LocalDate date3 = date2.withDayOfMonth(25); //2022-08-25
LocalDate date4 = date3.with(ChronoField.MONTH_OF_YEAR, 4); //2022-04-25
- get과 with 메서드로 Temporal 객체의 필드값을 읽거나 고칠 수 있지만 어떤 Temporal 객체가 지정된 필드를 지원하지 않으면 UnsupportedTemporalTypeException이 발생한다.
- Instant에 ChronoField.MONTH_OF_YEAR, LocalDate에 ChronoField.NANO_OF_SECOND를 사용하면 예외 발생
LocalDate date1 = LocalDate.of(2023, 8, 16); //2023-08-16
LocalDate date2 = date1.plusWeeks(1); //2023-08-23
LocalDate date3 = date2.minusYears(6); //2017-08-23
LocalDate date4 = date3.plus(6, ChronoUnit.MONTHS); //2018-02-23
- 선언형으로 with, get 메서드와 비슷한 plus, minus메서드를 통해서 지정된 시간을 추가하거나 뺄 수 있다.
TemporalAdjusters 사용하기
돌아오는 평일, 어떤 달의 마지막 날 등 오버로드된 with 메서드에 TemporalAdjuster를 전달해 좀 더 복잡한 날짜 조정이 가능하다.
LocalDate date1 = LocalDate.of(2014, 3, 16); //date1.getDayOfWeek() sunday
LocalDate date2 = date1.with(TemporalAdjusters.nextOrSame(DayOfWeek.MONDAY)); //2014-03-17
LocalDate date3 = date2.with(TemporalAdjusters.lastDayOfMonth()); //2014-03-31
TemporalAdjusters 클래스의 팩토리 메서드 말고 필요한 기능이 있을 경우 TemporalAdjuster 인터페이스를 쉽게 커스텀 가능하다.
@FunctionalInterface
public interface TemporalAdjuster {
Temporal adjustInto(Temporal temporal);
}
- UnaryOperator<Temporal>처럼 Temporal 객체를 어떻게 다른 Temporal 객체로 변환할지 정의하면 된다.
토요일과 일요일은 건너뛰는 날짜를 구할 때
TemporalAdjuster는 함수형 인터페이스이므로 람다 표현식을 이용할 수 있다.
date.with(temporal -> {
DayOfWeek dow = DayOfWeek.of(temporal.get(ChronoField.DAY_OF_WEEK));
int datToAdd = 1;
if (dow == DayOfWeek.FRIDAY) datToAdd = 3;
else if (dow == DayOfWeek.SATURDAY) datToAdd = 2;
return temporal.plus(datToAdd, ChronoUnit.DAYS);
});
UnaryOperator<LocalDate>를 인수로 받는 TemporalAdjusters 팩토리 메서드 사용해서 캡슐화하기
TemporalAdjuster nextWorkingDay = TemporalAdjusters.ofDateAdjuster(
temporal -> {
DayOfWeek dow = DayOfWeek.of(temporal.get(ChronoField.DAY_OF_WEEK));
int datToAdd = 1;
if (dow == DayOfWeek.FRIDAY) datToAdd = 3;
else if (dow == DayOfWeek.SATURDAY) datToAdd = 2;
return temporal.plus(datToAdd, ChronoUnit.DAYS);
});
date = date.with(nextWorkingDay);
날짜와 시간 객체 출력과 파싱
포매팅과 파싱 전용 패키지인 java.time.format에서 DateTimeFormatter가 가장 중요한 클래스다.
- 정적 팩토리 메서드와 상수를 이용해서 손쉽게 포매터를 만들 수 있다.
- 스레드에서 안전하게 사용할 수 있다.
LocalDate date = LocalDate.of(2014, 3, 18);
String s1 = date.format(DateTimeFormatter.BASIC_ISO_DATE); //20140318
String s2 = date.format(DateTimeFormatter.ISO_LOCAL_DATE); //2014-03-18
parse 팩토리 메서드를 통해서 반대로 시간을 표현하는 문자열을 파싱해서 날짜 객체로 다시 만들 수 있다.
LocalDate date1 = LocalDate.parse("20140318", DateTimeFormatter.BASIC_ISO_DATE);
LocalDate date2 = LocalDate.parse("2014-03-18", DateTimeFormatter.ISO_LOCAL_DATE);
DateTimeFormatter는 특정 패턴으로 포매터를 만들 수 있는 정적 팩토리 메서드도 제공한다.
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd/MM/yyyy");
LocalDate date1 = LocalDate.of(2014, 3, 18);
String formattedDate = date1.format(formatter);
LocalDate date2 = LocalDate.parse(formattedDate, formatter);
ofPattern 메서드도 Locale로 포매터를 만들 수 있도록 오버로드된 메서드를 제공한다.
DateTimeFormatter italianFormatter = DateTimeFormatter.ofPattern("d. MMMM yyyy", Locale.ITALIAN);
LocalDate date1 = LocalDate.of(2014, 3, 18);
String format = date1.format(italianFormatter); //18. marzo 2014
DateTimeFormatterBuiler 클래스로 복합적인 포매터를 정의해서 좀 더 세부적을 포매터를 제어할 수 있다.
정해진 형식과 정확하게 일치하지 않는 입력을 해석할 수 있도록 체험적 방식의 파서 사용하기, 패딩, 포매터의 선택사항 등을 활용 가능
DateTimeFormatter italianFormatter = new DateTimeFormatterBuilder()
.appendText(ChronoField.DAY_OF_MONTH)
.appendLiteral(". ")
.appendText(ChronoField.MONTH_OF_YEAR)
.appendLiteral(" ")
.appendText(ChronoField.YEAR)
.parseCaseInsensitive()
.toFormatter(Locale.ITALIAN);
- italianFormatter를 DateTimeFormatterBuilder에 이용해서 포매터 만들기
다양한 시간대와 캘린더 활용 방법
java.util.TimeZone을 대체할 수 있는 java.time.ZoneId 클래스가 새롭게 등장했다.
- 새로운 클래스를 이용하면 서머타임(Daylight Saving Time)같은 복잡한 사항이 자동으로 처리되며 ZoneId는 불변 클래스다.
시간대 사용하기
표준 시간이 같은 지역을 묶어서 시간대(time zone) 규칙 집합을 정의한다.
- ZoneRules 클래스에는 약 40개 정도의 시간대가 있으며 ZoneId의 getRules()를 이용해서 해당 시간대의 규정을 획득할 수 있다.
지역 ID는 '{지역}/{도시}' 형식으로 이루어지며 IANA Time Zone Database에서 제공하는 지역 집합 정보를 사용한다.
ZoneId romeZone = ZoneId.of("Europe/Rome");
ZoneId zoneId = TimeZone.getDefault().toZoneId();
- 지역 ID로 특정 ZoneId를 얻을 수 있고 TimeZone 객체를 ZoneId 객체로 변환할 수 있다.
ZoneId 객체를 통해서 LocalDate, LocalDateTime, Instant를 이용해서 ZonedDateTime 인스턴스로 변환할 수 있다.
ZoneId romeZone = ZoneId.of("Europe/Rome");
LocalDate date = LocalDate.of(2014, Month.MARCH, 18);
ZonedDateTime zdt1 = date.atStartOfDay(romeZone); //zdt1 = 2014-03-18T00:00+01:00[Europe/Rome]
LocalDateTime dateTime = LocalDateTime.of(2014, 3, 18, 13, 45);
ZonedDateTime zdt2 = dateTime.atZone(romeZone); //zdt2 = 2014-03-18T13:45+01:00[Europe/Rome]
Instant instant = Instant.now();
ZonedDateTime zdt3 = instant.atZone(romeZone); //zdt3 = 2023-08-16T17:15:25.050940+02:00[Europe/Rome]
ZoneId를 이용해서 LocalDateTime을 Instant로 바꿀 수도 있고 LocalDateTime을 얻을 수도 있다.
Instant instant = Instant.now();
LocalDateTime timeFromInstant = LocalDateTime.ofInstant(instant, romeZone); //2023-08-16T17:18:05.391633
- 기존 Date 클래스를 처리하기 위해서는 toInstant(), fromInstant()메서드를 통해서 Instant로 작업하는 것이 유리하다.
UTC/Greenwich 기준의 고정 오프셋
Universal Time Coordinated(협정 세계시)/Greenwich Mean Time(그리니치 표준시)를 기준으로 시간대를 표현하기도 한다.
'뉴욕은 런던보다 5시간 느리다'를 표현하려면 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(2014, Month.MARCH, 18, 13, 45);
OffsetDateTime dateTimeInNewYork = OffsetDateTime.of(dateTime, newYorkOffset);
- 새로운 날짜와 시간 API는 ISO 캘린더 시스템에 기반하지 않은 정보도 처리할 수 있는 기능을 제공한다.
대안 캘린더 시스템 사용하기
ISO-8601 캘린더 시스템은 실질적으로 전 세계에서 통용되지만 java 8에서는 추가로 ThaiBuddhistDate, MinguoDate, JapaneseDate, HijrahDate로 총 4개의 클래스가 캘린더 시스템을 대표한다.
- 위 4개와 LocalDate 클래스는 ChronoLocalDate 인터페이스를 구현하며 임의의 연대기에서 특정 날짜를 표현할 수 있다.
- LocalDate를 통해서 위 4개의 클래스 중 하나의 인스턴스를 만들 수 있다.
LocalDateTime dateTime = LocalDateTime.of(2014, Month.MARCH, 18, 13, 45);
JapaneseDate japaneseDate = JapaneseDate.from(dateTime);
Chronology japaneseChronology = Chronology.ofLocale(Locale.JAPAN);
ChronoLocalDate now = japaneseChronology.dateNow();
- 정적 메서드로 Temporal 인스턴스를 만들거나, 특정 Locale과 Locale에 대한 날짜 인스턴스로 캘린더 시스템을 만들 수 있다.
멀티캘린더 시스템에서는 1년은 12개월, 1달은 31일 이하거나 최소한 1년은 정해진 수의 달로 가정할 수 없다.
따라서 프로그램의 입출력을 지역화하는 상황을 제외하고는 모든 데이터 저장, 조작, 비즈니스 규칙 해석등의 작업에서는 ChronoLocalDate보다는 LocalDate를 사용하자
이슬람력 - HijrahDate
이슬람력에는 변형(variant)이 있어 새로 추가된 캘린더 중에서 가장 복잡하다.
- Hijrah 캘린더 시스템은 태음월(lunar moth)에 기초한다.
- 새로운 달(month)을 결정할 때 새로운 달(moon)을 전 세계 어디에서나 볼 수 있는지 아니면 사우디아라비아에서 처음으로 새로운 달을 볼 수 있는지 등의 변형 방법을 withVariant 메서드로 원하는 변형 방법을 선택할 수 있다.
java 8에는 HijrahDate의 표준 변형 방법으로 UmmAl-Qura를 제공한다.
HijrahDate ramadanDate = HijrahDate.now().with(ChronoField.DAY_OF_MONTH, 1)
.with(ChronoField.MONTH_OF_YEAR, 9); //얻은 Hijrah 날짜를 Ramadan의 첫 번째 날, 즉 9째 달로 바꿈
System.out.println("Ramadan starts on " +
IsoChronology.INSTANCE.date(ramadanDate) + //INSTANCE는 IsoChronology 클래스의 정적 인스턴스
" and ends on " +
IsoChronology.INSTANCE.date(
ramadanDate.with(
TemporalAdjusters.lastDayOfMonth())));
'책 > 모던 자바 인 액션' 카테고리의 다른 글
13 디폴트 메서드 (0) | 2023.08.17 |
---|---|
11. null 대신 optional 클래스 (0) | 2023.08.11 |
10. 람다를 이용한 도메인 전용 언어 (1) | 2023.08.04 |
9. 리팩터링, 테스팅, 디버깅 (0) | 2023.08.02 |
8. 컬렉션 API 개선 (0) | 2023.07.28 |