안동민 개발노트

본문 시작

Period·Duration과 시간 단위

날짜 사이를 Duration으로 계산하는 타입 오류를 재현하고 달력 기간·경과 시간·ChronoUnit·ChronoField의 질문 차이를 구분합니다.

“두 시점 사이”라는 말에는 달력상의 연·월·일 차이와 초 단위 경과 시간이 함께 섞여 있습니다.

Period는 LocalDate 같은 날짜 차이를 연·월·일로, Duration은 시간 기반 객체 사이를 초·나노초로 표현합니다.

TemporalUnit은 얼마나 더하거나 재는지, TemporalField는 한 시점에서 어떤 값을 꺼내는지 나타냅니다.


Duration과 날짜 타입 불일치

LocalDate에는 하루 안의 시간 정보가 없습니다.

Duration은 초 단위 차이를 요구하므로 두 날짜를 직접 넘기면 지원되지 않는 단위 예외가 발생합니다.

lab/DurationBetweenDatesFailure.java
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

날짜 차이는 Period 또는 ChronoUnit.DAYS를 사용합니다.

실제 경과 시간이 필요한 경우 각 날짜에 시간과 ZoneId를 결합해 순간을 만든 뒤 Duration을 계산합니다. 두 날짜가 이틀 차이라고 해서 계산 결과가 반드시 48시간인 것은 아닙니다.

DST가 있는 지역에서는 “다음 날 같은 현지 시각”이 항상 24시간 뒤가 아닙니다.


날짜 간격을 나타내는 Period

src/BoardPeriod.java
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

getMonths는 전체 개월 수가 아니라 연도를 제외한 월 구성 요소입니다.

전체 개월 수가 필요하면 ChronoUnit.MONTHS.between을 사용하되 남은 일수를 어떻게 처리하는지 확인합니다.

Period는 달력 문맥을 유지합니다.


경과 시간을 나타내는 Duration

src/BoardDuration.java
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 30s

toMinutes는 초 나머지를 버린 전체 분입니다.

화면에서 시간·분·초 구성 요소가 필요하면 toHoursPart 같은 부분 추출 메서드를 사용합니다.

달력 구성 요소와 시간 총량을 구분한다

서로 다른 원본 입력의 Period 구성 요소와 Duration 총량·부분을 비교하고 LocalDate에 Duration을 적용한 실패를 분리한다.

두 프로그램은 입력도 계산 질문도 다릅니다. 같은 간격을 서로 다른 단위로만 표시한 예제가 아닙니다.

달력 구성 요소와 시간 총량을 구분한다
원본과 입력연산·결과결과를 읽는 방법
BoardPeriod · 2025-01-15 → 2026-03-20P1Y2M5D · years=1, months=2, days=5연·월·일 구성 요소. months=2는 전체 개월 수가 아님
같은 BoardPeriod 입력start.plus(period) · 2026-03-20이 입력에서 기간을 더하면 종료 날짜로 복원
BoardDuration · 09:10 → 10:40:30PT1H30M30S · seconds=5430, minutes=90총 초·총 분. 전체 분에서는 남은 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를 포함한 실제 경과 시간으로 확대하지 않습니다.

자정을 넘는 LocalTime끼리만 계산하면 날짜 정보가 없어 음수나 잘못된 차이가 될 수 있으므로 LocalDateTime을 사용합니다.


ChronoUnit 계산 단위

ChronoUnit은 NANOS부터 DAYS, WEEKS, MONTHS, YEARS까지 표준 단위를 enum으로 제공합니다.

모든 시간 타입이 모든 단위를 지원하지는 않습니다.

src/ChronoUnitBoardPlan.java
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);
    }
}
end=2025-02-12
days=42
second=2025-01-01T10:35

date.plusWeeks(6) 같은 편의 메서드가 더 읽기 쉬우면 우선 사용합니다.

단위를 설정이나 인수로 전달해야 할 때 TemporalUnit 추상화가 유용합니다.


ChronoField 시점 필드

YEAR, MONTH_OF_YEAR, DAY_OF_MONTH, HOUR_OF_DAY 같은 필드는 TemporalField를 구현합니다.

LocalDate는 시간 필드를 지원하지 않으므로 isSupported로 확인하거나 타입에 맞는 편의 getter를 사용합니다.

src/ChronoFieldInspection.java
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=true

일반 업무 코드는 getYear, getMonthValue처럼 의미가 드러나는 편의 메서드가 좋습니다.

필드를 동적으로 선택하는 범용 도구와 포매터에서 ChronoField를 사용합니다.

단위는 더하거나 재고, 필드는 값을 읽는다

ChronoUnitBoardPlan의 주·분 연산과 ChronoFieldInspection의 분 필드 읽기를 각각 원본 결과에 연결한다.

같은 ‘분’이라도 더할 양과 한 시각의 분 필드는 다른 질문입니다.

단위는 더하거나 재고, 필드는 값을 읽는다
질문·원본사용한 단위 또는 필드실제 결과
2025-01-01에서 6주 뒤? · ChronoUnitBoardPlanplus(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의 분? · ChronoFieldInspectionget(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 같은 편의 메서드를 사용할 수 있습니다.


간격·단위·필드 선택

  • 생일까지 남은 연·월·일: Period
  • 게시글 시작부터 종료까지 초: Duration
  • 날짜 사이 총 일수: ChronoUnit.DAYS
  • 설정에 따라 주·월 단위 더하기: TemporalUnit
  • 한 날짜시간의 연도 필드 동적 조회: ChronoField
  • 고정된 연도 조회: getYear 편의 메서드

달력 기간과 경과 시간을 하나의 long으로 통일하면 월 길이와 DST 의미를 잃습니다.

타입이 질문을 보존하게 합니다.


연습 문제

09:00부터 11:15까지 게시글 검토에 걸린 전체 Duration을 구하고, 5분 휴식이 두 번 있었다면 실제 검토 시간을 분으로 계산하세요.

해설 보기
src/PostReviewDurationExercise.java
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);
        long reviewMinutes = elapsed.toMinutes() - 2 * 5;

        System.out.println("elapsed=" + elapsed.toMinutes());
        System.out.println("review=" + reviewMinutes);
    }
}
elapsed=135
review=125

휴식 횟수는 시작·종료 시각만으로 알 수 없으므로 실제 휴식 기록이나 명시 정책이 필요합니다.

계산에 필요한 사실을 타입과 입력으로 분리합니다.