Java 8부터 도입된 java.time 패키지는 날짜·시간을 다루는 표준 API다. 테스트에서 "3일 전", "5초 이내" 같은 시간 계산이 필요할 때 자주 쓰이는데, 매번 메서드 이름이나 단위 지정 방식이 헷갈린다. 이 문서는 실제로 자주 쓰는 패턴만 정리한다.
Instant — 특정 시점
Instant는 UTC 기준의 특정 시점을 나타낸다. 타임스탬프라고 생각하면 된다. DB의 TIMESTAMP 컬럼과 가장 잘 매핑되는 타입이다.
Instant now = Instant.now(); // 현재 시점
Instant epoch = Instant.EPOCH; // 1970-01-01T00:00:00Z
JPA 엔티티에서 @CreatedDate, @LastModifiedDate에 Instant 타입을 쓰는 이유가 여기에 있다. 시간대 변환 없이 절대 시점을 저장할 수 있기 때문이다.
ChronoUnit — 시간 단위
ChronoUnit은 시간의 단위를 정의하는 enum이다. Instant의 시간 연산에서 "얼마만큼"을 지정할 때 쓴다.
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()로 특정 시점에서 앞뒤로 이동한다.
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초 이내에 실행됐는지" 같은 검증에 쓴다.
(검증 시작점) 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));
@CreatedDate, @LastModifiedDate는 Spring 컨텍스트의 AuditingEntityListener가 처리한다. @ExtendWith(MockitoExtension.class)만 쓰는 단위 테스트에서는 Spring이 없으므로 시간 필드가 자동 갱신되지 않는다. 시간 관련 검증은 @DataJpaTest 같은 통합 테스트에서 하는 게 맞다.
자주 하는 실수
ChronoUnit은 java.time.temporal.ChronoUnit이다. java.time.ChronoUnit이 아니다. IDE 자동완성에 의존하면 되지만, 수동으로 import할 때 temporal 패키지를 빠뜨리기 쉽다.
[!DANGER] Instant에 MONTHS나 YEARS 단위를 쓴다
Instant.now().minus(1, ChronoUnit.MONTHS)는 UnsupportedTemporalTypeException이 터진다. Instant는 절대 시점이라 "1개월"이 며칠인지 알 수 없기 때문이다. 월/년 단위 연산은 LocalDateTime이나 ZonedDateTime을 쓰자.