python - 날짜와 시간 다루기, datetime 모듈 해부! 그리고 third-party

정현우·2023년 6월 17일
4
post-thumbnail
post-custom-banner

[ 글의 목적: python datetime 구조, 다양한 시간대를 다루는 방법과 활용법을 제대로 알고 통일된 시간대 적용과 국제화 서비스 관점에서 바라보기 ]

Python 날짜와 시간 다루기

프로젝트에서 "시간대"를 다루는 것은 굉장히 중요하다. 특히 batch 관점에서는 cron 성격의 "실행시간" 이 달라지는 것은 엄청나게 치명적일 수 있다. 그리고 timezone에 따라 다르게 보여주는 시간값, 절대 시간값 등은 data의 created, updated 시간에도 아주 지대한 영향을 준다. 이제 python 시간대를 매번 다르게 사용하지 말고, 정확하게 알고 사용하자!

🔥 python datetime module 공식 문서 기준으로 작성했습니다! 해당 페이지에서 original code를 많이 참조했습니다.

1. 컴퓨터 날짜와 시간

  • 날짜와 시간은 생각보다 많은 세월동안 아주 다양한 형태로 수정되고, 표현되어 왔다. 당연히 해당 주제 자체가 빛으로 시작해 지구 과학까지 이어진다. digital time을 기억해야하는 컴퓨터는 기본적으로 날짜와 시간을 표현하는 방법이 정해져 있지 않았고 OS마다 다른 시간 표현 방법이 안착되었다.

  • 시간 표현의 시작은, 윈도우는 1601-01-01 00:00:00 부터 현재 시간까지 몇 100ns 떨어져 있는지를 8바이트로 나타내기 시작했고, 유닉스와 리눅스는 1970-01-01 00:00:00부터 현재 시간까지의 초를 누적한 시간을 사용했다. 이렇게 표현되는 시간을 "timestamp" 라고 한다.

  • 자세한 내용은 한빛미디어의 타임스탬프, 그리고 파이썬으로 날짜와 시간 다루기 칼럼을 추천한다. 그리고 OS별 시스템 시간 에서 OS별로 시간을 어떻게 표현하는지 살펴볼 수 있다.

1) UTC Time

  • UTC(Universal Time Coordinated)는 "협정 세계시" 정도로 해석이 된다. 1972년 1월 1일부터 시행된 국제 표준시이며, 1970년 1월 1일 자정을 0 밀리초로 설정하여 기준을 삼아 그 후로 시간의 흐름을 밀리초로 계산한다. UTC는 국제원자시와 윤초 보정을 기반으로 표준화되었다. (출처: wiki)

  • UTC는 그리니치 평균시(GMT)에 기반하므로 GMT로도 불리기도 하는데, UTC와 GMT는 초의 소숫점 단위에서만 차이가 나기 때문에 일상에서는 혼용된다. 기술적인 표기에서는 UTC가 사용된다. (출처: wiki)

  • UTC를 기준으로 시간이 빠르면 +시차, 시간이 느리면 -시차로 표시한다. (클릭) wiki에서 나라별 UTC기준 시간차를 확인해보자!

2) (unix) timestamp, 에코프(Epoch) 시간

  • 위에서 언급했듯이 유닉스 타임스탬프(Unix Timestamp)는 1970년 1월 1일 00:00:00 UTC부터 경과한 시간을 초 단위로 나타낸 정수 값이다. 이를 통해 전 세계 컴퓨터 시스템에서 일관된 시간을 표기할 수 있다.

  • 대표적으로 DBMS에서 timestamp로 저장된 값은 UTC 기준이라 전세계 어디서든 timezone 기반으로 그에 맞는 시간대로 표현해줄 수 있다. 여담으로 일반적인 날짜와 시간 표현보다 작은 공간을 차지한다. 그래서 저장 공간을 절약할 수 있다.

  • 컴퓨터 시스템 간 시간을 표현하고 처리하는 표준이 필요했고 개발자들은 1970년 1월 1일을 새로운 시간 표현의 시작점으로 선택해서 유닉스 타임스탬프의 기준이 1970년 1월 1일 00:00:00 UTC로 정해졌다고 알려져있다.

