본문 바로가기
데이터베이스

MySQL 타임존 설정, TIMESTAMP와 DATETIME이 다르게 움직인다

by 꼼냥냥 2026. 9. 10.
728x90

MySQL에서 시간이 9시간 어긋날 때 흔히 서버 타임존 하나만 본다. 그런데 갈리는 지점은 서버·세션·JDBC 세 곳이고, 셋이 서로 다른 값을 들고 있을 수 있다. 더 고약한 건 TIMESTAMP와 DATETIME이 다르게 움직인다는 것이다. TIMESTAMP만 세션 타임존으로 변환되고 DATETIME은 저장된 그대로 나온다. 그래서 증상이 "전부 9시간 밀림"이 아니라 "어떤 값은 밀리고 어떤 값은 안 밀림"으로 나타난다. 이게 원인 추적을 어렵게 만든다.

컬럼 타입에 따라 다르게 움직인다

이걸 먼저 알아야 나머지가 읽힌다. MySQL 문서가 명확하게 갈라 놓는다.

TIMESTAMP — 저장할 때 세션 타임존에서 UTC로 바꿔 넣고, 읽을 때 UTC에서 세션 타임존으로 되돌린다. 세션 타임존이 바뀌면 같은 행에서 다른 값이 나온다.

DATETIME · DATE · TIME — 변환하지 않는다. UTC로 저장하지도 않는다. 넣은 값이 그대로 나온다.

같은 테이블에 두 타입이 섞여 있으면 이렇게 된다.

세션이 +00:00일 때 세션이 +09:00일 때
TIMESTAMP 컬럼 2026-09-10 01:00 2026-09-10 10:00
DATETIME 컬럼 2026-09-10 10:00 2026-09-10 10:00

한 화면에서 두 값이 9시간 벌어져 보인다. 데이터가 깨진 게 아니라 타입이 다른 것이다.

UTC_TIMESTAMP() 같은 함수도 세션 타임존의 영향을 받지 않는다. 반면 NOW()는 받는다.

어디서 갈리는지 — 세 곳을 각각 본다

1. 서버가 인식한 OS 타임존

system_time_zone은 서버가 기동할 때 호스트에서 알아낸 값이고 읽기 전용이다. 바꾸려면 mysqld를 띄우기 전에 TZ 환경변수를 주거나 기동 옵션을 쓴다.

2. 세션 타임존

time_zone의 기본값은 SYSTEM이다. 즉 위의 system_time_zone을 따라간다. 전역과 세션이 따로 있고, 클라이언트마다 자기 세션 값을 갖는다. 접속한 뒤 바꾸면 그 연결에만 적용된다.

여기 함정이 하나 더 있다. SYSTEM으로 두면 시간 계산이 필요한 함수 호출마다 시스템 라이브러리를 부른다. 전역 뮤텍스로 보호되는 호출이라 경합이 생길 수 있다. 값을 명시해 두는 편이 낫다.

3. JDBC 드라이버

자바 앱이라면 여기서 갈리는 경우가 가장 많다. Connector/J는 세 가지 값을 본다.

connectionTimeZone   LOCAL / SERVER / 특정 타임존
preserveInstants     시각(instant)을 보존할지
forceConnectionTimeZoneToSession   세션 time_zone을 실제로 바꿀지
  • SERVER — 서버에 세션 타임존을 물어본다. JVM과 다르고 preserveInstants=true면 드라이버가 변환한다.
  • LOCAL — 서버 세션 타임존이 JVM과 같다고 가정한다.
  • forceConnectionTimeZoneToSession=true — 가정에 그치지 않고 세션 time_zone을 실제로 바꾼다.

preserveInstants=false는 변환을 아예 안 한다. 시각이 아니라 보이는 문자열을 보존한다.

버전 경계가 하나 있다. Connector/J 8.0.23부터 serverTimezone은 connectionTimeZone의 별칭이다. 8.0.22 이하에서는 serverTimezone이 세션 타임존을 덮어쓰는 용도였다. 설정 이름은 같은데 의미가 달라졌으므로, 드라이버를 올렸다면 여기부터 본다.

진단은 이 순서로

세 곳을 각각 찍어 본다. 추측하지 않는다.

SELECT @@global.system_time_zone AS os_tz,
       @@global.time_zone        AS global_tz,
       @@session.time_zone       AS session_tz,
       NOW()                     AS now_session,
       UTC_TIMESTAMP()           AS now_utc;

now_session과 now_utc가 9시간 차이면 세션이 +09:00인 것이고, 같으면 UTC다.

그다음 앱이 실제로 쓰는 연결에서 같은 쿼리를 돌린다. 콘솔에서 붙은 세션과 앱 커넥션 풀의 세션은 다른 값을 가질 수 있다. 앱에서 이 쿼리 결과를 로그로 한 번 찍어 보는 게 확실하다.

