목차
큰 시계가 세는 수를 이번 글에서 뜯어본다. 정체는 놀랄 만큼 단순하다.
어느 날부터 세기 시작했나
기준은 1970년 1월 1일 0시 0분 0초다. 이 시점을 에폭이라 부르고, 큰 시계는 그때부터 흐른 초를 센다.
1970-01-01 00:00:00 → 0
2025-09-30 06:00:00 → 1759208400왜 하필 1970년이냐면, 깊은 이유가 없다. 유닉스를 만들던 사람들이 1970년대 초에 기준점이 필요했고, 다루기 편한 가까운 과거의 딱 떨어지는 날을 골랐을 뿐이다. 우주의 시작도 아니고 컴퓨터의 생일도 아니다.
중요한 건 날짜가 아니라 모두가 같은 날을 기준으로 삼기로 했다는 사실이다. 기준이 하나면 수 하나가 곧 순간 하나가 된다.
초만 세면 달력이 필요 없다
이 방식의 값은 달력을 안 들고 다녀도 된다는 데 있다.
두 시점 중 어느 쪽이 먼저인지 알고 싶으면 수를 비교하면 끝이다. 몇 시간 차이인지 알고 싶으면 빼면 된다. 월마다 날수가 다르다는 것도, 윤년이 있다는 것도, 이 수를 다루는 동안에는 전혀 몰라도 된다.
30일 뒤 = 현재값 + 60 * 60 * 24 * 30달력 규칙은 사람에게 보여 줄 때만 꺼내 쓴다. 안쪽에서는 그냥 큰 수다. 순간을 다루는 일과 달력을 다루는 일이 갈리는 자리가 여기다.
다만 “30일 뒤”와 “다음 달 같은 날”은 다른 요구다. 뒤엣것은 초 덧셈으로 안 나온다. 달마다 날수가 달라서 달력 규칙을 꺼내야 한다. 초 덧셈으로 풀리는 요구인지 먼저 확인하는 습관이 사고를 많이 줄인다.
칸이 몇 개냐가 언제까지 셀지를 정한다
이 수를 담는 칸의 크기가 곧 셀 수 있는 한계다.
오래된 시스템은 이 값을 부호 있는 32비트 정수에 담았다. 담을 수 있는 최대가 21억 남짓이고, 초로 21억은 약 68년이다. 1970에서 68년을 더하면 2038년이 나온다.
그날 이후로 넘어가면 값이 최대에서 최소로 되감긴다. 2038년이 1901년으로 읽히는 것이다. 이 되감김이 왜 일어나는지는 정수를 담는 칸 이야기에서 이미 봤다. 시간은 그 이야기가 실제 날짜를 달고 나타난 경우다.
2038년까지 아직 멀어 보이지만 지금도 걸린다. 30년 만기 상품, 장기 보증, 만료일이 먼 인증서처럼 미래 날짜를 계산하는 코드는 이미 2038년을 넘는 값을 만든다. 요즘 언어와 DB는 대개 64비트로 담아 문제가 없지만, 오래된 C 코드나 임베디드 장비는 그렇지 않다.
64비트로 담으면 한계가 약 2900억 년이 된다. 사실상 걱정할 일이 없다.
같은 순간이 1000배 큰 수로 적힌다
여기서 가장 흔한 사고가 나온다. 모두가 초를 세지는 않는다.
1759208400
10자리
1759208400000
13자리
같은 순간이라도 적은 단위와 읽는 단위가 어긋나면 값이 무너진다. 1970년 언저리가 보이면 대개 이 사고다.
유닉스 계열 도구와 대부분의 DB는 초를 센다. 자바스크립트와 자바는 밀리초를 센다. 어떤 시스템은 마이크로초, 어떤 시스템은 나노초를 센다.
문제는 둘 다 그냥 수라서 섞여도 아무도 안 막는다는 점이다.
- 밀리초 값을 초로 읽으면 → 5만 년쯤 뒤의 날짜가 나온다
- 초 값을 밀리초로 읽으면 → 1970년 1월 언저리가 나온다
두 번째가 특히 지독하다. “1970년 1월 21일”처럼 그럴듯한 날짜가 나와서 값이 깨졌다는 걸 눈치채기 어렵다. 화면에 1970이 보이면 대개 이 사고다.
1970년 이전은 음수다
에폭보다 앞선 순간은 음수로 적힌다. 1969년은 -31536000 언저리다.
대개는 문제가 없지만 두 자리에서 걸린다. 하나는 음수를 안 받는 타입에 담는 경우고, 다른 하나는 나눗셈이다. 음수를 초 단위로 자를 때 언어마다 버림 방향이 달라 하루가 어긋나기도 한다.
생년월일처럼 오래된 날짜를 다룬다면 음수 구간을 실제로 넣어 시험해 보는 것이 안전하다.
에폭이 하나만 있는 게 아니다
여기서 비유가 깨진다. 공항에 큰 시계가 하나뿐이라고 했지만, 실은 시스템마다 자기 기준점을 갖고 있다.
| 어디 | 기준점 | 단위 |
|---|---|---|
| 유닉스 계열 | 1970-01-01 | 초 |
| 윈도우 파일 시각 | 1601-01-01 | 100나노초 |
| GPS | 1980-01-06 | 주 + 초 |
| 스프레드시트 | 1900-01-01 | 일 |
기준점이 다르면 같은 수가 전혀 다른 순간을 가리킨다. 시스템 사이에서 시각을 주고받을 때 **“이 수는 어느 기준으로 몇 초냐”**를 먼저 맞춰야 하는 이유다.
이 시계는 윤초를 세지 않는다
비유가 한 번 더 깨진다. 큰 시계가 한결같이 앞으로만 센다고 했는데, 유닉스 시간은 그보다 이상한 약속을 갖고 있다.
지구 자전은 조금씩 느려져서, 가끔 하루에 1초를 끼워 넣어 시계를 맞춘다. 이걸 윤초라 한다. 그런데 유닉스 시간은 하루를 무조건 86400초로 못 박아 두었다. 윤초를 셀 자리가 아예 없다.
그래서 윤초가 들어오는 날 시스템은 같은 수를 두 번 내놓거나, 그 1초를 하루에 걸쳐 아주 조금씩 늦게 흘려보내는 식으로 넘긴다. 어느 쪽이든 그 순간의 값은 믿을 게 못 된다.
일반적인 서비스에서 이걸 신경 쓸 일은 거의 없다. 다만 “1초 안에 두 번 일어날 수 없다”를 전제로 짠 코드가 있다면 그 전제가 깨질 수 있다는 건 알아 둘 만하다.
실무에서: 단위를 이름에 박는다
에폭 사고는 대부분 단위 착각이다. 그리고 단위는 값만 봐서는 알 수 없다.
가장 값싼 방어는 이름에 단위를 적는 것이다.
// 나쁨: 이 수가 초인지 밀리초인지 이름이 말해 주지 않는다
long timestamp;
long expiry;
// 좋음: 틀리게 쓰면 읽는 사람이 먼저 알아챈다
long createdAtEpochSeconds;
long expiresAtEpochMillis;한 걸음 더 나가면 수를 그대로 들고 다니지 않는 것이 낫다. 요즘 언어에는 순간을 담는 전용 타입이 있고, 그 타입은 단위를 자기가 안다. 수로 바꾸는 건 바깥과 주고받는 경계에서만 한다.
경계에서 확인할 것도 정해져 있다.
- 외부 API가 주는 값이 초인지 밀리초인지 문서에서 확인한다. 안 적혀 있으면 자릿수를 세 본다 (초는 10자리, 밀리초는 13자리)
- DB 컬럼의 정밀도를 확인한다. 초까지만 담는 컬럼에 밀리초를 넣으면 조용히 잘린다
- 로그에 남길 때는 수보다 사람이 읽는 꼴이 낫다. 사고 났을 때 눈으로 바로 걸러진다
정리
- 에폭은 1970년 1월 1일이고, 큰 시계는 그때부터 흐른 초를 센다. 기준일 자체에 깊은 뜻은 없다.
- 초만 세면 달력을 안 들고 다녀도 된다. 비교는 대소 비교, 간격은 뺄셈으로 끝난다.
- 32비트에 담으면 2038년에 되감긴다. 미래 날짜를 계산하는 코드는 지금도 걸린다.
- 초·밀리초·나노초가 섞이면 1970년 언저리나 먼 미래가 나온다. 화면의 1970은 대개 이 사고다.
- 기준점은 시스템마다 다르고, 유닉스 시간은 윤초를 아예 안 센다.
- 방어는 단위를 이름에 박고, 안쪽에서는 수 대신 전용 타입을 쓰는 것이다.
다음 글은 팻말 쪽으로 간다. 큰 시계가 가리키는 값을 0으로 삼는 기준선이 무엇인지, 그리고 팻말에 적힌 숫자가 정확히 무슨 뜻인지를 본다.