epoch time

  • epoch는 "특정 기간이나 사건의 시작점으로 간주되는 시간의 순간" 정도로 정의 되는 영어 단어다. "기준 지점을 선택하고 해당 지점 이후 경과된 시간" 을 에코프 타임이라고 한다. 대표적인 예시가 timestamp인 것이다.
  1. 실제 세계의 Epoch 시간은 00:00:00 UTC 시간에 시작
  2. Apple macOS는 Epoch Time을 1904년 1월 1일부터 시작하는 것으로 간주
  3. Microsoft Windows는 Epoch 시간을 1601년 1월 1일부터 시작하는 것으로 간주
  4. Unix 및 Linux 시스템은 Epoch 시간을 1970년 1월 1일부터 시작하는 것으로 간주
  • Unix 시간은 32비트 부호 있는 정수 값으로 저장된다. 매 초마다 값이 계속 증가하기 때문에 32비트 부호 있는 정수 값이 오버플로될 가능성이 있다. 즉, 2038년 1월 19일 03:14:07에 제한에 도달하게 되어 있다. Linux 커널은 버전 2.6.19의 epoch 이후 경과된 초 수를 저장하기 위해 이 64비트 값을 도입했다.

3) Server Time, 컴퓨터가 시간을 다루는 방법 RTC

  • 하드웨어의 시스템 클럭(CPU를 비롯한 컴퓨터의 모든 부품들은 특정한 신호에 맞추어 동작을 하는데, 이 특정한 "신호"를 클럭이라 한다.)을 이용한다. 시스템 클럭은 "RTC(Real Time Clock)라는 모듈(하드웨어)" 을 사용한다.

  • RTC는 메인보드에 붙어있어 전원을 끄더라도 계속 작동한다. 그러므로 컴퓨터 전원을 끄더라도 시간은 계속 흘러간다. RTC는 카운터 회로를 통해 클럭을 발생시킨다. 특정 시각(Epoch)을 기준으로 시스템 클럭의 틱을 세는 것으로 구현하는데 이를 시스템 시간이라 부른다.

  • python을 예시로 들자면, datetime.now()는 (사용 가능하다면) C의 gettimeofday(...) 를 사용하며 gettimeofday는 커널로 부터 system time을 가져온다. (shell로 예시를 들자면, date로 입력한 값) 즉, local time은 RTC로 계산된 시간이며, 해당 코드는 OS를 통해 해당 값을 가져오게 되는 것이다.

  • 결국 UTC를 사용하지 않고, server의 timezone을 그대로 따라가며 날짜와 시간을 가져온다면, 코드가 어떤 OS에서 실행되냐에 따라 또 값이 달라진다. 그래서 우리는 이 시간을 "확실한 기준을 가지고 다룰" 필요가 있다.

  • 물론 "네트워크 타임 프로토콜(NTP)"에 대한 얘기가 나올 수 있으나, 여기서는 하지 않겠다. 네트워크 시간 프로토콜 글을 추천한다.


2. python datetime module, 시간 다루는 module

  • datetime module은 날짜와 시간을 조작하는 아주 다양한 class를 내포하고 있다. 그리고 모듈과 이름이 같은 datetime class는 개인적으로 열받는다

  • 대표적으로 date, time, datetime, timedelta, timezone 등이 있다.

1) "Naive" datetime vs "Aware" datetime

  • datetime은 크게 어웨어와 나이브로 나뉘어 진다. 정보를 얼마나 포함하고 있냐에 따라 형태가 달라진다.

