devseop08 님의 블로그

[API] 람다와 스트림: Mordern Java - 12. 새로운 날짜와 시간 API 본문

Language/Java

[API] 람다와 스트림: Mordern Java - 12. 새로운 날짜와 시간 API

devseop08 2025. 7. 30. 20:04
  • 자바 1.0에서는 java.util.Date 클래스 하나로 날짜와 시간 관련 기능을 제공
  • Date 클래스의 문제
    • 1.'Date'라는 클래스 이름과 달리 Date 클래스는 특정 시점을 날짜가 아닌 밀리초 단위로 표현
    • 2.1900년을 기준으로 하는 오프셋, 0에서 시작하는 달 인덱스 등 모호한 설계로 인한 유용성하락
    • 3.Date 클래스의 toString으로는 반환되는 문자열을 추가로 활용하기 어렵다.(Date는 JVM 기본 시간대인 CET, 즉 중앙 유럽 시간대를 사용했는데 이것이 문자열로 반환된다.)
      1. Date 클래스는 자체적으로 시간대 정보를 알고 있지도 못한다.
  • 자바 1.1에서는 Date 클래스의 여러 메서드를 deprecated시키고 java.util.Calendar 클래스를 대안으로 제공
  • Calendar 클래스의 문제: 1900년을 기준으로 하는 오프셋은 없앴지만 여전히 달의 인덱스는 0부터 시작, Date 클래스와 혼동, DateFormat 같은 기능은 Date 클래스에만 작동
  • java.text 패키지의 DateFormat 클래스의 문제: 스레드에 안전하지 못하다 => 두 스레드가 동시에 하나의 포매터(DateFormat)로 날짜를 파싱할 때 예기치 못한 결과 발생
  • "Date formats are not synchronized."
  • DateCalendar는 모두 가변 클래스다. => 가변 클래스 설계는 유지보수가 아주 어려워진다.
  • 부실한 날짜와 시간 라이브러리를 대체하기 위해 Joda-Time 같은 서드파티 날짜 시간 라이브러리가 만들어졌고 자바 9부터는 Joda-Time의 많은 기능을 java.time 패키지로 추가하게 됐다.

12.1 LocalDate, LocalTime, Instant, Duration, Period 클래스

  • 날짜와 시간, 그리고 그것들의 간격을 정의하기 위한 java.time 패키지
  • LocalDate, LocalTime, LocalDateTime, Instant, Duration, Period 등 새로운 클래스 제공

12.1.1 LocalDate와 LocalTime 사용

  • LocalDate 타입의 인스턴스는 시간을 제외한 날짜를 표현하는 불변 객체
  • LocalDate 객체는 어떤 시간대 정보도 포함하지 않는다.
public final class LocalDate implements Temporal, TemporalAdjuster, 
    ChronoLocalDate, Serializable {  
        public static final LocalDate MIN = of(-999999999, 1, 1);  
        public static final LocalDate MAX = of(999999999, 12, 31);  
        public static final LocalDate EPOCH = of(1970, 1, 1);  
        private static final long serialVersionUID = 2942565459149668126L;  
        private static final int DAYS_PER_CYCLE = 146097;  
        static final long DAYS_0000_TO_1970 = 719528L;  
        private final int year;  
        private final short month;  
        private final short day;
        ...
}
  • 정적 팩토리 메서드 ofLocalDate 인스턴스를 만들 수 있다.
public static LocalDate of(int year, Month month, int dayOfMonth) {  
    ChronoField.YEAR.checkValidValue((long)year);  
    Objects.requireNonNull(month, "month");  
    ChronoField.DAY_OF_MONTH.checkValidValue((long)dayOfMonth);  
    return create(year, month.getValue(), dayOfMonth);  
}  

public static LocalDate of(int year, int month, int dayOfMonth) {  
    ChronoField.YEAR.checkValidValue((long)year);  
    ChronoField.MONTH_OF_YEAR.checkValidValue((long)month);  
    ChronoField.DAY_OF_MONTH.checkValidValue((long)dayOfMonth);  
    return create(year, month, dayOfMonth);  
}

private static LocalDate create(int year, int month, int dayOfMonth) {  
    if (dayOfMonth > 28) {  
        int dom = switch (month) {  
            case 2 -> (IsoChronology.INSTANCE.isLeapYear(year) ? 29 : 28);  
            case 4, 6, 9, 11 -> 30;  
            default -> 31;  
        };  
        if (dayOfMonth > dom) {  
            if (dayOfMonth == 29) {  
                throw new DateTimeException(
                "Invalid date 'February 29' as '" 
                + year + "' is not a leap year");  
            } else {  
                throw new DateTimeException("Invalid date '" 
                + Month.of(month).name() + " " + dayOfMonth + "'");  
            }  
        }  
    }  
    return new LocalDate(year, month, dayOfMonth);  
}
  • LocalDate 인스턴스는 연도, 달, 요일 등을 반환하는 메서드를 제공한다.
  • getYear, getMonth, getDayOfMonth, getDayOfWeek
  • LocalDate 만들고 값 읽기
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 (윤년이 아님)
  • 팩토리 메서드 now는 시스템 시계의 정보를 이용해서 현재 날짜 정보를 얻는다.
