Clockhub Logo
블로그문의하기
전체 글
레퍼런스13 분 소요

UNIX 타임스탬프 완전 가이드: 1970년부터 현재까지

현재 Unix 타임스탬프실시간
0000000000.000
1970-01-01 00:00:00 UTC · 목요일
1970-01-01 00:00:00 UTC 부터의 초 수

1738368000 이라는 숫자를 예로 들어 보자. 규칙을 알면 한눈에 2025년 2월 1일 00:00:00 UTC 임을 읽어낼 수 있다. 모르면 그저 잡음이다.

이것이 Unix 시간의 설계 전부다. 정수 하나, 시간대 없음, 달력 없음, 서머타임 없음, "어느 2월이냐"를 두고 다툴 필요도 없음. 이 글의 나머지는 모두 이 한 문장의 귀결이다. 그 정수가 언젠가 바닥난다는 이야기까지 포함해서.

I. 정수 하나, 두 가지 읽기

정의는 한 줄이면 끝난다. 다만 그 한 줄의 모든 단어에 무게가 있다.

Unix 시간은 1970년 1월 1일 00:00:00 UTC 부터 흐른 초 수이며, 윤초는 세지 않는다.

이 문장에는 네 가지가 들어 있다.

UTC 의 고정된 한 순간부터 센다. 당신 지역의 자정이 아니다. 도쿄와 시카고의 두 사람이 같은 타임스탬프를 보면 같은 숫자를 보고, 각자 자기 쪽 벽시계로 환산한다.

단위는 초. 초기 Unix 는 60분의 1초로 셌다. 다음 절의 이야기지만, 숫자가 이렇게 작은 이유가 그것이다.

윤초는 제외된다. 거의 모두가 틀리는 지점이라 따로 절을 두었다.

음수도 될 수 있다. 1970년 이전 날짜는 그냥 음수이지 오류 상태가 아니다. -1 은 1969-12-31 23:59:59 UTC 다.

같은 순간을 여섯 가지로 써 보자. 시간대를 가진 것은 뒤의 셋뿐이고, 컴퓨터가 실제로 저장하는 것은 앞의 둘뿐이다.

표현값
Unix 초1738368000
Unix 밀리초1738368000000
ISO 8601, UTC2025-02-01T00:00:00Z
ISO 8601, UTC+82025-02-01T08:00:00+08:00
ISO 8601, UTC−52025-01-31T19:00:00-05:00
RFC 2822Sat, 1 Feb 2025 00:00:00 +0000

타임스탬프 열은 끝까지 그대로이고 날짜 열은 세 번 바뀐다. 타임스탬프는 날짜가 아니다. 하나의 지구적 시간선 위의 한 점이다. 날짜란 "그 점이 지구의 이 부근에서 보면 어디에 떨어지는가"를 물었을 때 돌아오는 답이다.

II. 왜 1970년이고, 왜 1971년이 아닌가

1969년에 누군가 좋은 날을 골라 1970년 1월 1일로 정한 것이 아니다. 이 기점은 2년 사이에 세 번 바뀌었고, 1970년은 마지막에 우연히 자리 잡은 곳일 뿐이다.

60 Hz 라는 골칫거리

최초의 Unix 는 PDP-11 에서 돌았다. 이 기계의 시스템 클록은 초당 60번 튄다. 상용 전원이 60 Hz 인 나라를 위해 시간을 세도록 설계되었기 때문이다. 그래서 저장하기에 자연스러운 것은 "몇 번 튀었나"였다.

문제는 그 카운터가 32비트뿐이라는 점이었다. 초당 60번이면 32비트는 한 바퀴 도는 데 이만큼 걸린다.

2^32 / 60 초 = 71,582,788 초 ≈ 2.26 년

2년 3개월. 1972년 3월 15일자 『Unix 프로그래머 매뉴얼』 제3판은 이 사실을 유난히 노골적으로 적어 두었다.

"The time is stored in 32 bits. This guarantees a crisis every 2.26 years."

