java.time을 다시 만나다.

이성민·2025년 11월 9일

woowacourse

목록 보기
7/11
post-thumbnail

java.time을 쓰게 된 계기

우테코 오픈미션을 하던 중 예약 기능을 만들면서 "보류 기한은 3일"이라는 정책이 생겼다. 처음엔 단순히 int day = 3; 이런 식으로 처리할까 했지만 이건 그냥 숫자일 뿐 '시간'이라는 의미를 표현하지 못한다는 점이 마음에 걸렸다.

그래서 시간을 표현하는 방법을 고민하던 중java.time 패키지를 떠올렸다.

이번 여름방학 때 『자바의 정석』을 읽으며 LocalDateTime이나 Duration같은 클래스 이름은 한 번쯤 본 기억이 있었다. 하지만 그땐 단순히 "아 이런 게 있구나"정도로만 읽었지 내가 이걸 직접 프로젝트에서 쓰게 될 줄은 정말 몰랐다.

이번엔 단순히 읽고 지나가는 게 아니라 책에서 본 개념을 직접 코드로 써보며 배우는 진짜 도전이었다.

이 글에서는 내가 공부한 java.time의 일부 핵심 개념들과 그것을 내 코드에 적용하면서 느꼈던 생각과 배움을 함께 풀어보려 한다.


java.time이란?

java.time 패키지는 자바 8부터 추가된 현대적 시간 API다.
날짜, 시간, 시점, 시간대 같은 개념을 분리해 더 명확하고 테스트 가능한 방식으로 다룰 수 있게 해준다.

여러가지 클래스가 있지만 내가 프로젝트에 직접적으로 사용한 클래스들을 중점으로 내용을 정리했다.

LocalDateTime - 지역 기준의 날짜와 시간

"2025-11-09 22:30" 같은 형태로 사람이 인식할 수 있는 시간을 표현한다.

  • 생성
    - now() / now(Clock)
    - of(year, month, day, hour, minute[, second[, nano]])
    - ofInstant(Instant, ZoneId)

  • 연산
    - plusDays/Hours/Minutes/Seconds(...)
    - plus(Duration) / minus(Duration)
    - withYear/Month/DayOfMonth/Hour/Minute/Second(...)

  • 비교
    - isBefore/ isAfter(ChronoLocalDateTime)
    - isEqual(ChronoLocalDateTime)

  • 변환/포맷
    - atZone(ZoneId)
    - toLocalDate() / toLocalTime()
    - format(DateTimeFormatter)

  • 주의
    타임존이 없다. 타임존 필요하면 atZone(ZoneId) 또는 ZonedDateTime 사용.

다음 코드는 사용 예시이다.

LocalDateTime now = LocalDateTime.now(); // 현재 시각
LocalDateTime threeDaysLater = now.plusDays(3); // 3일 뒤
LocalDateTime custom = LocalDateTime.of(2025, 11, 9, 22, 30); // 직접 생성

Duration - 시간 간격

"얼마 동안 지속되는가?"를 표현하는 클래스

  • 생성
    - ofDays/Hours/Minutes/Seconds/Millis/Nanos(...)
    - between(Temporal startInclusive, Temporal endExclusive)

  • 연산
    - plus(...) / minus(...)
    - multipliedBy(long) / dividedBy(long)
    - isZero() / isNegative() / isPositive()

  • 사용패턴
    - LocalDateTime.plus(Duration) / minus(Duration)
    - Instant.plus(Duration) / minus(Duration)

  • 주의
    달/년은 가변 길이라 Duration에 없다. (그럴 땐 Period)

다음 코드는 사용 예시이다.

Duration threeDays = Duration.ofDays(3);    // 3일
Duration twoHours = Duration.ofHours(2);    // 2시간
LocalDateTime holdUntil = LocalDateTime.now().plus(threeDays); // 지금 + 3일

Clock - 현재 시각 공급자(테스트 주입용)

"지금 몇 시인지"를 직접 코드에 의존하지 않게 해준다.

  • 생성
    - systemDefaultZone() / system(ZoneId)
    - fixed(Instant, ZoneId) ← 테스트용 고정 시계
    - offset(Clock, Duration) ← 기준 시계에서 오프셋

  • 사용
    - LocalDateTime.now(clock)
    - Instant.now(clock)

  • 주의
    운영은 systemDefaultZone() 같은 실시간 시계
    테스트는 fixed(...)로 시간을 멈춘다.

다음 코드는 사용 예시이다.