나이브 객체

  • 나이브는 날짜와 시간 정보만 가지고 있다. 자신과 다른 날짜/시간 객체의 상대적인 위치를 파악할 수 있는 충분한 정보를 포함하지 않는다. 나이브 객체가 UTC(Coordinated Universal Time), 지역 시간 또는 다른 시간대의 시간 중 어느 것을 나타내는지는 "프로그램에 달려있다".

  • 예를 들어 datetime.now() 는 컴퓨터 시간 그 자체 값만 가져오며, timezone에 대한 정보가 없어 어떤 시간인지 파악할 수 없다.

어웨어 객체

  • 그에 반해 어웨어는 "자의적으로 해석할 여지 없는 특정 시간"을 나타낸다. 컴퓨터 시간으로 부터 날짜, 시간 정보 뿐 아니라 timezone에 대한 정보도 같이 있어서 "정확하게 어떤 시간" 인지 파악할 수 있다.

  • 그에 따라서 다른 어웨어 객체와의 상대적인 위치를 파악할 수 있다. 즉, timezone info도 있기 때문에 KR timezone과 KR timezone 끼리 연산 뿐 아니라 다른 timezone의 경우는 utc 기준으로 연산을 하여 다시 적합한 timezone으로 표현이 가능하다.

  • 어웨어 객체가 필요한 응용 프로그램을 위해, datetime 과 time 객체에는 추상 tzinfo 클래스의 서브 클래스 인스턴스로 설정할 수 있는 선택적 시간대 정보 어트리뷰트인 tzinfo 가 있다. 이러한 tzinfo 객체는 UTC 시간으로부터의 오프셋, 시간대 이름 및 일광 절약 시간이 적용되는지에 대한 정보를 보관한다.

2) class datetime.date(year, month, day)

  • 모든 인자가 필수이며 아래 범위에 있는 값만 유효하고, 범위가 벗어나면 ValueError를 raise한다.
    • MINYEAR <= year <= MAXYEAR (datetime module 상수값)
    • 1 <= month <= 12
    • 1 <= day <= 주어진 month와 year에서의 날 수
>>> test = datetime.date(2023, 3, 3)
>>> test
datetime.date(2023, 3, 3)
>>> type(test)
<class 'datetime.date'>

기본 상수 값 및 attribute

  • 기본적으로 year, month, day attribute 모두 접근 가능하며 모두 int 이다.

  • 그리고 date class instance는 "min", "max"라는 attribute가 있다. 그 외 datetime module이 가지는 상수값도 있다.

>>> test.min
datetime.date(1, 1, 1)
>>> test.max
datetime.date(9999, 12, 31)
>>> datetime.MINYEAR
1
>>> datetime.MAXYEAR
9999

date.weekday() & isoweekday()

  • weekday method로 datetime instance의 "요일"을 가져올 수 있다.

  • 월요일 0 / 화요일 1 / 수요일 2 / ... / 일요일 6, isoweekday 의 경우 월요일은 1이고 일요일은 7. iso format은 국제 표준이다. ISO 8601

>>> test.weekday()
4 # 금요일이 맞다!
>>> test.isoweekday()
5 # 금요일이 맞다!

date.isoformat()

  • ISO 8601 형식으로 날짜를 나타내는 문자열을 반환합니다, YYYY-MM-DD .
>>> test.isoformat()
'2023-03-03'
  • 개인적으로 날짜를 표현할때 무조건 iso formatting으로 협의하는걸 선호한다. 그래야 사용자, FE, BE 모두가 평안해지는 길이라 생각한다..

date.replace(year=int, month=int, day=int)

  • 새로운 값이 주어진 매개 변수들을 제외하고, 같은 값을 가진 date를 return한다. original 변수 값 변경 없이 replace로 다른 date instance 만들기 편하다.
>>> result_replace = test.replace(year=2022, month=11)
>>> test
datetime.date(2023, 3, 3)
>>> result_replace
datetime.date(2022, 11, 3)
  • 그 외는 official docs로 대체하며 날짜 연산과 현재 시간(today or now)는 datetime을 보고 더 살펴보자!

3) class datetime(date)

  • date class를 상속받으며 그렇기 때문에 인스턴스를 만들때 year, month, day 값을 필수로 입력받는다. 시, 분, 초 time부분은 다 0이 default 값 세팅으로 된다.