public static LocalDate now() {  
    return now(Clock.systemDefaultZone());  
}  

public static LocalDate now(ZoneId zone) {  
    return now(Clock.system(zone));  
}  

public static LocalDate now(Clock clock) {  
    Objects.requireNonNull(clock, "clock");  
    Instant now = clock.instant();  
    return ofInstant(now, clock.getZone());  
}
LocalDate today = LocalDate.now();
  • get 메서드에 java.time.temporal 패키지의 TemporalField를 전달해서 정보를 얻는 방법도 있다.
// LocalDate.java
@Override 
public int get(TemporalField field) {  
    if (field instanceof ChronoField) {  
        return get0(field);  
    }  
    return ChronoLocalDate.super.get(field);  
}
  • java.time.temporal.TemporalField는 시간 관련 객체에서 어떤 필드의 값에 접근할지 정의하는 인터페이스다.
  • 열거자 ChronoFieldjava.time.temporal.TemporalField 인터페이스를 정의하므로 ChronoField의 열거자 요소를 이용해서 원하는 정보를 쉽게 얻을 수 있다.
public interface TemporalField { ... }
public enum ChronoField implements TemporalField { ... }
// TemporalField를 이용해서 LocalDate 값 읽기
int year = date.get(ChronoField.YEAR);
int month = date.get(ChronoField.MONTH_OF_YEAR);
int day = date.get(ChronoField.DAY_OF_MONTH);
  • 내장 메서드 getYear(), getMonthValue(), getDayOfMonth() 등을 이용용해서 가독성을 높일 수 있다.
// LocalDate.java
public int getYear() {  
    return year;  
}

public int getMonthValue() {  
    return month;  
}

public Month getMonth() {  
    return Month.of(month);  
}

public int getDayOfMonth() {  
    return day;  
}
int year = date.getYear();
int month = date.getMonthValue();
int day = date.getDayOfMonth();
  • 마찬가지로 13:45:20 같은 시간은 LocalTime 클래스로 표현할 수 있다.
public final class LocalTime  
        implements Temporal, TemporalAdjuster, Comparable<LocalTime>, Serializable {  

    public static final LocalTime MIN;  
    public static final LocalTime MAX;  
    public static final LocalTime MIDNIGHT;  
    public static final LocalTime NOON;  
    private static final LocalTime[] HOURS = new LocalTime[24];  
    static {  
        for (int i = 0; i < HOURS.length; i++) {  
            HOURS[i] = new LocalTime(i, 0, 0, 0);  
        }  
        MIDNIGHT = HOURS[0];  
        NOON = HOURS[12];  
        MIN = HOURS[0];  
        MAX = new LocalTime(23, 59, 59, 999_999_999);  
    }
    ...
}
  • 오버로드 버전의 두 가지 정적 메서드 of로 LocalTime 인스턴스를 만들 수 있다.
  • 시간과 분을 인수로 받는 of 메서드와 시간과 분, 초를 인수로 받는 of 메서드가 있다.
// LocalTime.java
public static LocalTime of(int hour, int minute) {  
    HOUR_OF_DAY.checkValidValue(hour);  
    if (minute == 0) {  
        return HOURS[hour];  // for performance  
    }  
    MINUTE_OF_HOUR.checkValidValue(minute);  
    return new LocalTime(hour, minute, 0, 0);  
}

public static LocalTime of(int hour, int minute, int second) {  
    HOUR_OF_DAY.checkValidValue(hour);  
    if ((minute | second) == 0) {  
        return HOURS[hour];  // for performance  
    }  
    MINUTE_OF_HOUR.checkValidValue(minute);  
    SECOND_OF_MINUTE.checkValidValue(second);  
    return new LocalTime(hour, minute, second, 0);  
}

public static LocalTime of(int hour, int minute, int second, int nanoOfSecond) {  
    HOUR_OF_DAY.checkValidValue(hour);  
    MINUTE_OF_HOUR.checkValidValue(minute);  
    SECOND_OF_MINUTE.checkValidValue(second);  
    NANO_OF_SECOND.checkValidValue(nanoOfSecond);  
    return create(hour, minute, second, nanoOfSecond);  
}
  • LocalDate 클래스와 같이 LocalTime 클래스도 게터 메서드를 제공한다.
LocalTime time = LocalTime.of(13, 45, 20);
int hour = time.getHour(); // 13
int minute = time.getMinute(); // 45
int second = time.getSecond(); // 20
  • 정적 메서드 parse를 이용해 날짜와 시간 문자열로 LocalDate와 LocalTime 인스턴스를 만드는 방법도 있다.
  • parse 메서드에 DateTimeFormatter를 전달할 수도 있다.
  • DateTimeFormatter의 인스턴스는 날짜, 시간, 객체의 형식을 지정한다.
  • 문자열을 LocalDateLocalTime으로 파싱할 수 없을 때 parse 메서드는 DateTimeParsedException을 일으킨다.