Clock systemClock = Clock.systemDefaultZone(); // 실제 시스템 시계
Clock fixedClock = Clock.fixed(
                LocalDateTime.of(2025, 11, 12, 10, 0)
                .toInstant(ZoneOffset.UTC), ZoneId.of("UTC")); // 테스트용 고정 시계
LocalDateTime now = LocalDateTime.now(systemClock);

Instant - UTC 기준 절대 시점

"세계 공통의 지금 이 순간"을 나타냄.

  • 생성
    - now() / now(Clock)
    - ofEpochSecond(long) / ofEpochMilli(long)

  • 연산
    - plus(Duration) / minus(Duration)
    - isBefore/ isAfter(Instant)

  • 변환
    - LocalDateTime.ofInstant(Instant, ZoneId)
    - ZonedDateTime.ofInstant(Instant, ZoneId)

  • 주의
    사람 친화적 포맷 아님 → 화면/도메인에는 보통 LocalDateTime로 변환

다음 코드는 사용 예시이다.

Instant now = Instant.now(clock);          // 절대 시각
Instant after = now.plus(Duration.ofDays(3)); // 3일 뒤 절대 시각

ZoneId - 시간대

"어느 지역 기준으로 시간을 계산할 것인가?"를 결정한다.

  • 얻기
    - of("Asia/Seoul"), of("UTC")
    - systemDefault() ← 서버의 기본 시간대

  • 사용
    - LocalDateTime.ofInstant(instant, zoneId)
    - LocalDateTime.atZone(zoneId)

  • 주의
    서버/배포 환경의 기본 타임존이 바뀌면 systemDefault() 결과가 달라질 수 있다.
    정책적으로 고정하려면 ZoneId.of("Asia/Seoul")을 명시.

다음 코드는 사용 예시이다.

ZoneId seoulZone = ZoneId.of("Asia/Seoul");
LocalDateTime seoulTime = LocalDateTime.ofInstant(Instant.now(), seoulZone);

내 Reservation 코드에 적용

공부한 내용을 실제 코드에 녹였다.
예약 관련된 모든 곳에 많이 사용되었지만 일부만 보여주겠다.
그중에서 예약 선두를 찾아 보류(HOLD_READY) 상태로 전환하고 3일 뒤 만료 시각을 계산하는 메서드를 보여주겠다.

public void promoteHeadToHoldReady(Long storedBookId, Duration holdDuration, Clock clock) {
    // 1) 선두(QUEUED) 예약 찾기
    Reservation head = reservations.stream()
            .filter(Reservation::isQueued)
            .findFirst()
            .orElseThrow(() -> new IllegalStateException("대기 중인 예약이 없습니다."));

    // 2) 현재(절대 시각) + 보류 기간(정책) → 만료 시각 계산
    Instant now = Instant.now(clock);
    
    LocalDateTime holdUntil = LocalDateTime.ofInstant(
            now.plus(holdDuration),
            ZoneId.systemDefault()
    );

    // 3) 예약 상태 전이: HOLD_READY + 소장본/기한 지정
    head.prepareHold(storedBookId, holdUntil);
}

코드의 이해를 돕기 위해 추가로 정보를 주겠다.

  • Duration = 보류 정책(3일)
  • Clock = 현재 시각 주입(테스트에서 고정 가능)

마무리

이번 오픈 미션을 통해 또 한 번 새로운 도전을 하게 되었다.
여름방학 때 책으로만 봤던 java.time이 단순한 개념이 아니라
실제 코드 속에서 살아 움직이는 도구로 쓰이게 된 것이다.

그때는 “이런 게 있구나” 하고 가볍게 넘겼던 개념이 지금은 내 도메인의 언어가 되어 있었다.
이 과정에서 java.time을 더 깊이 이해하게 된 것도 좋았지만 그보다 더 크게 느낀 건 “배움에 헛된 건 하나도 없다”는 사실이었다.

자바의 정석을 읽을 때 나는 java.time을 대충 훑고 지나쳤다.
그때 조금만 더 집중해서 공부했더라면 이번 미션이 더 수월했을지도 모르겠다는 생각이 든다. 하지만 동시에 이렇게 새로운 도전 속에서 다시 배우게 된 경험이 나중에 java.time을 사용할 때 훨씬 큰 자산이 될 거라 믿는다.

앞으로 무엇을 공부하든, 어떤 경험을 하든 그 어느 것도 헛된 시간은 없다는 마음으로 나아가고 싶다.

마음 한켠에 이 말을 꼭 가지고 다녀야겠다.

모든 배움은 결국 어딘가에서 연결된다.

참고

16. Java 자바 [API] - java.time 패키지
[Java] 날짜 및 시간 API (Date and Time API) 사용 방법

profile
BE 개발자

0개의 댓글