(시간은 32비트에 저장된다. 이는 2.26년마다 위기를 보장한다.)

2년에 세 번 바뀐 기점

손쉬운 대응은 출발선을 계속 앞으로 미는 것이었다. 1971년 11월 3일 제1판 매뉴얼은 이렇게 정의한다.

"the time since 00:00:00, Jan. 1, 1971, measured in sixtieths of a second"

(1971년 1월 1일 00:00:00 부터의 60분의 1초 수.)

넉 달 뒤 제3판에서는 기점이 이미 1972년 1월 1일로 옮겨가 있다. 반년도 안 되어 기점 두 개 — 카운터가 바닥나는 속도가 문서를 쓰는 속도보다 빨랐던 것이다.

진짜 해법은 "튐"을 세는 것을 멈추고 "초"를 세는 것이었다. 이것으로 여유가 예순 배가 된다. 32비트는 2.26년에서 약 136년으로 늘었다. 단위가 바뀌자 기점을 최근으로 둘 이유가 사라졌고, 1970년 1월 1일로 정해졌다. 매뉴얼 자신의 표현대로 "다루기 편한 날짜"로서 임의로 고른 것이다.

왜 다시는 바뀌지 않았나

1970년대 중반에는 이미 디스크 위에 데이터가 있었기 때문이다. 파일, 테이프, inode, 백업 아카이브 — 모두가 "1970년부터의 초"를 품고 있었다. 기점을 또 바꾸는 것은 전 세계 Unix 기계의 모든 파일을 조용히 다른 의미로 다시 읽는 일이었다.

그렇게 우연한 선택은 인터페이스로 굳었다. 56년이 지난 지금도 지구상의 모든 휴대폰, 서버, 데이터베이스가 그 목요일 아침부터 계속 세고 있다.

컴퓨터 역사 박물관에 전시된 DEC PDP-11/20 과 1970년 당시의 안내판

PDP-11/20. Unix 가 태어난 기종 계열. 안내판에는 "Digital Equipment Corporation (DEC), US, 1970" 이라 적혀 있는데, 기점과 같은 해다. 사진: Don DeBold, CC BY 2.0, Wikimedia Commons

III. 하루는 정확히 86,400초

여기서부터 Unix 시간은 물리 세계와 어긋난다.

지구 자전은 일정하지 않다. 느려지고, 조금 빨라지고, 흔들린다. 천문학자들은 지구의 실제 자세에서 유도한 UT1 이라는 시간 척도로 이를 추적하고, 원자시계는 그런 것에 관심이 없다. 두 척도는 조금씩 벌어진다.

1972년에 발명된 땜질이 윤초다. 6월 30일이나 12월 31일의 마지막 1분에 23:59:60 으로 표시되는 초를 삽입해 원자시와 천문시를 0.9초 이내로 유지한다. 지금까지 27번 삽입되었고 가장 최근은 2016년 말이다.

Unix 시간은 그것을 모두 무시한다. POSIX 표준은 사실상 하루는 정확히 86,400초이며 예외는 없다고 정한다. 따라서 이 숫자는 "물리적으로 몇 초 흘렀나"가 아니라, 실제 시간 위에 깔린 이상화되고 완전히 균일한 격자 위의 좌표다.

그 대가

두 가지이고, 둘 다 눈에 띄지 않는다.

모호성. 양의 윤초가 일어나면 하나의 Unix 값을 재사용해야 한다. 1998년 말에 915148800 은 1998-12-31 23:59:60 과 1999-01-01 00:00:00 을 동시에 가리켰다. 2016년 말의 1483228800 도 마찬가지다. 이 1초 동안 타임스탬프는 유일한 순간을 가리키지 않는다.

아무도 쓰지 않는 평행 정의. 일부 Linux 는 right 설정으로 돌아가는데, 이쪽은 정말로 윤초를 세어 하루를 86,401초로 만든다. 1972년 첫 윤초 이래 right 시각은 보통의 posix 시각보다 앞서 왔고, 2017년 기준 정확히 27초 앞선다. 두 기계를 비교했는데 27초 차이가 났다면 원인이 이것이다.

