12장

1. 기존 날짜와 시간 API 문제점

자바 1.0에서 제공된 java.util.Date 클래스는 문제가 많았다.

  • Date 클래스인데 밀리초 단위로 표현

  • 1900년을 기준으로 하는 오프셋

  • 0에서 시작하는 달 인덱스

  • toString으로 반환되는 문자열(추가 활용 어려움)

  • JVM의 기본 시간대인 CET(Central European Time) 사용

  • DateFormat이 스레드에 안전하지 않음

Date 클래스의 문제를 해결하기 위해 자바 1.1에서는 java.util.Calendar 클래스를 제공했다. 안타깝게도 이 클래스 역시 문제가 많았다.

  • 0에서 시작하는 달 인덱스

  • DateFormat 같은 기능은 Date 클래스에서만 작동

Date와 Calendar 클래스 둘 다 가변(mutable) 클래스라 유지보수가 어려웠다. 부실한 API로 인해 Joda-Time 같은 외부 라이브러리가 유행했다. 결국 자바 8에서 Joda-Time의 많은 기능을 java.time 패키지로 추가했다.

2. java.time 패키지

2.1. LocalDate

LocalDate 인스턴스는 시간을 제외한 날짜를 표현하는 불변 객체다. LocalDate 클래스는 팩토리 메서드 of, now, get 등을 제공한다. 팩토리 메서드 of는 LocalDate 인스턴스를 생성하고, 팩토리 메서드 now는 현재 날짜 정보를 얻을 수 있고, 팩토리 메서드 get은 TemopralField를 전달해서 정보를 얻을 수 있다. ToemporalField는 시간 관련 객체에서 어떤 필드의 값에 접근할지를 정의하는 인터페이스다.

// of 사용
LocalDate date = LocalDate.of(2017, 9, 21); // 2017-09-21
int year = date.getYear(); // 2017
Month month = date.getMonth(); // SEPTEMBER
int day = date.getDayOfMonth(); // 21
DayOfWeek dow = date.getDayOfWeek(); // THURSDAY
int len = date.lengthOfMonth(); // 31(3월의 날짜 수)
boolean leap = date.isLeapYear(); // false(윤년이 아님)

// 날짜와 시간 문자열 사용
LocalDate date = LocalDate.parse("2017-09-21");

// now 사용
LocalDate today = LocalDate.now();

// get 사용
int year = date.get(ChronoField.YEAR);
int month = date.get(ChronoField.MONTH_OF_YEAR);
int day = date.get(ChronoField.DAY_OF_MONTH);

// 내장메서드 사용
int year = date.getYear();
int month = date.getMonthValue();
int day = date.getDayOfMonth();

2.2. LocalTime

LocalTime 인스턴스는 시간을 표현하는 객체다.

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

// 날짜와 시간 문자열 사용
LocalTime time = LocalTiem.parse("13:45:20");

2.3. LocalDateTime

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

// 2017-09-21T13:45:20
LocalDateTime dt1 = LocalDateTime.of(2017, Month.SEPTENBER, 21, 13, 45, 20);
LocalDateTime dt2 = LocalDateTime.of(date, time);
LocalDateTime dt3 = date.atTime(13, 45, 20);
LocalDateTime dt4 = date.atTime(time);
LocalDateTime dt5 = time.atDate(date);

2.4. Instant

java.time.Instant 클래스는 유닉스 에포크 시간(Unix epoch time, 1970.01.01.00:00:00 UTC)를 기준으로 특정 지점까지의 시간을 초로 표현한다. Instant에서는 Duration과 Period 클래스를 함께 활용할 수 있다. 팩토리 메서드 ofEpochSecond에 초를 넘겨줘서 Instant 클래스의 인스턴스를 만들 수 있다. Instant 클래스는 나노초(10억분의 1초)의 정밀도르 제공한다.

Instant.ofEpochSecond(3);
Instant.ofEpochSecond(3, 0);
Instant.ofEpochSecond(2, 1_000_000_000);
Instant.ofEpochSecond(4, -1_000_000_000);

2.5. Duration과 Period

Duration과 Period 클래스로 두 시간 객체 사이의 지속 시간을 만들 수 있다. Duration 클래스는 초와 나노초로 시간 단위를 표현하므로 LocalDate를 넘길 수 없다. Period는 LocalDate를 인수로 받을 수 있다.

// between(시간 단위)
Duration d1 = Duration.between(time1, time2);
Duration d2 = Duration.between(dateTime1, dateTime2);
Duration d3 = Duration.between(instant1, instant2);

// 시간 객체를 사용하지 않음
Duration threeMinutes = Duration.ofMinutes(3);
Duration threeMinutes = Duration.of(3, ChronoUnit.MINUTES);

// between(날짜 단위)
Period tenDays = Period.between(LocalDate.of(2017, 9, 11), 
                                LocalDate.of(2017, 9, 21));

// 시간 객체를 사용하지 않음
Period tenDays = Period.ofDays(10);
Period threeWeeoks = Period.ofWeeks(3);
Period twoYearsSixMonthsOneDay = Period.of(2, 6, 1);

3. 날짜 조정, 파싱, 포매팅

3.1. 날짜 조정

withAttribute 메서드로 기존의 LocalDate를 바꾼 버전을 간단하게 만들 수 있다. 단, 모든 메서드는 기존 객체를 바꾸지 않고 새로운 객체를 반환한다.

// 절대적 방식으로 LocalDate 속성 변경
LocalDate date1 = LocalDate.of(2017, 9, 21); // 2017-09-21
LocalDate date2 = date1.withYear(2011); // 2011-09-21
LocalDate date3 = date2.withDayOfMonth(25); // 2011-09-25
LocalDate date4 = date3.with(ChronoField.MONTH_OF_YEAR, 2); // 2011-02-25

// 상대적 방식으로 LocalDate 속성 변경
LocalDate date1 = LocalDate.of(2017, 9, 21); // 2017-09-21
LocalDate date2 = date1.plusWeeks(1); // 2017-09-28
LocalDate date3 = date2.minusYears(6); // 2011-09-28
LocalDate date4 = date3.plus(6, ChronoUnit.MONTHS); // 2012-03-28

좀 더 복잡하게 날짜를 조정하기 위해 TemporalAdjusters를 사용할 수 있다.

import static java.time.temporal.TemporalAdjusters.*;

LocalDate date1 = LocalDate.of(2014, 3, 18); // 2014-03-18
LocalDate date2 = date1.with(nextOrSame(DayOfWeek.SUNDAY)); // 2014-03-23
LocalDate date3 = date2.with(lastDayOfMonth()); // 2014-03-31

3.2. 파싱과 포매팅

java.util.DateFormat을 대체할 수 있도록 파싱과 포매팅 전용 패키지인 java.time.format이 추가되었다. 이 패키지에 정의된 DateTimeFormatter로 포매터를 만들 수 있다. DateTimeFormatter는 스레드에서 안전하게 사용할 수 있다. DateTimeFormatter는 특정 패턴으로 포매터를 만들 수 있는 정적 팩토리 메서드도 제공한다.

// 포매터로 문자열 생성
LocalDate date = LocalDate.of(2014, 3, 18);
String s1 = date.format(DateTimeFormatter.BASIC_ISO_DATE); // 20140318
Strign 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", DateTiemFormatter.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);

// 지역화된 DateTimeFormatter 생성
DateTimeFormatter italianFormatter =
    DateTimeFormatter.ofPattern("d. MMMM yyyy", Local.ITALIAN);
LocalDate date1 = LocalDate.of(2014, 3, 18);
String formattedDate = date.format(italianFormatter); // 18. marzo 2014
LocalDate date2 = LocalDate.parse(formattedDate, italianFormatter);

// DateTimeFormatterBuilder로 DateTimeFormatter 생성
DateTimeFormatter italianFormatter = 
    new DateTimeFormatterBuilder()
        .appendText(ChronoField.DAY_OF_MONTH)
        .appendLiteral(". ")
        .appendText(ChronoField.MONTH_OF_YEAR)
        .appendLiteral(" ")
        .appendText(ChronoField.YEAR)
        .parseCaseInsensitive()
        .toFormatter(Locale.ITALIAN);

4. 다양한 시간대와 캘린더 활용

java.util.TimeZone을 대체할 수 있도록 시간대를 처리하는 java.time.ZoneId 클래스가 새로 등장했다. java.time.ZoneId는 서머 타임(DST)과 같은 복잡한 사항이 자동으로 처리된다. 다른 API와 마찬가지로 ZoneId는 불변 클래스다. 표준 시간대가 같은 지역을 묶어서 시간대(time zone) 규칙 집합을 정의한다. ZoneRules 클래스에는 40개 정도의 시간대가 있다. ZoneId의 getRules()를 이용해서 시간대의 규정을 획득할 수 있다. 지역 ID는 '{지역}/{도시}' 형식으로 ZoneId를 구분하는 기준이다. 기존의 TimeZone 객체는 toZoneId로 ZoneId 객체로 변환할 수 있다.ZoneId 객체는 ZoneDateTime 인스턴스로 변환할 수 있다. ZoneDateTime은 지정한 시간대의 상대적인 시점으 표현한다.

// 지역 ID로 ZoneId 획득
ZoneId romeZone = ZoneId.of("Europe/Rome");

// TimeZone 객체를 ZoneId로 변환
ZoneId zoneId = TimeZone.getDefauilt().toZoneId();

// 특정 시점에 시간대 적용
LocalDate date = LocalDate.of(2014, Month.MARCH, 18);
ZonedDateTime zdt1 = date.atStartOfDay(romeZone);
LocalDateTime dateTime = LocalDateTime.fo(2014, Month.MARCH, 18, 13, 45);
ZonedDateTime zdt2 = dateTime.atZone(romeZone);
Instant instant = Instant.now();
ZonedDateTime zdt3 = instant.atZone(romeZone);
png

참고 자료

  • 모던 자바 인 액션 - 한빛미디어

Last updated