시간을 다루는 코드에는 서로 성격이 완전히 다른 두 개의 시계가 있습니다. epoch 시계(벽시계, wall clock)는 "지금이 몇 시 몇 분이냐"를 알려주고, 단조 시계(monotonic clock)는 "어떤 시점에서 얼마나 흘렀냐"를 재는 데 씁니다. 결정적 차이는 이겁니다 — epoch 시계는 거꾸로 갈 수 있지만, 단조 시계는 절대 뒤로 가지 않습니다.
"시간을 측정한다"는 한 문장 뒤에 이 두 시계가 숨어 있고, 둘을 헷갈리면 아주 드물게, 아주 고약하게 터지는 버그가 생깁니다. 나도 이걸 몰라서 타임아웃이 음수가 되는 유령 버그를 만난 적이 있어요.
epoch 시계 — 절대 시각, 그러나 흔들린다
epoch 시계는 1970년 1월 1일 UTC(유닉스 에포크)로부터 흐른 초를 셉니다. Date.now(), time.time(), System.currentTimeMillis()가 전부 이겁니다. "이 로그가 몇 시에 찍혔나", "이 토큰이 언제 만료되나"처럼 사람·외부 세계와 공유하는 절대 시각에는 이걸 써야 합니다.
문제는 이 시계가 바깥 요인으로 조정된다는 점입니다. 이유는 여럿입니다.
- NTP 동기화 — 서버가 표준 시각에 맞춰 시계를 앞으로 당기거나 뒤로 미룹니다.
- 사용자·관리자 변경 — 누군가 수동으로 시스템 시간을 바꿉니다.
- 윤초(leap second) — 드물게 1초가 삽입·조정됩니다.
그래서 두 번의 Date.now() 차이가 음수가 되는 일이 실제로 일어납니다. 그 사이에 NTP가 시계를 뒤로 돌렸다면요. 경과 시간을 이걸로 재면 안 되는 이유입니다.
단조 시계 — 오직 증가만 하는 스톱워치
단조 시계는 "임의의 고정 시점"부터 흐른 시간을 재는 스톱워치입니다. 기준점이 언제인지는 의미가 없고(대개 부팅 시각), 오직 두 값의 차이만 의미가 있습니다. NTP도, 사용자도 이 시계를 건드리지 못합니다. 그래서 항상 앞으로만 흐릅니다.
# 파이썬 — 경과 시간엔 반드시 monotonic
import time
start = time.monotonic()
do_work()
elapsed = time.monotonic() - start # 절대 음수가 안 됨
# 자바스크립트
const t0 = performance.now();
doWork();
const dt = performance.now() - t0; // 밀리초, 단조
# Go
start := time.Now() // Go의 time.Now는 단조 성분을 내장
// ... elapsed := time.Since(start) 로 안전하게 측정
여기서 절대 시각을 물어보면 답이 없습니다. 단조 시계의 12345.6이라는 값 자체는 "그냥 부팅 후 몇 초"일 뿐, 벽시계와 아무 관계가 없거든요.
언제 뭘 쓰나 — 한눈 정리
| 하고 싶은 것 | 써야 할 시계 |
|---|---|
| 로그 타임스탬프, 만료 시각 | epoch(벽시계) |
| 함수 실행 시간 측정 | 단조 |
| 타임아웃·재시도 간격 | 단조 |
| 애니메이션·프레임 타이밍 | 단조 |
| 여러 서버의 이벤트 정렬 | epoch(+ 논리시계 병행) |
규칙은 간단합니다. "몇 시냐"는 epoch, "얼마나 걸렸냐"는 단조. 이 한 줄만 지켜도 시간 관련 버그의 상당수가 사라집니다.
실전에서 무너지는 지점
내가 겪은 건 캐시 TTL 계산이었습니다. 만료 판정을 Date.now() 차이로 했는데, NTP가 시계를 몇 초 뒤로 당긴 순간 "아직 안 지났다"고 오판해 만료된 데이터를 계속 내보냈죠. 게다가 어떤 노드는 미래로 튀어 캐시가 즉시 죽고요. 경과 시간 계산을 단조 시계로 바꾸자 증상이 사라졌습니다.
비슷한 함정이 또 있습니다. 단조 시계 값을 디스크에 저장하거나 네트워크로 보내면 안 됩니다. 기준점이 머신·부팅마다 다르니, 다른 곳에서 그 숫자는 아무 의미가 없습니다. 저장·전송에는 반드시 epoch를 쓰세요. 분산 시스템에서 이벤트 순서를 맞출 땐 벽시계만으로는 부족해서, 램포트 타임스탬프 같은 논리 시계를 함께 씁니다.
자주 묻는 질문
그냥 항상 단조 시계만 쓰면 안 되나요?
안 됩니다. 단조 시계는 "지금 몇 시"를 모릅니다. 로그에 시각을 찍거나, 토큰 만료를 특정 날짜로 정하거나, 사람에게 시간을 보여주려면 반드시 epoch(벽시계)가 필요합니다. 둘은 용도가 갈리는 도구이지 우열 관계가 아닙니다.
왜 경과 시간을 벽시계로 재면 위험한가요?
측정 도중 NTP 동기화나 수동 시간 변경이 끼면 두 시각의 차이가 실제 경과와 달라지고, 심하면 음수가 됩니다. 타임아웃이 즉시 만료되거나 영원히 안 끝나는 버그로 이어집니다. 이런 조정에 면역인 단조 시계가 경과 측정의 정답입니다.
Go의 time.Now()는 어느 쪽인가요?
Go는 영리하게도 time.Now() 값에 벽시계 와 단조 성분을 함께 담습니다. 그래서 time.Since(start)처럼 두 시각을 빼면 자동으로 단조 성분을 써 안전하게 경과를 계산하고, 포맷 출력에는 벽시계를 씁니다. 개발자가 실수하기 어렵게 설계한 좋은 예입니다.
단조 시계도 컴퓨터가 절전 모드로 들어가면 멈추지 않나요?
구현에 따라 다릅니다. 일부 단조 시계는 시스템 서스펜드 동안 흐르지 않습니다. 절전 시간까지 포함해 재고 싶다면 리눅스의 CLOCK_BOOTTIME처럼 부팅 후 서스펜드 포함 경과를 세는 별도 시계를 써야 합니다. 대부분의 애플리케이션 타이머엔 기본 단조 시계로 충분합니다.

댓글 0