>>> test = datetime.datetime(2023, 3, 3)
>>> type(test)
<class 'datetime.datetime'>
>>> test
datetime.datetime(2023, 3, 3, 0, 0)
  • "시간"이 들어가는 순간부터 timezone에 대한 내용은 더 중요 해진다.

datetime.now()

  • 아래는 cpython 에서 datetime module의 실제 코드다.
import time as _time

...(생략)

@classmethod
def now(cls, tz=None):
	"Construct a datetime from time.time() and optional time zone info."
    t = _time.time()
    return cls.fromtimestamp(t, tz)
  • 여기가 시스템 시간을 가져오는 부분이다. (today도 동일하다)

  • time module의 time() 호출 -> cpython을 통해 C의 libc library의 <time.h> -> time_t 구조체 (여기서 너무 상세하게 접근하면 C자체에 대한 얘기가 되어버린다,, 가능하다면 c의 gettimeofday를 사용도 한다.) 를 통한 unix timestamp 값 을 가져온다.

  • date.today 역시 동일하다. 결국 위와 같은 과정을 거치면서 date부분만 가져와 return date(...)가 되는 것이다. 그리고 today method도 datetime class instance 역시 사용 가능하다. (class method)

  • fromtimestamp_fromtimestamp 를 호출하고 utc unix timestamp 값으로 가져온 값을 적절한 형태들로 casting 한 뒤 return한다.

datetime.timestamp()

  • 아래 코드 역시 cpython 에서 datetime module의 실제 코드다.
def timestamp(self):
	"Return POSIX timestamp as float"
	if self._tzinfo is None:
		s = self._mktime()
			return s + self.microsecond / 1e6
	else:
		return (self - _EPOCH).total_seconds()
  • 위 method로 생성한 datetime class instance의 timestamp값을 가져올 수 있다.
>>> test
datetime.datetime(2023, 3, 3, 0, 0)
>>> type(test)
<class 'datetime.datetime'>
>>> test.timestamp
<built-in method timestamp of datetime.datetime object at 0x7fdcc09349c0>
>>> test.timestamp()
1677769200.0
>>> type(test.timestamp())
<class 'float'>

datetime.combine(date object, time object)

  • class method 이며, date class instance와 time class instance를 합쳐서 datetime으로 만들 수 있다. 인자 역시 해당 인스턴만 받는다.
>>> date = datetime.date(year=2022, month=10, day=4)
>>> time = datetime.time(hour=5, minute=3, second=55)
>>> datetime2 = datetime.datetime.combine(date, time)
>>> datetime2
datetime.datetime(2022, 10, 4, 5, 3, 55)
>>> type(datetime2)
<class 'datetime.datetime'>
  • 내부 코드를 보면 그냥 datetime class의 생성자를 호출하는 격이다.

datetime.isoformat(sep='T', timespec='auto')

  • 역시 isoformat으로 표현해주는 method가 있는데, 이 경우 시분초 마이크로초 까지 포함이 된다.
    • microsecond가 0이 아니면: YYYY-MM-DDTHH:MM:SS.ffffff
    • microsecond가 0이면: YYYY-MM-DDTHH:MM:SS
>>> datetime.datetime(2019, 5, 18, 15, 17, 12, 23435).isoformat()
'2019-05-18T15:17:12.023435'
>>> datetime.datetime(2019, 5, 18, 15, 17).isoformat()
'2019-05-18T15:17:00'
>>> datetime.datetime(2019, 5, 18, 15, 17, tzinfo=datetime.timezone.utc).isoformat()
'2019-05-18T15:17:00+00:00'
>>> datetime.datetime(2019, 5, 18, 15, 17, 12, 23435, tzinfo=datetime.timezone.utc).isoformat()
'2019-05-18T15:17:12.023435+00:00'
  • 역시 timezone 여부에 따라 formatting이 조금 상이해진다. UTC timezone으로 세팅하면 조금 포멧팅이 달라진다.

    • microsecond가 0이 아니면: YYYY-MM-DDTHH:MM:SS.ffffff+HH:MM[:SS[.ffffff]]
    • microsecond가 0이면: YYYY-MM-DDTHH:MM:SS+HH:MM[:SS[.ffffff]]
  • 추가로 date를 상속받으니 위에서 살펴보면 date method들은 다 사용이 가능하다.

