본문으로 건너뛰기
개발 머꼬
개발 노트SQL
hohyeon.dev15

서버 시간대를 바꿨더니 저장된 시각이 통째로 밀린 이유

  • #Engineering Note
  • #SQL

문제 발생

서버를 옮기며 시간대 설정이 달라졌더니, 이미 저장돼 있던 시각들이 다르게 조회됐습니다. 어떤 컬럼은 그대로였고 어떤 컬럼만 9시간씩 밀렸습니다.

밀린 쪽은 TIMESTAMP, 그대로인 쪽은 DATETIME이었습니다.

원인 분석

두 타입은 시간대를 다루는 방식이 다릅니다. MySQL 문서가 핵심을 한 문장으로 적습니다 — MySQL은 TIMESTAMP 값을 저장할 때 현재 시간대에서 UTC로 변환하고, 조회할 때 UTC에서 현재 시간대로 되돌립니다. (DATETIME 같은 다른 타입에서는 이런 변환이 일어나지 않습니다.)

그리고 그 결과도 문서에 있습니다 — 시간대 설정이 일정하게 유지되는 한 저장한 값을 그대로 돌려받지만, TIMESTAMP 값을 저장한 뒤 시간대를 바꾸고 조회하면 저장했던 값과 다른 값을 받습니다.

범위 차이도 있습니다.

  • DATETIME'1000-01-01 00:00:00' ~ '9999-12-31 23:59:59'
  • TIMESTAMP'1970-01-01 00:00:01' UTC ~ '2038-01-19 03:14:07' UTC

TIMESTAMP의 상한이 2038년이라는 점은 만료일·예약일처럼 미래를 담는 컬럼에서 실제 제약이 됩니다.

해결 방안

  1. 의미를 먼저 정합니다. "특정 순간"(생성 시각, 로그인 시각)인지, "달력상의 날짜와 시각"(예약 시간, 생일)인지에 따라 답이 갈립니다.
CREATE TABLE events (
  id          BIGINT PRIMARY KEY AUTO_INCREMENT,
  occurred_at DATETIME(3) NOT NULL,   -- 앱이 UTC 값을 넣는다. DB가 변환하지 않는다
  expires_at  DATETIME     NULL       -- 2038년 상한이 없다
);
  1. 순간을 저장한다면 시간대 규약을 고정합니다. 애플리케이션·DB 세션·서버를 모두 UTC로 맞추면 두 타입의 차이가 사고로 이어지지 않습니다. 이 저장소가 DB 세션은 UTC로 고정된다 테스트를 두는 이유이기도 합니다.

  2. 표시할 때만 지역 시간으로 바꿉니다. 저장·계산은 UTC, 렌더링만 Asia/Seoul입니다.

  3. TIMESTAMP의 2038년 상한을 확인합니다. 구독 만료일처럼 먼 미래를 담는 컬럼에는 DATETIME이 안전합니다.

  4. 시간대 설정이 어디서 오는지 확인합니다. 문서 설명대로 기본적으로 각 연결의 현재 시간대는 서버의 시간이고, 연결 단위로 설정할 수 있으며 그 값은 time_zone 시스템 변수에서 확인합니다 — 서버·커넥션·앱이 서로 다른 값을 쓰고 있는지 먼저 봅니다.

  5. 자동 초기화/갱신 속성을 함께 점검합니다. MariaDB에서 nullable TIMESTAMP가 조용히 NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP로 바뀌는 함정은 이 저장소가 실제로 운영에서 겪은 문제입니다 — 컬럼 정의를 만든 뒤 information_schema로 확인합니다.

공식 문서

마지막 수정

좋아요북마크

댓글0

아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.