LocalDate, LocalTime Instant, Duration, Period 클래스
LocalDate, LocalTime 사용
LocalDate는 시간을 제외한 날짜를 표현하는 객체로, 시간 정보를 포함하지 않는다.
정적 팩토리 메서드 of를 사용해 아래와 같이 인스턴스를 만들 수 있다. 또한 다양한 메서드로 날짜정보에 관한 값을 얻을 수 있다.
LocalDate date = LocalDate.of(2023,10,09);//인스턴스 생성
int year = date.getYear();
Month month = date.getMonth();
int day = date.getDayOfMonth();
DayOfWeek dow = date.getDayOfWeek();// 요일 -> 월요일 반환
int len = date.lengthOfMonth();// 달의 길이, 10월은 30일까지 있음.
boolean leap = date.isLeapYear();// 윤년인지 확인
또한, 현재시간은 now라는 팩토리 메서드를 통해 얻을 수있다.
LocalDate today = LocalDate.now();
위에서 내장 메서드 get+필드명을 통해 가독성을 높여 연도, 월, 날짜에 대한 정보를 가져왔다. 이외에도 get메서드에 TemporalField를 전달해서 정보를 얻을 수도 있다.
TemporalField란, 시간, 날짜 객체에서 어떤 필드의 값을 접근할지 정의한 인터페이스이다.
이를 구혆나 ChronoField의 Enum 이용해 원하는 정보를 얻을 수 있다.
LocalDate date = LocalDate.of(2023,10,9);
int year = date.get(ChronoField.YEAR);
int month = date.get(ChronoField.MONTH_OF_YEAR);
마찬가지로 시간에 대한 정보는 `LocalTime`을 통해 표현할 수 있다.
LocalTime time = LocalTime.of(13,45,20);//13시 45분 20초
또한 `parse`를 통해 새로운 인스턴스를 만들 수 있다.
날짜와 시간 조합
LocalDateTime은 날짜를 표현하는 LocalDate와 시간을 표현하는 LocalTime을 쌍으로 갖는 복합 클래스이다. of를 사용해 직접 만들수도 있고, 날짜와 시간을 적절히 조합할 수 있다.
LocalDateTime dt1 = LocalDateTime.of(1998, Month.APRIL,17,21,06,00);
LocalDateTime dt2 = LocalDateTime.of(date, time);//날짜, 시간 조합
또한 LocalDate의 `atTime`를 통해 시간을 제공하거나 LocalTime에 atDate를 제공해 LocalDateTime을 만들 수도 있다.
LocalDateTime dt3 = date.atTime(13, 45, 20);
LocalDateTime dt4 = date.atTime(time);
LocalDateTime dt5 = time.atDate(date);
LocalDateTime의 toLocalDate, toLocalTime을 통해서 특정 date, time만을 추출할 수도 있다.
LocalDate date1 = dt1.toLocalDate();
LocalTime time1 = dt1.toLocalTime();
Instant 클래스: 기계의 날짜와 시간
기계는 사람과 다르게 주, 날짜 단위로 시간을 구분하기 어렵다. 기계의 관점에선 연속된 시간에서 특정 지점을 하나의 큰 수로 표현하는 것이 자연스러운데 이를 java.time.Instant 클래스에서 기계적인 관점으로 시간을 표현한다. Instant 클래스는 유닉스 에포크 시간(1970.1.1T00:00:00UTC)을 기준으로 특정 지점까지의 시간을 초로 표현한다.
나노초의 정밀도를 제공하며, ofEpochSecond 메서드를 이용해 인스턴스를 생성할 수 있다.
Instant in1 = Instant.ofEpochSecond(3);
Instant in2 = Instant.ofEpochSecond(3, 0);//3초 이후에 추가시간 0
Instant in3 = Instant.ofEpochSecond(2, 1_000_000_000); //2초 이후에 1억 나노초
Instant는 기계전용 유틸리티이기에 현재 시간을 기계에 전달할 필요가 있다.
위와 같이 기계가 읽을 수 있는 시간 정보를 나타낼 수 있지만 이는 UnsupportedTemporalTypeException을 일으킨다.
Duration과 Period
지금까진 특정 시간을 나타내는 Temporal 인터페이스를 구현했다. 이번엔, 두 시간사이의 지속시간을 나타내는 duration에 대해 다뤄보겠다. `between` 메서드를 통해 두 시간 객체 사이의 지속 시간을 찾을 수 있다.
Duration 클래스는 초와 나노초로 시간 단위를 표현하므로 LocalDate에 관한 정보를 전ㄷ라할 수없다.
LocalDateTime ldt1 = LocalDateTime.of(2023, Month.OCTOBER,9,0,6,06);
LocalDateTime ldt2 = LocalDateTime.now();
LocalTime time1 = LocalTime.of(21,0,0);
LocalTime time2 = LocalTime.now();
Duration d1 = Duration.between(time1, time2);
Duration d2 = Duration.between(ldt1, ldt2);
이때, 사람이 이해하는 LocalDateTime과 기계가 이해하는 Instant는 서로 혼용할 수 없다.
또한 Period 클래스의 between을 사용해 두 LocalDate의 차이를 확인할 수 있다.
LocalDate date1 = LocalDate.of(1998,04,17);
LocalDate date2 = LocalDate.now();
Period p1 = Period.between(date1, date2);
int years = p1.getYears();
System.out.println("years = " + years);//출력결과 25
반대로 출력하면 -25가 나온다. (뒤에 값 - 앞에 값)
Period p1 = Period.between(date2, date1);
int years = p1.getYears();
System.out.println("years = " + years);//-25
지금까지 위에서 다룬 모든 클래스는 불변 클래스이다. 따라서, ThreadSafe하다.
하지만 새로운 날짜와 시간 API에선 날짜에 값을 더하는, 이러한 방법을 제공한다.
날짜 조정, 파싱, 포매팅
with + 필드 형식의 withAttribute 메서드로 기존의 LocalDate를 바꾼 버전을 만들 수 있다.
LocalDate date1 = LocalDate.of(2017, 9, 21);
LocalDate date2 = date1.withMonth(10);
LocalDate date3 = date1.withYear(2022);
LocalDate date4 = date1.with(ChronoField.MONTH_OF_YEAR, 2);
위 메서드를 사용했을땐 새로운 객체를 반환한다. 따라서, 기존의 date1이 변하지는 않는다.
with 메서드는 get메서드와 같이 사용할 수 있으며, Temporal 객체의 필드값을 읽거나 고칠 수 있다.(함수 갱신)
Temporal 객체가 지정한 필드를 지원하지 않으면 UnsupportedTemporalTypeException이 발생한다.
또한, 선언형으로 LocalDate를 바꾸는 방법도 존재한다.
LocalDate date5 = date1.plusDays(9);
LocalDate date6 = date1.minusWeeks(1);//1주일(7일) 감소
LocalDate, LocalTime, LocalDateTime, Instant 모두 서로 비슷한 메서드를 제공한다.
TemporalAdjusters 사용하기
지금까진 단순하게 더하기, 빼기, 또는 지정한 값으로 시간과 날짜를 조정했다. 조금 더 복잡하게 날짜를 바꾸고 싶으면 어떨까? 다음과 같은 요구를 생각해보자.
`다음 주 일요일, 돌아오는 평일, 어떤 달의 마지막 날`
이땐, with 메서드의 오버로드 된 버전을 사용해 다양한 동작을 활용할 수 있다. 새로운 날짜, 시간 API는 이러한 상황에 적합한 `TemproalAdjuster`를 제공한다.
LocalDate date1 = LocalDate.now();//10월 9일
LocalDate date2 = date1.with(nextOrSame(DayOfWeek.SUNDAY));//10월 15일
LocalDate date3 = date2.with(lastDayOfMonth());//10월 31일
다음은 TemporalAdjusters 클래스의 팩토리 메서드이다.
또한 필요한 기능이 정의되어있지 않을땐 쉽게 커스텀을 할 수 있다.
TemporalAdjuster의 함수형 인터페이스는 아래와 같은데 UnaryOperator<Temporal>과 같은 형식이다.
커스텀 TemopralAdjuster 구현하기
주5일 주중 출근을 할때 다음 출근 날짜를 구하는 NextWorkingDay를 구현해보자. 주말은 건너뛰고 주중만 선택해 하루씩 더하면 된다.
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);
}
}
아래와 같이 실행할 수 있다.
LocalDate date1 = LocalDate.now();
LocalDate date = date1.with(new NextWorkingDay());
System.out.println("date = " + date);// 오늘 기준 10월 10일
또한 함수형 인터페이스인만큼 람다를 아래와 같이 활용할 수도 있다.
(하지만 람다 안에 분기문이 많이 들어있는 로직을 짜는게 좋아보이지는 않는다.)
LocalDate date = date1.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);
});
날짜와 시간 객체 출력과 파싱
날짜와 시간 관련한 작업에서 포매팅과 파싱 전용 패키지인 java.time.format에 대해 알아보자. 이 패키지의 DateTimeFormatter 클래스에선 정적 팩토리 메서드와 상수를 이용해서 포매터를 만들 수 있다.
BASIC_ISO_DATE, ISO_LOCAL_DATE와 같은 상수를 미리정의하고 이러한 형식의 날짜와 시간을 문자열로 만들 수 있다.
LocalDate date = LocalDate.of(2023, 9, 8);
String s1 = date.format(DateTimeFormatter.ISO_DATE);//2023-09-08
String s2 = date.format(DateTimeFormatter.BASIC_ISO_DATE);//20230908
반대로 날짜와 시간을 표현하는 문자열을 파싱해서 날짜 객체를 만들 수 있다.
LocalDate p1 = LocalDate.parse("2023-08-08", DateTimeFormatter.ISO_DATE);
LocalDate p2 = LocalDate.parse("20230808", DateTimeFormatter.BASIC_ISO_DATE);
출력하면 둘 다 2023-08-08의 값을 갖는다.
DateTimeFormatter에서 제공하는 정적 팩토리 메서드 `DateTimeFormatter.ofPattern`을 통해 특정 패턴으로 포매터를 만들 수 있다.
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd/MM/yyyy");
LocalDate now = LocalDate.now();
String customFormat = now.format(formatter);// 09/10/2023
LocalDate p3 = LocalDate.parse(customFormat, formatter);//2023-10-09
또한, 지역에 특화된 Lcoal 포매터를 만들 수 있다. 아래는 한국에 특화된 포매터를 구현한 코드이다.
DateTimeFormatter koreanFormatter = DateTimeFormatter.ofPattern("dd MMMM yyyy", Locale.KOREAN);
LocalDate now = LocalDate.now();
String customFormat = now.format(koreanFormatter);
LocalDate p3 = LocalDate.parse(customFormat, koreanFormatter);
System.out.println("customFormat = " + customFormat);
System.out.println("p3 = " + p3);
DateTimeFormatterBuilder클래스로 복합적인 포매터를 보다 세부적으로 만들 수 있다.
DateTimeFormatter koreanFormatter2 = new DateTimeFormatterBuilder()
.appendText(ChronoField.MONTH_OF_YEAR)
.appendLiteral(" ")
.appendText(ChronoField.DAY_OF_MONTH)
.appendLiteral(" 일 ")
.appendText(ChronoField.YEAR)
.appendLiteral("년")
.parseCaseInsensitive()
.toFormatter();
다양한 시간대와 캘린더 활용 방법
지금까지 살펴본 모든클래스는 어떤 시간대를 다루는지 정보가 없었다. 새로운 시간 날짜 API의 java.time.ZoneId을 통해 시간대를 간단하게 처리할 수 있다. 이는 불변 클래스이다.
시간대 사용하기
ZoneRules에는 약 40개의 시간대가 존재한다. 여러 시간대에서 getRules()를 통해 특정 시간대의 규정을 가져올 수 있다.
ZoneId romeZone = ZoneId.of("Europe/Rome");
이 때 키는 `지역/도시`의 형식으로 이루어지며 IANA Time Zone DB에서 제공하는 정보를 활용한다.
아래 코드를 통해 현재 시간의 기본 값을 받아올 수 있다.
ZoneId zoneId = TimeZone.getDefault().toZoneId();
zoneId를 얻은 후에는 LocalDate, LocalDateTime, LocalTime, Instant를 이용해 ZonedDateTime 인스턴스로 변환할 수 있다. ZonedDateTime은 지정한 시간대에 상대적인 시간을 표현한다.
만약 로마의 현재 시간을 LocalDate와 LocalDateTime으로 얻고 싶다면 아래처럼 코드를 작성하면 된다.
ZoneId zoneId = ZoneId.of("Europe/Rome");
LocalDate date = LocalDate.now();
ZonedDateTime zdt1 = date.atStartOfDay(zoneId);
LocalDateTime dateTime = LocalDateTime.now();
ZonedDateTime zdt2 = dateTime.atZone(zoneId);
zoneId를 통해 LocalDateTime를 Instant에서 바꿀 수 있다.
Instant instant = Instant.now();
LocalDateTime timeFromInstant = LocalDateTime.ofInstant(instant, zoneId);
기존의 Date클래스를 처리하는 코드를 사용할 상황이 있으므로 Instant로 작업하는 것이 유리하다. toInstant(), fromInstant() 두 개의 메서드가 있다.
UTC/Greenwich 기준 고정 오프셋
UTC(협정 세계시) / GMT(그리니치 표준시)를 기준으로 시간을 표현할 수 있다.
ZoneId의 서브 클래스 ZoneOffset으로 런던과 특정 지역의 시간값 차이를 표현할 수 있다.
이는 서머 타임을 제대로 처리할 수 없으므로 권장하는 방식은 아니다.
ISO-8601 캘린더시스템에서 정의하는 UTC/GMT로 ㅇ날짜와 시간을 표현하는 OffsetDateTime을 만들 수 있다.
ZoneOffset newYorkOffset = ZoneOffset.of("-05:00");
LocalDateTime now = LocalDateTime.now();
OffsetDateTime dateTimeInNewYork = OffsetDateTime.of(now, newYorkOffset);
System.out.println("dateTimeInNewYork = " + dateTimeInNewYork);
대안 캘리던 시스템 이용하기
자바8에선 ISO-8601외에도 추가로 4가지의 캘린더를 제공한다. ThaiBuddhistDate, MinguoDate, JapansesDate, HijrahDate를 제공한다.
LocalDate now = LocalDate.now();
JapaneseDate japaneseDate = JapaneseDate.from(now);
또한 특정 Locale에 대한 날짜 인스턴스로 캘린더를 만들 수도 있다. `ofLocale`을 이용해 Chronology의 인스턴스를 획득할 수 있다.
Chronology koreaChronology = Chronology.ofLocale(Locale.KOREA);
ChronoLocalDate now1 = koreaChronology.dateNow();
System.out.println("now1 = " + now1);
'PL > 모던 자바 인 액션' 카테고리의 다른 글
모던 자바 인 액션 - 회고록 (1) | 2023.10.27 |
---|---|
모던 자바 인 액션 - Chap11 (0) | 2023.10.09 |
자바 병렬 프로그래밍 5.2 병렬 컬렉션 (0) | 2023.09.30 |
모던 자바 인 액션 7주차 - Chap 08 (0) | 2023.09.30 |
모던 자바 인 액션 6주차 - Chap07 (0) | 2023.09.21 |