← 블로그 목록

Developer Knowledge

개발자가 알아야 할 지식: 단조 시계, 타임아웃은 현재 시각으로 재면 안 된다

벽시계 시간과 단조 시계의 차이를 이해하고, 타임아웃·성능 측정·분산 시스템 디버깅에서 시간 버그를 줄이는 법을 정리합니다.

Python Software Foundation
  • 개발자가 알아야 할 지식
  • Software Engineering
  • Time
  • Reliability
  • Debugging

왜 개발자가 알아야 하나

개발자는 생각보다 자주 시간을 잽니다. API 요청이 몇 초 안에 끝나야 하는지, 캐시가 언제 만료되는지, 배치 작업이 얼마나 걸렸는지, 재시도 간격을 얼마나 기다릴지, 락을 언제 포기할지 모두 시간 계산입니다. 그런데 여기서 “현재 시각”과 “경과 시간”을 같은 것으로 취급하면 아주 고약한 버그가 생깁니다.

예를 들어 Date.now()time.time()으로 시작 시각을 저장하고, 나중에 다시 현재 시각을 읽어 now - start를 계산한다고 해봅시다. 평소에는 잘 됩니다. 하지만 운영체제가 NTP로 시간을 보정하거나, 사용자가 노트북 시간을 수동으로 바꾸거나, 가상 머신이 일시정지 후 재개되거나, 컨테이너가 다른 시간 설정 위에서 움직이면 벽시계 시간은 앞으로도 뒤로도 움직일 수 있습니다. 그러면 “10초 타임아웃”이 갑자기 음수가 되거나, 아직 기다려야 하는 작업이 이미 만료된 것처럼 보일 수 있습니다.

단조 시계(monotonic clock)는 이런 문제를 피하기 위한 도구입니다. Python 문서는 time.monotonic()을 뒤로 가지 않는 시계이며 시스템 시각 업데이트의 영향을 받지 않는 값으로 설명합니다. 중요한 제약도 함께 있습니다. 이 값의 기준점은 정의되어 있지 않으므로 절대 시각으로 해석하면 안 되고, 두 번 호출한 값의 차이만 의미가 있습니다.

이 구분은 언어와 런타임을 막론하고 중요합니다. JavaScript에는 브라우저의 performance.now()가 있고, Go에는 time.Time 값에 포함되는 monotonic clock reading이 있으며, Java에는 System.nanoTime()이 있습니다. 이름은 다르지만 원칙은 같습니다. “언제였는가”를 기록하려면 벽시계 시간을 쓰고, “얼마나 지났는가”를 재려면 단조 시계를 써야 합니다.

핵심 개념

벽시계 시간(wall-clock time)은 사람이 달력과 시계로 이해하는 시간입니다. “2026년 6월 10일 오전 8시 30분”, “UTC 2026-06-09T23:30:00Z”처럼 기록하고, 비교하고, 사용자에게 보여줄 수 있습니다. 로그 타임스탬프, 데이터 생성일, 예약 발송 시각, 인증 토큰의 만료 시각 같은 것은 벽시계 시간이 필요합니다.

문제는 벽시계 시간이 항상 일정한 속도로 앞으로만 흐른다는 보장이 없다는 점입니다. 시스템 시각은 운영체제 설정, 시간대 변경, NTP 보정, 윤초 처리 방식, 가상화 환경, 절전 모드의 영향을 받을 수 있습니다. 대부분의 시스템은 큰 점프를 피하려고 조금씩 보정하지만, 개발자가 모든 환경에서 그럴 것이라고 가정할 수는 없습니다.

단조 시계는 경과 시간을 재기 위한 시계입니다. 이름 그대로 값이 뒤로 가지 않는 것을 목표로 합니다. “지금이 몇 시인가”는 알려주지 않습니다. 대신 “아까보다 얼마나 지났는가”를 안정적으로 계산하게 해줍니다. 그래서 타임아웃, 데드라인까지 남은 시간, 성능 측정, 재시도 대기 시간, 이벤트 루프 지연 측정에는 단조 시계가 맞습니다.

