MySQL 타임존

강정우·2026년 6월 9일

DB

목록 보기
32/33
post-thumbnail

Django + MySQL 타임존: __date 필터가 로컬에서만 0건 나오는 이유

DB 에 저장은 됐는데 조회해보면 로컬은 0건. 코드 버그처럼 보였지만 범인은 로컬 MySQL의 타임존 테이블이었다. 그 과정을 디버깅하며 정리한 타임존 개념 노트.

TL;DR

  • CONVERT_TZ설정이 아니라 시각 변환 계산기다. DB에 저장된 값을 바꾸지 않고, 읽을 때 임시로 계산만 한다.
  • USE_TZ=True면 Django는 서버 시간과 무관하게 무조건 UTC로 저장한다.
  • KST 변환은 두 군데서 따로 일어난다: 보여주기 = 파이썬, 날짜 필터(__date 등) = DB의 CONVERT_TZ.
  • CONVERT_TZ('...','UTC','Asia/Seoul')처럼 타임존 이름을 쓰면 MySQL에 타임존 테이블(이름표 사전)이 적재돼 있어야 한다. 없으면 NULL 반환 → 모든 비교가 거짓 → 0건.

증상

방송 목록 API에 날짜 필터를 걸었다.

/api/v2/broadcasting/?start_date=2026-04-26&end_date=2026-06-07
  • 로컬: 0건 ❌
  • 프로덕션: 정상 ✅

같은 코드인데 결과가 달랐다.

진단 — 어디서 깨졌나

네 가지 경로로 같은 데이터를 조회해봤다.

방법결과의미
API 호출0건증상
ScheduleFilter (ORM)0건증상 재현
start_time__date 범위 (CONVERT_TZ 경로)0건여기가 깨짐
start_time raw __gte/__lte (CONVERT_TZ 안 씀)199건데이터는 멀쩡

마지막 줄이 결정적이었다. __date를 거치지 않는 raw 비교는 199건이 정상으로 나온다.
데이터/저장 문제가 아니라, 그 필터가 의존하는 DB 기능(CONVERT_TZ)이 고장난 것.


개념 1 — CONVERT_TZ는 설정이 아니라 계산기다

CONVERT_TZ(시각, '이 타임존에서', '저 타임존으로')

시각 하나를 받아 다른 타임존 기준으로 바꿔 돌려주는 함수일 뿐이다. 서버나 DB에 무언가를 설정하지 않는다.

CONVERT_TZ('2026-06-08 05:00:00', 'UTC', 'Asia/Seoul')
-- 결과: '2026-06-08 14:00:00'   (UTC + 9시간)
  • DB에 저장된 원본 값을 바꾸지 않는다.
  • 읽어올 때 임시로 계산만 한다. 원본은 그대로 UTC.

개념 2 — 저장은 Django가, 날짜 필터는 DB가 변환한다

변환의 주체가 방향에 따라 다르다. 이게 가장 헷갈리는 지점.

저장할 때 (Django → DB)

Django(한국시간 14:00) → Django가 직접 UTC 05:00으로 변환 → DB에 05:00 저장
  • 변환 주체: Django(파이썬).
  • USE_TZ=True서버 시간(UTC든 KST든)과 무관하게 무조건 UTC로 저장.
  • 이유: 서버를 어디로 옮기든 DB 값은 늘 같은 단일 기준(UTC)이어야 어긋나지 않으니까.
  • 여기선 CONVERT_TZ가 쓰이지 않는다.

주의: 흔히 "서버가 UTC라서 UTC로 저장된다"고 생각하지만, 정확히는 "Django 설정이 UTC로 저장한다". 서버 시간은 보지 않는다.

읽을 때 — 보여주기 (DB → 화면)

DB에서 05:00(UTC) 꺼냄 → 파이썬이 14:00(KST)으로 표시
  • 변환 주체: Django(파이썬).
  • CONVERT_TZ 안 씀.

읽을 때 — 날짜 필터/집계 (__date, __year, __month)

DB가 WHERE 단계에서 05:00(UTC) → 14:00(KST) 변환 후 날짜 비교
  • 변환 주체: DB(MySQL).
  • 이때 CONVERT_TZ를 쓴다.

