Skip to Content

4장. 1970년 1월 1일 — epoch와 2038년 문제

만화로 보는 요약 — 먼저 읽어보세요

만화로 보는 요약 — 먼저 읽어보세요

1970년을 0으로 잡고 초를 센 정수 → 32비트의 2038년 오버플로 → 순간과 지역시를 분리하는 설계.

면접 실전 질문: ① 32비트 time_t는 2038년에 왜 1901년으로 되돌아가는가? ② MySQL TIMESTAMP와 DATETIME의 시간 범위가 다른 이유는? ③ epoch와 타임존 정보를 분리하는 이점은?


컴퓨터는 하늘도 나라도 원자도 물려받지 않았다 — 임의의 순간 하나를 0으로 찍고 초를 세는 정수 하나로 시간을 대신했고, 그 정수의 폭이 2038년에 바닥난다.

면접 실전 질문: ① Unix epoch는 왜 하필 1970년 1월 1일인가? ② time_t가 32비트일 때 2038년에 정확히 무슨 일이 일어나는가? ③ MySQL TIMESTAMPDATETIME과 달리 2038년 한계를 갖는 이유는?


배경 — 정수 하나로 시간을 대신하다

1~3장까지 시간을 정한 건 차례로 하늘, 나라, 원자였습니다. 그런데 컴퓨터는 이 셋 중 어느 것도 그대로 물려받지 않았습니다. 달력도, 타임존도, 윤초표도 계산하려면 값이 비쌉니다. 오늘이 그레고리력으로 며칠인지, 이 나라의 표준 오프셋이 지금 몇 시인지, 그사이 윤초가 몇 번 끼어들었는지 — 전부 표를 찾아보거나 규칙을 계산해야 나오는 값이에요.

컴퓨터가 택한 답은 훨씬 단순했습니다. 임의의 순간 하나를 정해 0이라고 찍고, 그 뒤로는 그냥 초를 센다. 이 값이 **epoch(기원)**입니다. 시각을 달력·요일·시간대 없이 정수 하나로만 표현하면, 두 시각을 비교하는 건 정수 두 개를 빼는 것과 같아집니다. 정렬도, 경과 시간 계산도, 데이터베이스 인덱스도 전부 정수 연산 하나로 끝나요. 1장에서 봤던 “연속적인 값을 정수로 근사한다”는 움직임이 여기서 다시 나타납니다. 이번엔 회귀년의 길이가 아니라, 시간 축 위의 한 위치를 정수로 근사하는 차례입니다.

스토리 — 1970년은 처음부터 1970년이 아니었다

Unix epoch가 1970년 1월 1일이라는 사실은 잘 알려져 있습니다. 그런데 “왜 하필 그 날짜인가”의 진짜 답은 흔히 도는 이야기보다 지저분합니다.

1971년에 나온 첫 Unix 프로그래머 매뉴얼은 시간을 이렇게 정의합니다. “1971년 1월 1일 0시부터 흐른 시간을, 60분의 1초 단위로 센 값.” 즉 최초의 스킴은 초 단위조차 아니었고, 기준점도 1970년이 아니라 1971년이었습니다. 60Hz 틱을 32비트 정수로 세면, 2년 3개월 남짓(약 2.26년)마다 정수가 꽉 차 버립니다. 1972년 3월에 나온 3판 매뉴얼을 보면 기준점이 1972년 1월 1일로 다시 밀려 있는데, 여전히 60분의 1초 단위였고, 여전히 같은 오버플로 문제를 안고 있었습니다.

결국 해결책은 두 가지를 동시에 바꾸는 것이었습니다. 단위를 60분의 1초에서 1초로 낮추고(정밀도를 버리고 범위를 삼), 기준점을 1970년 1월 1일로 되돌렸습니다. 왜 1970년이었는지는 데니스 리치 본인의 설명으로도 그냥 깔끔한 라운드 넘버였다는 게 전부입니다. 특별한 사건이 있었던 날이 아니라, 다루기 편한 둥근 해를 하나 고른 것뿐이에요.

이 대목이 중요합니다. “1970년은 원래부터 정해진 값”이 아니라, 1971년·1972년이라는 두 번의 시행착오 끝에 오버플로를 피하려고 정착한 값이라는 것. 흥미로운 건 이 오버플로가 사고 보고서가 아니라 매뉴얼에 미리 적힌 예고였다는 점입니다. 3판 매뉴얼은 스스로 “2.26년마다 위기가 오는 것이 확정돼 있다”고 못 박아 뒀어요. 그리고 실제로 그 위기가 닥치기 전에, 초 단위·1970년 기준으로 갈아타 버렸습니다. 문제를 겪고 고친 게 아니라, 겪을 것을 알고 미리 피한 것이죠. 지금부터 볼 2038년 문제는 규모만 커졌을 뿐 구조가 똑같은데 — 이번에는 예고를 받고도 아직 다 못 갈아탔다는 점이 다릅니다.