// LocalDate.java
public static LocalDate parse(CharSequence text) {  
    return parse(text, DateTimeFormatter.ISO_LOCAL_DATE);  
}

public static LocalDate parse(CharSequence text, DateTimeFormatter formatter) {  
    Objects.requireNonNull(formatter, "formatter");  
    return formatter.parse(text, LocalDate::from);  
}
// LocalTime.java
public static LocalTime parse(CharSequence text) {  
    return parse(text, DateTimeFormatter.ISO_LOCAL_TIME);  
}

public static LocalTime parse(CharSequence text, DateTimeFormatter formatter) {  
    Objects.requireNonNull(formatter, "formatter");  
    return formatter.parse(text, LocalTime::from);  
}
LocalDate date = LocalDate.parse("2017-09-21");
LocalTime time = LocalTime.parse("12:45:20");

12.1.2 날짜와 시간 조합

  • LocalDateTimeLocalDateLocalTime을 쌍으로 갖는 복합 클래스
  • 즉, LocalDateTime은 날짜와 시간을 모두 표현할 수 있다.
public final class LocalDateTime  
        implements Temporal, TemporalAdjuster, ChronoLocalDateTime<LocalDate>, Serializable {  

    public static final LocalDateTime MIN 
        = LocalDateTime.of(LocalDate.MIN, LocalTime.MIN);  
    @java.io.Serial  
    private static final long serialVersionUID = 6207766400415563566L;  

    private final LocalDate date;  
    private final LocalTime time;
    ...
}
  • 정적 메서드 of를 이용하여, 직접 LocalDateTime을 만들 수도 있고 날짜와 시간을 조합하여 LocalDateTime을 만들 수도 있다.
// LocalDateTime.java
public static LocalDateTime of(int year, Month month, int dayOfMonth, int hour, int minute) {  
    LocalDate date = LocalDate.of(year, month, dayOfMonth);  
    LocalTime time = LocalTime.of(hour, minute);  
    return new LocalDateTime(date, time);  
}

public static LocalDateTime of(int year, Month month, int dayOfMonth, int hour, int minute, int second) {
        LocalDate date = LocalDate.of(year, month, dayOfMonth);
        LocalTime time = LocalTime.of(hour, minute, second);
        return new LocalDateTime(date, time);
}

public static LocalDateTime of(int year, Month month, int dayOfMonth, int hour, int minute, int second, int nanoOfSecond) {  
    LocalDate date = LocalDate.of(year, month, dayOfMonth);  
    LocalTime time = LocalTime.of(hour, minute, second, nanoOfSecond);  
    return new LocalDateTime(date, time);  
}

public static LocalDateTime of(LocalDate date, LocalTime time) {  
    Objects.requireNonNull(date, "date");  
    Objects.requireNonNull(time, "time");  
    return new LocalDateTime(date, time);  
}
  • atTime 메서드를 이용해 날짜와 시간을 조합하여 LocalDateTime을 만드는 방법도 있다.
  • LocalDateatTime 메서드에 시간을 제공하거나 LocalTimeatTime 메서드에 날짜를 제공해서 LocalDateTime을 만들 수 있다.
//LocalDate.java
@Override  
public LocalDateTime atTime(LocalTime time) {  
    return LocalDateTime.of(this, time);  
}
// LocalTime.java
public LocalDateTime atDate(LocalDate date) {  
    return LocalDateTime.of(date, this);  
}
// 2017-09-21T13:45:20
LocalDateTime dt1 = LocalDateTime.of(2017, Month.SEPTEMBER, 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);
  • LocalDateTimetoLocalDatetoLocalTime 메서드로 LocalDateLocalTime 인스턴스를 추출할 수 있다.
// LocalDateTime.java
@Override  
public LocalDate toLocalDate() {  
    return date;  
}

@Override  
public LocalTime toLocalTime() {  
    return time;  
}
LocalDate date1 = dt1.toLocalDate(); // 2017-09-21
LocalTime time1 = dt1.toLocalTime(); // 13:45:20

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

  • 사람은 보통 주, 날짜, 시간, 분으로 날짜와 시간을 계산한다.
  • 하지만 기계에서는 이와 같은 단위로 시간을 표현하기가 어렵다.
  • 기계의 관점에서는 연속된 시간에서 특정 지점을 하나의 큰 수로 표현하는 것이 가장 자연스러운 시간 표현 방법이다.
  • 새로운 java.time.Instant 클래스에서는 기계적인 관점에서 시간을 표현한다. => 유닉스 에포크 시간(1970년 1월 1일 0시 0분 0초 UTC)를 기준으로 특정 지점까지의 시간을 초로 표현