이 장치는 폐지된다

2022년 11월, 제27차 국제도량형총회는 윤초 폐지를 결정하고 목표를 2035년 또는 그 이전으로 잡았다. 그리고 2026년 10월, 제28차 총회가 날짜를 확정했다. 2027년 5월 20일부터 UTC는 끊김 없이 흐른다. 이유는 흔들림이 멈춰서가 아니라, 흔들림이 엔지니어링 부채가 되었기 때문이다. 윤초는 라우터를 멈추게 하고 거래 시스템을 어지럽혔으며, 10년에 몇 번만 실행되어 충분히 테스트할 수 없는 예외 코드를 모든 대형 소프트웨어 회사에 강요해 왔다.

이후 UTC 는 지구 자전에서 더 멀어지는 것이 허용되고, Unix 시간은 처음으로 진짜 균일한 초의 계수가 된다.

현재 Unix 타임스탬프실시간
0000000000.000
1970-01-01 00:00:00 UTC · 목요일
1970-01-01 00:00:00 UTC 부터의 초 수

실무 규칙

두 타임스탬프를 빼서 소요 시간을 재지 마라.

end - start 는 사이에 윤초가 끼면 틀리고, NTP 나 사용자가 시스템 시계를 조정해도 틀린다. 경과 시간을 재려면 단조 시계를 쓴다. C 의 CLOCK_MONOTONIC, 브라우저의 performance.now(), Python 의 time.monotonic() 이다. 이것들은 실제 경과를 세고 두 문제 모두에 영향받지 않는다. 타임스탬프는 순간을 표시하는 것이고, 단조 시계는 구간을 측정하는 것이다. 둘을 섞으면 1년에 한 번 나타나고 재현되지 않는 버그가 생긴다.

IV. 32비트가 남긴 빚

카운터 이야기로 돌아가자.

부호 있는 32비트 정수의 범위는 −2,147,483,648 부터 2,147,483,647 까지다. 이를 "1970년부터의 초"에 적용하면 고정된 창이 나온다.

경계값날짜
최솟값-21474836481901-12-13 20:45:52 UTC
기점01970-01-01 00:00:00 UTC
최댓값21474836472038-01-19 03:14:07 UTC

최댓값 다음 1초에 값은 넘쳐 최솟값으로 되감긴다. 이 방식으로 저장하는 시스템은 2038년 1월 19일 03:14:08 UTC 에 지금이 1901년 12월 13일이라고 믿게 된다.

1900년부터 2120년까지의 시간축에 int32 와 uint32 범위를 그리고 2026년까지 채운 그림

int32 는 1901년부터 2038년까지, 부호 없는 판은 1970년부터 2106년까지를 덮는다. 채워진 부분이 이미 쓴 비율이다.

얼마나 왔나

int32 창은 총 136.1년이고 2026년은 정확히 91.7% 지점에 있다. 가장 과소평가되는 지점이다. 2038년 문제는 먼 마감일이 아니라 아홉 할까지 찬 진행 표시줄이다.

부호 없는 판은 68년을 더 벌어 2106-02-07 06:28:15 UTC 까지 간다. 그림의 uint32 행이 거기서 나왔고, 일부 형식이 "두 번 망가지는" 이유이기도 하다.

실제로 망가지는 곳

