~ / posts / developer

Unix 타임스탬프란? 2038년 문제가 생기는 이유와 점검할 곳

Unix 타임스탬프는 1970년 1월 1일 UTC부터 센 초예요. 32비트 부호 정수에 담으면 2038년 1월 19일에 음수로 뒤집혀 1901년이 찍혀요. 비트 단위로 왜 그런지 보여주고, 정수 폭별 한계 연도와 지금 점검할 곳을 정리했습니다.

로그 타임스탬프가 갑자기 1901년으로 찍히거나, 임베디드 장비 날짜가 1970년으로 리셋되는 걸 본 적 있다면 Unix 타임스탬프의 한계와 마주친 거예요. 결론부터 말하면 Unix 타임스탬프는 1970년 1월 1일 0시(UTC)부터 흐른 초의 개수고, 이걸 32비트 부호 있는 정수에 저장하면 2038년 1월 19일 03:14:07 UTC 이후 숫자가 음수로 뒤집혀 1901년으로 돌아가요. 이게 "2038년 문제"(Y2K38)예요.

2000년 문제(Y2K)의 사촌 격인데, 이번엔 저장 방식이 아니라 정수의 비트 수가 원인이에요. 비트 단위로 왜 뒤집히는지 직접 계산해서 보여 드리고, 정수 폭에 따라 한계 연도가 어떻게 달라지는지, 그리고 지금 점검해 둘 만한 곳을 정리했어요.

핵심 요약

Unix 타임스탬프는 1970-01-01 00:00:00 UTC(에포크)부터 센 초예요. 시간대와 무관한 절대 시각이에요.

32비트 부호 정수의 최댓값 2,147,483,647초가 2038-01-19 03:14:07 UTC이고, 1초 더하면 음수가 돼 1901년으로 뒤집혀요.

64비트로 저장하면 약 2,922억 년까지 담겨서 사실상 영원히 안전해요.

현대 OS·DB·언어는 대부분 64비트로 넘어갔지만, 오래된 임베디드·C 코드·int 컬럼·파일 포맷에는 아직 남아 있어요.

Unix 타임스탬프: 1970년부터 센 초

Unix 타임스탬프(에포크 시간)는 기준점인 1970년 1월 1일 0시 UTC부터 지금까지 흐른 초의 개수예요. 예를 들어 1759622400 은 2025-10-05 00:00:00 UTC예요.

import datetime as dt
dt.datetime.fromtimestamp(1759622400, dt.timezone.utc)
# 2025-10-05 00:00:00+00:00
int(dt.datetime.now(dt.timezone.utc).timestamp())
# 현재 시각의 타임스탬프

핵심은 시간대와 무관한 하나의 숫자라는 점이에요. 서울이든 뉴욕이든 같은 순간에는 같은 타임스탬프를 봐요. 그래서 로그, DB, API에서 시각을 주고받을 때 기준으로 삼기 좋아요. 사람이 읽을 날짜로 바꾸는 건 보여줄 때만 하면 돼요. 이 "저장은 숫자로, 표시는 현지 시간으로" 원칙은 타임존 KST·UTC DB 저장 글에서 자세히 다뤘어요.

숫자를 날짜로, 날짜를 숫자로 바꿔 볼 땐 타임스탬프 변환기가 편해요. 10자리면 초, 13자리면 밀리초로 알아서 판별해 줘요.

2038년 문제: 비트가 뒤집히는 순간

C 언어의 전통적인 time_t 타입과 많은 옛 시스템은 타임스탬프를 32비트 부호 있는 정수에 저장했어요. 부호 있는 32비트가 담을 수 있는 최댓값은 2,147,483,647이에요. 이게 바로 2038-01-19 03:14:07 UTC예요.

32비트 부호 있는 정수가 최댓값에서 1초 증가하면 부호 비트가 1이 되어 음수로 뒤집히고 1901년이 되는 과정, 그리고 정수 폭별 한계 연도 비교
비트 하나가 넘치면 시간이 과거로 돌아간다

여기서 1초가 더해지면 어떻게 되는지 직접 계산해 봤어요.

import datetime as dt
MAX = 2**31 - 1                 # 2147483647
dt.datetime.fromtimestamp(MAX, dt.timezone.utc)
# 2038-01-19 03:14:07+00:00

overflow = MAX + 1              # 2147483648 — 32비트 부호 정수로는 표현 불가
as_signed = -(2**31)           # 비트가 그대로면 -2147483648로 읽힌다
dt.datetime.fromtimestamp(as_signed, dt.timezone.utc)
# 1901-12-13 20:45:52+00:00

