Unix 타임스탬프, 에폭, 그리고 2038년 문제

Unix 타임스탬프가 실제로 무엇인지, 모두를 무는 초 vs 밀리초 버그, 그리고 부호 있는 32비트 시간이 왜 2038년 1월 19일 03:14:07 UTC에 오버플로하는지 정리합니다.

Unix 타임스탬프는 정수 하나입니다. 에폭이라 불리는 고정 시점인 1970-01-01 00:00:00 UTC 이후 경과한 초의 수입니다. UTC의 고정 지점에서 초를 센다는 그 하나의 설계 결정이 타임스탬프를 저장하기에 싸고, 비교하기에 간단하고, 타임존 모호성에서 자유롭게 만듭니다. 그것은 또한 프로덕션 시스템에서 가장 흔한 두 버그를 마련합니다. 초와 밀리초 사이의 1000배 차이 버그, 그리고 2038년의 정확한 순간에 부호 있는 32비트 시계를 덮칠 오버플로입니다.

그 숫자가 실제로 나타내는 것

값은 초의 단조 증가 카운트입니다. 0은 에폭입니다. 10000000002001-09-09 01:46:40 UTC입니다. 17000000002023-11-14 22:13:20 UTC 입니다. 2026년 중반에 초 단위 현재 타임스탬프는 17로 시작하는 열 자리 숫자입니다.

카운트는 저장 시점에 타임존 독립적입니다. 타임스탬프는 오프셋도, 로캘도, "이것은 도쿄 시각이었다"는 표시도 지니지 않습니다. 그것은 전역 타임라인 위의 한 순간이고, UTC의 에폭 이후 초로 표현되므로, 지구 반대편의 두 머신이 같은 순간에 같은 타임스탬프를 만듭니다. 타임존은 그 정수를 사람이 읽을 수 있는 문자열로 변환할 때에만 들어옵니다. 표현의 문제이지 저장의 문제가 아닙니다.

그것이 타임스탬프를 저장에 좋게 만드는 점입니다. 한 순간당 정확히 하나의 정규 값이고, 서로 다른 출처의 이벤트를 정렬·비교·차분하는 데 변환이 필요 없습니다.

초 vs 밀리초 vs 마이크로초

가장 잦은 타임스탬프 버그는 2038과 아무 상관이 없습니다. 단위 혼동입니다. 플랫폼마다 다른 해상도를 씁니다.

  • — 대부분의 Unix 시스템 호출, POSIX time(), 리눅스 date +%s 명령, PostgreSQL EXTRACT(EPOCH …), 대부분의 데이터베이스 에폭 컬럼, JWT exp/iat/nbf 클레임.
  • 밀리초 — 자바스크립트. Date.now()는 초가 아니라 밀리초를 반환합니다. 자바의 System.currentTimeMillis()와 대부분의 JVM 날짜 API도 그렇습니다.
  • 마이크로초 / 나노초 — 고해상도 타이머, 일부 트레이싱 시스템, Go의 time.UnixNano().

전형적 실패는 자바스크립트 밀리초 값을 초를 기대하는 함수에 넘기거나 그 반대입니다. 당신을 엉뚱한 천 년에 떨어뜨리는 1000배 오차입니다. 초 값 1700000000을 밀리초로 읽으면 1970-01-20, 에폭에서 20일 뒤입니다. 밀리초 값 1700000000000을 초로 읽으면 55840년입니다.

보통 자릿수로 어느 단위인지 알 수 있습니다. 현재 시대의 어느 시각에든 그렇습니다.

  • 10자리 → 초(예: 1748000000, 2025년 5월)
  • 13자리 → 밀리초(예: 1748000000000)
  • 16자리 → 마이크로초
  • 19자리 → 나노초

이 어림법은 2001년(10자리 초 시작)부터 2286년(초가 11자리에 도달)까지 유효합니다. 그래서 현실적으로 다룰 어떤 타임스탬프든 자릿수가 단위를 알려줍니다. 의심스러우면 두 해석을 모두 변환해 그럴듯한 십 년대에 떨어지는 쪽을 택하세요.

