[도서][모던 자바 인 액션] - 새로운 날짜와 시간 API

Junseo Kim·2021년 3월 13일
0

[모던 자바 인 액션]

목록 보기
10/13

새로운 날짜와 시간 API

새로운 날짜와 시간 API가 나온 이유

기존에는 Date 클래스 하나로 날짜와 시간 관련 기능을 제공했다. Date클래스는 특정 시점을 날짜가 아닌 밀리초 단위로 표현한다. 1900년을 기준으로 하며, 달(month)를 나타내는 인덱스는 0부터 시작하는 등의 모호한 설계로 사용하기 좋지 않았으며 결과도 직관적이지 않다. 그 다음으로 등장한 Calendar도 달의 인덱스는 0부터였고 Date와 함께 사용되면서 혼란이 가중되었다. Date와 Calendar은 모두 가변 클래스이기도 해서 유지보수 또한 어렵다.

LocalDate, LocalTime, Instant, Duration, Period

LocalDate와 LocalTime

LocalDate 인스턴스는 시간을 제외한 날짜를 표현하는 불변 객체다.

LocalDate date = LocalDate.of(2021, 3, 13);
int year = date.getYear(); // 2021
Month month = date.getMonth(); // MARCH
int day = date.getDayOfMonth(); // 13
DayOfWeek dow = date.getDayOfWeek(); // SATURDAY
int len = date.lengthOfMonth(); // 31 (days in March)
boolean leap = date.isLeapYear(); // false (not a leap year)

now 팩토리 메서드를 이용해서 현재 날짜를 얻을 수도 있다.

LocalDate today = LocalDate.now();

LocalDate 인스턴스는 시간을 나타낸다. 시간과 분을 입력받거나, 시, 분, 초를 입력받아 생성할 수 있다.

LocalTime time = LocalTime.of(13, 45, 20); // 13:45:20
int hour = time.getHour(); // 13
int minute = time.getMinute(); // 45
int second = time.getSecond(); // 20

문자열을 통해서도 LocalDate와 LocalTime의 인스턴스를 만들수도 있다.

LocalDate date = LocalDate.parse("2021-03-13");
LocalTime time = LocalTime.parse("13:45:20");

파싱 불가능한 문자열이 들어온 경우 DateTimeParseException을 일으킨다.

LocalDateTime

LocalDate와 LocalTime을 쌍으로 갖는 복합 클래스. 날짜와 시간을 모두 표현할 수 있다.

LocalDateTime dt1 = LocalDateTime.of(2021, Month.MARCH, 13, 14, 45, 20); // 2021-03-13T14:45:20
LocalDateTime dt2 = LocalDateTime.of(date, time);
LocalDateTime dt3 = date.atTime(14, 45, 20);
LocalDateTime dt4 = date.atTime(time);
LocalDateTime dt5 = time.atDate(date);

LocalDateTime에서 toLocalDate, toLocalTime 메서드를 사용해서 LocalDate, LocalTime 인스턴스를 추출할 수 도 있다.

Instant 클래스: 기계의 날짜와 시간

사람은 보통 주, 날짜, 시간, 분으로 날짜와 시간을 계산하지만 기계는 이런 단위로 시간을 표현하기 힘들다. 기계의 관점에서는 연속된 시간에서 특정 지점을 하나의 큰 수로 표현하는 식으로 날짜 시간을 표현한다. 이럴 때 사용하는 것이 Instant 클래스이다.

Instant 클래스는 유닉스 에포크 시간(1970-01-01T00:00:00 UTC)을 기준으로 특정 지점까지의 시간을 초로 표현한다. 나노초의 정밀도를 제공해주며, 나노초 단위로 시간을 보정할 수 도 있다.

Instant.ofEpochSecond(3); // 1970-01-01T00:00:03Z
Instant.ofEpochSecond(3, 0); // 1970-01-01T00:00:03Z
Instant.ofEpochSecond(2, 1_000_000_000); // 1970-01-01T00:00:03Z
Instant.ofEpochSecond(4, -1_000_000_000); // 1970-01-01T00:00:03Z

Duration과 Period

