29CM TEAM
Published in

29CM TEAM

[Java] 타임존, 날짜 그리고 시간객체 뽀개기

항상 헷갈리는 타임존과 관련된 용어를 정리고, 데이터베이스에 어떻게 저장되고, 어플리케이션에선 어떻게 보여지는지 알아봅니다.

TL;DR

1. 타임존을 아는(aka. Timezone-aware) 객체는 개발자가 신경쓰지 않아도 어플리케이션에서 설정한 기본 타임존으로 자동으로 변환하여 불러오고, 저장합니다.

# APP에 타임존을 KST(Korean Standard Time)로 설정한 경우, 
# 데이터베이스에 UTC로 저장된 시간값을 자동으로 KST로 변환하여 읽어온다.
# 또한 저장하는 설정(여기서는 Hibernate)에 맞게 타임존을 변환하여 저장한다.
DB ==> APP ==> DB
UTC -- KST --- UTC

2. DateTime 또는 Time객체를 저장할 땐 타임존이 있는 객체를 사용하는 것을 지향하고, 특히 여러 어플리케이션에서 동일한 컬럼값을 참조할 경우 타임존이 있는 객체를 사용해야합니다.

good
- 2022-08-11T11:04:00.000000-06:00[America/Guatemala]
- 04:00.000000-06:00

bad
- 2022-08-11T11:04:00.000000
- 04:00.000000

3. 비지니스 요구사항에 따라 타임존 변환이 필요없는 상황에서는 굳이 Time 객체 대신 String으로 값을 관리하는 것이 유리합니다.

용어

헷갈렸던 용어를 정리하고 나면 타임존을 전반적으로 이해하는데 도움이 됩니다.

GMT, UTC, Z, T

GMT(Greenwich Mean Time): 그리니치 천문대 기준시각입니다.

UTC(Coordinated Universal Time): 협정 시계시, 세슘 원자의 운동량을 기반으로 계산된 정확한 시간 UTC와 GMT는 엄연히 다른 의미지만 소숫점 단위에서만 차이가 나기 때문에 일상 혼용합니다.

Z: UTC 시간을 나타내는 축약문자열입니다.

ZoneId.of("Z") == ZoneOffset.UTC
> true

T: ISO-8601 형식에서 날짜와 시간사이의 구분자(Delimiter for the time)

2021-05-01T18:30:19.653060

ZoneId, ZoneOffset, DateTime, Time

ZonedId: 타임존을 구분하는 문자열 코드로 TZ Database name 이라고도 부릅니다. 이 객체만 있으면 Timezone-aware 시간과 날짜 객체를 만들 수 있습니다.

val timeZone = ZoneId.of("America/Guatemala")

ZoneOffset: UTC와 현지 시간과의 차이를 의미하며, KST 타임존 표시 할때 UTC+09:00 에서 +09:00를 ZoneOffset이라고 합니다.

val zoneOffset = ZoneOffset.of("-06:00")

ZonedDateTime: LocalDateTime과 ZoneOffset(시차)또는 ZoneId(타임존을 구분하는 문자열이 포함된 객채, 타임존으로 이해하면 쉬움) 으로 구성되어 있습니다.

val timeZone = ZoneId.of("America/Guatemala")
val zonedDateTime = ZonedDateTime.now(timeZone)
println("현재시간: $zonedDateTime")
> 현재시간: 2022-08-11T11:04:32.818124-06:00[America/Guatemala]
val zoneOffset = ZoneOffset.of("-06:00")
val zonedDateTimeWithOffset = ZonedDateTime.now(zoneOffset)
println("현재시간: $zonedDateTimeWithOffset")
> 현재시간: 2022-08-11T11:04:32.819653-06:00

LocalDateTime: LocalDate와 LocalTime으로 구성. 각각은 연월일 / 시분초로 구성되어있습니다.

val localDateTime = LocalDateTime.now();
println("현재시간: $localDateTime")
> 현재시간: 2022-08-12T02:09:00.005210

OffsetTime: LocalTime과 ZoneOffset으로 구성된 객체

val localTime = LocalTime.of(10, 0)
val zoneOffset = ZoneOffSet.UTC
OffsetTime.of(localTime, zoneOffset)
09:00:00+09:00

LocalTime: 시분초 값으로 구성된 객체

LocalTime.of(10, 0)

데이터베이스와 어플리케이션에서 타임존

  1. 데이터베이스에 타임존을 설정할 수 있고, 데이터를조회할 때 해당 타임존으로 변환하여 보여줍니다.
>> SHOW TIMEZONE;
UTC

