CodeGym /행동 /JAVA 25 SELF /ZonedDateTime, Instant, 타임존 작업

ZonedDateTime, Instant, 타임존 작업

JAVA 25 SELF
레벨 13 , 레슨 3
사용 가능

1. 시간대의 개념

세상에는 많은 시간대가 존재합니다. 민스크가 정오일 때, 뉴욕은 아침이고 도쿄는 이미 저녁일 수 있습니다. 시간대를 고려하지 않고 날짜와 시간을 저장하면 쉽게 혼란이 생깁니다. 예를 들어 서버는 독일에 있고 사용자는 블라디보스토크에 있다면, "2025-06-01 12:00"라는 표시는 각자에게 전혀 다른 의미가 됩니다.

시간대(timezone)는 특정 지역의 “현지” 시간을 얻기 위해 그리니치(UTC) 기준 시간에 얼마나 더하고 빼야 하는지를 정의하는 규칙입니다.

Java에서는 시간대 작업을 위해 ZoneId 클래스를 사용합니다. 몇 가지 존 식별자 예시는 다음과 같습니다.

  • "Europe/Minsk"
  • "UTC"
  • "America/New_York"
  • "Asia/Tokyo"

왜 중요한가요?

  • 서로 다른 국가의 사용자에게 시간을 올바르게 표시합니다.
  • 이벤트 시간 기록의 정확성(예: 로깅, 항공권 예약, 마감 시간).
  • 서머타임/표준시 전환을 반영합니다.

2. ZonedDateTime — 시간대를 포함한 날짜와 시간

ZonedDateTime은 날짜, 시간 시간대 정보를 함께 저장하는 클래스입니다. LocalDateTime과 유사하지만, “어느 지역에 있는지”까지 알고 있습니다.

ZonedDateTime 생성

시스템 기본 시간대의 현재 날짜와 시간

import java.time.ZonedDateTime;

ZonedDateTime now = ZonedDateTime.now();
System.out.println(now); // 예: 2025-06-01T15:30:00+03:00[Europe/Minsk]

특정 시간대의 시간

import java.time.ZoneId;

ZonedDateTime MinskTime = ZonedDateTime.now(ZoneId.of("Europe/Minsk"));
ZonedDateTime newYorkTime = ZonedDateTime.now(ZoneId.of("America/New_York"));

System.out.println("민스크: " + MinskTime);
System.out.println("뉴욕: " + newYorkTime);

LocalDateTime에서 생성

import java.time.LocalDateTime;

LocalDateTime meeting = LocalDateTime.of(2025, 6, 1, 18, 0);
ZonedDateTime meetingInMinsk = meeting.atZone(ZoneId.of("Europe/Minsk"));
System.out.println(meetingInMinsk); // 2025-06-01T18:00+03:00[Europe/Minsk]

시간대 가져오기와 설정

ZoneId tokyoZone = ZoneId.of("Asia/Tokyo");
ZonedDateTime tokyoTime = ZonedDateTime.now(tokyoZone);
System.out.println("도쿄: " + tokyoTime);

존 사이 변환: withZoneSameInstant()

같은 사건이 다른 존에서 어떻게 보이는지 알아야 할 때가 있습니다. 이때 withZoneSameInstant()를 사용합니다.

ZonedDateTime MinskMeeting = ZonedDateTime.of(2025, 6, 1, 18, 0, 0, 0, ZoneId.of("Europe/Minsk"));
ZonedDateTime newYorkMeeting = MinskMeeting.withZoneSameInstant(ZoneId.of("America/New_York"));

System.out.println("민스크의 회의 시간: " + MinskMeeting);
System.out.println("같은 순간의 뉴욕 시간: " + newYorkMeeting);

주의: withZoneSameInstant()는 다른 존에서도 같은 순간이 되도록 시간을 변환합니다. withZoneSameLocal()을 사용하면 날짜와 시간은 그대로 두고 존만 바뀌므로 — 거의 항상 오류입니다!

3. Instant — 절대 시각

