Java 8부터 도입된 java.time 패키지는 날짜·시간을 다루는 표준 API다. 테스트에서 "3일 전", "5초 이내" 같은 시간 계산이 필요할 때 자주 쓰이는데, 매번 메서드 이름이나 단위 지정 방식이 헷갈린다. 이 문서는 실제로 자주 쓰는 패턴만 정리한다.

Instant — 특정 시점

Instant는 UTC 기준의 특정 시점을 나타낸다. 타임스탬프라고 생각하면 된다. DB의 TIMESTAMP 컬럼과 가장 잘 매핑되는 타입이다.

graph LR A[Global Timeline] --- B(UTC 0) B --- C[Instant: 2024-03-17T10:00:00Z] C --> D{Timezone Offset} D -- +9h --> E[KST: 19:00:00] D -- -5h --> F[EST: 05:00:00] style C fill:#f96,stroke:#333,stroke-width:2px style B fill:#fff,stroke-dasharray: 5 5
Instant now = Instant.now();           // 현재 시점
Instant epoch = Instant.EPOCH;         // 1970-01-01T00:00:00Z

JPA 엔티티에서 @CreatedDate, @LastModifiedDateInstant 타입을 쓰는 이유가 여기에 있다. 시간대 변환 없이 절대 시점을 저장할 수 있기 때문이다.

ChronoUnit — 시간 단위

ChronoUnit은 시간의 단위를 정의하는 enum이다. Instant의 시간 연산에서 "얼마만큼"을 지정할 때 쓴다.

mindmap root((ChronoUnit)) Time Units SECONDS[초
SECONDS] MINUTES[분
MINUTES] HOURS[시
HOURS] Date Units DAYS[일
DAYS] WEEKS[주
WEEKS] Unsupported in Instant MONTHS[월
MONTHS] YEARS[년
YEARS]

자주 쓰는 단위만 보면 이렇다.

  • ChronoUnit.SECONDS — 초
  • ChronoUnit.MINUTES — 분
  • ChronoUnit.HOURS — 시간
  • ChronoUnit.DAYS — 일

import는 java.time.temporal.ChronoUnit이다. java.time까지만 치면 안 나와서 헷갈리기 쉽다.

시간 연산

과거/미래 시점 만들기

plus()minus()로 특정 시점에서 앞뒤로 이동한다.

gitGraph commit id: "3일 전 (minus)" commit id: "2시간 전" commit id: "현재 (now)" tag: "기준점" commit id: "5분 후 (plus)"
Instant now = Instant.now();

Instant threeDaysAgo = now.minus(3, ChronoUnit.DAYS);      // 3일 전
Instant fiveMinutesLater = now.plus(5, ChronoUnit.MINUTES); // 5분 후
Instant twoHoursAgo = now.minus(2, ChronoUnit.HOURS);       // 2시간 전

Duration을 쓸 수도 있지만 ChronoUnit이 더 직관적이다.

// Duration 방식 — 같은 결과
Instant threeDaysAgo = now.minus(Duration.ofDays(3));

시간 비교

isBefore(), isAfter()로 두 시점의 선후 관계를 확인한다.

Instant created = Instant.now().minus(3, ChronoUnit.DAYS);
Instant updated = Instant.now();

updated.isAfter(created)    // true — updated가 더 최근
created.isBefore(updated)   // true — created가 더 이전

두 시점 사이의 차이 계산

ChronoUnit.between()으로 두 시점 사이의 차이를 특정 단위로 구한다.

long daysBetween = ChronoUnit.DAYS.between(created, updated);    // 3
long hoursBetween = ChronoUnit.HOURS.between(created, updated);  // 72

테스트에서 자주 쓰는 패턴

과거 시점 고정

엔티티의 createdAt을 과거로 고정해서, 수정 후 updatedAt이 변경됐는지 비교할 때 쓴다. 테스트에서 Instancio와 함께 사용하는 패턴이다.

Feed feed = Instancio.of(Feed.class)
        .set(all(Instant.class), Instant.now().minus(3, ChronoUnit.DAYS))
        .create();

all(Instant.class)로 모든 Instant 필드(createdAt, updatedAt)를 한꺼번에 3일 전으로 세팅한다. Instancio 셀렉터의 타입 셀렉터를 활용한 것이다.

최근 시점 검증

"이 작업이 5초 이내에 실행됐는지" 같은 검증에 쓴다.

sequenceDiagram participant T as Timeline participant C as Comparison Note over T: [now - 5s]
(검증 시작점) Note right of T: [Actual Event]
(실제 발생 시점) Note over T: [now]
(현재 시각) C->>T: isAfter(now - 5s)? alt Success T-->>C: true (최근 5초 이내) else Failure T-->>C: false (5초보다 더 이전) end
assertThat(result.updatedAt()).isAfter(Instant.now().minus(5, ChronoUnit.SECONDS));
단위 테스트에서 JPA Auditing은 동작하지 않는다

@CreatedDate, @LastModifiedDate는 Spring 컨텍스트의 AuditingEntityListener가 처리한다. @ExtendWith(MockitoExtension.class)만 쓰는 단위 테스트에서는 Spring이 없으므로 시간 필드가 자동 갱신되지 않는다. 시간 관련 검증은 @DataJpaTest 같은 통합 테스트에서 하는 게 맞다.

자주 하는 실수

ChronoUnit의 import 경로를 잊는다

ChronoUnitjava.time.temporal.ChronoUnit이다. java.time.ChronoUnit이 아니다. IDE 자동완성에 의존하면 되지만, 수동으로 import할 때 temporal 패키지를 빠뜨리기 쉽다.

[!DANGER] Instant에 MONTHS나 YEARS 단위를 쓴다

Instant.now().minus(1, ChronoUnit.MONTHS)UnsupportedTemporalTypeException이 터진다. Instant는 절대 시점이라 "1개월"이 며칠인지 알 수 없기 때문이다. 월/년 단위 연산은 LocalDateTime이나 ZonedDateTime을 쓰자.