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
Instant ↔ ZonedDateTime/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);
}
}
LocalDateTime을 ZonedDateTime으로, 그리고 반대로
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. ZonedDateTime과 Instant를 다룰 때의 흔한 오류
오류 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()를 사용하세요.
GO TO FULL VERSION