이름 있는 타임존은 테이블이 있어야 쓴다

'Asia/Seoul' 같은 이름을 쓰려면 mysql 데이터베이스의 타임존 테이블이 채워져 있어야 한다. 없으면 이렇게 거부된다.

mysql> SET time_zone = 'UTC';
ERROR 1298 (HY000): Unknown or incorrect time zone: 'UTC'

확인은 한 줄이다.

SELECT COUNT(*) FROM mysql.time_zone_name;

0이면 안 채워진 것이다. 채우는 방법은 zoneinfo가 있는 시스템이면 이렇다.

mysql_tzinfo_to_sql /usr/share/zoneinfo | mysql -u root -p mysql

넣은 뒤에는 서버를 재기동해야 한다. 캐시된 옛 타임존 데이터를 계속 쓰지 않도록.

윈도우처럼 zoneinfo가 없는 환경은 MySQL이 배포하는 SQL 패키지를 받아 넣는다.

오프셋 표기('+09:00')는 테이블 없이도 된다. 급하면 이쪽을 쓴다. 다만 서머타임이 있는 지역에서는 이름 표기를 써야 한다 — 한국은 해당 없다.

고칠 때 조심할 것

TIMESTAMP에 이미 들어간 데이터는 안 건드려도 된다. UTC로 저장돼 있으므로 세션 타임존만 맞추면 제대로 나온다.

DATETIME은 반대다. 잘못된 타임존으로 쓰인 값이 그대로 박혀 있다. 세션 설정을 고쳐도 과거 데이터는 안 바뀐다. 옮기려면 실제로 UPDATE를 쳐야 하고, 어느 구간이 어느 타임존으로 쓰였는지 먼저 갈라야 한다.

배포 시점을 경계로 두 규칙이 섞인 경우가 흔하다. 고치기 전에 경계 날짜를 찾는 게 먼저다.

로그 시각도 같은 함정이다. RDS 느린 쿼리 로그의 # Time:이 UTC라 04시 KST 배치를 전날 19시대에서 찾아야 했던 일이 있었다. DB 안의 값과 DB가 뱉는 로그가 서로 다른 기준일 수 있다.

FAQ

서버를 KST로 바꾸면 되지 않나요

되기는 한다. 다만 리전이 늘거나 해외 지점이 생기면 그때 다시 갈린다. 서버는 UTC로 두고 표시 계층에서 바꾸는 쪽이 나중에 덜 아프다. 이미 KST로 돌고 있다면 억지로 바꿀 이유는 없고, 어느 쪽이든 세 곳이 같은 값인지가 중요하다.

DATETIME 대신 TIMESTAMP를 쓰면 해결되나요

시각을 다루는 컬럼이면 맞는 방향이다. TIMESTAMP가 MySQL에서 시각(instant)을 저장하도록 설계된 유일한 타입이다. 다만 표현 범위 제한이 있고, "달력상의 날짜"(생일, 영업일)는 타임존 변환이 되면 안 되므로 DATE·DATETIME이 맞다. 값의 성격으로 고른다.

드라이버 설정만 바꾸면 되나요

앱이 하나면 된다. 여러 앱이 같은 DB를 보면 각자 다른 설정으로 붙을 수 있다. 배치, 관리자 도구, 리포트 툴까지 전부 같은 기준인지 확인해야 한다. 한 곳만 어긋나도 그 경로로 들어간 데이터가 9시간 밀린다.

확인한 것과 확인하지 못한 것

이 글의 동작은 MySQL 8.4 매뉴얼과 Connector/J 문서에서 확인했다. system_time_zone과 time_zone의 관계, time_zone 기본값이 SYSTEM인 것, TIMESTAMP만 UTC 변환을 받고 DATETIME·DATE·TIME은 안 받는 것, 이름 있는 타임존이 테이블 없이는 ERROR 1298로 거부되는 것, mysql_tzinfo_to_sql 사용법과 재기동 필요, 그리고 Connector/J 8.0.23부터 serverTimezone이 별칭이 된 것이 그렇다.

확인하지 못한 것도 적어 둔다. 관리형 DB의 기본 타임존은 문서 페이지를 읽지 못해 이 글에 적지 않았다. 관리형이라면 파라미터 그룹에서 time_zone 값을 직접 확인하는 편이 정확하다 — 어차피 위 진단 쿼리 한 줄이면 실제 값이 나온다.

추측으로 세 곳 중 하나를 고르지 않는 게 이 문제의 핵심이다. 세 곳을 각각 찍어 보면 어디서 갈렸는지 바로 보인다.

댓글