핵심 — 같은 병이 더 큰 규모로 돌아오다

1970년 1월 1일 00:00:00 UTC를 0으로 잡고 초를 세는 이 값을 흔히 time_t라 부릅니다. 문제는 이 정수에 이 있다는 겁니다. 오랫동안 표준 구현은 부호 있는 32비트 정수였습니다. 32비트 부호 있는 정수의 최댓값은 2,147,483,647. 이 값이 가리키는 순간이 정확히 2038년 1월 19일 03:14:07 UTC입니다. 그 1초 뒤, 정수는 부호 비트가 뒤집히며 음수로 넘어가고, 그 값은 1901년 12월 13일 20:45:52로 튕겨나갑니다. 실제로 JDK 21로 확인해보면 이렇습니다.

import java.time.Instant; public class Epoch2038 { public static void main(String[] args) { Instant maxInt = Instant.ofEpochSecond(Integer.MAX_VALUE); System.out.println("Integer.MAX_VALUE = " + Integer.MAX_VALUE); System.out.println("Instant.ofEpochSecond(그 값) = " + maxInt); int overflowed = Integer.MAX_VALUE + 1; System.out.println(); System.out.println("Integer.MAX_VALUE + 1 = " + overflowed); Instant wrapped = Instant.ofEpochSecond(overflowed); System.out.println("Instant.ofEpochSecond(그 값) = " + wrapped); System.out.println(); System.out.println("System.currentTimeMillis() = " + System.currentTimeMillis()); System.out.println("Instant.now() = " + Instant.now()); System.out.println("Instant.MAX = " + Instant.MAX); System.out.println(); long farFuture = 3000000000L; // 2065년경, 32비트 범위 밖 Instant real = Instant.ofEpochSecond(farFuture); int truncated = (int) farFuture; System.out.println("long epochSecond = " + farFuture + " -> " + real); System.out.println("(int) 캐스팅 결과 = " + truncated + " -> " + Instant.ofEpochSecond(truncated)); } }
Integer.MAX_VALUE = 2147483647 Instant.ofEpochSecond(그 값) = 2038-01-19T03:14:07Z Integer.MAX_VALUE + 1 = -2147483648 Instant.ofEpochSecond(그 값) = 1901-12-13T20:45:52Z System.currentTimeMillis() = 1785397603745 Instant.now() = 2026-07-30T07:46:43.749712692Z Instant.MAX = +1000000000-12-31T23:59:59.999999999Z long epochSecond = 3000000000 -> 2065-01-24T05:20:00Z (int) 캐스팅 결과 = -1294967296 -> 1928-12-18T22:51:44Z

java.timeInstant 자체는 이 문제가 없습니다. 내부적으로 초를 **64비트 long**에 담고 나노초를 별도 int로 붙이거든요. Instant.MAX가 서기 10억 년 근처를 가리키는 게 그 증거입니다. 문제는 Instant가 아니라, 초를 32비트로 좁혀서 담는 자리입니다. 마지막 두 줄이 그 모양을 그대로 보여줘요. 30억 초 뒤(2065년)라는 멀쩡한 순간을 int로 캐스팅하는 순간, 부호 비트가 다시 뒤집혀 1928년이라는 엉뚱한 과거로 떨어집니다. API가 32비트 필드를 요구하거나, 직렬화 포맷이 4바이트를 고정해뒀거나, 오래된 라이브러리가 (int) epochSeconds를 아무 생각 없이 써둔 자리 — 실제 2038년 버그는 대부분 이 형태로 숨어 있습니다.

이게 남의 얘기가 아닌 이유는 이 32비트 폭이 아직도 살아 있는 자리가 많기 때문입니다. 임베디드 기기, 오래된 파일 포맷, 그리고 이 책의 독자에게 가장 가까운 사례로는 — MySQL의 TIMESTAMP 타입입니다. TIMESTAMP는 내부적으로 32비트 부호 있는 정수를 쓰고, 표현 범위가 정확히 1970-01-01 00:00:01부터 2038-01-19 03:14:07.499999까지로 막혀 있습니다. 반면 같은 테이블의 DATETIME은 연·월·일·시·분·초를 그대로 저장하는 방식이라 이 epoch 정수 자체와 무관하고, 범위도 1000-01-01부터 9999-12-31까지 훨씬 넓습니다. 왜 같은 데이터베이스 안에 정반대 두 타입이 있는지, 그리고 어느 쪽을 언제 써야 하는지는 13장에서 저장 구조까지 파고들며 다시 다룹니다. 여기서는 이 폭의 문제가 이미 지금 여러분의 스키마 안에 들어와 있을 수 있다는 것만 짚어둡니다.

고치는 방법 자체는 명확합니다. time_t64비트로 늘리면 다음 오버플로는 서기 2920억 년 근처까지 밀려납니다. 실제로 최근 리눅스 커널과 주요 배포판은 32비트 아키텍처에서도 64비트 time_t를 쓰도록 이미 전환했습니다. 그런데도 이 마이그레이션이 느린 이유는 폭 하나 늘리는 게 절대 국소적인 변경이 아니기 때문입니다. 디스크에 이미 32비트로 저장된 파일 포맷, time_t를 담은 구조체 크기에 의존하는 ABI, 그리고 무엇보다 펌웨어를 다시 굽기 어려운 임베디드 기기 — 이런 자리들은 재컴파일 한 번으로 끝나지 않습니다. 1장에서 본 엑셀의 1900년 윤년 버그가 “고쳐지지 않고 호환성이라는 이름으로 상속”됐던 것과 같은 모양이, 여기서 32비트라는 폭을 두고 반복됩니다.

마지막으로 epoch가 애초에 버린 것을 짚어야 합니다. epoch 초는 UTC 기준의 순간 하나만 가리킵니다. 어느 나라, 어느 타임존, 몇 시 몇 분이라는 정보는 여기 없어요. 이건 결함이 아니라 설계입니다 — 타임존과 오프셋을 빼야 두 순간을 정수 하나로 비교할 수 있으니까요. 대신 그 대가는 “이 정수가 실제로 몇 시를 가리키는가”를 알려면 별도의 규칙표가 있어야 한다는 것으로 돌아옵니다. 그 규칙표가 다음 장의 주제입니다.

정리

  • 시각을 달력·타임존 없이 **정수 하나(기준점 + 경과 초)**로 표현하면 비교·정렬·뺄셈이 전부 정수 연산이 된다 — 이게 epoch가 산 대가다.
  • 1970년은 원래부터 정해진 값이 아니었다. 최초 Unix(1971년 매뉴얼)는 1971년 기준, 60분의 1초 단위 카운터를 썼고, 32비트로는 2.26년마다 오버플로하게 돼 있었다. 3판 매뉴얼은 그 사실을 “2.26년마다 위기가 온다”고 스스로 적어 뒀다. 기준을 1972년으로 미뤄봐도 구조는 같았고, 결국 위기가 닥치기 전에 단위를 1초로 낮추고 기준을 1970년으로 되돌리며 정착했다. 리치 본인의 말로도 1970년은 그저 “깔끔한 라운드 넘버”였다.
  • time_t가 부호 있는 32비트일 때, 셀 수 있는 최댓값 2,147,483,647초는 정확히 2038-01-19T03:14:07Z를 가리킨다. 그 1초 뒤 부호 비트가 뒤집혀 1901-12-13T20:45:52로 떨어진다 — 1970년대에 미리 피했던 그 구조가, 규모만 키워 다시 놓여 있는 것이다.
  • java.timeInstant는 64비트 long 초를 쓰므로 이 한계가 없다. 문제는 32비트로 값을 좁히는 자리다 — (int) 캐스팅, 오래된 직렬화 포맷, 그리고 MySQL TIMESTAMP (DATETIME과 달리 이 32비트 폭에 그대로 묶여 있다, 13장에서 계속).
  • 고치는 법은 64비트 time_t지만, 디스크 포맷·ABI·임베디드 기기 때문에 마이그레이션은 느리다 — 호환성이 버그를 상속시킨다는 1장의 패턴과 같다.
  • epoch 초는 UTC 순간만 가리키고 타임존·오프셋 정보는 없다. 이 단순함이 epoch의 힘이자, “이 정수가 실제로 몇 시인가”를 알려면 별도 규칙표가 필요한 이유다.

생각해볼 질문: epoch는 시간대 정보를 일부러 버렸습니다. 그런데 화면에는 결국 “서울 기준 오후 3시” 같은 값을 보여줘야 합니다. 정수 하나뿐인 epoch와, 화면에 찍힐 지역 시각 사이를 이어주는 규칙은 어디에 있을까요? 그리고 그 규칙은 1장 말미에서 봤듯 물리 법칙이 아니라 각국의 입법 기록이라면, 그 기록은 대체 어떤 형태로 컴퓨터 안에 존재할까요?

5장 · 타임존은 파일이다 — IANA tz 데이터베이스의 실체

Last updated on