ZonedDateTime — 개념 설명, 시간대와 날짜시간 다루기

저자: IT Sectr 게시일: 2026-07-13 읽는 시간: 10 분

ZonedDateTime은 java.time 패키지의 불변(immutable) 클래스로, 시간대 정보(ZoneId)와 함께 날짜와 시간을 저장합니다. LocalDateTime과 달리 ZonedDateTime은 시간선상의 한 순간을 명확하게 식별합니다. Oracle Java 17 (2024) 사양에 따르면, 이 클래스는 IANA Time Zone Database의 영역 규칙을 통해 일광 절약 시간(DST) 전환을 올바르게 처리합니다.

핵심 사항

  • ZonedDateTime은 날짜, 시간, 시간대(ZoneId)를 하나의 객체로 결합하는 불변 클래스입니다.
  • LocalDateTime과 달리 ZonedDateTime은 시간선상의 순간을 명확하게 정의하며 글로벌 시스템에 적합합니다.
  • 이 클래스는 IANA Time Zone Database 규칙에 따라 일광 절약 시간(DST) 전환을 자동으로 처리합니다.
  • 시간대 간 변환에는 withZoneSameInstant(ZoneId) 메서드를 사용합니다.
  • 데이터베이스에 ZonedDateTime을 저장할 때는 OffsetDateTime 또는 TIMESTAMP WITH TIME ZONE을 사용하는 것이 좋습니다.

ZonedDateTime이란?

ZonedDateTime은 java.time 패키지의 핵심 클래스 중 하나로, 완전한 시간대 정보와 함께 날짜와 시간을 나타냅니다. 이 클래스는 세 가지 구성 요소를 결합합니다: LocalDateTime(날짜와 시간), ZoneId(영역 식별자), ZoneOffset(UTC 기준 오프셋).

LocalDateTime이 시간대 바인딩 없이 벽시계 시간(wall-clock time)만 저장하는 것과 달리, ZonedDateTime은 순간을 명확하게 식별합니다. 서로 다른 시간대에 있는 두 개의 동일한 LocalDateTime 인스턴스는 시간상 다른 순간을 나타냅니다. 두 개의 동일한 ZonedDateTime 인스턴스는 같은 순간을 나타냅니다.

이 클래스는 완전히 불변이며 스레드 안전(thread-safe)합니다. 모든 산술 연산은 새 객체를 반환합니다. ZonedDateTime은 ChronoZonedDateTime 인터페이스를 구현하며 Java에서 시간대별 시간 처리가 필요한 모든 곳에서 사용할 수 있습니다.

Oracle Java 17 사양에 따르면, ZonedDateTime은 600개 이상의 시간대를 포함하는 IANA Time Zone Database의 모든 영역에서 작동을 지원합니다.

ZonedDateTime vs LocalDateTime: 차이점은?

주요 차이점 — ZonedDateTime에는 시간대가 포함되어 있지만 LocalDateTime에는 없습니다. 이 근본적인 차이가 각 클래스의 적용 범위를 결정합니다.

LocalDateTime은 지역 이벤트에 사용됩니다: 콘서트 시간, 수업 일정, 생년월일. 모스크바에서 15:00에 이벤트가 발생하면 LocalDateTime은 바인딩 없이 15:00을 기록합니다. 서버를 뉴욕으로 옮겨도 시간은 15:00으로 유지되지만, 이는 다른 물리적 순간이 됩니다.

ZonedDateTime은 글로벌 데이터에 사용됩니다: 서버 로그, API 타임스탬프, 국제 회의. 회의가 15:00 MSK로 예정된 경우 ZonedDateTime은 시간과 영역을 모두 보존합니다. 뉴욕에서는 8:00 EST로 올바르게 표시됩니다. Baeldung (2024)에 따르면, LocalDateTime과 ZonedDateTime 중 선택은 날짜 작업 시 가장 일반적인 아키텍처 결정입니다.

실용적인 규칙: 데이터가 단일 지역에 저장되는 경우 — LocalDateTime을 사용합니다. 데이터가 시간대 경계를 넘는 경우 — ZonedDateTime을 사용합니다. 절대적인 순간을 표현해야 하는 경우 — Instant를 사용합니다.

java.time에서 시간대는 어떻게 작동하나요?

java.time의 시간대는 ZoneId 클래스로 표현됩니다. ZoneId는 “대륙/지역” 형식의 영역 식별자로, 예를 들어 “Europe/Moscow”, “America/New_York”, “Asia/Tokyo” 등이 있습니다. ZoneId는 정적 메서드 of(String zoneId) 또는 시스템 기본 시간대를 통해 얻을 수 있습니다.

