본문 바로가기

일::개발

NAS 사진 정리하기 - (2) python에서의 시간대(timezone)

두번째 글은 원래 목적에서 약간 벗어나서 python 에서의 시간 처리에 대해 정리해본다.

 

python의 date, time object는 'aware' 와 'naive'로 구분할 수 있다. (docs.python.org/3/library/datetime.html)
요약하면 timezone과 DST(daylight saving time) 정보가 object 안에 명시되어 있으면 aware, 그냥 날짜, 시간 정보만 있으면 naive object로 구분되는 것이다.

 

주로 국내에서만 사용되는 stand alone application에서는 이 구분이 크게 중요하지 않을 수도 있다. 그러나 timezone 정보를 제대로 취급하는 시스템과 정보를 교환하거나, 여러 나라의 node와 시간 정보를 교환할 때 naive 정보를 사용하면 예상치 못한 오류를 만나게 된다.

 

간단하게 몇 가지 코드를 보면 문제가 보이기 시작한다.

우리나라에서 11월 20일 9시 45분에 아래의 코드를 실행하면 실행 환경의 로컬 시간이 timezone 정보 없이 반환된다.

>>> import datetime
>>> datetime.datetime.now()
datetime.datetime(2020, 11, 20, 9, 45, 58, 375247)    # timezone, dst 정보가 없는 naive datetime

 

컴퓨터의 시간대를 하와이로 바꾸고(이럴 때라도 가보자 ㅠㅠ) 실행하면

>>> datetime.datetime.now()    # 한국 시간 11월 20일 9시 55분
datetime.datetime(2020, 11, 19, 14, 58, 9, 13816)    # 하와이 시간 11월 19일 14시 55분

시간은 하와이 시간으로 나오지만, 시간대(timezone)에 대한 어떤 정보도 datetime object에 저장되어 있지 않다.

즉, 내가 시간대를 명시하지 않으면 naive object로 취급되는 것이다.

 

그럼 어떨 때 문제가 생길까?

시간 정보를 naive로 취급할 때 문제가 생기는 빈번한 예는

  • 시간대가 UTC로 설정된 DB에서 데이터를 가져오는 경우
  • UTC 타임을 반환하는 API를 호출하는 경우
  • 다른 시간대를 쓰는 클라이언트가 naive datetime을 전송하는 경우
  • time offset이 설정된 시간 문자열 (ex. '2019-11-20T09:45:58+00:00')에서 YYYY-MM-DDThh:mm:ss 만 datetime object로 변환하는 경우

게다가 naive object와 aware object는 선후 비교를 할 수 없다.

>>> datetime.datetime.now() < datetime.datetime.fromisoformat('2019-11-20T09:45:58+09:00')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: can't compare offset-naive and offset-aware datetimes

그렇다면 날짜, 시간을 취급할 일이 있을 때 모두 aware object로 취급하면 되는 것이 아닌가!
그렇다. 앞으로 모든 시간은 timezone 정보를 추가하여 aware 타입으로 사용하기로 하자!

그런데 어떻게?

소스에 timezone offset 정보가 존재하면 간단하다.

>>> datetime.datetime.fromisoformat('2019-11-20T09:45:58+09:00')
datetime.datetime(2019, 11, 20, 9, 45, 58, tzinfo=datetime.timezone(datetime.timedelta(seconds=32400)))

그럼 앞으로는 이렇게 timezone  정보를 다 넣어주자! 하고 끝내려니 또 문제가 남아있다.

 

datetime.tzinfo 는 utcoffset(), dst(), tzname(), fromutc() 함수를 구현해야 하는 abstract class로, 여기에 어떤 구현체를 넣느냐에 따라  변수가 생긴다. 

 

위의 예처럼 datetime.fromisoformat() 함수를 이용해서 datetime object를 만들면 datetime.timezone object가 tzinfo에 저장된다.

datetime.timezone class는 딱 필요한 것만 구현해놓았기 때문에 좀 더 편리하게 사용하려면 (ex. timezone string(ex. 'Asia/Seoul')을 이용해서 시간대 정보를 만들고자 하면) tzinfo의 다른 구현체(ex. pytz.timezone)를 이용하게 된다.

>>> # datetime.timezone 을 이용
>>> tz1 = datetime.timezone(datetime.timedelta(hours=9), 'Asia/Seoul')
>>> datetime.datetime.now().replace(tzinfo=tz1)
datetime.datetime(2020, 11, 23, 15, 45, 21, 392910, tzinfo=datetime.timezone(datetime.timedelta(seconds=32400), 'Asia/Seoul'))
>>> datetime.datetime.now().replace(tzinfo=timezone1).tzinfo
datetime.timezone(datetime.timedelta(seconds=32400), 'Asia/Seoul')