2038년 문제

많은 구형 시스템이 시간을 부호 있는 32비트 정수로 저장합니다. 32비트 플랫폼에서 역사적으로 정의됐던 C time_t 타입입니다. 부호 있는 32비트 정수는 −2147483648부터 2147483647까지의 값을 담을 수 있습니다. 그 상한, 에폭 이후 2^31 − 1 = 2147483647초는 다음입니다.

2038-01-19 03:14:07 UTC

2038년 1월 19일 03:14:08 UTC 정각에, 카운터는 오버플로 없이 증가할 수 없습니다. 부호 있는 정수에서 2147483647을 지나 증가하면 −2147483648로 래핑되며, 이는 1901-12-13 20:45:52 UTC입니다. 이것을 하는 시계는 2038년 1월에서 1901년 12월로 점프합니다. 이것이 2038년 문제이고, 때때로 Y2038이나 "Epochalypse"라 씁니다.

실제로 깨지는 것은 다음입니다.

  • 임베디드·레거시 시스템 — 라우터, 산업용 컨트롤러, POS 단말기, 그리고 패치되지 않을 32비트 펌웨어를 돌리는 그 밖의 장수명 장치.
  • 구형 데이터베이스 스키마 — 시간을 32비트로 고정한 컬럼이나 직렬화 형식. MySQL의 TIMESTAMP 타입은 이 이유로 역사적으로 2038-01-19 03:14:07 UTC에서 상한이었습니다. DATETIME은 그 한계가 없었습니다.
  • 미래 날짜를 계산하는 소프트웨어 — 30년 만기 모기지 상환, 장기 만료 인증서, 먼 미래의 캐시 TTL. 이들은 2038년이 아니라 오늘 2038 산술을 마주합니다. 2038년 1월 이후 날짜를 계산하는 코드는 32비트 time_t에서 수년째 실패해 왔습니다.

해법은 64비트 time_t입니다. 부호 있는 64비트 정수는 약 292277026596년 까지 초를 셉니다. 대략 2,920억 년 뒤이고, 문제를 영구히 폐기합니다. 64비트 운영체제와 현대 언어는 이미 64비트 시간을 쓰고, 리눅스는 2020–2021년경 32비트 아키텍처의 커널과 glibc에서 time_t를 확장했습니다. 마이그레이션 현실은, 위험이 이제 현재 컴파일러가 아니라 배포된 펌웨어와 오래된 바이너리에 산다는 것입니다. 64비트 플랫폼에서 새로 컴파일한 것은 무엇이든 이미 안전하고, 위험은 아무도 재컴파일하지 않을 현장의 32비트 장치입니다.

1901년 하한과 부호 없는 변형

같은 32비트 폭이 바닥도 만듭니다. 부호 있는 32비트 time_t1901-12-13 20:45:52 UTC(−2147483648 경계) 이전의 어떤 순간도 표현할 수 없습니다. 그보다 이른 날짜, 19세기 생년월일이나 역사적 사건은 아래로 오버플로합니다. 이것이 일부 구형 시스템이 1901년 이전 날짜를 통째로 잘못 다루는 이유입니다.

몇몇 시스템은 대신 부호 없는 32비트 정수를 썼습니다. 에폭 이전 시각을 표현할 수 없지만(음수 없음) 상한을 2^32 − 1 = 4294967295, 즉 2106-02-07 06:28:15 UTC로 밉니다. 모든 1970년 이전 타임스탬프를 대가로 약 68년을 더 법니다. 일부 네트워크 프로토콜과 파일 형식에서 여전히 보이지만, 일반적 해법이 아니라 우회책입니다.

타임존은 표시 계층에 산다

타임스탬프는 정의상 UTC이므로, 타임존은 저장된 값의 일부가 결코 아닙니다. 그것은 정수를 사람에게 보여주려 포맷하는 계층에 전적으로 속합니다. 같은 1748000000은 도쿄·런던·뉴욕에서 다른 벽시계 문자열로 렌더링되지만, 같은 순간이고 같은 저장 숫자입니다.