이 차이를 코드로 보면 간단합니다.

import time

started_at = time.monotonic()
do_work()
elapsed = time.monotonic() - started_at
print(f"작업 시간: {elapsed:.3f}s")

위 코드에서 started_at은 사람이 읽을 수 있는 시각이 아닙니다. 로그에 그대로 남겨도 별 의미가 없습니다. 하지만 같은 프로세스 안에서 두 값의 차이를 계산하는 데는 적합합니다. 반대로 사용자에게 “작업 시작 시각”을 보여줘야 한다면 datetime.now(timezone.utc) 같은 벽시계 시간을 별도로 기록해야 합니다.

데드라인을 다룰 때도 원칙은 같습니다. “이 요청은 최대 3초 안에 끝나야 한다”면 시작 순간의 단조 시계 값에 3초를 더해 deadline을 만들고, 루프 안에서는 deadline - monotonic_now로 남은 시간을 계산합니다. 매번 고정된 3초를 다시 넣거나 벽시계 시간으로 만료 여부를 판단하면, 여러 단계의 작업에서 실제 제한 시간이 늘어나거나 줄어듭니다.

작은 예시 또는 체크리스트

다음은 여러 하위 호출을 가진 요청 처리 코드에서 흔히 필요한 패턴입니다.

import time

def handle_request():
    deadline = time.monotonic() + 3.0

    remaining = deadline - time.monotonic()
    if remaining <= 0:
        raise TimeoutError("deadline exceeded before db call")
    query_database(timeout=remaining)

    remaining = deadline - time.monotonic()
    if remaining <= 0:
        raise TimeoutError("deadline exceeded before api call")
    call_external_api(timeout=remaining)

이 방식은 “각 단계에 3초”가 아니라 “전체 요청에 3초”라는 정책을 코드에 반영합니다. 첫 번째 단계가 2.4초를 썼다면 두 번째 단계에는 약 0.6초만 남습니다. 이 패턴은 HTTP 서버, RPC 클라이언트, 작업 큐 컨슈머, 테스트 타임아웃에서 모두 유용합니다.

시간 관련 코드를 볼 때는 아래 체크리스트를 적용해보세요.

  • 경과 시간 측정에 벽시계 시간을 쓰고 있지 않은가?
  • 로그, 감사 기록, 사용자 표시에는 단조 시계 값을 쓰고 있지 않은가?
  • 타임아웃을 단계마다 새로 시작해서 전체 제한 시간을 무너뜨리고 있지 않은가?
  • sleep 후 정확히 그만큼 지났다고 가정하지 않는가?
  • 테스트에서 시간을 직접 기다리지 않고 fake clock이나 주입 가능한 clock을 쓸 수 있는가?
  • 분산 시스템에서 서로 다른 머신의 벽시계 시간을 절대적인 순서로 믿고 있지 않은가?

특히 마지막 항목은 운영 디버깅에서 중요합니다. 서버 A의 로그가 10:00:00.100이고 서버 B의 로그가 10:00:00.050이라고 해서 B가 반드시 먼저 처리했다고 단정하면 안 됩니다. 머신 간 시계 오차, 로그 전송 지연, 버퍼링이 끼어듭니다. 분산 추적의 span 관계나 요청 ID가 필요한 이유가 여기에 있습니다.