눈에 보이는 피해의 대부분은 운영체제 안에서 일어나지 않는다. Linux 는 오래전에 64비트 time_t 로 옮겼고 주요 소프트웨어의 64비트 빌드도 따라왔다. 위험은 바꾸기 어려운 곳에 몰려 있다.

  • 데이터베이스. MySQL 의 TIMESTAMP 열은 부호 있는 32비트 초 수 그 자체로, 2038-01-19 03:14:07 에 멈춘다. 그 자리에서 확장할 방법이 없다. DATETIME 은 범위가 훨씬 넓어 2038년을 넘겨 살아야 하는 열에는 옳은 선택이다. 다만 마이그레이션은 수작업이고 모든 테이블을 살펴야 한다.
  • 파일 형식. 아카이브, 이미지 메타데이터, 메일 저장소, 백업 카탈로그의 32비트 타임스탬프 필드는 그것을 읽는 기계가 교체된 훨씬 뒤까지 되감긴 날짜를 계속 써 넣는다.
  • 임베디드 시스템. 산업용 컨트롤러, 의료기기, 차량 인포테인먼트, 빌딩 관리. 20년의 운용 수명을 가지고 업데이트 경로가 없으며 제조사가 이미 사라진 것이 바로 이 부류다.
  • 32비트 사용자 공간. 오래된 ARM 보드와 낡은 베이스 이미지로 만든 컨테이너에서 여전히 흔하다.

데이터센터에 늘어선 서버 랙

서버 랙. 2038년에 실제로 망가지는 시스템은 대개 이것들보다 더 오래되고 더 눈에 띄지 않는다. 사진: Carl Lender, CC BY 2.0, Wikimedia Commons

왜 Y2K 보다 까다로운가

Y2K 는 형식 문제였다. 마감이 분명했고 시험 방법도 분명했다. 시계를 앞으로 돌려 날짜가 맞게 보이는지 확인하면 되었다.

2038년은 부호 있는 산술 문제다. 넘쳐도 오류가 나지 않는다. 1901년의 유효한 날짜가 되고, 거기서 전파된다. 인증서는 124년 전에 만료된 것처럼 보이고, 세션 토큰은 고색을 띠고, 정렬 비교가 조용히 뒤집힌다.

고장은 조용하고 부분적으로 온다. 그것이 최악의 조합이다.

V. 초로는 부족해졌다

컴퓨터의 가장 빠른 연산이 밀리초 걸리던 시절에는 초로 충분했다. 지금은 아니다.

정밀도전형적 저장어디서 만나나
1초time_t파일 시스템, 데이터베이스, HTTP 헤더
1밀리초JavaScript Date, Java Instant브라우저, JSON API, Date.now()
1마이크로초struct timevalgettimeofday, 오래된 C API
1나노초struct timespecclock_gettime, APFS, ext4, 현대 커널

타임스탬프를 다루는 코드에서 가장 흔한 버그 원인이다. 1738368000 과 1738368000000 은 같은 순간의 두 단위이고, 둘 다 그럴듯한 정수로 보인다. 천 배라는 계수는 반대 방향으로 곱하기 쉽고 눈으로 찾기란 거의 불가능하다.

JavaScript 의 천장

JavaScript 의 Date 는 밀리초를 배정도 부동소수로 담는다. 명세는 ±8.64 × 10¹⁵ 밀리초, 즉 ±1억 일, 기점 전후 약 ±273,790년으로 제한한다. int32 창보다 훨씬 넓어 JavaScript 에 2038년 문제는 없다. 그러나 밀리초라는 단위 때문에 많은 API 가 열세 자리 수를 돌려주고, Date.now() / 1000 이라는 줄이 이렇게 익숙해졌다.

시간을 품은 식별자

현대 시스템은 타임스탬프를 ID 자체에 넣는 일이 늘었고, 그래서 정밀도 선택이 구현 세부에서 설계 결정으로 바뀌었다. UUIDv7 은 앞머리에 48비트 밀리초를 두어 1970년부터 약 8,900년을 덮고, ID 가 생성 시각 순으로 자연스럽게 정렬되게 한다. Snowflake 계열 ID 도 41비트 밀리초로 같은 일을 하며, 그 대가로 범위가 수십 년으로 줄어든다.

따로 짚어 둘 함정