Duration 객체는 두 시간 객체 사이의 지속시간을 만들 수 있다.

Duration d1 = Duration.between(time1, time2); // 2개의 LocalTime 객체
Duration d2 = Duration.between(dateTime1, dateTime2); // 2개의 LocalDateTime 객체 
Duration d3 = Duration.between(instant1, instant2); // 2개의 Instant 객체 
Duration threeMinutes = Duration.ofMinutes(3);

LocalDateTime은 사람이 사용하고, Instant는 기계가 사용하므로 서로 혼합할 수 없고, Duration은 초와 나노초로 시간 단위를 표현하므로 LocalDate로는 생성할 수 없다.

Period클래스는 년, 월, 일로 시간을 표현할 때 사용한다.

Period tenDays = Period.between(LocalDate.of(2021, 3, 13), LocalDate.of(2021, 3, 23));
Period tenDays2 = Period.ofDays(10);
Period threeWeeks = Period.ofWeeks(3);
Period twoYearsSixMonthsOneDay = Period.of(2, 6, 1);

날짜 조정, 파싱, 포매팅

위의 객체들은 모두 불변이라서 값을 바꿀 수 없다. 하지만 날짜가 지남에따라 날짜를 수정하는 등의 이유로 값이 바뀌어야 할 때가 있다.

withAttribute 메서드로 기존의 LocalDate를 바꾼 버전을 만들 수 있다. 모든 메서드는 기존 객체를 바꾸지 않는다.

LocalDate date1 = LocalDate.of(2021, 3, 13); // 2021-03-13
LocalDate date2 = date1.withYear(2002); // 2002-03-13
LocalDate date3 = date2.withDayOfMonth(25); // 2002-03-25
LocalDate date4 = date3.with(ChronoField.MONTH_OF_YEAR, 2); // 2002-02-25

선언형을 사용하는 방법도 있다.

LocalDate date1 = LocalDate.of(2021, 3, 13); // 2021-03-13
LocalDate date2 = date1.plusWeeks(1); // 2021-03-20
LocalDate date3 = date2.minusYears(3); // 2018-03-20
LocalDate date4 = date3.plus(6, ChronoUnit.MONTHS); // 2018-09-20

TemporalAdjusters 사용하기

복잡한 날짜 조정(다음 주 일요일, 돌아오는 평일 등)이 필요한 경우 사용한다. 오버로드 된 with 메서드에 TemporalAdjuster를 전달하는 방법으로 문제를 해결한다.

LocalDate date1 = LocalDate.of(2021, 3, 13); // 2021-03-13
LocalDate date2 = date1.with(nextOrSame(DayOfWeek.SUNDAY)); // 2021-03-14
LocalDate date3 = date2.with(lastDayOfMonth()); // 2021-03-31

참고
TemporalAdjusters 클래스의 팩토리 메서드

필요한 기능이 없는 경우는 커스텀할 수 도 있다. TemporalAdjuster는 하나의 메서드만 정의되어 있는 인터페이스인 함수형 인터페이스이다. Temporal 객체를 어떻게 다른 Temporal 객체로 변환할지 정의한다.

날짜와 시간 객체 출력과 파싱

날짜와 시간 관련 작업에서 포매팅과 파싱은 서로 떨어질 수 없는 관계이다.

DateTimeFormatter는 정적 팩토리 메서드와 상수를 이용해서 쉽게 포매터를 만들 수 있다. DateTimeFormatter를 이용해서 날짜나 시간을 특정 형식의 문자열로 만들 수 있다.

LocalDate date = LocalDate.of(2021, 3, 13);
String s1 = date.format(DateTimeFormatter.BASIC_ISO_DATE); // 20210313
String s2 = date.format(DateTimeFormatter.ISO_LOCAL_DATE); // 2021-03-13

반대로 날짜나 시간을 표현하는 문자열을 파싱해서 날짜 객체로 만들 수도 있다.

LocalDate date1 = LocalDate.parse("20210313", DateTimeFormatter.BASIC_ISO_DATE); // 2021-03-13
LocalDate date2 = LocalDate.parse("2021-03-13", DateTimeFormatter.ISO_LOCAL_DATE); // 2021-03-13