부호 있는 정수는 맨 앞 비트(부호 비트)가 1이면 음수예요. 최댓값 0111...1 에 1을 더하면 1000...0 이 되는데, 이건 가장 큰 양수가 아니라 가장 작은 음수(-2,147,483,648)예요. 그래서 시간이 2038년에서 1901년으로 점프해요. 자동차 계기판이 999,999km를 넘어 0으로 돌아가는 것과 같은 자릿수 넘침(오버플로)인데, 부호 비트 때문에 0이 아니라 음수로 가는 거죠.

정수 폭에 따른 한계 연도

저장하는 정수의 크기와 부호 유무에 따라 한계가 달라져요. 각 경우의 최댓값을 날짜로 바꿔 봤어요.

타임스탬프 저장 방식별 표현 가능한 마지막 해
32비트 부호 있음32비트 부호 있음 · 2,038년2,038년32비트 부호 없음32비트 부호 없음 · 2,106년2,106년64비트 부호 있음(로그축 표기)64비트 부호 있음(로그축 표기) · 2,922,000,000년2,922,000,000년

64비트는 약 2922억 년 — 막대가 너무 길어 실제로는 비교 불가할 만큼 안전하다

단위: 년 · 자료: Python datetime 으로 각 정수 최댓값(2^31-1, 2^32-1, 2^63-1)을 UTC 날짜로 변환해 직접 계산
표로 보기
구분한계 연도
32비트 부호 있음2,038
32비트 부호 없음2,106
64비트 부호 있음(로그축 표기)2,922,000,000

차트에서 64비트 막대가 압도적으로 긴 건 로그 축이 아니라 실제 비율이에요. 32비트에서 64비트로 한 번만 바꾸면 이 문제는 끝나요.

왜 하필 1970년일까, 그리고 윤초 이야기

기준점이 1970년인 건 Unix가 그 무렵 벨 연구소에서 개발됐기 때문이에요. 초기 Unix는 60Hz로 째깍거리는 하드웨어 시계를 세었는데, 그 방식으로는 몇 년이면 카운터가 넘쳐서 기준점을 "최근"으로 둘 수밖에 없었어요. 이후 1초 단위로 세는 방식으로 바뀌면서 1970-01-01 00:00:00 UTC가 관례로 굳었고, 지금은 POSIX 표준이 됐어요.

한 가지 미묘한 점은 Unix 시간이 윤초를 세지 않는다는 거예요. 지구 자전이 조금씩 느려져서 가끔 국제 표준시에 1초를 더하는 윤초가 있는데, Unix 타임스탬프는 "하루는 정확히 86,400초"라고 가정하고 윤초를 건너뛰어요. 덕분에 날짜 계산은 단순해지지만, 윤초가 삽입되는 순간 같은 타임스탬프가 두 번 나타나거나 1초가 사라지는 효과가 생겨요. 대부분의 서비스는 신경 쓸 필요가 없지만, 금융 거래 체결 순서나 분산 시스템의 정밀한 시각 동기화에서는 "윤초 스미어링"(윤초를 하루에 걸쳐 조금씩 나눠 적용하는 기법)을 써서 이 불연속을 피해요.

그래서 Unix 타임스탬프는 "절대적인 물리 시간"이 아니라 "약속된 셈법"이라고 이해하는 게 정확해요. 대부분의 용도에선 이 약속이 충분히 정확하고 편리해요.

지금 우리 시스템도 위험할까

대부분의 현대 환경은 이미 안전해요. 그래도 확인해 둘 만한 곳이 있어요.

영역상태확인할 것
64비트 리눅스·맥·윈도우안전time_t 가 64비트
최신 MySQL·PostgreSQL대체로 안전TIMESTAMP vs DATETIME (아래)
JavaScript안전Date 는 64비트 부동소수점(밀리초)
Python 3, Java Instant안전64비트 이상
32비트 임베디드·IoT위험할 수 있음펌웨어의 time_t 폭
오래된 C 코드위험할 수 있음int 로 시간 계산하는 곳
네트워크·파일 포맷포맷마다 다름32비트 시간 필드

특히 조심할 게 MySQL의 TIMESTAMP 타입이에요. MySQL TIMESTAMP 는 내부적으로 32비트라 2038-01-19가 상한이에요. 반면 DATETIME 은 9999년까지 담겨요. 날짜 범위가 넓어야 하는 컬럼(만기일, 예약, 보험)이라면 DATETIME 이나 더 넓은 타입을 쓰세요. 타입 선택은 타임존 KST·UTC DB 저장에서 더 비교했어요.

애플리케이션 코드에서는 이런 패턴을 찾아 두면 좋아요.