Instant는 시간대와 무관한 절대적인 순간을 나타내는 클래스입니다. 기술적으로는 1970-01-01 자정(그리니치, UTC)부터 경과한 초와 나노초의 합입니다. 시간이 여권을 가진다면 — Instant는 그 번호에 해당합니다.

Instant 생성

import java.time.Instant;

Instant now = Instant.now();
System.out.println(now); // 예: 2025-06-01T12:30:00.123Z

굵은 Z는 “Zulu time”, 즉 UTC를 의미합니다.

Unix epoch 초로부터 생성

Instant fromEpoch = Instant.ofEpochSecond(1685616000L);
System.out.println(fromEpoch); // 2023-06-01T00:00:00Z

InstantZonedDateTime/LocalDateTime 변환

ZonedDateTime에서 Instant

ZonedDateTime zoned = ZonedDateTime.now();
Instant instant = zoned.toInstant();
System.out.println(instant);

Instant에서 ZonedDateTime으로

ZoneId zone = ZoneId.of("Europe/Minsk");
ZonedDateTime fromInstant = Instant.now().atZone(zone);
System.out.println(fromInstant);

Instant에서 LocalDateTime으로

import java.time.LocalDateTime;
import java.time.Instant;
import java.time.ZoneId;

LocalDateTime local = LocalDateTime.ofInstant(Instant.now(), ZoneId.of("Europe/Minsk"));
System.out.println(local);

4. 실습: 여러 타임존의 현재 시간, 존 간 변환

여러 타임존의 현재 시간 얻기

민스크, 뉴욕, 도쿄의 현재 시간을 보여주는 미니 애플리케이션을 만들어 봅시다.

import java.time.ZonedDateTime;
import java.time.ZoneId;

public class TimeZonesDemo {
    public static void main(String[] args) {
        ZonedDateTime Minsk = ZonedDateTime.now(ZoneId.of("Europe/Minsk"));
        ZonedDateTime newYork = ZonedDateTime.now(ZoneId.of("America/New_York"));
        ZonedDateTime tokyo = ZonedDateTime.now(ZoneId.of("Asia/Tokyo"));

        System.out.println("민스크:    " + Minsk);
        System.out.println("뉴욕:  " + newYork);
        System.out.println("도쿄:     " + tokyo);
    }
}

존 사이 시간 변환

민스크에서 18:00로 예정된 이벤트가 있다고 합시다. 이것이 뉴욕과 도쿄에서는 몇 시일까요?

import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;

public class MeetingTime {
    public static void main(String[] args) {
        LocalDateTime eventTime = LocalDateTime.of(2025, 6, 1, 18, 0);
        ZonedDateTime minskEvent = eventTime.atZone(ZoneId.of("Europe/Minsk"));

        ZonedDateTime newYorkEvent = minskEvent.withZoneSameInstant(ZoneId.of("America/New_York"));
        ZonedDateTime tokyoEvent = minskEvent.withZoneSameInstant(ZoneId.of("Asia/Tokyo"));

        System.out.println("민스크에서의 회의:   " + minskEvent);
        System.out.println("뉴욕 시간:        " + newYorkEvent);
        System.out.println("도쿄 시간:            " + tokyoEvent);
    }
}

LocalDateTimeZonedDateTime으로, 그리고 반대로

Local → Zoned:

import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;

LocalDateTime localTime = LocalDateTime.of(2025, 6, 1, 14, 0);
ZonedDateTime zonedTime = localTime.atZone(ZoneId.of("Europe/Minsk"));
System.out.println(zonedTime);

Zoned → Local:

LocalDateTime extracted = zonedTime.toLocalDateTime();
System.out.println(extracted);

5. 중요한 참고 사항과 유의점

LocalDateTime만 저장하면 안 되는가

LocalDateTime시간대 없는 단순 날짜/시간일 뿐입니다. 대부분의 비즈니스 로직에는 이걸로 충분하지 않습니다! 예를 들어 "2025-06-01 12:00"LocalDateTime으로 저장하면, 민스크 사용자와 뉴욕 사용자에게 이는 실제로 서로 다른 순간을 의미합니다.