왜 필터만 DB에서 변환하나?
"6월 7일 방송만 줘"라는 조건은 DB가 행을 고르는 단계(WHERE) 에서 날짜를 잘라야 한다. 파이썬이 데이터를 꺼낸 뒤에 거를 수 없다. DB 안에 값은 UTC로 있으니, DB가 KST로 바꿔서 날짜를 잘라야 정확하다.

핵심 구분:

  • 데이터를 꺼낸 뒤의 변환 = 파이썬
  • 데이터를 고르는 중(WHERE)의 변환 = DB(CONVERT_TZ)
-- start_time__date=2026-06-07 는 내부적으로 이렇게 바뀐다
WHERE DATE(CONVERT_TZ(start_time, 'UTC', 'Asia/Seoul')) = '2026-06-07'

개념 3 — 타임존 "이름표 사전"이 있어야 한다

CONVERT_TZ'Asia/Seoul'이 UTC+9라는 걸 알려면, MySQL의 mysql.time_zone* 시스템 테이블(이름표 사전)이 적재돼 있어야 한다.

-- 로컬 (사전 없음)
CONVERT_TZ('2026-06-08 05:00:00', 'UTC', 'Asia/Seoul')NULL-- 프로덕션 (사전 있음)
CONVERT_TZ('2026-06-08 05:00:00', 'UTC', 'Asia/Seoul')'2026-06-08 14:00:00'

NULL = '2026-06-07' 비교는 항상 거짓이므로 → 전부 0건.

참고: CONVERT_TZ('...', '+00:00', '+09:00')처럼 숫자 오프셋으로 쓰면 사전이 필요 없다. Django가 자동으로 이름('Asia/Seoul')을 쓰기 때문에 사전이 필요했던 것.


전체 그림

[저장] — 문제 없음 ✅
  Django(KST 14:00) → Django가 UTC 05:00으로 변환 → DB에 05:00 저장
  (서버 시간 무관, CONVERT_TZ 안 씀)

[보여주기] — 문제 없음 ✅
  DB에서 05:00(UTC) 꺼냄 → 파이썬이 14:00(KST)으로 표시
  (CONVERT_TZ 안 씀)

[날짜 필터] — 여기가 깨짐 ❌
  DB가 WHERE에서 05:00(UTC) → 14:00(KST) 변환 시도 → 이름표 사전 없음 → NULL → 0건

해결 — 코드 변경 0

로컬 MySQL에 타임존 테이블을 한 번만 적재하면 된다.

# Docker MySQL (호스트에서):
mysql_tzinfo_to_sql /usr/share/zoneinfo | docker exec -i <컨테이너이름> mysql -u root -p mysql

# 컨테이너 안에서:
mysql_tzinfo_to_sql /usr/share/zoneinfo | mysql -u root -p mysql

적재 후 확인:

SELECT CONVERT_TZ('2026-06-08 05:00:00','UTC','Asia/Seoul');
-- NULL이 아니라 '2026-06-08 14:00:00' 이면 성공
  • 맨 끝의 mysql 인자는 적재 대상 DB 이름이다 (앱 DB가 아니라 시스템 스키마 mysql).
  • 시스템 테이블을 건드리므로 root 권한이 필요하다.
  • 한 번 적재하면 컨테이너를 재생성하지 않는 한 유지된다.

Windows + Docker라 호스트에 /usr/share/zoneinfo가 없다면, 컨테이너 안에서 실행한다:

docker exec -i <컨테이너이름> sh -c "mysql_tzinfo_to_sql /usr/share/zoneinfo | mysql -u root -p'<루트비번>' mysql"

한 줄 정리

__date / __year / __month 같은 날짜 부분 필터USE_TZ=True인 Django에서 CONVERT_TZ(컬럼,'UTC','Asia/Seoul')로 변환된다. 로컬 MySQL에 타임존 이름표 사전이 비어 있으면 그 계산이 NULL이 되어 모든 결과가 0건이 된다. 코드 버그가 아니라 로컬 MySQL 환경 문제이고, 타임존 테이블만 적재하면 해결된다.

profile
智(지)! 德(덕)! 體(체)!

0개의 댓글