4) 날짜 연산하기 & class timedelta

  • 예를 들어서 특정 날짜를 기준으로 20일 후, 3시간 전의 날짜 정보를 얻고 싶다고 가정해보자. 그럴때 timedelta class instance를 활용하면 된다.

  • def __new__(cls, days=0, seconds=0, microseconds=0, milliseconds=0, minutes=0, hours=0, weeks=0) 가 timedelta class의 생성자다.

>>> test + datetime.timedelta(days=20)
datetime.datetime(2023, 3, 23, 0, 0)
>>> test - datetime.timedelta(days=20)
datetime.datetime(2023, 2, 11, 0, 0)
>>> test + datetime.timedelta(days=20, hours=3)
datetime.datetime(2023, 3, 23, 3, 0)
>>> test + datetime.timedelta(days=20, hours=3, weeks=1)
datetime.datetime(2023, 3, 30, 3, 0)

위 isoformat에서 timezone 활용

  • timezone은 tzinfo의 sub class이며, 생성자로 "offset" 값을 받는데, 해당 값의 instance는 timedelta class instance 여야 한다.

  • 즉 이 부분을 활용해 우리는 자체 timezone을 세팅할 수 있다. 위 isoformat에 예시를 보자면 아래와 같다.

>>> datetime.datetime(2019, 5, 18, 15, 17, 12, 23435, tzinfo=datetime.timezone(datetime.timedelta(hours=9))).isoformat()
'2019-05-18T15:17:12.023435+09:00'
  • datetime.timezone(datetime.timedelta(hours=9)) 를 통해 시차가 +9가 되는 timedelta를 만들어서 timezone 세팅을 하면 위와 같은 표현식이 된다. 즉 한국/도쿄 시간대와 같은 timezone임을 명시할 수 있다.

두 datetime 또는 date object끼리 연산

  • datetime, date class모두 __eq__, __ge__ 등의 연산자 dunder method가 다 정의가 되어 있기 때문에 기본적인 덧셈 뺄셈 동등 연산이 가능하다.
>>> test_a = datetime.datetime(2023, 2, 10)
>>> test_b = datetime.datetime(2023, 3, 3, 12)
>>> test_a
datetime.datetime(2023, 2, 10, 0, 0)
>>> test_b
datetime.datetime(2023, 3, 3, 12, 0)
>>> test_a - test_b
datetime.timedelta(days=-22, seconds=43200)
  • datetime과 date 끼리는 연산이 불가능하다.

5) pytz module, timezone 세팅하기

  • 이제 진짜 timezone을 세팅해보자. 가장 사용하기 좋은 방법은, datetime module과 같이 쓰기 좋은 모듈인 pytz 를 사용하는 것이다. (설치를 해야한다)
import datetime
import pytz

# 한국 시간대를 나타내는 `Asia/Seoul` 시간대 객체 생성
korea_tz = pytz.timezone("Asia/Seoul")

# 현재 날짜와 시간을 한국 시간대로 가져오기
now = datetime.datetime.now(korea_tz)

# 시간대 정보가 포함된 현재 날짜와 시간 출력
now
# datetime.datetime(2023, 6, 18, 1, 5, 41, 194961, tzinfo=<DstTzInfo 'Asia/Seoul' KST+9:00:00 STD>)
  • datetime class의 now class method는 tz(timezone)을 받는다. 그리고 tz는 class tzinfo 의 instance여야 한다. 해당 class의 time값을 조절하게 되는 핵심 값이 "utcoffset" 값이다.

  • 이런 부분을 활용해서 나만의 시간대를 만들 수 있다,, UTC기준 +9h35m 인 아찔한 나만의 시간대가 만들어진다.

