서버에서 시간예약 및 시간관련 데이터를 다루는 경우, 기준을 어떻게 정의하고 활용하고 있는지를 정확하게 이해하는 것을 필요로 합니다. 사용자가 추가적인 설정을 하지 않으면, 컴퓨터는 UTC 시간대를 기준으로 시간 데이터를 연산 합니다.

이번에는 대한민국을 기준으로 Linux 서버 -> MariaDB -> Django 단계로 시간대 내용을 알아보도록 하겠습니다.


Host Server

Ubuntu 22.04 시간대 변경

리눅스에서 시간대를 확인하고 설정값을 변경하는 내용은 다음과 같습니다.

$ timedatectl list-timezones | grep Seoul 
Asia/Seoul

$ sudo timedatectl set-timezone Asia/Seoul
$ timedatectl  
      Local time: 토 2020-01-01 09:44:02 KST
    Universal time: 토 2020-01-01 00:44:02 UTC
      RTC time: 토 2020-01-01 00:44:02
      Time zone: Asia/Seoul (KST, +0900)
System clock synchronized: yes
      NTP service: active
    RTC in local TZ: no

MariaDB Timezone 변경

MariaDB 에서 현재 설정되어있는 시간대를 확인해 보겠습니다. 내 컴퓨터가 UTC로 되어있는 경우에는 MariaDB TimeZone of Django 문서를 참고하면 됩니다.

MariaDB root@localhost:mysql> SELECT @@global.time_zone, @@session.time_zone, @@system_time_zone;
+--------------------+---------------------+--------------------+
| @@global.time_zone | @@session.time_zone | @@system_time_zone |
+--------------------+---------------------+--------------------+
| SYSTEM             | SYSTEM              | KST                |
+--------------------+---------------------+--------------------+

1 row in set
Time: 0.014s


Django

DataBase 정의된 시간대와 Django 시간대가 어긋나면 다음과 같은 오류를 출력합니다. 앞에서 진행한 MariaDB(MySQL) 의 시간대가 Django 에서 정의한 시간대와 일치하지 않으면 발생합니다.

ImproperlyConfigured: Connection 'default' cannot set TIME_ZONE because USE_TZ is False.
ValueError: MySQL backend does not support timezone-aware datetimes when USE_TZ is False.

settings.py

서버와 DB 시간대 설정을 앞의 내용과 같이 작업을 완료한 뒤, Django 에서 다음과 같이 설정값을 정의 합니다. 이제는 제대로 동작하는 것을 확인할 수 있습니다.

# settings.py
TIME_ZONE = 'Asia/Seoul'
USE_I18N = True
USE_TZ = False


DateTime

datetime (naive vs aware)

Python 의 datetime 객체는 다음과 같이 2가지 종류 가 있습니다.

  • naive 객체 : tzinfo 속성이 설정되지 않는 datetime 객체
  • aware 객체 : tzinfo 속성이 설정된 datetime 객체

아래의 코드는 2가지 객체를 각각 출력한 것으로 Out[1] 이 시간대 정의가 없는 naive, Out[2]aware 객체 입니다.

In [1]: from app.models import TimeTable
   ...: item = TimeTable.objects.first()
   ...: item.datetime
Out[1]: datetime.datetime(2023, 1, 11, 15, 30)

In [2]: from django.utils import timezone
   ...: timezone.make_aware(item.datetime)
Out[2]: datetime.datetime(2023, 1, 11, 15, 30, tzinfo=zoneinfo.ZoneInfo(key='Asia/Seoul'))

ORM of Datetime

앞에서 알려드린대로 USE_TZ = False 정의를 한 경우에는 datetime naive 객체 만 활용하면 원활하게

In [3]: from datetime import datetime
        date_raw = datetime(2024,7,26,15,0,0)
        date_raw
Out[3]: datetime.datetime(2024, 7, 26, 15, 0)

In [4]: from app_stock.models import PriceDatetime
        from datetime import datetime
        _dt = datetime(2024,7,26,1,0,0)
        PriceDatetime.objects.filter(datetime__gte=_dt).count()
Out[4]: 8034

ORM of Datetime with TZ_info

만약 Time Zone 정보를 필요로 한다면 다음의 코드내용을 참고하여 진행하여야 합니다. 하지만 이러한 내용을 찾아보고 있다면 무언가 문제가 생겼다고 생각하고 앞의 내용들을 참고하여 설정값을 해결하는 것이 보다 확실한 해답이 되어줄 것입니다.

In [5]: import pytz
        from django.conf import settings
        from app_stock.models import PriceDatetime
        tz_txt  = settings.TIME_ZONE
        tz_info = pytz.timezone(tz_txt)
        date_tz = tz_info.localize(_date_raw)
        date_tz
Out[5]: datetime.datetime(2024, 7, 26, 15, 0, tzinfo=<DstTzInfo 'Asia/Seoul' KST+9:00:00 STD>)

Django Timezone with Datetime

Django 내부의 Admin 이나, API 작업을 할 때에도 시간대 문제가 발생할 수 있는데, 이러한 경우에는 추가 설정 내용을 참고하면 원하는 결과를 얻을 수 있습니다.

# admin.py
class Model(models.Model):
    datetime = models.DateTimeField()
    code = models.ForeignKey(Code, on_delete=models.CASCADE)
    
    def __str__(self) -> str:
        from django.utils import timezone
        time_date = timezone.make_naive(self.datetime)
        return str(self.datetime) + " | " + self.code.name

Rest Api 에서도 시간대 내용을 활용할 수 있습니다

from pytz import timezone
from django.conf import settings
seoul = timezone(settings.TIME_ZONE) 
# <DstTzInfo 'Asia/Seoul' LMT+8:28:00 STD>

item = Model.objects.all()[0].datetime # tzinfo:utc
item.astimezone(seoul)  # DstTzInfo:'Asia/Seoul'

import datetime
from django.utils.timezone import now
now().astimezone(seoul) # DstTzInfo:'Asia/Seoul'
datetime.datetime.now().astimezone(seoul) # DstTzInfo 'Asia/Seoul'
now().astimezone(seoul).utcnow() # tzinfo:utc