2. Hibernate 설정으로 데이터베이스에 저장되는 시간객체의 타임존을 변경할 수 있습니다. 일반적으로 UTC로 변환하여 저장합니다.

# application.ymlspring:
jpa:
properties:
hibernate:
jdbc:
time_zone: UTC

3. 어플리케이션에서는 시간객체를 서버 런타임시 설정한 Timezone에 맞게 변환하여 계산하여 줍니다. 예를들어 UTC 시간으로 저장된 값을 조회하여 API로 응답하면 설정한 Timzone으로 바뀌어 응답이 됩니다.

TimeZone.setDefault(TimeZone.getTimeZone("Asia/Seoul"));

예시1) OffsetTime 객체를 조회
예상처럼 데이베스 UTC zoneOffset으로 저장된 시간을 어플리케이션에서 타임존을 변경하여 줍니다.

TIME WITH TIME ZONE 컬럼, UTC 오프셋으로 저장되어 있음.
OffsetTime 객체, 데이터베이스에서 읽은 값을 KST 오프셋으로 변환.

예시2) LocalTime 객체를 조회
LocalTime은 ZoneOffset이 없기 때문에 어플리케이션에서도 시간을 변경하지 않을 것으로 예상했지만, 실제로는 ZoneOffset이 있는 것 처럼 시간을 계산하는 동작을 합니다. ZoneOffset이 없기 때문에 값의 명시성도 떨어지고, 시간변환 또한 예상과는 다르기 때문에 더더욱 사용시 고민이 필요하다고 느낍니다.

TIME WITHOUT TIME ZONE 컬럼, UTC 오프셋으로 저장되어 있음.
LocalTime객체, 오프셋이 없는 객체임에도 불구하고 KST로 변환함.

4. Java객체와 Database Data type 매칭. 아래처럼 LocalTime과 OffsetTime은 데이터베이스 컬럼 타입을 맞추어 사용하는 것이 좋습니다.

// Java 객체 ====> DB Data Type
- LocalTime ====> TIME [WITHOUT TIME ZONE](생략가능)
- OffsetTime ===> TIME WITH TIME ZONE

5. 시간 객체를 저장시 Offset이 없을 경우 어떤 타임존으로 저장되었는지 알기 매우 어렵습니다. 단일 어플리케이션에서 참조하는 값이면 문제가 없더라도, 타임존 설정이 다른 어플리케이션에서 읽을 때는 문제가 됩니다.

예시) Spring 서버에서는 데이터를 UTC로 저장하고 KST로 변환하여 읽는다고 할때, Django 서버에서는 이러한 컨텍스트를 모르는 상황입니다. 따라서 Django에선 Offset이 빈 값의 Time객체로 그대로 읽게됩니다. 시간 또는 날짜시간 객체를 사용할 때는 ZoneOffset 또는 ZoneId 객체를 아는 (aware)하는 객체를 사용하는 것이 좋습니다.

데이터베이스에 저장된 시간
Spring에서 조회한 시간
Django에서 조회한 시간

6. 타임존을 변경에 대한 대응이 불필요할 경우에는 String으로 저장하고 Domain Entity에서 LocalTime으로 사용하는 방법도 좋습니다.

예를들어 서울에 있는 신라호텔을 예약할 경우, 미국에 사는 사람이 해당 호텔 정보를 볼 때 체크인 시간이 14:00+09:00라는 시간을 굳이 05:00+00:00 으로 보여줄 이유가 없다는 뜻입니다. 오히려, 새벽 다섯시부터 체크인이 가능한 것인지 착각을 일으킬 수 있기 때문에 이럴 경우 String으로 메타정보처럼 관리하는 것이 유리합니다.

val timeString = "06:00:00";
val localTime = LocalTime.parse(timeString);

지금까지 타임존과 관련된 용어와 객체를 정리해보았습니다. 조금이나마 도움이 되셨길 바랍니다.

함께 성장할 동료를 찾습니다

29CM (에이플러스비) 는 3년 연속 거래액 2배의 성장을 이루었습니다.

이제 더 큰 성장을 위해 기존 모놀리틱 서비스 구조를 마이크로서비스 구조로 전환하고, 앵귤러 기반 프론트엔드 코드를 리엑트로 전환하는 등의 기술적인 시도를 진행하고 있습니다.

함께 성장하고 유저 가치를 만들어낼 동료 개발자분들을 찾습니다
많은 지원 부탁합니다!

--

--

29CM | 이십구센티미터를 만드는 모든 이야기를 담습니다.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store