>>> my_time_zone = datetime.timezone(datetime.timedelta(hours=9, minutes=35))
>>> datetime.datetime.now(my_time_zone)
datetime.datetime(2023, 6, 18, 1, 45, 21, 290185, tzinfo=datetime.timezone(datetime.timedelta(seconds=34500)))
  • timezone class instance는 "UTC로부터의 고정 오프셋으로 정의된 시간대"를 나타낸다. 이 custom timezone을 활용해서 바로 datetime class instance는 아래와 같이 만들 수 있다.
>>> test = datetime.datetime(2023, 3, 3, tzinfo=my_time_zone)
>>> test
datetime.datetime(2023, 3, 3, 0, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=34500)))
>>> print(test)
2023-03-03 00:00:00+09:35

pytz를 이용한 Naive vs Aware

  • 위에서 살펴본 바와 같이 timezone이 없는 datetime.now는 "나이브" 하다. 하지만 kr timezone을 가지고 생성한 datetime.now(korea_tz)은 "어웨어" 하다. 그 두개는 다를까? (초까지는 날리고 비교해보자)
korea_tz = pytz.timezone("Asia/Seoul")

now_naive = datetime.datetime.now()
now_aware = datetime.datetime.now(korea_tz)
now_naive = now_naive.replace(microsecond=0)
now_aware = now_aware.replace(microsecond=0)

# runtime 에서 비교해보기
>>> now_naive
datetime.datetime(2023, 6, 18, 2, 20, 48)
>>> now_aware
datetime.datetime(2023, 6, 18, 2, 20, 48, tzinfo=<DstTzInfo 'Asia/Seoul' KST+9:00:00 STD>)
>>> now_naive == now_aware
False

pytz 기본 제공 timezone

  • pytz.all_timezones 를 통해 기본 제공하는 timezone을 다 가져올 수 있다. print를 찍으면 굉장히 많으니,, 조심,, 대표적으로 아래 10가지가 있다.
  1. UTC (Coordinated Universal Time): 세계 표준 시간대로, 시차가 없는 기준 시간
  2. GMT (Greenwich Mean Time): 그리니치 평균 시간으로, 국제 표준 시간의 기준
  3. Europe/Amsterdam: 네덜란드의 암스테르담 시간대
  4. Europe/Athens: 그리스의 아테네 시간대
  5. Europe/Berlin: 독일의 베를린 시간대
  6. Europe/London: 영국의 런던 시간대
  7. America/New_York: 미국 동부 시간대
  8. America/Los_Angeles: 미국 서부 시간대
  9. Asia/Tokyo: 일본의 도쿄 시간대
  10. Asia/Seoul: 한국의 서울 시간대

6) strftime(...), strpime(...)

대표적으로 아래 포멧팅이 있다.

FormatExplanationExample
%yYear without century as zero-padded decimal00, 01, ..., 99
%YYear with century as decimal number0001, 0002, ..., 2013, 2014, ..., 9998, 9999
%mMonth as zero-padded decimal01, 02, ..., 12
%dDay of the month as zero-padded decimal01, 02, ..., 31
%HHour (24-hour clock) as zero-padded decimal00, 01, ..., 23
%MMinute as zero-padded decimal00, 01, ..., 59
%SSecond as zero-padded decimal00, 01, ..., 59

strftime

  • date, datetime 및 time 객체는 모두 strftime(format) 메서드를 지원하여, 명시적 포맷 문자열로 제어된 시간을 나타내는 문자열을 만든다. (즉 python datetime module class의 instance to string)
>>> test = datetime.datetime(2023, 3, 3)
>>> test.strftime("%d/%m/%y")
'03/03/23'
>>> test.strftime("%A %d. %B %Y")
'Friday 03. March 2023'
>>> test.strftime("%Y, %m, %d, %H, %M, %S")
'2023, 03, 03, 00, 00, 00'