>>> # pytz.timezone.localize() 이용
>>> tz2.localize(datetime.datetime.now())
datetime.datetime(2020, 11, 23, 15, 48, 11, 806676, tzinfo=<DstTzInfo 'Asia/Seoul' KST+9:00:00 STD>)
>>> tz2.localize(datetime.datetime.now()).tzinfo
<DstTzInfo 'Asia/Seoul' KST+9:00:00 STD>

pytz를 이용해서 시간대 정보를 생성할 때는 가능하면 localize() 함수를 이용하는 것이 좋다.

 

아래의 예를 보면 pytz.timezone.localize() 가 어떻게 동작하는지 알 수 있다.

>>> naive_dt = datetime.datetime(2020, 5, 5, 12, 0)	# 시간대 정보 없는 naive datetime
>>> tz_seoul = pytz.timezone('Asia/Seoul')		# pytz.timezone object로 KST 생성
>>> tz_amsteram = pytz.timezone('Europe/Amsterdam')	# pytz.timezone object로 CEST 생성

>>> tz_seoul.localize(naive_dt)	# 5월 5일 12시에 올바르게 시간대(KST, +09) 정보 추가
datetime.datetime(2020, 5, 5, 12, 0, tzinfo=<DstTzInfo 'Asia/Seoul' KST+9:00:00 STD>)

>>> datetime.datetime(2020, 5, 5, 12, 0, tzinfo=tz_seoul)	# datetime 생성자에 pytz.timezone object 사용
datetime.datetime(2020, 5, 5, 12, 0, tzinfo=<DstTzInfo 'Asia/Seoul' LMT+8:28:00 STD>)
>>> naive_dt.replace(tzinfo=tz_seoul)	# naive datetime의 tzinfo 정보를 pytz.timezone으로 치환
datetime.datetime(2020, 5, 5, 12, 0, tzinfo=<DstTzInfo 'Asia/Seoul' LMT+8:28:00 STD>)
# 두 경우 모두 KST가 아닌 LMT로 들어감

>>> tz_seoul.localize(naive_dt) == naive_dt.replace(tzinfo=tz_seoul)
False

주의해야할 점은 datetime 생성자에 pytz.timezone 을 직접 할당하거나 tzinfo를 replace 하는 경우에 원하는 시간대(KST+09:00)가 아니라 LMT로 입력된다. (LMT는 1893년까지 사용되던 시간대란다.)

 

naive 데이터를 aware 데이터로 변경하기 위해 pytz.timezone을 사용하려면 꼭 localize() 를 이용하자.

이러면 끝인가 싶지만, aware 데이터의 시간대를 변경해야 하는 경우가 있다. (ex. UTC 시간을 KST로, KST 시간을 UTC로 변경하는 경우) 이럴 때에는 astimezone()을 이용한다.

 

한국시간 2020년 5월 5일 12시 0분을 UTC로 변환해보자.

>>> tz_seoul.localize(naive_dt)	# naive datetime을 aware datetime(KST)로 
datetime.datetime(2020, 5, 5, 12, 0, tzinfo=<DstTzInfo 'Asia/Seoul' KST+9:00:00 STD>)

>>> tz_seoul.localize(naive_dt).astimezone(pytz.UTC) # KST to UTC
datetime.datetime(2020, 5, 5, 3, 0, tzinfo=<UTC>)

>>> tz_seoul.localize(naive_dt).astimezone(pytz.UTC).astimezone(tz_seoul) # 다시 UTC를 KST로
datetime.datetime(2020, 5, 5, 12, 0, tzinfo=<DstTzInfo 'Asia/Seoul' KST+9:00:00 STD>)

>>> tz_seoul.localize(naive_dt).astimezone(pytz.UTC).astimezone(tz_seoul) == tz_seoul.localize(naive_dt)
True

aware datetime에 astimezone()을 사용하면 원하는 target 시간대로 잘 변경된다.

그런데, naive datetime에서 바로 astimezone()을 호출하면 어떻게 될까?

 

docs.python.org/3/library/datetime.html 에 나와 있듯이, naive datetime에 astimezone()을 호출하면 현재 시스템의 시간대로 인식하고 시간대를 변경한다. 즉, 시스템에 따라 naive datetime의 시간대를 다르게 부여하고, 그것을 다시 변환하는 것이다.

그러니, astimezone()을 이용해서 시간대를 변경할 때에는 꼭 원본 데이터가 aware 인 것을 확인하도록 하자.

 

일단 시간 정보를 다루는 방법은 여기까지!