가장 흔한 타임존 버그는 그 반대 실수입니다. 로컬 벽시계 시각을 가져와 그 구성요소를 이미 UTC 타임스탬프인 것처럼 저장하는 것입니다. America/New_York의 서버가 로컬 "오후 3:00"을 읽고 "오후 3:00 UTC"의 에폭 이후 초를 쓰면, 모든 소비자는 오프셋만큼 어긋납니다. 겨울에 다섯 시간, 여름에 네 시간, 일광 절약에 따라 이동합니다. 규칙은 이렇습니다. 한 순간을 포착하는 즉시 UTC로 변환하고, 타임스탬프를 저장하고, 사람에게 렌더링할 때에만 타임존을 적용하세요.

윤초: Unix 시간은 없는 척한다

UTC는 지구 자전과 시계를 맞추려고 가끔 윤초를 삽입합니다. 1972년 이후 27개가 추가됐습니다. Unix 시간은 그것들을 무시합니다. 정의상 Unix 타임스탬프는 모든 날이 정확히 86400초라고 가정합니다. 윤초가 발생하면 Unix 시간은 새 값을 얻지 않습니다. 카운트가 하루 86400 모델과 일관되도록, 시계가 보통 한 초를 반복하거나 뭉갭니다(smear).

실용적 함의는 다음입니다.

  • Unix 타임스탬프는 1970년 이후 물리적 SI 초의 참된 카운트가 아닙니다. 삽입된 윤초 수만큼 어긋나 있습니다.
  • 윤초를 가로지르는 두 타임스탬프 사이의 정확한 기간 계산은 실제 스톱워치 대비 1초 어긋납니다.
  • 윤초 동안 두 개의 서로 다른 실제 순간이 같은 Unix 타임스탬프로 매핑될 수 있어, 타임스탬프는 1초 미만 경계에서 엄밀히 고유하다고 보장되지 않습니다.

거의 모든 애플리케이션 코드에서 이것은 중요하지 않습니다. 고정밀 타이밍, 거래 순서, 과학 작업에서는 중요하고, 그럴 때는 TAI나 윤초를 뭉개는(leap-smearing) 시계 소스로 손을 뻗습니다.

ISO 8601 vs 원시 타임스탬프

Unix 타임스탬프는 기계에 이상적이고 사람에게는 쓸모없습니다. 1748000000은 한눈에 아무것도 말해 주지 않습니다. ISO 8601은 사람이 읽을 수 있는 교환 형식입니다. 2025-05-23T11:33:20Z이고, 끝의 Z는 UTC("Zulu")를 뜻하거나, 2025-05-23T07:33:20-04:00처럼 명시적 오프셋과 함께 옵니다.

무엇을 언제 쓸지는 다음과 같습니다.

  • Unix 타임스탬프를 저장 — 값이 내부용이고 산술이나 비교를 위해 가장 작고 빠른 표현을 원할 때.
  • ISO 8601을 저장하거나 전송 — 사람이나 이질적 시스템이 값을 읽을 때, 명시적 오프셋이 필요할 때, 또는 누군가 눈으로 볼 로그·API 페이로드·설정 파일에 값이 들어갈 때. 덤으로 ISO 8601은 UTC에서 사전식으로 정렬됩니다.

합리적 기본값은 이렇습니다. 타임스탬프를 내부적으로 저장하고, API 경계에서 ISO 8601로 직렬화하세요.

둘 사이 변환

로그 줄에서 열 자리나 열세 자리 숫자를 노려보며 그 순간을 알아야 할 때, 또는 주어진 날짜의 타임스탬프를 만들어야 할 때, 우리 타임스탬프 변환기는 양방향을 처리하고, 초인지 밀리초인지 자동 감지하고, 결과를 UTC와 로컬 타임존으로 나란히 보여줍니다. REPL을 열지 않고도 "1000배 어긋났나 타임존이 어긋났나" 문제를 가장 빠르게 해결하는 방법입니다.