strpime

  • 위와 반대의 역할이다. string으로 표현된 날짜를 datetime class instance로 바꾸는 함수다. 물론 datetime class method이다.
from datetime import datetime

date_string = "2023-06-17"
date_format = "%Y-%m-%d"
parsed_date = datetime.strptime(date_string, date_format)
# datetime.datetime(2023, 6, 17, 0, 0)

time_string = "10:30:45"
time_format = "%H:%M:%S"
parsed_time = datetime.strptime(time_string, time_format)
# datetime.datetime(1900, 1, 1, 10, 30, 45)

datetime_string = "2023-06-17 10:30:45"
datetime_format = "%Y-%m-%d %H:%M:%S"
parsed_datetime = datetime.strptime(datetime_string, datetime_format)
# datetime.datetime(2023, 6, 17, 10, 30, 45)

timezone_string = "2023-06-17 10:30:45 +0800"
timezone_format = "%Y-%m-%d %H:%M:%S %z"
parsed_timezone = datetime.strptime(timezone_string, timezone_format)
# datetime.datetime(2023, 6, 17, 10, 30, 45, tzinfo=datetime.timezone(datetime.timedelta(seconds=28800)))
  • strptime 클래스 메서드를 사용할 때는 문자열에 맞는 형식 문자열을 사용자가 제공해야 한다. 그러나 dateutil 패키지의 parse 함수를 쓰면 자동으로 형식 문자열을 찾아 datetime 클래스 객체를 만들어 준다.

  • dateuitl은 여기 블로그 글 에서 사용 예시를 살펴보길 바란다.

7) 프로젝트에서는 어떻게 관리하는게 좋을까

  • 일단 어떤 프로젝트에서든 시간에 대한 규칙을 정하는게 좋다. 무지성 datetime.datetime.now 는 무조건적으로 지양해야 한다. "기준을 꼭 잡아야한다." 이런식으로 사용하면, 이제 알 수 있듯이, 배포되는 서버마다(리전이 다르거나 설정이 다르다면) now가 달라지는 기적을 볼 수 있다.

  • 즉, 일단 "보관되는 데이터"는 무조건 나이브객체는 피하고 "어웨어" 중심으로 가야한다. 그리고 시간이 중심이 되는 action의 경우도, 특히 batch, "어웨어" 해야한다.

  • 다국적, 장기보관의 목적이 되는 경우는 당연히 "어웨어"하되 기준시를 UTC 자체로 잡는 것이 좋다. 그래야 다양하게 표현할 수 있다.

django project

# settings.py
LANGUAGE_CODE = "ko-kr"  # 언어 - 국가 설정
USE_TZ = True  # 장고 시간대
TIME_ZONE = "Asia/Seoul"  # 시간대
USE_I18N = True  # 국제화 -> Internationalization
USE_L10N = True  # 지역화 -> localization
  • django는 기본적으로 settings.py 에서 TIME_ZONE = "Asia/Seoul" 세팅값이 있을 것이다. 이 것이 project의 기본이되는 timezone이다.

  • 그리고 "시간"을 사용할때 위 timezone을 사용하기 위해 from django.utils import timezone 를 가져와서 timezone.localdate() 또는 timezone.localtime() 를 사용하자.

  • 전자는 timezone 기준 datetime.datetime 객체를 get_default_timezone를 활용해 return하고, 후자는 전자를 date() 로 바꾼값을 준다.

>>> from django.utils import timezone
>>> timezone.localdate()
datetime.date(2023, 6, 18)
>>> timezone.localtime()
datetime.datetime(2023, 6, 18, 12, 55, 56, 305679, tzinfo=backports.zoneinfo.ZoneInfo(key='Asia/Seoul'))
>>> timezone.now()
datetime.datetime(2023, 6, 18, 3, 56, 40, 90003, tzinfo=datetime.timezone.utc)

