로그 타임스탬프가 갑자기 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예요.
여기서 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비트 부호 있음: 2038년. 과거(1970년 이전)도 표현하려면 부호가 필요해서 이 방식이 쓰였어요.
- 32비트 부호 없음: 2106년. 음수를 포기하는 대신 시간을 2배로 벌어요. 1970년 이전은 표현 못 해요.
- 64비트 부호 있음: 약 2,922억 년. 우주 나이의 수십 배라 사실상 영원히 안전해요.
차트에서 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 점검을 이미 진행해 왔어요. 점검 순서를 요약하면 이래요.
- 미래 날짜를 다루는 컬럼·필드의 타입이 32비트인지 확인(특히 MySQL
TIMESTAMP). - C/C++ 코드에서
int로 시간을 담거나 계산하는 곳을 찾아time_t(64비트)나int64_t로 교체. - 외부와 주고받는 바이너리 포맷·프로토콜에 32비트 시간 필드가 있는지 확인.
- 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 코드, 그리고 지금도 미래 날짜를 계산하는 장기 상품 시스템은 점검해 둘 가치가 있어요.
타임스탬프를 날짜로 바꿔 확인할 땐 타임스탬프 변환기를, 날짜 간격 계산은 날짜 차이 계산기를 쓰면 편해요.