본문으로 건너뛰기

안동민 개발노트

본문 시작

Period·Duration과 시간 단위

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

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

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

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

Period와 Duration은 서로 다른 시간 축을 계산한다

Period는 달력의 연·월·일을 더하고 Duration은 타임라인의 초·나노초를 더합니다.

  1. Period

    LocalDate 달력 단위 · 1개월·1일의 현지 의미

  2. Duration

    Instant 타임라인 단위 · 정확한 초·나노초


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를 사용합니다.

정말 정확한 48시간 경과가 필요한 경우 각 날짜에 시간과 ZoneId를 결합해 순간을 만든 뒤 Duration을 계산해야 합니다.

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는 달력 문맥을 유지합니다.

Period 구성 요소와 총 단위 수는 다른 질문에 답한다

between 결과 P1Y2M5D의 getMonths는 월 구성 요소 2만 돌려주며 전체 개월 수가 아닙니다.

  1. 시작·끝

    두 LocalDate

  2. between

    P1Y2M5D

  3. 구성 요소

    getMonths=2

  4. 총 단위

    MONTHS.between

  5. 재구성

    start.plus(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
viewCount=90
parts=1h 30m 30s

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

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

자정을 넘는 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 추상화가 유용합니다.

TemporalUnit은 거리, TemporalField는 좌표를 묻는다

단위는 두 시간 사이를 재거나 값을 더하고 필드는 한 시간 객체 안의 특정 칸을 읽습니다.

  1. TemporalUnit

    DAYS·MINUTES · plus와 between의 이동·거리

  2. TemporalField

    YEAR·HOUR_OF_DAY · get의 현재 좌표


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를 사용합니다.

기간 요구를 달력·타임라인·동적 연산으로 나눈다

같은 '기간'이라는 말도 결과 단위와 시간 축에 따라 API가 달라집니다.

요구도구결과
연·월·일 차이Period달력 구성
정확한 경과Duration초·나노초
총 일수ChronoUnit.DAYSlong
동적 단위 더하기TemporalUnit새 시간 값
동적 필드 읽기ChronoField필드 숫자

간격·단위·필드 선택

  • 생일까지 남은 연·월·일: 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
board=125

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

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