public final class Instant  
        implements Temporal, TemporalAdjuster, Comparable<Instant>, Serializable {  
    public static final Instant EPOCH = new Instant(0, 0);  
    private static final long MIN_SECOND = -31557014167219200L; 
    private static final long MAX_SECOND = 31556889864403199L;  
    public static final Instant MIN = Instant.ofEpochSecond(MIN_SECOND, 0);  
    public static final Instant MAX = Instant.ofEpochSecond(MAX_SECOND, 999_999_999);  

    @java.io.Serial  
    private static final long serialVersionUID = -665713676816604388L;  

    private final long seconds;  
    private final int nanos;
  • 팩토리 메서드 ofEpochSecond에 초를 넘겨줘서 Instant 클래스 인스턴스를 만들 수 있다.
  • Instant 클래스는 나노초(10억분의 1초)의 정밀도를 제공한다.
  • 오버로드된 ofEpochSecond 메서드 버전에서는 두 번째 인수를 이용해서 나노초 단위로 시간을 보정할 수 있다.
  • 두 번째 인수에는 0에서 999,999,999 사이의 값을 지정할 수 있다.
public static Instant ofEpochSecond(long epochSecond) {  
    return create(epochSecond, 0);  
}

public static Instant ofEpochSecond(long epochSecond, long nanoAdjustment) {  
    long secs = Math.addExact(epochSecond, Math.floorDiv(nanoAdjustment, NANOS_PER_SECOND));  
    int nos = (int)Math.floorMod(nanoAdjustment, NANOS_PER_SECOND);  
    return create(secs, nos);  
}
  • 같은 Instant를 반환하는 코드
Instant.ofEpochSecond(3);
Instant.ofEpochSecond(3, 0);
Instant.ofEpochSecond(2, 1_000_000_000);
Instant.ofEpochSecond(4, -1_000_000_000);
  • Instant 클래스도 사람이 확인할 수 있도록 시간을 표시해주는 정적 팩토리 메서드 now를 제공한다.
  • Instant는 초와 나노초 정보를 포함하므로 Instant는 사람이 읽을 수 있는 시간 정보(년,월,일,시,분,초)는 제공하지 않는다.
public static Instant now() {  
    return Clock.currentInstant();  
}

public static Instant now(Clock clock) {  
    Objects.requireNonNull(clock, "clock");  
    return clock.instant();  
}
  • Instant 클래스 인스턴스에 대해 사람이 읽을 수 있는 시간 정보에 대한 게터 메서드를 호출하면 UnsupportedTemporalTypeException 발생한다.
int day = Instant.now().get(ChronoField.DAY_OF_MONTH);
// java.time.temporal.UnsupportedTemporalTypeException

12.1.4 Duration과 Period 정의

  • Duration 클래스의 정적 팩토리 메서드 between으로 두 시간 객체 사이의 지속시간을 만들 수 있다.
public static Duration between(
    Temporal startInclusive, Temporal endExclusive
) {  
    long secs = startInclusive.until(endExclusive, SECONDS);  
    if (secs == 0) {  
        return ofNanos(startInclusive.until(endExclusive, NANOS));  
    }  
    long nanos;  
    try {  
        nanos = endExclusive.getLong(NANO_OF_SECOND) - 
                            startInclusive.getLong(NANO_OF_SECOND);  
    } catch (DateTimeException ex2) {  
        nanos = 0;  
    }  
    if (nanos < 0 && secs > 0) {  
        secs++;  
    } else if (nanos > 0 && secs < 0) {  
        secs--;  
    }  
    return ofSeconds(secs, nanos);  
}
  • 두 개의 LocalTime, 두 개의 LocalDateTime, 또는 두 개의 Instant로 Duration을 만들 수 있다.
Duration d1 = Duration.between(time1, time2);
Duration d2 = Duration.between(dateTime1, dateTime2);
Duration d3 = Duration.between(instant1, instant2);
  • LocalDateTime은 사람이 사용하도록, Instant는 기계가 사용하도록 만들어진 클래스로 두 인스턴스는 서로 혼합할 수 없다.
  • Duration 클래스는 초와 나노초로 시간 단위를 표현하므로 between 메서드에 시간대 정보를 포함하지 않는 LocalDate를 전달할 수 없다.
  • 대신 년,월,일로 날짜 시간을 표현할 때는 Period 클래스를 사용한다. => Period 클래스의 팩토리 메서드 between을 이용하면 두 LocalDate의 차이를 확인 가능
Period tendays = Period.between(
    LocalDate.of(2017, 9, 11), LocalDate.of(2017, 9, 21));
  • Duration과 Period 클래스는 자신의 인스턴스를 만들 수 있도록 다양한 팩토리 메서드를 제공한다.
  • 두 시간 객체를 만들지 않고도 Duration과 Period 클래스의 인스턴스를 만들 수 있다.
Duration threeMinutes = Duration.ofMinutes(3);
Duration threeMinutes = Duration.of(3, ChronoUnit.MINUTES);

Period tenDays = Period.ofDays(10);
Period threeWeeks = Period.ofWeeks(3);
Period twoYearsSixMonthsOneDay = Period.of(2, 6, 1);

12.2 날짜 조정, 파싱, 포매팅

  • 날짜와 시간, 그리고 그것들의 간격을 정의하는 java.time 패키지의 클래스들: LocalDate, LocalTime, LocalDateTime, Instant, Duration, Period
  • 이것들은 모두 불변 클래스다 => 불변 클래스는 함수형 프로그래밍 그리고 스레드 안정성과 도메인 모델의 일관성을 유지하는 데 좋은 특징
  • 다만, 자바 9에서 새로 추가된 java.time 패키지의 날짜와 시간 API에서는 변경된 객체 버전을 만들 수 있는 메서드를 제공한다.
  • 예를 들어, LocalDate 인스턴스에 3일을 더하거나 dd/MM/yyyy 같은 형식으로 새로운 날짜와 시간 포매터를 만들거나 새로운 포매터로 날짜를 새로 파싱하고 출력하는 경우 등이 있다.

절대적인 방식으로 LocalDate 속성 바꾸기

// LocalDate.java
public LocalDate withYear(int year) {  
    if (this.year == year) {  
        return this;  
    }  
    YEAR.checkValidValue(year);  
    return resolvePreviousValid(year, month, day);  
}

public LocalDate withDayOfMonth(int dayOfMonth) {  
    if (this.day == dayOfMonth) {  
        return this;  
    }  
    return of(year, month, dayOfMonth);  
}

private static LocalDate resolvePreviousValid(int year, int month, int day) {  
    switch (month) {  
        case 2 -> day = 
        Math.min(day, IsoChronology.INSTANCE.isLeapYear(year) ? 29 : 28);  
        case 4, 6, 9, 11 -> day = Math.min(day, 30);  
    }  
    return new LocalDate(year, month, day);  
}
// LocalDate.java
@Override  
public LocalDate with(TemporalField field, long newValue) {  
    if (field instanceof ChronoField chronoField) {  
        chronoField.checkValidValue(newValue);  
        return switch (chronoField) {  
            case DAY_OF_WEEK -> plusDays(newValue - getDayOfWeek().getValue());

            case ALIGNED_DAY_OF_WEEK_IN_MONTH -> 
            plusDays(newValue - getLong(ALIGNED_DAY_OF_WEEK_IN_MONTH));  

            case ALIGNED_DAY_OF_WEEK_IN_YEAR -> 
            plusDays(newValue - getLong(ALIGNED_DAY_OF_WEEK_IN_YEAR));  

            case DAY_OF_MONTH -> withDayOfMonth((int) newValue);  

            case DAY_OF_YEAR -> withDayOfYear((int) newValue);  

            case EPOCH_DAY -> LocalDate.ofEpochDay(newValue);  

            case ALIGNED_WEEK_OF_MONTH -> 
            plusWeeks(newValue - getLong(ALIGNED_WEEK_OF_MONTH));  

            case ALIGNED_WEEK_OF_YEAR -> 
            plusWeeks(newValue - getLong(ALIGNED_WEEK_OF_YEAR));  

            case MONTH_OF_YEAR -> withMonth((int) newValue);  

            case PROLEPTIC_MONTH -> plusMonths(newValue - getProlepticMonth());

            case YEAR_OF_ERA -> 
            withYear((int) (year >= 1 ? newValue : 1 - newValue));  

            case YEAR -> withYear((int) newValue);  

            case ERA -> (getLong(ERA) == newValue ? this : withYear(1 - year));

            default -> 
            throw new UnsupportedTemporalTypeException(
            "Unsupported field: " + field);  
        };  
    }  
    return field.adjustInto(this, newValue);  
}
  • withAttribute 메서드로, 기존의 LocalDate를 바꾼 버전을 간단하게 만들 수 있다.
  • withAttribute 메서드는 기존 객체를 바꾸지 않는다. 복사본을 생성한다.
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
  • java.time.temporal 패키지의 Temporal 인터페이스는 LocalDate,LocalTime, LocalDateTime, Instant처럼 특정 시간을 정의한다.
  • get과 with 메서드로 Temporal 객체의 필드값을 읽거나 고칠 수 있는데, 어떤 Temporal 객체가 지정된 필드를 지원하지 않으면 UnsupportedTemporalTypeException이 발생한다.
  • Instant에 ChronoField.MONTH_OF_YEAR를 사용하거나 LocalDate에 ChronoField.NANO_OF_SECOND를 사용하면 해당 예외가 발생한다.

상대적인 방식으로 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

특정 시점을 표현하는 날짜 시간 클래스의 공통 메서드

  • 날짜와 시간 클래스 LocalDate, LocalTime, LocalDateTime, Instant는 공통적으로 java.time.temporal 패키지의 Temporal 인터페이스를 구현
메서드 정적 설명
of 주어진 구성요소에서 Temporal 객체의 인스턴스를 생성함.
from 주어진 Temporal 객체를 이용해서 Temporal 클래스의 인스턴스를 생성함.
now 시스템 시계로 Temporal 객체를 생성함.
parse 문자열을 파싱해서 Temporal 객체를 생성함.
format 아니오 주어진 지정 포매터를 이용해서 Temporal 객체를 문자열로 반환,
Instant 클래스는 지원하지 않는다.
get 아니오 Temporal 객체의 상태를 읽음
minus 아니오 특정 시간을 뺀 Temporal 객체의 복사본을 생성함.
plus 아니오 특정 시간을 더한 Temporal 객체의 복사본을 생성함.
with 아니오 일부 상태를 바꾼 Temporal 객체의 복사본을 생성함.
atOffice 아니오 시간대 오프셋과 Temporal 객체를 합침
atZone 아니오 시간대 오프셋과 Temporal 객체를 합침

12.2.1 TemporalAdjuster 인터페이스 구현체를 반환하는 TemporalAdjusters 클래스의 정적 팩토리 메서드를 사용하여 날짜 조정하기

  • 다음 주 일요일, 돌아오는 평일, 어떤 달의 마지막 날 등 좀 더 복잡한 날짜 조정 기능이 필요한 경우가 있다.
  • 오버로드된 버전의 with 메서드에 좀 더 다양한 동작을 수행할 수 있도록 하는 기능을 제공하는 java.time.temporal 패키지의 TemporalAdjuster를 전달하는 방법으로 문제 해결 가능하다.
  • Temporal 객체의 오버로드된 버전의 with 메서드는 파라미터로 TemporalAdjuster 타입의 객체를 전달받아 TemporalAdjuster 객체의 adjustInto를 호출하여 기존 Temporal 객체의 날짜를 조정한 새로운 Temporal 객체를 반환한다.
// LocalDate.java
@Override  
public LocalDate with(TemporalAdjuster adjuster) {  
    // optimizations  
    if (adjuster instanceof LocalDate) {  
        return (LocalDate) adjuster;  
    }  
    return (LocalDate) adjuster.adjustInto(this);  
}
  • 날짜와 시간 API는 다양한 상황에서 사용할 수 있도록 다양한 TemporalAdjuster를 제공한다.
  • java.time.temporal 패키지의 TemporalAdjusters에서 정의하는 정적 팩토리 메서드로 이들 기능을 이용할 수 있다.
  • java.time.temporal 패키지의 TemporalAdjusters 클래스의 정적 팩토리 메서드를 이용해서 TemporalAdjuster 인터페이스의 구현체를 생성할 수 있다.
public final class TemporalAdjusters {

    public static TemporalAdjuster ofDateAdjuster(
            UnaryOperator<LocalDate> dateBasedAdjuster) {  
        Objects.requireNonNull(dateBasedAdjuster, "dateBasedAdjuster");  
        return (temporal) -> {  
            LocalDate input = LocalDate.from(temporal);  
            LocalDate output = dateBasedAdjuster.apply(input);  
            return temporal.with(output);  
        };  
    }

    public static TemporalAdjuster firstDayOfMonth() {  
        return (temporal) -> temporal.with(DAY_OF_MONTH, 1);  
    }
    ...
    public static TemporalAdjuster nextOrSame(DayOfWeek dayOfWeek) {  
        int dowValue = dayOfWeek.getValue();  
        return (temporal) -> {  
            int calDow = temporal.get(DAY_OF_WEEK);  
            if (calDow == dowValue) {  
                return temporal;  
            }  
            int daysDiff = calDow - dowValue;  
            return temporal.plus(
                daysDiff >= 0 ? 7 - daysDiff : -daysDiff, DAYS);  
        };
    }
}
import static java.tima.temporal.TemporalAdjusters.*
LocalDate date1 = LocalDate.of(2014, 3, 18);
LocalDate date2 = date1.with(nextOrSame(DayOfWeek.SUNDAY));
LocalDate date3 = date2.with(lastDayOfMonth());

TemporalAdjuster 인터페이스의 커스텀 구현체를 이용해 날짜 조정

  • java.time.temporal.TemporalAdjusters에서 제공하는 정적 팩토리 메서드로는 필요한 기능을 하는 TemporalAdjuster 인터페이스 구현체를 얻을 수 없을 때는 TemporalAdjuster 인터페이스를 직접 커스텀 구현할 수 있다.
  • java.time.temporal.TemporalAdjusters 인터페이스 구현은 Temporal 객체를 어떻게 다른 Temporal 객체로 변환할 지 정의한다.
@FunctionalInterface
public interface TemporalAdjuster {
    Temporal adjustInto(Temporal temporal);
}
  • java.time.temporal.TemporalAdjuster 인터페이스를 구현하는 NextWorkingDay 클래스
  • 이동된 날짜가 평일이 아니라면, 즉 토요일이나 일요일이라면 월요일로 이동한다.
date = date.with(new NextWorkingDay());
public class NextWorkingDay implements TemporalAdjuster {
    @Override
    public Temporal adjustInto(Temporal temporal) {
        int dayToAdd = 1;
        DayOfWeek dow = DayOfWeek.of(temporal.get(ChronoField.DAY_OF_WEEK));
        if(dow == DayOfWeek.FRIDAY) dayToAdd = 3;
        else if(dow == DayOfWeek.SATURDAY) dayToAdd = 2;
        return temporal.plus(dayToAdd, ChronoUnit.DAYS);
    }
}
  • java.time.temporal.TemporalAdjuster는 함수형 인터페이스이므로 람다 표현식을 이용할 수 있다.
date = date.with( temporal -> {
        int dayToAdd = 1;
        DayOfWeek dow = DayOfWeek.of(temporal.get(ChronoField.DAY_OF_WEEK));
        if(dow == DayOfWeek.FRIDAY) dayToAdd = 3;
        else if(dow == DayOfWeek.SATURDAY) dayToAdd = 2;
        return temporal.plus(dayToAdd, ChronoUnit.DAYS);
});
  • java.time.temporal.TemporalAdjusterFuntion<Temporal, Temporal>을 상속한 UnaryOperator<Temporal>과 같은 형식이다.
@FunctionalInterface  
public interface Function<T, R> {
    R apply(T t);
    ...
}
@FunctionalInterface  
public interface UnaryOperator<T> extends Function<T, T> {
    ...
}
  • java.time.temporal.TemporalAdjuster 클래스의 정적 팩토리 메서드 중에 ofDateAdjuster 메서드는 UnaryOperator<LocalDate> 타입의 객체를 받아 TemporalAdjuster 인터페이스 구현체를 반환한다.
// TemporalAdjuster.java
public static TemporalAdjuster ofDateAdjuster(
    UnaryOperator<LocalDate> dateBasedAdjuster) {  
        Objects.requireNonNull(dateBasedAdjuster, "dateBasedAdjuster");  
        return (temporal) -> {  
            LocalDate input = LocalDate.from(temporal);  
            LocalDate output = dateBasedAdjuster.apply(input);  
            return temporal.with(output);  
        };  
}
  • java.time.temporal.TemporalAdjuster 클래스의 ofDateAdjuster에 원하는 날짜 조정 기능을 구현하는 람다를 전달하여 java.time.temporal.TemporalAdjuster 인터페이스의 구현체를 얻을 수 있다.
TemporalAdjuster nextWorkingDay = TemporalAdjusters.ofDateAdjuster(
    temporal -> {
        int dayToAdd = 1;
        DayOfWeek dow = DayOfWeek.of(temporal.get(ChronoField.DAY_OF_WEEK));
        if(dow == DayOfWeek.FRIDAY) dayToAdd = 3;
        else if(dow == DayOfWeek.SATURDAY) dayToAdd = 2;
        return temporal.plus(dayToAdd, ChronoUnit.DAYS);
    }
);

date = date.with(nextWorkingDay);

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

  • 포매팅과 파싱 전용 패키지 java.time.format 패키지의 DateTimeFormatter

  • 정적 팩토리 메서드와 상수를 이용해서 손쉽게 포매터를 만들 수있다.

  • java.time.format.DateTimeFormatter 클래스는 BASIC_ISO_DATE와 ISO_LOCAL_DATE 등의 상수를 미리 정의하고 있다.

  • java.time.format.DateTimeFormatter 를 이용해서 날짜나 시간을 특정 형식의 문자열로 만들 수 있다.

    LocalDate date = LocalDate.of(2014, 3, 18);
    String s1 = date.format(DateTimeFormatter.BASIC_ISO_DATE);
    String s2 = date.format(DateTimeFormatter.ISO_LOCAL_DATE);
  • 또한 반대로 날짜나 시간을 표현하는 문자열을 파싱해서 날짜 객체를 다시 만들 수 있다.

  • 날짜와 시간 API에서 특정 시점이나 간격을 표현하는 모든 클래스의 모든 팩토리 메서드 parse를 이용해서 문자열을 날짜 객체로 만들 수 있다.

LocalDate date1 = LocalDate.parse("20140318",DateTimeFormatter.BASIC_ISO_DATE);
LocalDate date2 = LocalDate.parse("2014-03-18",DateTimeFormatter.ISO_LOCAL_DATE);
  • 기존의 java.text.DateFormat 클래스와 달리 모든 java.time.format.DateTimeFormatter는 스레드에서 안전하게 사용할 수 있는 클래스다.
  • 특정 패턴으로 포매터를 만들 수 있는 java.time.format.DateTimeFormatter의 정적 팩토리 메서드 ofPattern
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);
  • 지역화된 java.time.format.DateTimeFormatter
DateTimeFormatter italianFormatter = DateTimeFormatter.ofPattern(
                                        "d. MMMM yyyy", Locale.ITALIAN);
LocalDate date1 = LocalDate.of(2014, 3, 18);
String formattedDate = date1.format(italianFormatter);
LocalDate date2 = LocalDate.parse(formattedDate, italianFormatter);
  • java.time.format.DateTimeFormatterBuilder 클래스를 이용하여 복합적인 포매터 만들기
DateTimeFormatter italianFormatter = new DateTimeFormatterBuilder()
                                        .appendText(ChronoField.DAY_OF_MONTH)
                                        .appendLiteral(". ")
                                        .appendText(ChronoField.MONTH_OF_YEAR)
                                        .appendLiteral(" ")
                                        .appendText(ChronoField.YEAR)
                                        .parseCaseInsensitive()
                                        .toFormatter(Locale.ITALIAN);

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

  • LocalDate, LocalTime, LocalDateTime, Instant와 같은 클래스에는 시간대 정보가 포함되지 않는다.

  • 시간대 정보를 정의하기 위한 기존의 java.util.TimeZone 클래스를 대체할 수 있는 java.time.ZoneId 클래스 => 서머타임 같은 복잡한 사항이 자동으로 처리된다.

  • 날짜와 시간 API에서 제공하는 다른 클래스들과 마찬가지로 ZoneId는 불변 클래스다.

    12.3.1 시간대 사용하기

  • 표준 시간이 같은 지역을 묶어서 시간대 규칙 집합을 정의한다.

  • ZoneRules 클래스에는 약 40 개 정도의 시간대가 있다.

  • ZoneId 클래스의 getRules()를 이용해서 해당 시간대의 규정을 획득 가능