타임스탬프를 float32 에 저장하지 마라. 밀리초 자릿수에서는 float64 도 조심해야 한다. 32비트 부동소수는 가수부가 24비트뿐이라 현재 타임스탬프 규모에서는 약 128초 간격밖에 구분하지 못한다. 값이 틀린 것이 아니라 반올림된 것이고, 그쪽이 더 나쁘다. 1분 차이의 두 사건이 같은 타임스탬프를 가질 때까지 모든 것이 정상으로 보인다.

VI. 실무에서 깨지는 일곱 가지

실제로 버그 트래커에 올라오는 것들을 대략 빈도순으로 정리했다.

1. 초인가 밀리초인가. 숫자가 와도 자릿수만으로는 판단할 수 없고, 잘못 고르면 날짜가 1970년이나 서기 57,000년이 된다. 간단한 점검: 현재 타임스탬프는 초로 약 10자리, 밀리초로 13자리다.

2. 로컬 시각은 타임스탬프가 아니다. date -d @0 은 1970-01-01 00:00:00 을 출력하지 않는다. UTC 자정을 당신의 시간대로 표현한 값을 낸다. 한국이면 1970-01-01 09:00:00, 뉴욕이면 1969-12-31 19:00:00 이다. 숫자는 같고 표시가 국소적일 뿐이다.

3. 타임스탬프에 시간대는 없다. 이것은 Unix 시간의 결함이 아니라 목적 그 자체다. 다만 시간대 없는 "2025-02-01 00:00:00" 은 순간을 이루지 못하고, 그런 문자열을 저장하는 시스템은 머지않아 다른 시스템과 그 의미를 두고 어긋난다.

4. 타임스탬프를 문자열로 정렬하면 자릿수 경계에서 깨진다. 2001년 9월 9일 1000000000 이 왔을 때, 타임스탬프를 숫자가 아니라 문자열로 비교하던 소프트웨어는 새 항목을 옛 항목 앞에 놓기 시작했다. "999999999" 가 "1000000000" 보다 뒤로 정렬되기 때문이다. 앞자리 0 을 채우거나 정수로 저장하면 해결된다. 다른 방법은 없다.

5. MySQL 의 TIMESTAMP. 앞서 다뤘지만, 평범한 웹 애플리케이션에서 가장 흔한 2038년 노출 지점이라 따로 적는다.

6. 1970년 이전. 음수는 합법이지만 "타임스탬프 = 양의 정수"를 전제한 검증기, 데이터베이스 열 타입, API 스키마에 자주 거부된다. 생년월일, 역사 기록, 천문 데이터는 모두 기점의 반대편에 산다.

7. 벽시계를 빼기. 앞서 언급했지만, 간헐적이고 재현 불가능한 운영 장애를 만드는 것이 바로 이 방식이므로 다시 적는다. 소요 시간에는 단조 시계를 써라.

VII. 사람들이 기념하는 타임스탬프

Unix 시간에는 소소한 "정수 덕후" 문화가 있다. 다음은 자주 회자되는 것들이다.

타임스탬프날짜(UTC)무슨 일이 있었나
01970-01-01 00:00:00기점 그 자체
-11969-12-31 23:59:59범위가 과거로 뻗는다는 증명
1197310171973-10-17 18:36:57자기 서술적 참조로 쓰인 첫 타임스탬프
10000000002001-09-09 01:46:40첫 열 자리. 문자열 정렬이 여기서 끊긴다
10737418242004-01-10 13:37:042³⁰, 이른바 "1337" 타임스탬프
12345678902009-02-13 23:31:30Google 이 두들을로 기념했다
21474836472038-01-19 03:14:07표현 가능한 마지막 초
42949672952106-02-07 06:28:15부호 없는 판의 천장

2001년 사례가 흥미로운 것은 2038년의 축소판이기 때문이다. 완전히 유효한 수였고 모든 시스템이 받아들였으며, 그리고 비교가 조용히 깨졌다.

VIII. 2038년 이후