fastAPI / Flask

  • 다양한 형태로 접근 가능하다. UTC로 저장된 DB timestamp를 처리하기 위해 자체 middleware를 만들어서 response줄 때 timezone을 injection하기 위해 replace를 사용한다던가, 해당 timezone을 app 변수에 등록해서 활용한다던가 형태 정도가 된다.

  • 사실 다른 프로젝트와 크게 다르지 않다. 가장 추천하는 방법은 그냥 django처럼 util함수를 만들어버리는 것이다. 그래서 어디서든 global하게 세팅해둔 상수 timezone을 기반으로 datetime을 핸들링하도록 하자

8) 각종 공휴일 정보는?!

  • 한국에서는 "pytimekr" 라는 모듈이 있다. 하지만 대체 공휴일이나 다양한 형태의 공휴일을 모두 대응하는데에는 한계가 있다.

  • 그래서 "공공데이터포털" 을 활용하는 것이다. 많은 사람들이 OPEN API를 통해 "한국천문연구원_특일 정보" api를 사용하고 있다. 대체 공휴일을 대비해서 Weekly Batch 정도로 해당 open api가 제공하는 공휴일 정보를 DB에 update해서 사용하는게 가장 효율적이다.

  • https://www.data.go.kr/data/15012690/openapi.do 에서 확인할 수 있다.

각국 공휴일 정보는?

  1. Nageru.Date API (https://date.nager.at/): Nageru.Date API는 전 세계 다양한 국가의 공휴일 정보를 제공한다. RESTful API 형식으로 공휴일 데이터에 접근할 수 있다.

  2. Calendarific API (https://calendarific.com/): Calendarific API는 전 세계 국가의 공휴일과 휴일 정보를 제공한다. RESTful API로 액세스할 수 있으며, 휴일 날짜, 국가별 공휴일 목록, 휴일 유형 등의 데이터를 얻을 수 있다.

  3. Google Calendar API (https://developers.google.com/calendar): Google Calendar API는 Google 캘린더를 통해 전 세계 다양한 국가의 공휴일 정보를 얻을 수 있다. API를 사용하려면 Google 계정과 API 키를 생성해야 한다.

  4. HolidayAPI (https://www.holidayapi.com/): HolidayAPI는 전 세계 국가의 공휴일 정보를 제공하는 상용 API다. RESTful API로 액세스할 수 있으며, 국가, 연도, 월 등의 매개변수를 사용하여 원하는 공휴일 데이터를 검색할 수 있다.

  • 가장 추천할 수 있는 api는 (3), (4) 정도가 될 것 같다.

마무리 & 출처

  • 많이 언급했지만, 결국 "시간"을 다루는 것은 중요하다. 그리고 한 번 꼬인 시간은 되돌릴 수 없다. 혹시나 지금 "나이브"한 객체만 사용하고 있다면, 언젠간 시간대가 무너져 내리는 때가 올것이다. 그래서 시작부터 세팅을 잘 해두는것이 좋다.

  • 다양한 서버에 배포, 다국적 서비스, 다양한 사람이 마음대로 사용하는 datetime 이런 것들이 project를 무조건 병들게 한다. django와 같이 "공통된 time setting을 기반으로 무조건적으로 "어웨어"한 datetime object를 사용하게끔" 꼭! 세팅하자.

  • 공휴일 역시 중요하다. 우리의 배치가 공휴일에 따라 달라진다면, 또는 공휴일에 다른 정보를 제공해야 한다면, 해당 공휴일이 다국적 서비스일때라면? 이 경우를 대비하기 위해 Weekly 로 공휴일을 update하는 것을 추천하며, 웬만하면 필요할때마다 실시간 API (unstable, network output 트래픽) 대신 db에 저장해서 사용하자.

profile
도메인 중심의 개발, 깊이의 가치를 이해하고 “문제 해결” 에 몰두하는 개발자가 되고싶습니다. 그러기 위해 항상 새로운 것에 도전하고 노력하는 개발자가 되고 싶습니다!
post-custom-banner

0개의 댓글