0. Intro
대부분의 서비스에서 시간은 아주 중요한 정보입니다.
다행히 Django에서는 datetime 필드에 auto_now_add=True 같은 옵션만 잘 지정 해주면 데이터베이스에 row가 생성되는 날짜와 시간이 함께 저장 됩니다.
하지만 timezone과 datetime에 대한 이해 없이 프로젝트를 진행하다 보면 종종 다음과 같은 문제에 직면합니다.
1.
원하는 시간을 저장할 수가 없다 — 데이터베이스에 예상과 다른 날짜와 시간이 저장되는 경우
2.
원하는 시간을 불러올 수가 없다 — 데이터베이스에서 불러온 시간이 예상과 다른 경우
위와 같은 상황을 해결할 수 있도록 Django가 어떻게 timezone을 처리하는 지에 대해서 알아보겠습니다.
1. in Python
1.1 datetime
python에는 datetime이라는 모듈이 있습니다. 이름 그대로 날짜와 시간을 다루는 모듈입니다.
datetime.now() 함수를 호출하면 현재의 (연, 월, 일, 시, 분, 초, micro seconds)를 알려줍니다.
import datetime
now = datetime.datetime.now()
# datetime.datetime(2020, 12, 5, 6, 36, 52, 606366)
print(now)
# 2020-12-05 06:36:52.606366
Python
복사
다음 두 장의 사진은 같은 시간에 캡쳐한 현재의 시간입니다.
figure 1. mac local time
figure 2. google UTC now
<figure 1>은 Mac의 로컬(서울) 시간이고, <figure 2>는 구글에 utc now로 검색한 UTC입니다.
UTC: Coordinated Universal Time; 협정세계시
서울(KST)의 시간은 12월 5일 아침 6시인데 반해 UTC는 12월 4일 저녁 9시입니다.
KST(UTC+9)가 UTC 기준으로 9시간 앞서 있기 때문에 위와 같은 시간 차이가 납니다.
같은 현재의 시간이라고 해도 어떤 timezone에 속해 있느냐에 따라 날짜와 시간이 달라집니다.
위의 datetime.now()의 결과에서 유추할 수 있듯이 python datetime은 기본적으로 사용자의 로컬 timezone으로 시간을 계산합니다.
만약 현재의 UTC를 알고 싶다면 다음과 같이 하면 됩니다.
import datetime
utc_now = datetime.datetime.utcnow()
# datetime.datetime(2020, 12, 4, 21, 36, 58, 297016)
print(utc_now)
# 2020-12-04 21:36:58.297016
Python
복사
1.2 aware vs. naive
Date and time objects may be categorized as “aware” or “naive” depending on whether or not they include timezone information.
timezone 정보가 아주 중요하기 때문에 datetime 오브젝트는 timezone의 정보를 갖고 있는지 그렇지 않은지에 따라 각각 aware datetime과 naive datetime으로 불립니다.
datetime 오브젝트의 timezone 정보는 tzinfo 속성을 보면 알 수 있습니다.
print(now.tzinfo)
# None
print(utc_now.tzinfo)
# None
Python
복사
위에서 구한 now와 utc_now는 모두 tzinfo가 None이기 때문에 naive datetime입니다.
2. in Django
이제 Django의 timezone에 대해서 얘기 해보겠습니다.
2.1 USE_TZ
프로젝트 내에서 Django가 시간을 계산할 때 timezone을 이용하도록 하기 위해서는 settings 안에 USE_TZ=True로 지정해야 합니다.
여러 국가(혹은 한 국가 내에서도 여러 timezone)의 시간을 다루기 위해 반드시 필요한 설정입니다.
또한 settings 안에 TIME_ZONE=America/Los_Angeles처럼 지정하면 기본으로 사용할 timezone을 변경할 수 있습니다. (default: America/Chicago)
timezone support가 켜져 있으면(USE_TZ=TRUE이면), Django는 데이터베이스에 시간을 저장할 때 UTC로 저장합니다.
위에서 기본 timezone을 정했는데 왜 정작 저장할 때는 UTC인지 의문이 생길 수 있습니다.
하지만 기본 timezone을 변경하는 경우를 대비하기 위해서는 UTC를 저장하는 게 가장 유연한 방법입니다.
따라서 datetime을 불러 올 때는 저장된 UTC를 timezone에 맞게 바뀐 값이 출력됩니다.
기억해야 할 사실은 데이터베이스를 열어봤을 때 보이는 시간은 UTC라는 점입니다.
2.2 saving naive datetime
데이터베이스에 시간을 한 번 저장 해보겠습니다.
숙소 예약을 관리하는 Reservation 모델이 있다고 하겠습니다.
from django.db import models
class Reservation(models.Model):
...
start_at = models.DateTimeField() # 예약이 시작되는 시간
Python
복사
12월 31일 12시로 예약하기 위해 다음과 같은 datetime 오브젝트를 생성했습니다.
import datetime
naive_datetime = datetime.datetime(2020, 12, 31, 12, 0)
# datetime.datetime(2020, 12, 31, 12, 0)
Python
복사
위에서 생성한 datetime은 tzinfo가 None인 naive datetime입니다.
이제 새로운 예약을 생성해봅시다.
from .models import Reservation
Reservation.objects.create(start_at=naive_datetime)
Python
복사
그런데 위의 코드를 실행해보면 다음과 같은 warning 메세지가 출력됩니다.
RuntimeWarning: DateTimeField Reservation.start_at received a naive datetime (2020-12-31 12:00:00) while time zone support is active.
프로젝트 내에 timezone support가 활성화 되어 있는데 naive datetime이 전달 되었기 때문에 정확한 시간을 계산할 수 없다고 알려주는 것입니다.
reservation이 생성되지 않은 것은 아닙니다. Warning이기 때문에
새로운 reservation의 예약 시작 시간이 어떻게 저장되었는지 확인해보겠습니다.
현재 프로젝트에 적용된 TIME_ZONE은 America/Los_Angeles(UTC-8)입니다.
from .models import Reservation
reservation = Reservation.objects.last()
reservation.start_at
# datetime.datetime(2020, 12, 31, 20, 0)
Python
복사
분명히 예약 시작 시간을 12시로 전달했지만 실제 저장된 값을 확인해보면 20시로 출력됩니다.
그 이유는 naive datetime을 전달했기 때문입니다.
Django는 naive datetime에 대해 default timezone의 시간으로 취급하여 UTC로 보정한 20시를 저장합니다.
예를 들어 서울에 있는 사용자가 12월 31일 12시로 예약 시간을 입력 했는데 Asia/Seoul이라는 timezone 정보가 없다면, 이는 naive datetime으로 인식되어 UTC 12월 31일 20시로 데이터베이스에 저장되는 것 입니다.
실제 유저가 예약하고자 했던 서울의 12월 31일 12시는 UTC 12월 31일 03시입니다.
따라서 datetime을 저장할 때 반드시 정확한 timezone 정보를 가진 aware datetime을 전달해야 합니다.
2.3 naive to aware
그럼 naive datetime을 timezone aware하게 바꾸는 방법에 대해 알아보겠습니다.
timezone aware하게 만드는 방법은 아주 다양하지만 상황에 맞게 적절한 방법을 사용해야 합니다.
가장 대표적인 두 가지 방법을 알아보겠습니다.
1. localize()
python에서 timezone을 다룰 때 pytz라는 모듈을 사용합니다.
위에서 만든 naive_datetime을 pytz를 이용하여 timezone aware하게 바꿔보겠습니다.
import pytz
pytz.utc.localize(naive_datetime)
# datetime.datetime(2020, 12, 31, 12, 0, tzinfo=<UTC>)
Python
복사
변경된 값을 보면 시간은 그대로 12시지만 UTC라는 tzinfo가 추가 되었습니다.
localize()는 시간은 바꾸지 않고 해당 시간에 tzinfo만 추가해줍니다.
따라서 다음과 같이 하더라도 시간은 12시로 동일하고 tzinfo만 다르게 적용됩니다.
pytz.timezone('Asia/Seoul').localize(naive_datetime)
# datetime.datetime(2020, 12, 31, 12, 0, tzinfo=<DstTzInfo 'Asia/Seoul' KST+9:00:00 STD>)
Python
복사
2. astimezone()
만약 tzinfo를 추가하면서 시간도 timezone에 맞게 바꾸고 싶다면 astimezone()을 이용하면 됩니다.
as_utc = naive_datetime.astimezone(tz=pytz.utc)
# datetime.datetime(2020, 12, 31, 20, 0, tzinfo=<UTC>)
Python
복사
naive datetime이 UTC에 맞게 시간이 변경되고(+8) tzinfo도 추가 되었습니다.
이번에는 KST로 바꾸어보겠습니다.
PST(America/Los_Angeles) ~ UTC: 8시간 차이
UTC ~ KST(Asia/Seoul): 9시간 차이
PST(America/Los_Angeles) ~ KST(Asia/Seoul): 17시간 차이
as_kst = naive_datetime.astimezone(tz=pytz.timezone('Asia/Seoul'))
# datetime.datetime(2021, 1, 1, 5, 0, tzinfo=<DstTzInfo 'Asia/Seoul' KST+9:00:00 STD>)
Python
복사
마찬가지로 KST에 맞게 17시간이 더해지고 tzinfo가 추가 되었습니다.
2.4 django.utils.timezone
Django에는 timezone이라는 모듈이 있어 timezone aware한 datetime을 바로 생성할 수 있습니다.
from django.utils import timezone
utc_now = timezone.now()
# datetime.datetime(2020, 12, 4, 21, 36, 58, 297016, tzinfo=<UTC>)
Python
복사
timezone 모듈을 사용하여 생성한 datetime은 UTC를 기준으로 생성됩니다.
만약 UTC가 아닌 프로젝트 TIME_ZONE을 기준으로 datetime을 만들고 싶다면 다음과 같이 하면 됩니다.
local_now = timezone.localtime()
# datetime.datetime(2020, 12, 4, 14, 36, 58, 297016, tzinfo=<DstTzInfo 'America/Los_Angeles' PST-1 day, 16:00:00 STD>)
Python
복사
3. Execises
마지막으로 몇 가지 예시를 통해 상황에 맞게 datetime을 조작하는 방법을 알아보겠습니다.
프로젝트 기본 TIME_ZONE과 Reservation 모델은 이전과 동일하다고 가정하겠습니다.
1.
현재 시간을 예약 시작 시간으로 저장하는 경우
from django.utils import timezone
utc_now = timezone.now()
# datetime.datetime(2020, 12, 4, 21, 36, 58, 297016, tzinfo=<UTC>)
Reservation.objects.create(start_at=utc_now)
local_now = timezone.localtime()
# datetime.datetime(2020, 12, 4, 14, 36, 58, 297016, tzinfo=<DstTzInfo 'America/Los_Angeles' PST-1 day, 16:00:00 STD>)
Reservation.objects.create(start_at=local_now)
Python
복사
utc_now와 local_now는 각각 다른 timezone을 기준으로 계산되었지만 UTC로 바꾸면 같은 시간이기 때문에 저장했을 때 결과가 같습니다.
두 방법 모두 사용 가능합니다.
2.
서울 기준으로 2020년 12월 31일 12시로 입력 받은 datetime을 저장하는 경우
import pytz
kst = pytz.timezone('Asia/Seoul')
input_datetime = kst.localize(datetime.datetime(2020, 12, 31, 12, 0))
Reservation.objects.create(start_at=input_datetime)
Python
복사
서울 시간을 전달했지만 tzinfo를 같이 전달했기 때문에 UTC로 변경되어 저장됩니다.
3.
서울에 있는 유저의 예약 날짜를 서울 기준으로 알려줘야 할 때
import pytz
start_at = reservation.start_at
# datetime.datetime(2020, 12, 31, 20, 0, tzinfo=<UTC>)
start_at.astimezone(tz=pytz.timezone('Asia/Seoul')).date()
# datetime.date(2020, 1, 1)
Python
복사
UTC로 저장된 값을 불러와 timezone에 맞게 현지 날짜와 시간으로 바꿔준 뒤 날짜만 출력합니다.