LocalDate 같은 java.time 객체를 다루다 보면 "그 달의 두 번째 화요일" 찾기처럼 복잡한 날짜 조정을 해야 할 때가 많아요.
이런 계산을 직접 하지 않고 **TemporalAdjusters**를 사용할 수 있어요.
java.time.temporal.TemporalAdjusters(마지막에 's'가 붙은 복수형이에요) 유틸리티 클래스는 자주 쓰는 날짜 조정을 위한 정적 메서드를 제공해요.
이런 조정은 LocalDate 같은 시간 객체에서 .with() 메서드를 사용해 적용해요.
with(TemporalAdjuster)를 사용하면 새로운 인스턴스가 반환되고 기존 인스턴스는 바뀌지 않아요. java.time 객체는 불변이기 때문이에요.
import java.time.LocalDate;
import java.time.Month;
import java.time.DayOfWeek;
import java.time.temporal.TemporalAdjusters;
LocalDate date = LocalDate.of(2024, Month.AUGUST, 15); // 2024-08-15 (a Thursday)
// Use a predefined adjuster: find the last day of the month
LocalDate lastDay = date.with(TemporalAdjusters.lastDayOfMonth());
// => 2024-08-31
일정 계산에 유용한 주요 조정자를 살펴봐요:
dayOfWeekInMonth(int ordinal, DayOfWeek dayOfWeek)를 사용하면 한 달에 몇 번째로 오는 요일인지 찾을 수 있어요.
// Find the second Tuesday of August 2024
LocalDate secondTuesday = date.with(TemporalAdjusters.dayOfWeekInMonth(2, DayOfWeek.TUESDAY));
// => 2024-08-13
firstInMonth(DayOfWeek dayOfWeek) 또는 lastInMonth(DayOfWeek dayOfWeek)를 사용해요.
// Find the last Monday of August 2024
LocalDate lastMonday = date.with(TemporalAdjusters.lastInMonth(DayOfWeek.MONDAY));
// => 2024-08-26
next(), previous(), nextOrSame(), previousOrSame() 같은 조정자는 기준 날짜를 중심으로 특정 요일이 다음에 또는 이전에 언제 오는지 찾아줘요.
next()/previous(): 현재 날짜가 해당 요일과 같으면 포함하지 않아요.nextOrSame()/previousOrSame(): 현재 날짜가 해당 요일과 같으면 포함해요.LocalDate thursday = LocalDate.of(2024, Month.AUGUST, 15); // A Thursday
// Find the next Thursday (exclusive)
LocalDate nextThursday = thursday.with(TemporalAdjusters.next(DayOfWeek.THURSDAY));
// => 2024-08-22
// Find the next Thursday (inclusive) - returns the same date
LocalDate nextOrSameThursday = thursday.with(TemporalAdjusters.nextOrSame(DayOfWeek.THURSDAY));
// => 2024-08-15
// Find the previous Monday (exclusive)
LocalDate previousMonday = thursday.with(TemporalAdjusters.previous(DayOfWeek.MONDAY));
// => 2024-08-12
TemporalAdjuster는 함수형 인터페이스라서, 조정할 시간 객체를 받아 조정된 객체를 반환하는 람다로 직접 조정자를 만들 수 있어요.
만든 조정자는 미리 정의된 조정자와 똑같이 .with()로 적용하고, 그 안에서 미리 정의된 조정자를 사용할 수도 있어요.
import java.time.temporal.TemporalAdjuster;
// An adjuster that moves a weekend date to the following Monday
TemporalAdjuster skipWeekend = temporal -> {
DayOfWeek day = DayOfWeek.from(temporal);
if (day == DayOfWeek.SATURDAY || day == DayOfWeek.SUNDAY) {
return temporal.with(TemporalAdjusters.next(DayOfWeek.MONDAY));
}
return temporal;
};
LocalDate saturday = LocalDate.of(2024, Month.AUGUST, 17);
LocalDate monday = saturday.with(skipWeekend);
// => 2024-08-19
TemporalAdjusters 클래스가 제공하는 이런 조정자를 활용하면 날짜를 다루는 작업이 훨씬 간단해져요.
뉴욕의 뷰티 살롱은 Booking Up For Beauty 연습 문제에서 도와드렸던 그 살롱인데, 그동안 잘 운영되어서 많은 손님이 단골이 되었어요. 단골손님은 방문할 때마다 따로 예약하는 대신 "격주 화요일마다", "매달 마지막 금요일"처럼 정해진 규칙으로 예약해요. 사장님이 이런 규칙에 해당하는 날짜와 살롱 자체의 월간 정기 업무 날짜를 계산해 주는 도우미를 만들어 달라고 했어요.
연도, 월, 몇 번째인지(1은 첫 번째, 2는 두 번째 등)와 요일을 받는 SalonCalendar.standingAppointment() 메서드를 구현해요.
이 메서드는 그 달 안에서 해당 요일이 그 순서로 오는 날의 날짜를 반환해요:
SalonCalendar calendar = new SalonCalendar();
calendar.standingAppointment(2024, Month.AUGUST, 2, DayOfWeek.TUESDAY);
// => LocalDate.of(2024, 8, 13)
어떤 단골손님은 자기 요일이 그 달에 마지막으로 오는 날을 더 좋아해요.
연도, 월, 요일을 받아 그 달 안에서 해당 요일이 마지막으로 오는 날의 날짜를 반환하는 SalonCalendar.lastAppointment() 메서드를 구현해요:
SalonCalendar calendar = new SalonCalendar();
calendar.lastAppointment(2024, Month.AUGUST, DayOfWeek.FRIDAY);
// => LocalDate.of(2024, 8, 30)
카운터에서 손님이 "다음 토요일"로 예약을 원해요.
문의한 날이 이미 토요일이라면 바로 그날을 뜻해요.
날짜와 요일을 받아, 주어진 날짜 당일 또는 그 이후에 해당 요일이 처음 오는 날짜를 반환하는 SalonCalendar.nextAvailable() 메서드를 구현해요:
SalonCalendar calendar = new SalonCalendar();
calendar.nextAvailable(LocalDate.of(2024, 8, 15), DayOfWeek.SATURDAY);
// => LocalDate.of(2024, 8, 17)
calendar.nextAvailable(LocalDate.of(2024, 8, 17), DayOfWeek.SATURDAY);
// => LocalDate.of(2024, 8, 17)
살롱은 매달 마지막 날에 장부를 마감해요.
그런데 주말에는 문을 닫기 때문에, 그 달의 마지막 날이 토요일이나 일요일이면 그 전 금요일에 장부를 마감해요.
연도와 월을 받아 그 달의 장부 마감 날짜를 반환하는 SalonCalendar.bookkeepingDay() 메서드를 구현해요:
SalonCalendar calendar = new SalonCalendar();
calendar.bookkeepingDay(2024, Month.SEPTEMBER);
// => LocalDate.of(2024, 9, 30)
calendar.bookkeepingDay(2024, Month.AUGUST);
// => LocalDate.of(2024, 8, 30)
직원들은 매달 15일에 급여를 받는데, 여기에도 같은 주말 규칙이 적용돼요. 15일이 토요일이나 일요일이면 급여일은 그 전 금요일이에요. 규칙을 두 번 작성하지 말고, 재사용할 수 있는 조정자로 만들어 봐요.
평일은 그대로 두고 토요일이나 일요일을 그 전 금요일로 옮기는 TemporalAdjuster를 반환하는 SalonCalendar.precedingWorkday() 메서드를 구현해요.
그다음 이 조정자를 사용해서, 연도와 월을 받는 SalonCalendar.payday() 메서드를 구현해요:
SalonCalendar calendar = new SalonCalendar();
LocalDate.of(2024, 6, 15).with(calendar.precedingWorkday());
// => LocalDate.of(2024, 6, 14)
calendar.payday(2024, Month.JUNE);
// => LocalDate.of(2024, 6, 14)
calendar.payday(2024, Month.AUGUST);
// => LocalDate.of(2024, 8, 15)
bookkeepingDay()로 돌아가서 이 조정자를 사용하도록 바꿔도 좋아요.
Exercism에 가입하고 Java 트랙을 개념 27개연습 문제 159개, 그리고 실제 사람의 멘토링과 함께 배우고 익혀 보세요. 모두 무료예요.