특정 패턴으로 포매터를 만들 수 도 있다.

DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd/MM/yyyy");
LocalDate date1 = LocalDate.of(2021, 3, 13);
String formattedDate = date1.format(formatter); // 13/03/2021
LocalDate date2 = LocalDate.parse(formattedDate, formatter); // 2021-03-13

지역화된 포매터를 만들 수 도 있다.

DateTimeFormatter italianFormatter = DateTimeFormatter.ofPattern("d. MMMM yyyy", Locale.ITALIAN);
LocalDate date1 = LocalDate.of(2021, 3, 13);
String formattedDate = date1.format(italianFormatter); // 13. marzo 2021
LocalDate date2 = LocalDate.parse(formattedDate, italianFormatter); // 2021-03-13

DateTimeFormatterBuilder 클래스로 복합적인 포매터를 만들 수 있다.

DateTimeFormatter complexFormatter = new DateTimeFormatterBuilder()
    .appendText(ChronoField.DAY_OF_MONTH)
    .appendLiteral(". ")
    .appendText(ChronoField.MONTH_OF_YEAR)
    .appendLiteral(" ")
    .appendText(ChronoField.YEAR)
    .parseCaseInsensitive()
    .toFormatter(Locale.ITALIAN);

기존에 존재하던 DateFormat 클래스와 달리 DateTimeFormatter는 스레드에서 안전하게 사용할 수 있다.

다양한 시간대와 캘린더 활용 방법

ZoneId 클래스를 이용해서 시간대를 간단하게 처리할 수 있다. 서머타임(DST) 같은 복잡한 사항이 자동으로 처리된다.

시간대 사용하기

표준 시간이 같은 지역을 묶어 시간대 규칙 집합을 정의한다. ZoneRules 클래스에는 40개 정도의 시간대가 존재하며 ZoneId의 getRuels()를 이용해서 해당 시간대의 규정을 획득할 수 있다.

ZoneId romeZone = ZoneId.of("Europe/Rome);

지역 ID는 {지역}/{도시} 형식으로 이루어져있다.

ZoneId 객체를 얻고 나면 LocalDate, LocalDateTime, Istant를 이용해서 ZonedDateTime 인스턴스로 변환할 수 있다. ZonedDateTime은 지정한 시간대에 상대적인 시점을 표현한다.

ZoneId romeZone = ZoneId.of("Europe/Rome");

LocalDate date = LocalDate.of(2021, 3, 21);
ZonedDateTime zdt1 = date.atStartOfDay(romeZone); // 2021-03-21T00:00+01:00[Europe/Rome]

LocalDateTime dateTime = LocalDateTime.of(2021, 3, 13, 13, 45, 20);
ZonedDateTime zdt2 = dateTime.atZone(romeZone); // 2021-03-13T13:45:20+01:00[Europe/Rome]

Instant instant = Instant.now();
ZonedDateTime zdt3 = instant.atZone(romeZone); // 2021-03-13T07:23:37.387+01:00[Europe/Rome]

ZoneId를 이용해서 LocalDateTime을 Instant로 바꿀 수도 있다.

Instant instant = Instant.now();
LocalDateTime tineFromInstant = LocalDateTime.ofInstant(instant, romeZone);

UTC/Greenwich 기준의 고정 오프셋

때로는 UTC/GMT 를 기준으로 시간대를 표현한다. ZoneId의 서브클래스인 ZoneOffset 클래스로 런던의 그리니치 0도 자오선과 시간값의 차이를 표현할 수 있다. 서머 타임을 처리할 수 없으므로 권장하지 않는다.

ZoneOffset newYorkOffset = ZoneOffset.of("-05:00");

대안 캘린더 시스템 사용하기

ISO-8601 캘린더 시스템이 실질적으로 전 세계에서 통용되지만, 자바 8에서는 추가로 ThaiBuddhistDate, MinguoDate, JapaneseDate, HijrahDate가 추가되었다.

프로그램의 입출력을 지역화하는 상황을 제외하고는 LocalDate를 사용한다.

0개의 댓글