int now = time(NULL);           // 위험: 2038년 이후 음수
time_t now = time(NULL);        // time_t가 64비트면 안전
# 미래 날짜 계산에서 32비트 한계를 넘기는지 테스트
import datetime as dt
far = dt.datetime(2040, 1, 1, tzinfo=dt.timezone.utc)
int(far.timestamp())            # 2208988800 — 2^31을 넘는다

2038년은 생각보다 가깝다

"아직 10년도 더 남았는데"라고 넘기기 쉽지만, 만기일이 미래인 데이터는 지금도 2038년을 넘겨요. 20년, 30년짜리 주택담보대출, 장기 보험, 연금, 구독 자동 갱신 같은 시스템은 이미 2038년 이후 날짜를 계산하고 있어요. 그 계산이 32비트 경로를 지나면 지금 당장 버그가 나요.

실제로 과거 사례도 있어요. 2022년 일부 시스템에서 미래 시각을 다루다 32비트 한계에 걸린 사례가 보고됐고, 장기 만기 상품을 다루는 금융권은 Y2K38 점검을 이미 진행해 왔어요. 점검 순서를 요약하면 이래요.

  1. 미래 날짜를 다루는 컬럼·필드의 타입이 32비트인지 확인(특히 MySQL TIMESTAMP).
  2. C/C++ 코드에서 int 로 시간을 담거나 계산하는 곳을 찾아 time_t(64비트)나 int64_t 로 교체.
  3. 외부와 주고받는 바이너리 포맷·프로토콜에 32비트 시간 필드가 있는지 확인.
  4. 2040년 같은 미래 날짜로 테스트 데이터를 넣어 라운드트립을 검증.

자주 묻는 질문

Unix 타임스탬프는 UTC인가요, 현지 시간인가요?

UTC 기준이에요. 1970년 1월 1일 0시 UTC부터 센 초라서 시간대와 무관한 하나의 숫자예요. 같은 순간이면 서울이든 런던이든 같은 타임스탬프를 봐요. 현지 시간은 보여줄 때 변환하는 거예요.

2038년 문제는 왜 2038년 1월 19일인가요?

32비트 부호 있는 정수의 최댓값 2,147,483,647초가 1970년 1월 1일부터 세면 2038-01-19 03:14:07 UTC이기 때문이에요. 그 다음 초에 정수가 넘쳐 음수가 되면서 1901년으로 뒤집혀요.

내 서비스가 2038년 문제에 영향을 받나요?

64비트 OS와 최신 언어·DB를 쓴다면 대부분 안전해요. 다만 MySQL TIMESTAMP 컬럼, 32비트 임베디드 장비, int 로 시간을 다루는 오래된 C 코드, 32비트 시간 필드가 있는 파일·네트워크 포맷은 확인이 필요해요. 특히 만기일이 먼 미래인 데이터가 있다면 지금 점검하세요.

32비트를 부호 없는 정수로 바꾸면 해결되나요?

2106년까지 늘어나지만 근본 해결은 아니에요. 1970년 이전 날짜를 표현할 수 없게 되고, 2106년에 같은 문제가 또 와요. 가능하면 64비트로 바꾸는 게 맞아요.

JavaScript의 Date도 2038년 문제가 있나요?

없어요. JavaScript Date 는 64비트 부동소수점에 밀리초를 담아서 약 ±27만 년까지 표현해요. 다만 밀리초 단위라 초 단위 타임스탬프와 섞으면 다른 버그가 생기는데, 이건 초와 밀리초 단위 버그에서 다뤘어요.

정리

Unix 타임스탬프는 1970년부터 센 초이고, 32비트 부호 정수에 담으면 2038년에 음수로 뒤집혀요. 64비트로 한 번만 바꾸면 사실상 영원히 안전해요. 대부분의 현대 시스템은 이미 넘어갔지만, MySQL TIMESTAMP, 임베디드 장비, 오래된 C 코드, 그리고 지금도 미래 날짜를 계산하는 장기 상품 시스템은 점검해 둘 가치가 있어요.

타임스탬프를 날짜로 바꿔 확인할 땐 타임스탬프 변환기를, 날짜 간격 계산은 날짜 차이 계산기를 쓰면 편해요.

#Unix 타임스탬프#2038년 문제#Y2K38#32비트 시간#time_t#타임스탬프 오버플로#유닉스 시간
← 이전 글정규식 기초 문법 치트시트 — 10가지로 읽는 법과 JS·Python 차이다음 글 →API 응답 디버깅 순서 — 데이터가 안 뜰 때 확인하는 6단계