ZoneId는 두 가지 유형으로 나뉩니다: fixed offset(고정 오프셋, 예: “+03:00”)과 region-based(지역 기반 영역, 예: “Europe/London”). 지역 기반 영역에는 일광 절약 시간 전환 규칙과 역사적 변경 사항이 포함됩니다. Fixed offset은 단순히 고정된 오프셋입니다.

특정 순간의 ZoneId 현재 오프셋을 얻으려면 getRules() 메서드를 사용하여 ZoneRules를 반환받습니다. ZoneRules에는 특정 영역의 모든 전환 및 오프셋이 포함됩니다. 이것이 올바른 DST 처리를 위한 핵심 메커니즘입니다.

모든 시간대는 tzdata 파일(IANA Time Zone Database)을 통해 JDK에 포함되어 있으며 정기적으로 업데이트됩니다. Android에서 tzdata 버전은 Google Play Services를 통한 시스템 업데이트에 따라 달라집니다.

ZonedDateTime 생성

ZonedDateTime은 여러 가지 방법으로 생성할 수 있습니다. 가장 간단한 방법은 now()로, 시스템 시간대의 현재 시간을 반환합니다. now(ZoneId) 변형은 지정된 영역의 현재 시간을 가져올 수 있습니다.

of(LocalDateTime, ZoneId) 메서드는 로컬 시간과 영역에서 ZonedDateTime을 생성합니다. of(int year, int month, int dayOfMonth, int hour, int minute, int second, int nanoOfSecond, ZoneId zone) 변형은 구성 요소에서 생성합니다.

LocalDateTime은 atZone(ZoneId) 메서드를 통해 ZonedDateTime으로 변환할 수 있습니다. Instant는 Instant.atZone(ZoneId)을 통해, Date는 Date.toInstant().atZone(ZoneId)를 통해 변환할 수 있습니다.

kotlin
val moscowZone = ZoneId.of("Europe/Moscow")

val nowInMoscow = ZonedDateTime.now(moscowZone)

val fromComponents = ZonedDateTime.of(
    2026, 7, 21, 15, 30, 0, 0, moscowZone
)

val fromLocal = LocalDateTime.now().atZone(moscowZone)

val fromInstant = Instant.now().atZone(moscowZone)

시간대 간 변환

주요 변환 메서드는 withZoneSameInstant(ZoneId)입니다. 이 메서드는 동일한 순간을 유지하면서 ZonedDateTime을 다른 시간대로 변환합니다. 예: 15:00 MSK → 8:00 EST. withZoneSameLocal(ZoneId) 메서드는 로컬 시간을 유지하면서 영역을 변경합니다 — 이는 다른 순간을 생성합니다.

UTC 기준 오프셋을 얻으려면 getOffset() 메서드를 사용하여 ZoneOffset을 반환받습니다. ZoneOffset은 ZoneId의 하위 클래스로, “+HH:mm” 또는 “-HH:mm” 형식의 고정 오프셋을 나타냅니다.

Instant로의 변환은 toInstant() 메서드를 통해 이루어집니다. Instant는 시간대와 무관한 절대적인 시간 순간입니다. 역변환은 Instant.atZone(ZoneId)입니다.

kotlin
val moscow = ZonedDateTime.of(
    2026, 7, 21, 15, 0, 0, 0,
    ZoneId.of("Europe/Moscow")
)

val newYork = moscow.withZoneSameInstant(
    ZoneId.of("America/New_York")
)

val utcInstant = moscow.toInstant()
val backToMoscow = utcInstant.atZone(ZoneId.of("Europe/Moscow"))

일광 절약 시간(DST) 다루기

일광 절약 시간 전환은 두 가지 문제를 만듭니다: 갭(gap)오버랩(overlap)입니다. 갭은 봄에 시계가 앞으로 설정될 때 발생하며 — 특정 시간이 존재하지 않습니다. 오버랩은 가을에 시계가 뒤로 설정될 때 발생하며 — 같은 시간이 두 번 발생합니다.

ZonedDateTime은 해결(resolve) 전략을 통해 이러한 상황을 처리합니다. 갭 중에 객체를 생성할 때 java.time은 자동으로 오프셋만큼 시간을 이동시킵니다. 오버랩 중에 생성할 때는 첫 번째 옵션(전환 전)이 선택됩니다. 이 동작은 withZoneSameInstant를 통해 변경할 수 있습니다.

zone.getRules().isDaylightSavings(instant)를 통해 시간이 DST에 있는지 확인할 수 있습니다. getOffset() 메서드는 특정 순간의 실제 오프셋을 표시하며, getRules().getDaylightSavings(instant)는 밀리초 단위의 DST 조정량을 표시합니다.

kotlin
fun checkDST(zdt: ZonedDateTime) {
    val rules = zdt.getZone().getRules()
    val instant = zdt.toInstant()

    if (rules.isDaylightSavings(instant)) {
        val dstAmount = rules.getDaylightSavings(instant)
        Log.d("일광 절약 시간", "DST 오프셋: $dstAmount")
    }
}

ZonedDateTime 형식 지정

ZonedDateTime의 형식을 지정하려면 DateTimeFormatter를 사용합니다. 표준 ISO 형식에는 날짜, 시간, 오프셋이 포함됩니다: “2026-07-21T15:30:00+03:00[Europe/Moscow]”. 미리 정의된 형식: ISO_ZONED_DATE_TIME, ISO_OFFSET_DATE_TIME, ISO_INSTANT.

지역화된 출력을 위해 DateTimeFormatter.ofLocalizedDateTime(FormatStyle)을 사용합니다. FormatStyle은 SHORT, MEDIUM, LONG, FULL이 될 수 있습니다. LONG에는 영역 이름(“MSK”)이 포함되고, FULL에는 전체 이름(“Moscow Standard Time”)이 포함됩니다.

중요: ZonedDateTime으로 문자열을 파싱할 때 형식에는 영역 또는 오프셋 정보가 포함되어야 합니다. 영역이 지정되지 않은 경우 LocalDateTime.parse()를 사용한 다음 atZone()을 사용합니다.

kotlin
val zdt = ZonedDateTime.now(ZoneId.of("Europe/Moscow"))

val iso = zdt.format(DateTimeFormatter.ISO_ZONED_DATE_TIME)

val custom = DateTimeFormatter
    .ofPattern("dd.MM.yyyy HH:mm z")
val formatted = zdt.format(custom)

val parsed = ZonedDateTime.parse(
    "2026-07-21T15:30:00+03:00",
    DateTimeFormatter.ISO_OFFSET_DATE_TIME
)

Android에서 ZonedDateTime: 실전 예제

첫 번째 예제 — 사용자의 로컬 시간대로 회의 시간을 표시합니다. 서버는 UTC로 ZonedDateTime을 보내고, 클라이언트는 기기의 로컬 시간대로 변환합니다.

kotlin
fun displayMeetingTime(
    serverUtc: ZonedDateTime
): String {
    val deviceZone = ZoneId.systemDefault()
    val localTime = serverUtc.withZoneSameInstant(deviceZone)
    val formatter = DateTimeFormatter
        .ofPattern("dd.MM.yyyy HH:mm z")
    return localTime.format(formatter)
}

두 번째 예제 — 시간대를 고려하여 다음 이벤트까지의 시간을 계산합니다. 서버 시간에는 ZonedDateTime을, 차이 계산에는 Duration.between()을 사용합니다.

kotlin
fun timeUntilEvent(eventTime: ZonedDateTime): String {
    val now = ZonedDateTime.now()
    val duration = Duration.between(now, eventTime)

    val hours = duration.toHours()
    val minutes = duration.toMinutes() % 60
    return "Remaining $hours h $minutes min"
}

세 번째 예제 — Retrofit API 작업입니다. 서버는 영역이 포함된 ISO-8601 문자열을 반환합니다. ZonedDateTime으로 변환하기 위해 사용자 정의 역직렬화기를 사용합니다.

kotlin
data class EventResponse(
    @JsonAdapter(ZonedDateTimeAdapter::class)
    val eventTime: ZonedDateTime
)

class ZonedDateTimeAdapter : JsonAdapter<ZonedDateTime>() {
    override fun fromJson(reader: JsonReader): ZonedDateTime? {
        return ZonedDateTime.parse(
            reader.nextString()
        )
    }
}

시간대 작업 시 흔한 실수

첫 번째 실수 — 서버 코드에서 ZoneId.systemDefault()를 사용하는 것. 서버의 시간대는 클라이언트와 다를 수 있으며, 서버에서 시스템 영역을 사용하면 잘못된 계산이 발생합니다. 항상 영역을 명시적으로 지정하거나 UTC를 기준으로 사용하세요.

두 번째 실수 — 기간 계산 시 DST를 무시하는 것. Duration.between()은 전환을 올바르게 처리하지만, 타임스탬프를 수동으로 빼면 일광 절약 시간이 1시간의 오차를 발생시킬 수 있습니다. 수동 계산 대신 ChronoUnit.HOURS.between()을 사용하세요.

세 번째 실수 — withZoneSameInstant와 withZoneSameLocal을 혼동하는 것. 전자는 순간을 유지하면서 영역을 변경하고 — 시간이 이동합니다. 후자는 로컬 시간을 유지하면서 영역을 변경하고 — 순간이 변경됩니다. SonarSource (2024)에 따르면 잘못된 메서드 선택은 가장 흔한 실수 중 하나입니다.

네 번째 실수 — 기기의 시간대가 항상 사용자의 시간대와 같다고 가정하는 것. 사용자가 여행 중일 수 있으며 현재 영역 대신 “집” 시간대의 시간을 앱이 표시하기를 기대할 수 있습니다. 이 경우 인터페이스를 통해 영역 선택을 제공하세요.

자주 묻는 질문

ZonedDateTime과 OffsetDateTime의 차이점은 무엇인가요?

ZonedDateTime에는 지역 영역 식별자(예: “Europe/Moscow”)가 포함되어 있으며 DST를 처리합니다. OffsetDateTime은 지역 규칙 없이 고정 오프셋(+03:00)만 저장합니다. 데이터베이스 저장에는 OffsetDateTime이 권장됩니다.

ZonedDateTime을 통해 UTC의 현재 시간을 얻으려면 어떻게 하나요?

ZonedDateTime.now(ZoneOffset.UTC) 또는 Instant.now().atZone(ZoneOffset.UTC)를 사용합니다. 두 옵션 모두 오프셋이 0인 현재 순간을 반환합니다. 간단한 타임스탬프의 경우 영역 바인딩 없이 Instant.now()를 사용합니다.

ZonedDateTime을 Gson이나 Moshi로 직렬화할 수 있나요?

네, 하지만 사용자 정의 어댑터가 필요합니다. Gson은 기본적으로 ZonedDateTime을 지원하지 않습니다. Moshi는 Rfc3339DateJsonAdapter를 통해 지원합니다. Jackson의 경우 Kotlinx Serialization 또는 JavaTimeModule 라이브러리 사용이 권장됩니다.

시간이 DST 갭에 해당할 때 어떻게 처리하나요?

java.time은 자동으로 오프셋만큼 시간을 앞으로 이동시킵니다. 예를 들어, 시계가 03:00으로 앞으로 설정될 때 02:30이 존재하지 않는 경우 ZonedDateTime은 03:30에 객체를 생성합니다. ZoneRules.getTransition(instant)를 통해 갭을 확인할 수 있습니다.

SQL 데이터베이스에 ZonedDateTime이 권장되지 않는 이유는 무엇인가요?

JDBC 4.2는 OffsetDateTime을 지원하지만 ZonedDateTime을 직접 지원하지 않습니다. ZonedDateTime에는 SQL에 해당하는 것이 없는 지역 영역이 포함되어 있습니다. OffsetDateTime 또는 Instant를 저장하고 영역은 별도 열에 저장하는 것이 좋습니다.

요약

  • ZonedDateTime은 시간대와 함께 날짜와 시간을 위한 불변 클래스로, IANA Time Zone Database를 통해 DST를 올바르게 처리합니다.
  • LocalDateTime과의 주요 차이점은 영역의 존재이며, 이는 ZonedDateTime을 시간 순간의 명확한 식별자로 만듭니다.
  • 영역 간 변환에는 순간을 유지하는 withZoneSameInstant()를 사용하고 withZoneSameLocal은 사용하지 마세요.
  • 일광 절약 시간 전환 중 java.time은 내장 영역 규칙을 통해 갭과 오버랩을 자동으로 해결합니다.
  • 데이터베이스 저장에는 OffsetDateTime을 사용하거나 Instant와 ZoneId를 별도로 저장하세요.
  • Android에서 ZonedDateTime을 기기 로컬 시간으로 변환하려면 ZoneId.systemDefault()를 withZoneSameInstant와 함께 사용하세요.
  • JSON 직렬화에는 사용자 정의 어댑터가 필요합니다 — Kotlinx Serialization 또는 Jackson JavaTimeModule을 사용하세요.

턴키 방식의 모바일 애플리케이션을 개발해 드립니다

IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.

프로젝트 논의

더 읽어보기