실무에서 자주 생기는 오해

  • “현재 시각 차이를 빼면 경과 시간이다”라는 습관이 가장 흔합니다. 로컬 개발 환경에서는 거의 티가 나지 않지만, 서버 시간 보정이나 절전 복귀가 있는 환경에서는 음수 duration, 너무 긴 타임아웃, 조기 만료 같은 문제가 생길 수 있습니다.

  • “단조 시계 값은 저장해두면 나중에도 쓸 수 있다”는 오해도 있습니다. 단조 시계의 기준점은 런타임이나 부팅 시점에 따라 달라질 수 있습니다. 같은 프로세스 안에서 차이를 계산하는 값으로 쓰는 것이 기본이며, 데이터베이스에 저장해 다른 프로세스가 해석하게 만들면 안 됩니다.

  • “성능 측정은 밀리초 정밀도면 충분하다”는 말은 상황에 따라 맞고 틀립니다. 사용자 요청 전체 지연 시간은 밀리초로 충분할 수 있지만, 짧은 함수나 이벤트 루프 지연을 볼 때는 더 높은 해상도의 카운터가 필요합니다. Python에서는 perf_counter()perf_counter_ns()가 이런 용도에 더 자연스러울 수 있습니다.

  • “sleep(1)은 정확히 1초 뒤에 돌아온다”는 가정도 위험합니다. 대부분의 런타임은 최소한 그 정도 쉬도록 노력하지만, 스케줄러 지연, 시스템 부하, 신호 처리 때문에 더 늦게 돌아올 수 있습니다. 반복 작업은 sleep(interval)을 누적하기보다 다음 deadline을 기준으로 남은 시간을 계산하는 편이 드리프트를 줄입니다.

  • “토큰 만료도 단조 시계로 하면 안전하다”는 생각은 반만 맞습니다. 외부 시스템과 약속한 만료 시각, JWT의 exp, 예약 발송처럼 달력 위의 의미가 있는 값은 벽시계 시간입니다. 단조 시계는 로컬 프로세스의 대기와 측정에는 좋지만, 다른 시스템과 공유하는 절대 시각을 대체하지 않습니다.

  • “서버 시간이 NTP로 맞으니 순서 판단에 충분하다”는 가정은 분산 시스템에서 자주 깨집니다. NTP는 유용하지만 모든 이벤트의 전역 순서를 보장하지 않습니다. 순서가 비즈니스적으로 중요하면 데이터베이스 트랜잭션 순서, 버전 번호, logical clock, 단일 writer 같은 별도 설계가 필요합니다.

오늘 바로 적용해보기

먼저 코드베이스에서 시간 차이를 계산하는 부분을 찾아보세요. JavaScript라면 Date.now() - startedAt, Python이라면 time.time() - started_at, Java라면 currentTimeMillis() 차이 계산이 후보입니다. 순수하게 경과 시간을 재는 코드라면 단조 시계 API로 바꾸는 것이 좋습니다.

두 번째로 타임아웃 전달 방식을 점검하세요. 서비스 A가 전체 5초 제한을 받았는데 DB 호출에 5초, 외부 API 호출에 다시 5초를 주고 있다면 실제 요청은 10초 이상 걸릴 수 있습니다. 요청 시작 시점에 deadline을 만들고, 하위 호출에는 “남은 시간”을 넘기는 방식이 더 예측 가능합니다.

세 번째로 로그에는 두 종류의 시간을 분리해서 남기세요. 사람이 읽고 시스템 간 상관관계를 볼 때는 UTC 타임스탬프가 필요합니다. 한 프로세스 안에서 단계별 소요 시간을 볼 때는 duration 필드를 별도로 남기는 편이 좋습니다. startedAtMonotonic=123456.7 같은 값은 로그 검색에 큰 도움이 되지 않습니다.

네 번째로 테스트를 바꾸세요. 시간 의존 테스트가 실제 sleep에 기대면 느리고 불안정해집니다. clock 인터페이스를 주입하거나 fake timer를 사용하면 “시간이 앞으로 갔다”는 조건을 즉시 만들 수 있습니다. 이때 벽시계와 단조 시계를 각각 주입할 수 있게 하면 설계가 더 선명해집니다.

마지막으로 운영 문서에 시간 가정을 적어두세요. 락 만료, 캐시 TTL, 재시도 deadline, 배치 스케줄, 토큰 만료가 각각 어떤 시계를 기준으로 하는지 한 줄만 있어도 장애 때 혼란이 줄어듭니다. 시간 버그는 보통 코드 한 줄보다 “우리가 어떤 시간을 말하는지”가 흐릿해서 생깁니다.

더 알아보기

오늘의 takeaway

사용자에게 보여줄 시간은 벽시계로 기록하고, 타임아웃과 성능 측정처럼 “얼마나 지났는가”를 묻는 코드는 단조 시계로 재야 합니다.