devseop08 님의 블로그
[API] 람다와 스트림: Mordern Java - 12. 새로운 날짜와 시간 API 본문
- 자바 1.0에서는
java.util.Date클래스 하나로 날짜와 시간 관련 기능을 제공 Date클래스의 문제- 1.'Date'라는 클래스 이름과 달리 Date 클래스는 특정 시점을 날짜가 아닌 밀리초 단위로 표현
- 2.1900년을 기준으로 하는 오프셋, 0에서 시작하는 달 인덱스 등 모호한 설계로 인한 유용성하락
- 3.
Date클래스의toString으로는 반환되는 문자열을 추가로 활용하기 어렵다.(Date는 JVM 기본 시간대인 CET, 즉 중앙 유럽 시간대를 사용했는데 이것이 문자열로 반환된다.) Date클래스는 자체적으로 시간대 정보를 알고 있지도 못한다.
- 자바 1.1에서는
Date클래스의 여러 메서드를 deprecated시키고java.util.Calendar클래스를 대안으로 제공 Calendar클래스의 문제: 1900년을 기준으로 하는 오프셋은 없앴지만 여전히 달의 인덱스는 0부터 시작,Date클래스와 혼동,DateFormat같은 기능은Date클래스에만 작동java.text패키지의DateFormat클래스의 문제: 스레드에 안전하지 못하다 => 두 스레드가 동시에 하나의 포매터(DateFormat)로 날짜를 파싱할 때 예기치 못한 결과 발생- "Date formats are not synchronized."
Date와Calendar는 모두 가변 클래스다. => 가변 클래스 설계는 유지보수가 아주 어려워진다.- 부실한 날짜와 시간 라이브러리를 대체하기 위해 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;
...
}- 정적 팩토리 메서드
of로LocalDate인스턴스를 만들 수 있다.
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는 시간 관련 객체에서 어떤 필드의 값에 접근할지 정의하는 인터페이스다.- 열거자
ChronoField는java.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의 인스턴스는 날짜, 시간, 객체의 형식을 지정한다.- 문자열을
LocalDate나LocalTime으로 파싱할 수 없을 때 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 날짜와 시간 조합
LocalDateTime은LocalDate와LocalTime을 쌍으로 갖는 복합 클래스- 즉,
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을 만드는 방법도 있다.LocalDate의atTime메서드에 시간을 제공하거나LocalTime의atTime메서드에 날짜를 제공해서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);LocalDateTime의toLocalDate나toLocalTime메서드로LocalDate나LocalTime인스턴스를 추출할 수 있다.
// 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:2012.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.UnsupportedTemporalTypeException12.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-25java.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.TemporalAdjuster는Funtion<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를 사용해야 한다.
'Language > Java' 카테고리의 다른 글
| [OOP] 불변 클래스 (0) | 2025.09.05 |
|---|---|
| [API] Object 클래스와 Class 클래스 (3) | 2025.08.05 |
| [API] 람다와 스트림: Mordern Java - 11. null 대신 Optional 클래스 (0) | 2025.07.21 |
| [OOP] 얕은 복사와 깊은 복사 (1) | 2025.07.20 |
| [Basic] 자바 기본 개념 개요 (3) | 2025.07.18 |