항상 절대 시간을 저장하세요(예: Instant). 혹은 이벤트가 특정 존에 진짜로 묶여 있다면 존과 함께 저장하세요(ZonedDateTime). LocalDateTime은 “유동” 날짜(예: 생일처럼 시간대와 하루 중 시간에 구애받지 않는 값)를 다룰 때에만 적합합니다.

서머타임/표준시 전환 문제

시간대는 UTC 대비 오프셋뿐 아니라 서머타임/표준시 전환 규칙도 포함합니다. 예를 들어 어떤 나라에서는 특정 날에 시계를 한 시간 앞으로 혹은 뒤로 옮깁니다 — 만약 LocalDateTime만 저장한다면, 그 시간이 실제로 존재했는지조차 알 수 없습니다.

시간의 ‘빈 구간’ 예시:

  • 미국에서는 3월 새벽 2:00에 시계를 3:00로 옮깁니다.
  • 뉴욕에서는 "2025-03-10 02:30"이라는 시간이 존재하지 않았습니다!

ZonedDateTime을 사용하면 이런 놀라움으로부터 보호됩니다. 라이브러리가 시간의 유효성을 스스로 검사합니다.

도식: LocalDateTime, ZonedDateTime, Instant의 관계

graph TD
    A["LocalDateTime 
(날짜 + 시간, 시간대 없음)"] -->|+ ZoneId| B["ZonedDateTime
(날짜 + 시간 + 시간대)"] B -->|"toInstant()"| C["Instant
(절대 시간, UTC)"] C -->|"atZone(ZoneId)"| B B -->|"toLocalDateTime()"| A

6. ZonedDateTimeInstant를 다룰 때의 흔한 오류

오류 1: 글로벌 이벤트에 LocalDateTime을 사용한다.
서로 다른 국가 사용자의 회의 일시를 LocalDateTime으로 저장하면, 모두가 자기 “12:00”을 보게 되지만 이는 서로 다른 순간입니다. 글로벌 이벤트에는 ZonedDateTime 또는 Instant를 사용하세요.

오류 2: 문자열 파싱 시 시간대를 무시한다.
"2025-06-01T12:00:00" 문자열을 존 없이 파싱하면 LocalDateTime이 만들어지며, ZonedDateTime이 아닙니다. ZonedDateTime을 얻으려면 존이 포함된 문자열을 사용하거나 명시적으로 추가하세요.

오류 3: 존 사이 변환을 잘못한다.
withZoneSameInstant() 대신 withZoneSameLocal()을 사용하면 잘못된 시간이 나올 수 있습니다. 다른 존에서 같은 순간을 얻고 싶다면 항상 withZoneSameInstant()를 사용하세요.

오류 4: 서머타임/표준시 전환을 고려하지 않는다.
전환 경계 시점에 이벤트를 계획한다면 반드시 ZonedDateTime을 사용하고 라이브러리를 신뢰하세요 — 모든 전환과 “빈 구간”을 알고 있습니다.

오류 5: ZonedDateTime을 존을 고려하지 않고 비교한다.
로컬 시간이 같아도 존이 다른 두 ZonedDateTime은 서로 다른 순간을 나타낼 수 있습니다. 비교에는 toInstant()를 사용하세요.

1
과제
JAVA 25 SELF, 레벨 13, 레슨 3
잠금
글로벌 커맨드 센터: 전 세계 시간 🌍
글로벌 커맨드 센터: 전 세계 시간 🌍
1
과제
JAVA 25 SELF, 레벨 13, 레슨 3
잠금
국제 컨퍼런스: 이벤트를 시간대에 고정하기 🌐
국제 컨퍼런스: 이벤트를 시간대에 고정하기 🌐
1
과제
JAVA 25 SELF, 레벨 13, 레슨 3
잠금
텔레포트 이동: 서로 다른 시간대에서의 동일한 이벤트 🚀
텔레포트 이동: 서로 다른 시간대에서의 동일한 이벤트 🚀
1
과제
JAVA 25 SELF, 레벨 13, 레슨 3
잠금
시간 연대기: Instant로 떠나는 여행 🌌
시간 연대기: Instant로 떠나는 여행 🌌
코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION