Period와 Duration으로 기간 계산하기
날짜 사이의 달력 기간은 Period로, 실제로 흐른 시간은 Duration으로 계산하는 법과 ChronoUnit·ChronoField가 각각 어떤 질문에 답하는지 배웁니다.
'두 시점 사이가 얼마인가'라는 질문에는 성격이 다른 두 가지 답이 있습니다. 2025년 1월 15일에 가입한 회원이 2026년 3월 20일에 탈퇴했다면, 가입 기간은 '1년 2개월 5일'이라고 말하는 것이 자연스럽습니다. 반면 게시글 검토가 9시 10분에 시작해 10시 40분 30초에 끝났다면 '1시간 30분 30초', 즉 5430초가 걸렸다고 말합니다. 앞의 답은 달력을 넘기며 센 연·월·일이고, 뒤의 답은 시계가 실제로 흐른 양입니다.
두 답은 서로 바꿀 수 없습니다. 한 달은 28일일 때도 있고 31일일 때도 있으니 '2개월'을 정해진 초로 바꿀 수 없습니다. 여름철에 시계를 한 시간 앞당기는 일광 절약 시간을 쓰는 지역에서는 하루가 23시간이나 25시간인 날도 있습니다. 그래서 java.time은 달력 기간을 나타내는 Period와 경과 시간을 나타내는 Duration을 따로 둡니다. 이 글에서는 두 타입을 언제 쓰는지 살펴보고, 계산 단위를 나타내는 ChronoUnit과 날짜·시각에서 값 하나를 꺼내는 ChronoField가 어떻게 다른지도 정리합니다. LocalDate, LocalDateTime, ZonedDateTime이 각각 어떤 정보를 담는지는 앞 글에서 다뤘습니다.
날짜 두 개로 Duration을 구하면 생기는 예외
두 타입을 헷갈리면 어떤 일이 생기는지부터 보겠습니다. 다음 코드는 2025년 1월 1일과 1월 3일 사이의 시간을 Duration.between으로 구하려고 합니다.
import java.time.Duration;
import java.time.LocalDate;
public final class DurationBetweenDatesFailure {
public static void main(String[] args) {
LocalDate start = LocalDate.of(2025, 1, 1);
LocalDate end = LocalDate.of(2025, 1, 3);
System.out.println(Duration.between(start, end));
}
}Exception in thread "main" java.time.temporal.UnsupportedTemporalTypeException: Unsupported unit: Seconds컴파일은 되지만 실행하면 곧바로 예외가 발생합니다. Unsupported unit: Seconds는 초라는 단위를 지원하지 않는다는 뜻입니다. Duration은 시간을 초와 나노초로 세는 타입이라서, Duration.between은 첫 번째 값의 until 메서드에 두 값 사이를 초 단위로 세어 달라고 요청합니다. 실제 출력에서 이 줄 아래에 이어지는 at 줄들을 보면 Duration.between이 LocalDate.until을 호출하다 실패했다는 것을 확인할 수 있습니다. 그런데 LocalDate에는 하루 안의 시각이 없으니 초를 셀 방법이 없습니다.
날짜 사이의 차이가 필요하다면 다음 절의 Period를 쓰고, 총 일수만 필요하다면 ChronoUnit.DAYS.between(start, end)를 씁니다. 이 예에서 총 일수는 2입니다. 실제로 흐른 시간이 필요할 때는 날짜에 시각과 시간대(ZoneId)를 붙여 실제 순간을 정한 뒤 Duration을 계산합니다. 다만 두 날짜가 이틀 차이라고 해서 결과가 언제나 48시간인 것은 아닙니다. 그 이유는 뒤의 일광 절약 시간 예제에서 확인합니다.
Period: 달력으로 센 연·월·일
Period는 '1년 2개월 5일'처럼 연·월·일 세 숫자로 기간을 나타냅니다. Period.between에 시작 날짜와 끝 날짜를 넘기면 그 사이의 기간을 구해 줍니다.
import java.time.LocalDate;
import java.time.Period;
public final class BoardPeriod {
public static void main(String[] args) {
LocalDate start = LocalDate.of(2025, 1, 15);
LocalDate end = LocalDate.of(2026, 3, 20);
Period period = Period.between(start, end);
System.out.println("period=" + period);
System.out.println("years=" + period.getYears());
System.out.println("months=" + period.getMonths());
System.out.println("days=" + period.getDays());
System.out.println("restored=" + start.plus(period));
}
}period=P1Y2M5D
years=1
months=2
days=5
restored=2026-03-20첫 줄의 P1Y2M5D는 ISO 8601 국제 표준의 기간 표기입니다. 맨 앞의 P는 기간(period)을 뜻하고, 그 뒤로 연(Y), 월(M), 일(D)이 차례로 붙습니다. Period.between은 시작 날짜에서 출발해 꽉 찬 달이 몇 개인지 먼저 세고, 남은 날을 일로 셉니다.
| 단계 | 도달한 날짜 | 센 기간 |
|---|---|---|
| 출발 | 2025-01-15 | 없음 |
| 꽉 찬 달을 셈 | 2026-03-15 | 14개월, 즉 1년 2개월 |
| 남은 날을 셈 | 2026-03-20 | 1년 2개월 5일 |
getYears(), getMonths(), getDays()는 이 세 숫자를 하나씩 꺼냅니다. 여기서 흔히 하는 실수가 getMonths()의 결과를 전체 개월 수로 읽는 것입니다. 2는 1년을 떼어 내고 남은 '월' 칸의 값일 뿐이고, 전체 개월 수는 14입니다. 전체 개월 수가 필요하면 period.toTotalMonths()를 쓰거나 ChronoUnit.MONTHS.between(start, end)로 직접 세며, 이 예에서는 둘 다 14를 돌려줍니다.
두 방법 모두 남은 일수는 버린다는 점도 알아 두세요. toTotalMonths()는 연과 월만으로 계산하고, MONTHS.between은 한 달이 꽉 찬 경우만 셉니다. 예를 들어 2025년 1월 31일부터 2월 28일까지는 2월의 마지막 날까지 왔지만, 끝 날짜의 일(28)이 시작 날짜의 일(31)보다 작으므로 MONTHS.between은 0을 돌려줍니다. 1월 31일에 한 달을 더하면 2월 28일이 되는데도 그렇습니다. 남은 날을 버릴지 반올림할지는 계산을 요구하는 쪽의 규칙으로 정해야 합니다.
마지막 줄은 시작 날짜에 Period를 더하면 끝 날짜로 돌아온다는 것을 보여 줍니다. 여기서 Period가 기간을 일수 하나로 바꿔 저장하지 않는 이유를 생각해 볼 수 있습니다. 1월 15일에 한 달을 더하면 2월 15일이 되어 31일이 지나지만, 2월 15일에 한 달을 더하면 3월 15일이 되어 28일만 지납니다. 같은 '1개월'이 어느 날짜에 더하느냐에 따라 다른 일수가 되므로, Period는 연·월·일을 달력 단위 그대로 들고 있습니다.
Duration: 시계로 잰 경과 시간
Duration은 시간의 양을 초와 나노초로 나타냅니다. LocalTime, LocalDateTime, ZonedDateTime, Instant처럼 시각을 가진 타입이라면, 같은 타입의 두 값 사이에서 Duration.between을 쓸 수 있습니다. 다음 코드는 게시글 검토에 걸린 시간을 구합니다.
import java.time.Duration;
import java.time.LocalTime;
public final class BoardDuration {
public static void main(String[] args) {
LocalTime start = LocalTime.of(9, 10);
LocalTime end = LocalTime.of(10, 40, 30);
Duration duration = Duration.between(start, end);
System.out.println("duration=" + duration);
System.out.println("seconds=" + duration.toSeconds());
System.out.println("minutes=" + duration.toMinutes());
System.out.println("parts=" + duration.toHoursPart() + "h "
+ duration.toMinutesPart() + "m " + duration.toSecondsPart() + "s");
}
}duration=PT1H30M30S
seconds=5430
minutes=90
parts=1h 30m 30sPT1H30M30S도 ISO 8601 표기입니다. P 뒤의 T는 여기서부터 시간 부분이라는 표시이고, 그 뒤로 시(H), 분(M), 초(S)가 이어집니다. 월과 분이 모두 M이라서 T로 둘을 구분합니다.
toSeconds()와 toMinutes()는 전체 길이를 한 단위로 바꾼 값입니다. 5430초는 90.5분이지만 toMinutes()는 남는 30초를 버리고 90을 돌려줍니다. 반면 화면에 '1시간 30분 30초'처럼 칸을 나눠 보여 줄 때는 toHoursPart(), toMinutesPart(), toSecondsPart()를 씁니다. 이 메서드들은 시 칸, 분 칸, 초 칸의 값을 각각 꺼내므로, 양수 기간이라면 분과 초는 0부터 59 사이의 값입니다. 이름에 Part가 붙었으면 칸의 값, 붙지 않았으면 전체를 그 단위로 바꾼 값이라고 기억하면 됩니다.
시 칸도 마찬가지라서, 24시간이 넘는 부분은 일 칸으로 넘어갑니다. 25시간 5분짜리 Duration이라면 toHoursPart()는 1이고, 넘어간 하루는 toDaysPart()가 1로 돌려줍니다. 하루가 넘는 시간을 '25시간 5분'처럼 보여 주려면 toHours()로 전체 시간을 구하고 분만 toMinutesPart()로 꺼내야 합니다. 참고로 toSeconds()와 Part로 끝나는 메서드들은 Java 9에서 추가되었으므로, Java 8 코드에서는 전체 초를 getSeconds()로 구합니다.
서로 다른 원본 입력의 Period 구성 요소와 Duration 총량·부분을 비교하고 LocalDate에 Duration을 적용한 실패를 분리한다.
Period는 달력 구성 요소, Duration은 시간량을 나타냅니다
두 프로그램은 입력도 계산 질문도 다릅니다. 같은 간격을 서로 다른 단위로만 표시한 예제가 아닙니다.
| 원본과 입력 | 연산·결과 | 결과를 읽는 방법 |
|---|---|---|
BoardPeriod2025-01-15 → 2026-03-20 | P1Y2M5D
| 연·월·일 구성 요소. months=2는 전체 개월 수가 아님 |
같은 BoardPeriod 입력 | start.plus(period) · 2026-03-20 | 이 입력에서 기간을 더하면 종료 날짜로 복원 |
BoardDuration09:10 → 10:40:30 | PT1H30M30S
| 총 초·총 분. 전체 분에서는 남은 30초를 버림 |
같은 BoardDuration 입력 | 1h 30m 30s | 시간·분·초 부분을 각각 추출한 표시 |
BoardPeriod· 2025-01-15 → 2026-03-20- 연산·결과:
P1Y2M5D· years=1, months=2, days=5결과를 읽는 방법: 연·월·일 구성 요소. months=2는 전체 개월 수가 아님 - 같은
BoardPeriod입력 - 연산·결과:
start.plus(period)· 2026-03-20결과를 읽는 방법: 이 입력에서 기간을 더하면 종료 날짜로 복원 BoardDuration· 09:10 → 10:40:30- 연산·결과:
PT1H30M30S· seconds=5430, minutes=90결과를 읽는 방법: 총 초·총 분. 전체 분에서는 남은 30초를 버림 - 같은
BoardDuration입력 - 연산·결과:
1h 30m 30s결과를 읽는 방법: 시간·분·초 부분을 각각 추출한 표시
Duration.between(LocalDate, LocalDate)는 초 단위를 지원하지 않아 실패합니다. 여기의 LocalTime 차이는 지역·날짜가 없는 시각 차이이며, DST를 포함한 실제 경과 시간으로 확대하지 않습니다.
그림은 앞의 두 예제를 나란히 놓은 것입니다. 두 예제는 같은 간격을 다른 단위로 보여 준 것이 아니라 입력도 질문도 다른 별개의 계산입니다. Period 쪽의 months=2는 전체 개월 수가 아니라 칸의 값이고, Duration 쪽의 90분은 남은 초를 버린 전체 분이라는 점을 함께 비교해 보세요.
자정을 넘는 시간 계산
LocalTime끼리 Duration을 구할 때는 함정이 하나 있습니다. 밤 11시 30분에 시작해 다음 날 0시 15분에 끝난 작업의 시간을 두 가지 방법으로 계산해 보겠습니다.
import java.time.Duration;
import java.time.LocalDateTime;
import java.time.LocalTime;
public final class OvernightDuration {
public static void main(String[] args) {
Duration timesOnly = Duration.between(LocalTime.of(23, 30), LocalTime.of(0, 15));
Duration withDates = Duration.between(
LocalDateTime.of(2025, 3, 10, 23, 30),
LocalDateTime.of(2025, 3, 11, 0, 15));
System.out.println("times-only=" + timesOnly + " minutes=" + timesOnly.toMinutes());
System.out.println("with-dates=" + withDates + " minutes=" + withDates.toMinutes());
}
}times-only=PT-23H-15M minutes=-1395
with-dates=PT45M minutes=45LocalTime에는 날짜가 없으므로 0시 15분이 다음 날이라는 사실을 알 수 없습니다. 그래서 같은 날 23시 30분에서 0시 15분으로 거꾸로 간 것으로 보고, 23시간 15분을 거슬러 간 음수 기간 PT-23H-15M을 돌려줍니다. 둘째 줄처럼 날짜를 붙인 LocalDateTime으로 계산하면 기대한 45분이 나옵니다. 자정을 넘길 수 있는 시간이라면 시각만 따로 저장하지 말고 날짜와 함께 저장해야 합니다.
LocalDateTime도 시간대가 없는 벽시계 값이라는 점은 그대로입니다. 두 값 사이에 일광 절약 시간 전환이 끼어 있다면 시곗바늘이 움직인 양과 실제로 흐른 시간이 달라집니다. 실제 경과 시간을 정확히 구하려면 ZonedDateTime이나 Instant로 두 순간을 정한 뒤 계산해야 하며, 다음 절이 바로 그런 경우입니다.
하루를 더할까, 24시간을 더할까
일광 절약 시간을 쓰는 지역에서는 '내일 같은 시각'과 '24시간 뒤'가 다를 수 있습니다. 프랑스 파리는 2025년 3월 30일 새벽 2시에 시계를 3시로 한 시간 앞당겼습니다. 그 전날 정오에 달력상 하루(Period.ofDays(1))와 정확히 24시간(Duration.ofDays(1))을 각각 더해 보겠습니다.
import java.time.Duration;
import java.time.Period;
import java.time.ZoneId;
import java.time.ZonedDateTime;
public final class DstPeriodVsDuration {
public static void main(String[] args) {
ZonedDateTime start = ZonedDateTime.of(
2025, 3, 29, 12, 0, 0, 0, ZoneId.of("Europe/Paris"));
ZonedDateTime nextDate = start.plus(Period.ofDays(1));
ZonedDateTime after24Hours = start.plus(Duration.ofDays(1));
System.out.println("calendar=" + nextDate);
System.out.println("calendar-hours="
+ Duration.between(start.toInstant(), nextDate.toInstant()).toHours());
System.out.println("duration=" + after24Hours);
System.out.println("duration-hours="
+ Duration.between(start.toInstant(), after24Hours.toInstant()).toHours());
}
}calendar=2025-03-30T12:00+02:00[Europe/Paris]
calendar-hours=23
duration=2025-03-30T13:00+02:00[Europe/Paris]
duration-hours=24출발 시각인 3월 29일 정오의 오프셋은 +01:00이고, 다음 날 정오에는 +02:00입니다. 첫 줄은 Period를 더한 결과로, 현지 달력에서 날짜만 하루 넘겨 3월 30일 정오가 됐습니다. 그런데 그사이 시계가 한 시간 앞당겨졌으므로 실제로 흐른 시간은 둘째 줄처럼 23시간뿐입니다. 셋째 줄은 Duration을 더한 결과입니다. Duration.ofDays(1)은 이름과 달리 달력의 하루가 아니라 정확히 86,400초, 즉 24시간이고, 실제로 출력해 보면 PT24H가 나옵니다. 그래서 24시간이 지난 순간을 현지 시각으로 나타낸 13시가 됩니다.
정리하면 ZonedDateTime에 Period를 더하는 것은 현지 달력과 시계를 기준으로 한 계산이고, Duration을 더하는 것은 실제로 흐르는 시간을 기준으로 한 계산입니다. plusDays(1)은 앞의 방식으로, plusHours(24)는 뒤의 방식으로 동작합니다. ZonedDateTime의 plusHours 설명도 지역 시간선(local time-line)과 순간 시간선(instant time-line)이라는 말로 이 차이를 구분합니다. 매일 같은 현지 시각에 열리는 회의라면 Period나 plusDays를, 발급하고 24시간 뒤에 만료되는 인증 토큰이라면 Duration이나 plusHours를 씁니다.
같은 계산을 2025년 1월 15일 정오에서 시작하면 두 결과 모두 1월 16일 정오이고, 흐른 시간도 24시간입니다. 그사이 오프셋이 바뀌지 않기 때문입니다. 그래서 둘을 섞어 써도 평소에는 버그가 드러나지 않다가, 일광 절약 시간이 시작되거나 끝나는 날에만 틀린 결과가 나옵니다. 한 입력에서 결과가 같다고 해서 두 연산이 같은 일을 하는 것은 아닙니다.
ChronoUnit: 더하거나 잴 때 쓰는 단위
지금까지는 plusDays, plusMonths처럼 단위가 이름에 들어간 메서드를 썼습니다. 단위 자체를 값으로 다루고 싶을 때는 ChronoUnit을 씁니다. ChronoUnit은 NANOS(나노초), SECONDS, MINUTES, HOURS, DAYS, WEEKS, MONTHS, YEARS 같은 표준 시간 단위를 모은 enum이고, 단위를 나타내는 TemporalUnit 인터페이스를 구현합니다.
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
public final class ChronoUnitBoardPlan {
public static void main(String[] args) {
LocalDate start = LocalDate.of(2025, 1, 1);
LocalDate end = start.plus(6, ChronoUnit.WEEKS);
long days = ChronoUnit.DAYS.between(start, end);
LocalDateTime first = LocalDateTime.of(2025, 1, 1, 9, 0);
LocalDateTime second = first.plus(95, ChronoUnit.MINUTES);
System.out.println("end=" + end);
System.out.println("days=" + days);
System.out.println("second=" + second);
System.out.println("date-supports-hours=" + start.isSupported(ChronoUnit.HOURS));
}
}end=2025-02-12
days=42
second=2025-01-01T10:35
date-supports-hours=falseChronoUnit은 두 가지 방식으로 씁니다. start.plus(6, ChronoUnit.WEEKS)처럼 양과 단위를 함께 plus에 넘기면 그만큼 이동한 새 값을 얻습니다. ChronoUnit.DAYS.between(start, end)처럼 단위의 between을 호출하면 두 값 사이에 그 단위가 몇 번 꽉 차게 들어가는지 셉니다. 그래서 첫 두 줄은 6주 뒤가 2월 12일이고 그사이가 42일이라는 뜻입니다. between은 다 채우지 못한 나머지를 버리므로, 2025년 1월 1일부터 2월 11일까지는 41일이지만 WEEKS로 세면 5가 나옵니다.
마지막 줄은 모든 타입이 모든 단위를 지원하지는 않는다는 것을 보여 줍니다. LocalDate에는 시각이 없으므로 HOURS를 지원하지 않고, isSupported는 false를 돌려줍니다. 이런 단위로 plus를 호출하면 첫 예제와 같은 UnsupportedTemporalTypeException이 발생합니다. 반대로 날짜가 없는 LocalTime은 DAYS와 그보다 큰 단위를 지원하지 않습니다.
단위가 정해져 있다면 start.plusWeeks(6)처럼 이름에 단위가 들어간 메서드가 더 읽기 쉽습니다. ChronoUnit이 쓸모 있는 것은 단위를 설정 값이나 메서드 인수로 받아야 할 때입니다. 예를 들어 알림 주기의 양과 단위를 인수로 받는 메서드 하나로 '3일마다'와 '2주마다'를 모두 처리할 수 있습니다.
더 깊이: 길이가 정해지지 않은 단위
Duration은 내부에 초를 담는 long 하나와 1초 미만의 나노초를 담는 int 하나만 저장합니다. 그래서 Duration에는 '일'이나 '월'이라는 칸이 없고, ChronoUnit으로 Duration을 만들 때도 길이가 정확히 정해진 단위만 받습니다. Duration.of(1, ChronoUnit.MONTHS)를 호출하면 UnsupportedTemporalTypeException이 발생하는데, 메시지 Unit must not have an estimated duration은 길이가 추정값인 단위는 쓸 수 없다는 뜻입니다.
ChronoUnit은 단위마다 isDurationEstimated()로 길이가 추정값인지 알려 줍니다. MONTHS.getDuration()은 1년을 365.2425일로 보고 12로 나눈 평균인 PT730H29M6S를 돌려주지만, 실제 한 달의 길이는 날짜에 따라 다르므로 추정값으로 분류됩니다. 일광 절약 시간 때문에 길이가 달라질 수 있는 DAYS도 추정 단위로 분류되지만, Duration.of는 예외적으로 DAYS만은 정확히 24시간으로 취급합니다. Duration.ofDays(1)이 PT24H가 되는 것도 이 규칙 때문입니다.
ChronoField: 날짜·시각에서 값 하나 꺼내기
단위가 '얼마만큼'을 나타낸다면, 필드는 '한 시점의 어느 칸'을 나타냅니다. 2025년 3월 10일 14시 25분 30초라는 시점에는 연도 칸에 2025, 월 칸에 3, 분 칸에 25가 들어 있습니다. ChronoField는 이런 칸의 이름을 모은 enum으로, YEAR, MONTH_OF_YEAR, DAY_OF_MONTH, HOUR_OF_DAY, MINUTE_OF_HOUR 같은 상수를 가지고 TemporalField 인터페이스를 구현합니다.
import java.time.LocalDateTime;
import java.time.temporal.ChronoField;
public final class ChronoFieldInspection {
public static void main(String[] args) {
LocalDateTime value = LocalDateTime.of(2025, 3, 10, 14, 25, 30);
System.out.println("year=" + value.get(ChronoField.YEAR));
System.out.println("month=" + value.getMonthValue());
System.out.println("minute=" + value.get(ChronoField.MINUTE_OF_HOUR));
System.out.println("nano-supported=" + value.isSupported(ChronoField.NANO_OF_SECOND));
}
}year=2025
month=3
minute=25
nano-supported=trueget(ChronoField.YEAR)는 연도 칸의 값 2025를, get(ChronoField.MINUTE_OF_HOUR)는 분 칸의 값 25를 꺼냅니다. 둘째 줄은 같은 종류의 일을 getMonthValue()라는 전용 메서드로 한 것입니다. 마지막 줄의 isSupported는 그 타입에 해당 필드가 있는지 묻습니다. LocalDateTime은 나노초까지 담으므로 NANO_OF_SECOND를 지원합니다.
필드도 단위처럼 타입마다 지원 범위가 다릅니다. LocalDate에서 get(ChronoField.HOUR_OF_DAY)를 호출하면 UnsupportedTemporalTypeException: Unsupported field: HourOfDay가 발생합니다. 그래서 어떤 필드를 읽을지 데이터로 받아 처리하는 코드라면 isSupported로 먼저 확인해야 합니다. 또 get은 int를 돌려주므로, 1970년 1월 1일부터 센 일수인 EPOCH_DAY처럼 값이 int 범위를 넘을 수 있는 필드는 getLong으로 읽어야 합니다.
평소 업무 코드에서는 getYear(), getMonthValue(), getMinute()처럼 의미가 이름에 드러나는 메서드가 더 좋습니다. ChronoField는 어떤 칸을 읽을지 실행 중에 정해야 하는 곳에서 씁니다. 사용자가 고른 칸만 보여 주는 표 출력 도구나, 다음 글에서 볼 날짜 포매터가 그런 예입니다.
ChronoUnitBoardPlan의 주·분 연산과 ChronoFieldInspection의 분 필드 읽기를 각각 원본 결과에 연결한다.
단위는 양을 더하거나 재고, 필드는 한 시점의 값을 읽습니다
같은 ‘분’이라도 더할 양과 한 시각의 분 필드는 다른 질문입니다.
| 질문·원본 | 사용한 단위 또는 필드 | 실제 결과 |
|---|---|---|
| 2025-01-01에서 6주 뒤?
| plus(6, WEEKS)
| 2025-02-12총 |
| 2025-01-01 09:00에서 95분 뒤? 같은 프로그램 | plus(95, MINUTES) | 2025-01-01T10:35 |
| 2025-03-10 14:25:30의 분?
| get(MINUTE_OF_HOUR) | 분 필드 25 |
- 2025-01-01에서 6주 뒤? ·
ChronoUnitBoardPlan - 사용한 단위 또는 필드:
plus(6, WEEKS)·DAYS.between(start, end)실제 결과:2025-02-12· 총42일 - 2025-01-01 09:00에서 95분 뒤? · 같은 프로그램
- 사용한 단위 또는 필드:
plus(95, MINUTES)실제 결과:2025-01-01T10:35 - 2025-03-10 14:25:30의 분? ·
ChronoFieldInspection - 사용한 단위 또는 필드:
get(MINUTE_OF_HOUR)실제 결과: 분 필드25
필드 25는 시작점부터의 경과 분이 아닙니다. 동적 단위·필드는 대상 타입의 지원 여부를 확인하며, 고정된 질문에는 plusWeeks나 getMinute 같은 편의 메서드를 사용할 수 있습니다.
그림은 같은 '분'이라도 두 질문이 다르다는 것을 보여 줍니다. 한쪽은 2025년 1월 1일 9시에 MINUTES 단위로 95분을 더해 10시 35분을 얻는 계산이고, 다른 쪽은 별개의 시점인 3월 10일 14시 25분 30초에서 MINUTE_OF_HOUR 필드로 25를 읽는 계산입니다. 이 25는 어느 시점부터 흐른 분이 아니라 그 시각의 분 칸에 적힌 값입니다.
질문에 맞는 도구 고르기
| 질문 | 도구 | 예 |
|---|---|---|
| 생일까지 남은 연·월·일 | Period | Period.between(today, birthday) |
| 작업 시작부터 끝까지 걸린 시간 | Duration | Duration.between(startedAt, endedAt) |
| 두 날짜 사이의 총 일수 | ChronoUnit.DAYS | ChronoUnit.DAYS.between(start, end) |
| 설정에 따라 주나 월 단위로 더하기 | TemporalUnit(ChronoUnit) | date.plus(amount, unit) |
| 실행 중에 고른 칸의 값 읽기 | TemporalField(ChronoField) | value.get(field) |
| 정해진 칸의 값 읽기 | 전용 getter | value.getYear() |
기간을 모두 초 단위 long 하나로 바꿔 저장하면 처음에는 단순해 보입니다. 하지만 그 순간 '2개월'이 어느 달부터의 2개월이었는지, 그사이에 일광 절약 시간 전환이 있었는지 같은 정보가 사라집니다. 질문에 맞는 타입을 고르면 그 정보가 값과 함께 남고, 달력 계산도 그 타입이 맞게 처리해 줍니다.
정리
Period는 연·월·일로 센 달력 기간이고,Duration은 초와 나노초로 센 경과 시간입니다. 한 달이나 하루의 길이가 날짜에 따라 다르기 때문에 둘은 서로 바꿀 수 없습니다.LocalDate끼리는Duration을 구할 수 없습니다. 날짜 차이는Period나ChronoUnit.DAYS로 구합니다.getMonths(),toMinutesPart()처럼 칸의 값을 꺼내는 메서드와toTotalMonths(),toMinutes()처럼 전체를 한 단위로 바꾸는 메서드를 구분합니다. 후자는 다 채우지 못한 나머지를 버립니다.- 시각만 있는
LocalTime으로는 자정을 넘는 시간을 계산할 수 없습니다. 날짜까지 함께 다뤄야 합니다. ZonedDateTime에Period를 더하면 현지 달력 기준으로,Duration을 더하면 실제 시간 기준으로 계산합니다. 일광 절약 시간 전환일에는 결과가 달라집니다.ChronoUnit은 더하거나 잴 단위,ChronoField는 한 시점에서 읽을 칸입니다. 타입마다 지원하는 단위와 필드가 다르므로 동적으로 쓸 때는isSupported로 확인합니다.
연습 문제
- 게시글 검토가 9시에 시작해 11시 15분에 끝났습니다. 중간에 5분 휴식이 두 번 있었다면 실제 검토 시간이 몇 분인지
Duration으로 계산하세요. DstPeriodVsDuration의 시작 시각을 2025년 10월 25일 정오로 바꾸면 네 줄이 각각 어떻게 출력될지 예상해 보세요. 파리는 2025년 10월 26일 새벽 3시에 시계를 2시로 한 시간 되돌렸습니다.- 2025년 1월 31일부터 2025년 3월 1일까지의
Period와 총 일수를 예상한 뒤 실행해 확인하세요. 결과에 들어 있는 '1개월'은 며칠에 해당할까요?
해설 보기
- 휴식 시간도
Duration으로 만들어 빼면, 분으로 바꾸기 전까지 단위를 신경 쓰지 않고 계산할 수 있습니다.
import java.time.Duration;
import java.time.LocalTime;
public final class PostReviewDurationExercise {
public static void main(String[] args) {
LocalTime start = LocalTime.of(9, 0);
LocalTime end = LocalTime.of(11, 15);
Duration elapsed = Duration.between(start, end);
Duration breaks = Duration.ofMinutes(5).multipliedBy(2);
Duration review = elapsed.minus(breaks);
System.out.println("elapsed=" + elapsed + " minutes=" + elapsed.toMinutes());
System.out.println("review=" + review + " minutes=" + review.toMinutes());
}
}elapsed=PT2H15M minutes=135
review=PT2H5M minutes=125전체 시간은 2시간 15분, 즉 135분입니다. multipliedBy(2)로 만든 10분짜리 휴식을 minus로 빼면 2시간 5분, 즉 125분이 남습니다. Duration도 불변이라서 minus는 elapsed를 바꾸지 않고 새 값을 돌려줍니다. 다만 휴식이 몇 번 있었는지는 시작·종료 시각만으로는 알 수 없습니다. 실제 프로그램이라면 휴식 기록이나 휴식 정책을 따로 입력받아야 합니다.
- 출력은 다음과 같습니다.
calendar=2025-10-26T12:00+01:00[Europe/Paris]
calendar-hours=25
duration=2025-10-26T11:00+01:00[Europe/Paris]
duration-hours=24가을에는 시계를 한 시간 되돌리므로 봄과 반대 결과가 나옵니다. 출발 시각의 오프셋은 +02:00이고 다음 날 정오는 +01:00이므로, 달력상 하루를 더한 다음 날 정오까지는 25시간이 흐릅니다. 정확히 24시간을 더하면 그보다 한 시간 이른 현지 시각 11시가 됩니다.
- 다음 코드로 확인할 수 있습니다.
import java.time.LocalDate;
import java.time.Period;
import java.time.temporal.ChronoUnit;
public final class MonthEndPeriodExercise {
public static void main(String[] args) {
LocalDate start = LocalDate.of(2025, 1, 31);
LocalDate end = LocalDate.of(2025, 3, 1);
Period period = Period.between(start, end);
System.out.println("period=" + period);
System.out.println("months=" + period.getMonths() + " days=" + period.getDays());
System.out.println("total-days=" + ChronoUnit.DAYS.between(start, end));
}
}period=P1M1D
months=1 days=1
total-days=29Period.between은 1월 31일에서 꽉 찬 달을 셉니다. 두 달 뒤인 3월 31일은 끝 날짜를 넘으므로 꽉 찬 달은 하나입니다. 1월 31일에서 한 달 뒤는 2월에 31일이 없어 2월 28일이 되고, 거기서 3월 1일까지 하루가 남으므로 결과는 P1M1D입니다. 이 '1개월'은 1월 31일부터 2월 28일까지의 28일이고, 총 일수는 29일입니다. '1개월 1일'을 보고 32일쯤이라고 짐작하면 틀리는 이유가 여기에 있습니다. 일수가 필요하면 Period의 숫자를 바꿔 계산하지 말고 ChronoUnit.DAYS.between으로 따로 세야 합니다.
다음 글
날짜 조회·조정과 DateTimeFormatter에서는 날짜 하나에서 요일 같은 값을 읽고, plus와 with로 새 날짜를 만들고, 날짜와 문자열을 서로 바꾸는 방법을 배웁니다. 1월 31일에 한 달을 더하면 왜 2월 28일이 되는지도 거기서 자세히 살펴봅니다.