로그에 찍힌 1790550000은 몇 시일까: 유닉스 시간과 2038년 문제
유닉스 타임스탬프가 무엇을 세는지, 10자리와 13자리의 차이, 타임스탬프에 시간대가 없다는 뜻, 윤초와 2038년 문제, 데이터베이스에 시각을 저장하는 방법을 예시와 함께 정리했습니다.
장애 대응 중에 로그를 열었더니 이런 줄이 있습니다.
{"event":"payment_failed","created_at":1790550000}
옆자리 동료가 묻습니다. “이거 몇 시예요?” 답은 한국 시각으로 2026년 9월 28일 오전 8시 정각입니다. 그런데 다른 서비스가 같은 일을 created_at: 1790550000000으로 남겼고 누군가 그 숫자를 초로 읽는다면, 답은 5만 년도 더 뒤의 날짜가 됩니다. 유닉스 시간은 단순한 만큼 헷갈리기도 쉽습니다.
1970년 1월 1일 0시부터 센 초
유닉스 시간(Unix time, epoch time)은 1970년 1월 1일 00:00:00 UTC부터 지난 초의 수입니다. 이 기준 시점을 에포크(epoch)라고 부릅니다. 1790550000초가 지나면 2026년 9월 27일 23:00:00 UTC가 되고, 한국은 UTC보다 9시간 빠르므로 9월 28일 오전 8시입니다.
날짜를 숫자 하나로 다루면 편한 점이 많습니다. 두 시각의 차이는 뺄셈 한 번이면 되고, 정렬도 숫자 정렬이면 끝이며, 나라마다 다른 날짜 표기법도 신경 쓸 필요가 없습니다. 운영체제, 데이터베이스, 대부분의 프로그래밍 언어가 내부적으로 이런 방식으로 시각을 다루는 이유입니다.
자릿수로 단위를 짐작한다
같은 순간을 초로 쓰느냐 밀리초로 쓰느냐에 따라 숫자는 1000배 차이가 납니다.
| 값 | 단위 | UTC 기준 |
|---|---|---|
1790550000 |
초 (10자리) | 2026-09-27 23:00:00 |
1790550000000 |
밀리초 (13자리) | 2026-09-27 23:00:00 |
1790550000을 밀리초로 잘못 읽으면 |
1970-01-21 17:22:30 |
C의 time(), Python의 time.time(), 셸의 date +%s는 초를 돌려줍니다. JavaScript의 Date.now()와 Java의 System.currentTimeMillis()는 밀리초입니다. 그래서 서버는 Python, 화면은 JavaScript인 서비스에서는 이 둘이 섞이기 쉽습니다.
실용적인 기준은 자릿수입니다. 지금 시각 근처라면 초는 10자리, 밀리초는 13자리입니다. 날짜가 1970년 1월로 나온다면 밀리초를 초로 쓴 것이 아닌지, 수만 년 뒤로 나온다면 반대가 아닌지 먼저 의심해 볼 만합니다.
숫자에는 시간대가 없다
1790550000은 서울에서 보든 뉴욕에서 보든 같은 순간입니다. 타임스탬프 자체에는 “한국 시각”이나 “UTC”라는 정보가 없습니다. 시간대는 이 숫자를 사람이 읽는 날짜로 바꿀 때 비로소 등장합니다.
const d = new Date(1790550000 * 1000);
d.toISOString(); // "2026-09-27T23:00:00.000Z"
d.toLocaleString("ko-KR", { timeZone: "Asia/Seoul" }); // "2026. 9. 28. 오전 8:00:00"
“한국 시간 타임스탬프로 바꿔 주세요”라는 요청이 성립하지 않는 이유가 여기에 있습니다. 9시간을 더한 값을 따로 저장하면, 그 숫자는 더 이상 유닉스 시간이 아니게 되고 나중에 어느 쪽인지 아무도 모르게 됩니다.
윤초는 없는 셈 친다
지구 자전이 조금씩 불규칙해서, 협정 세계시(UTC)에는 가끔 1초를 끼워 넣는 윤초가 있었습니다. 2016년 12월 31일 23:59:60 UTC가 마지막이었습니다. 그런데 POSIX 표준은 유닉스 시간에서 하루를 정확히 86,400초로 계산하도록 정합니다. 윤초는 세지 않는다는 뜻입니다.
그래서 유닉스 시간은 엄밀히 말해 “에포크 이후 실제로 흐른 초”보다 1972년 이후 들어간 윤초 27번만큼 적습니다. 그래서 윤초 순간에는 같은 타임스탬프가 두 번 나옵니다. 이를 피하려고 Google처럼 그 전후 몇 시간에 걸쳐 1초를 조금씩 나눠 흡수하는(leap smear) 곳도 있습니다. 대부분의 애플리케이션에는 영향이 없지만, 두 시각의 차이로 정밀한 시간 간격을 재야 한다면 알아 둘 만한 부분입니다.
2038년 1월 19일 03:14:07
오래된 시스템은 유닉스 시간을 부호 있는 32비트 정수에 담았습니다. 이 정수가 담을 수 있는 가장 큰 값은 2,147,483,647이고, 이 순간은 2038년 1월 19일 03:14:07 UTC(한국 시각 12시 14분 7초)입니다. 1초가 더 지나면 값이 넘쳐서 가장 작은 수인 −2,147,483,648로 돌아가고, 날짜로는 1901년 12월 13일이 됩니다.
64비트 운영체제와 최근 언어들은 대부분 64비트로 시각을 다루므로 이 한계에서 벗어났습니다. 남은 위험은 눈에 잘 띄지 않는 곳에 있습니다. 32비트 임베디드 장비, 오래된 파일 형식, 그리고 데이터베이스의 자료형입니다. 예를 들어 MySQL의 TIMESTAMP 형은 범위가 2038-01-19 03:14:07 UTC에서 끝납니다. 만료일이 20년 뒤인 보험 계약이나 장기 대출 데이터는 이미 이 날짜를 넘깁니다. 2038년은 먼 미래가 아니라 오늘 저장하는 데이터의 문제입니다.
데이터베이스에 저장할 때
시각을 저장하는 규칙은 대체로 이렇게 정리됩니다.
UTC 기준의 순간으로 저장하고, 보여 줄 때 사용자의 시간대로 바꿉니다. PostgreSQL이라면 timestamp with time zone(timestamptz)이 이 방식입니다. 이름과 달리 시간대를 저장하는 것이 아니라, 들어온 값을 UTC로 바꿔 저장하고 꺼낼 때 세션의 시간대로 보여 줍니다. 시간대 없는 timestamp에 한국 시각을 그대로 넣으면, 서버를 옮기거나 해외 사용자가 생기는 순간 그 값이 어느 시간대였는지 알 수 없게 됩니다.
사람이 읽는 문자열로 주고받을 때는 ISO 8601 형식이 안전합니다. 2026-09-28T08:00:00+09:00과 2026-09-27T23:00:00Z는 같은 순간이고, 어느 쪽이든 오프셋이 적혀 있어 오해할 여지가 없습니다. 반대로 2026-09-28 08:00:00처럼 오프셋이 없는 문자열은 받는 쪽이 제멋대로 해석합니다.
한 가지 예외가 있습니다. “매주 월요일 오전 9시 회의”처럼 미래의 벽시계 시각은 순간이 아닙니다. 서머타임이 있는 지역에서는 같은 9시가 UTC로 계절마다 달라지므로, 이런 값은 현지 시각과 시간대 이름(America/New_York)을 함께 저장해야 합니다.
바로 확인해 보기
유닉스 타임스탬프 변환에 초나 밀리초를 넣으면 UTC와 서울, 도쿄, 뉴욕, 런던 시각을 함께 보여 주고, 반대로 날짜를 타임스탬프로 바꿀 수도 있습니다. 이 글의 1790550000과 2147483647을 넣어 보세요. 지금 이 순간의 타임스탬프도 실시간으로 나옵니다.
참고
- IEEE Std 1003.1 (POSIX), Base Definitions, “Seconds Since the Epoch”
- ECMAScript 명세(ECMA-262), Time Values and Time Range
- ISO 8601, Date and time — Representations for information interchange / RFC 3339
- MySQL 8.0 Reference Manual, “The DATE, DATETIME, and TIMESTAMP Types”
- PostgreSQL 문서, Date/Time Types