ZoneId romeZone = ZoneId.of("Europe/Rome");
  • 기존의 TimeZone 객체를 ZoneId 객체로 변환
ZoneId zoneId = TimeZone.getDefault().toZoneId();
  • ZoneId 객체를 얻은 다음에는 LocalDate, LocalDateTime, Instant를 이용해서 ZonedDateTime 인스턴스로 변환 가능
  • ZonedDateTime은 지정한 시간대에 상대적인 시점을 표현한다.
LocalDate date = LocalDate.of(2014, Month.MARCH, 18);
ZonedDateTime zdt1 = date.atStartOfDay(romeZone);
LocalDateTime dateTime = LocalDateTime.of(2014, Month.MARCH, 18, 13, 45);
ZonedDateTime zdt2 = dateTime.atZone(romeZone);
Instant instant = Instant.now();
ZonedDateTime zdt3 = instant.atZone(romeZone);

12.3.2 UTC/Greenwich 기준의 고정 오프셋

  • 때로는 UTC(협정 세계시)/GMT(그리니치 표준시)를 기준으로 시간대를 표현하기도 한다.
  • ZoneId의 서브클래스인 ZoneOffset 클래스 => 런던의 그리니치 0도 자오선과 시간값의 차이를 표현 가능
ZoneOffset newYorkOffset = ZoneOffset.of("-5:00");
  • ZoneOffset은 ZoneId의 하위 타입이므로 ZoneId가 쓰이는 곳에 사용할 수 있다.
  • 서머타임을 제대로 처리할 수 없으므로 권장하지는 않는다.
  • 서머타임(Daylight Saving Time, DST): 일광을 더 오래 활용하기 위해 시계를 한 시간 앞당기는 제도로, 대한민국에서는 사용하지 않는다.
  • 서머타임을 적용하면 일몰 시간이 원래보다 1시간 늦추게 되는데 이는 저녁 시간대의 자연광 활용을 늘리고 에너지 소비를 줄이려는 취지에서 시작된 것이다.

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

  • ISO-8601 캘린더 시스템은 실질적으로 전 세계에서 통용되는데 자바 8에서는 이 캘린더 시스템과는 별개로 추가적인 4개의 캘린더 시스템을 제공한다.

  • ThaiBuddhistDate, MinguoDate, JapaneseDate, HijrahDate, 이 4개의 클래스가 각각의 캘린더 시스템을 대표한다.

  • 이 4개의 클래스와 LocalDate 클래스는 공통적으로 ChronoLocalDate 인터페이스를 구현하는데 ChronoLocalDate는 임의의 연대기에서 특정 날짜를 표현할 수 있는 기능을 제공하는 인터페이스다.

  • LocalDate를 이용해서 이들 4개의 클래스 중 하나의 인스턴스를 만들 수 있다.

LocalDate date = LocalDate.of(2014, Month.MARCH, 18);
JapaneseDate japaneseDate = JapaneseDate.from(date);
  • 날짜와 시간 API 설계자는 ChronoLocalDate보다는 LocalDate를 사용하라고 권고한다.
  • 멀티캘린더 시스템에서는 보통의 표준적인 달력 체계가 적용되지 않기 때문이다.(1년은 12개월, 1달은 31일 이하거나 최소 1년은 정해진 수의 달로 이뤄지는 등의 표준 체계를 채택하지 않음)
  • 프로그램 입출력을 지역화해야하는 상황을 제외하고는 LocalDate를 사용해야 한다.