해법은 복잡하지 않다. 부호 있는 64비트 time_t 는 전후 약 2,920억 년을 덮는다. 우주 나이의 약 스무 배다. 그 끝에 두 번째 2038년 문제는 없고, 현실의 소프트웨어가 이 상한을 다시 의식할 일도 없다.

현재의 64비트 플랫폼은 거의 모두 전환을 마쳤다. 남은 작업은 커널 안이 아니라 데이터베이스, 파일 형식, 펌웨어, 그리고 20년째 돌아가는 기기 안에 있다.

또 다른 두 번의 되감김

알아 둘 만한 날짜가 둘 더 있다. 유명하지 않지만 둘 다 실재한다.

2036-02-07 06:28:16 UTC — NTP 0번 시대의 끝. Network Time Protocol 은 1900년 1월 1일부터 부호 없는 32비트로 초를 센다. 2036년에 바닥나고, Unix 의 경우와 달리 패킷 형식을 깨지 않고 자릿수를 넓힐 여지가 없다. 프로토콜에는 확장 기구가 정의되어 있지만 양쪽 끝의 갱신을 요구한다. NTP 에서 그것은 시각 동기화를 설정한 적 있는 모든 기기를 뜻한다.

2106-02-07 — 부호 없는 32비트의 천장. 초를 int32 가 아니라 uint32 로 담은 형식은 68년을 더 얻는다. 일부 파일 형식과 임베디드 프로토콜이 정확히 그렇게 했고, 그림에 막대가 두 개인 이유다.

무엇을 해야 하나

2038년에도 돌아갈 시스템을 운영한다면:

  • 타임스탬프를 고정폭 정수로 저장하는 모든 곳과 TIMESTAMP 타입 열을 전부 조사한다.
  • 수명이 긴 데이터에는 DATETIME, 64비트 정수, ISO 8601 문자열을 우선한다.
  • 시스템 시계를 2038-01-19 이후로 돌려 놓고 읽기·쓰기 전 경로를 통과시키는 테스트를 쓴다. 값싼 테스트인데 거의 아무도 갖고 있지 않다.
  • 문자열 기반 타임스탬프 비교는 스타일 선호가 아니라 잠재적 버그로 취급한다.

맺음: 1970년은 하나의 결정이다

1738368000 이라는 숫자는 우주에 관한 사실처럼 보인다. 아니다. 1971년에 클록 튐을 세기로 한 결정, 1972년에 초를 세기로 한 결정, 그리고 "2년마다 기점을 옮기는" 운영이 더는 성립하지 않게 된 뒤 적당히 그은 출발선이 겹쳐 만들어진 결과다.

56년 뒤, 그 선택은 모든 파일 시스템, 모든 데이터베이스, 모든 로그 한 줄, 그리고 인터넷의 모든 API 에 박혀 있다. 그것은 결정한 사람보다 오래 살고, 태어난 기계보다 오래 살며, 아마 같은 뿌리 문제를 덮으려 발명된 윤초보다도 오래 살 것이다.

거기에는 시야가 조금 트이는 구석이 있다. 타임스탬프는 시간이 아니다. 관습이다. 쓸모 있을 만큼 정밀하고 정수 하나에 담길 만큼 단순한, 아주 잘 만든 관습이며, 모든 관습이 그러하듯 내력과 유통기한과 유지 비용을 함께 갖고 있다.

이 숫자가 어디서 왔는지 알면, 어떤 값이 초인지 밀리초인지, 어떤 날짜가 UTC 인지 로컬인지, 그리고 눈앞의 시스템이 2038년에 무사할지를 한눈에 판단할 수 있다.


특정 값을 확인하고 싶다면 타임스탬프 변환기 가 초·밀리초·ISO 8601 의 양방향 변환을 지원한다. 시간대 처리도 되니, 숫자는 있는데 단위를 모를 때 쓰면 된다.

현재 Unix 타임스탬프실시간
0000000000.000
1970-01-01 00:00:00 UTC · 목요일
1970-01-01 00:00:00 UTC 부터의 초 수