CS · Time · Backend

시계는 흔들린다 - 경과 시간을 재는 다른 시계

컴퓨터 안에는 시계가 둘이다. 하나는 지금이 몇 시인지 말해 주지만 뒤로 갈 수 있고, 다른 하나는 몇 시인지 모르지만 절대 뒤로 안 간다. 둘을 바꿔 쓰면 걸린 시간이 음수로 나온다.

·시간이라는 데이터 8편
목차
  1. 컴퓨터 안에는 시계가 둘이다
  2. 벽시계는 왜 뒤로 가나
  3. 걸린 시간을 벽시계로 재면 안 된다
  4. 단조 시계로는 할 수 없는 일
  5. 무엇을 어느 시계로 재나
  6. 여러 대가 되면 더 어렵다
  7. 실무에서: 측정과 타임아웃부터 본다
  8. 정리

지금까지 큰 시계는 한결같이 앞으로만 간다고 했다. 이번 글에서 그 전제를 무너뜨린다.

컴퓨터 안에는 시계가 둘이다

프로그램이 물을 수 있는 시각이 두 가지다. 이름은 언어마다 다르지만 성격은 어디서나 같다.

무엇을 답하나뒤로 갈 수 있나
벽시계”지금 몇 시인가”갈 수 있다
단조 시계”기준점에서 얼마나 지났나”안 간다

지금까지 이 시리즈가 다룬 건 전부 벽시계다. 사람이 읽는 시각을 만들려면 이쪽이 필요하다.

단조 시계는 성격이 다르다. 이 값은 몇 시인지 답하지 못한다. 기준점이 대개 컴퓨터가 켜진 시점이라 그 자체로는 아무 뜻이 없다. 대신 절대 뒤로 가지 않는다는 보장이 붙는다.

벽시계는 왜 뒤로 가나

컴퓨터의 시계는 정확하지 않다. 조금씩 빠르거나 느리게 가고, 하루면 몇 초씩 어긋나기도 한다.

그래서 바깥의 정확한 시계에 주기적으로 맞춘다. 이때 두 가지 방식이 있다.

  • 조금씩 당기거나 늦춘다. 차이가 작으면 시계를 미세하게 빠르게 또는 느리게 돌려 서서히 맞춘다. 값이 뛰지는 않는다
  • 한 번에 옮긴다. 차이가 크면 그냥 값을 바꿔 버린다. 이때 시각이 훌쩍 뛰거나 뒤로 간다

두 번째가 사고의 자리다. 그리고 이건 이상한 상황이 아니라 정상 동작이다. 가상 머신이 정지했다 깨어날 때, 오래 꺼져 있던 장비가 켜질 때, 설정이 바뀔 때 흔히 일어난다.

벽시계지금 몇 시인가
10:00
10:01
10:02
09:59
10:00
이 구간을 쟀다
단조 시계얼마나 지났나
0
60
120
180
240
실제로 흐른 시간
0초60초120초180초240초
벽시계로 재면 09:59 − 10:02 = −3분 보정이 그 사이에 일어났다
단조 시계로 재면 180 − 120 = 60초 보정의 영향을 안 받는다

같은 구간을 쟀는데 한쪽은 음수가 나온다. 경과 시간은 뒤로 안 가는 시계로 재야 한다.

걸린 시간을 벽시계로 재면 안 된다

가장 흔한 사고가 이것이다.

java
// 위험: 두 시점 사이에 시계가 보정되면 값이 무너진다
long start = System.currentTimeMillis();
doWork();
long elapsed = System.currentTimeMillis() - start;

작업 도중에 시계가 뒤로 가면 elapsed가 음수로 나온다. 앞으로 뛰면 실제로 0.1초 걸린 작업이 몇 시간 걸린 것으로 기록된다.

고치는 법은 간단하다. 경과 시간에는 단조 시계를 쓴다.

java
// 안전: 이 값은 보정의 영향을 안 받는다
long start = System.nanoTime();
doWork();
long elapsedNanos = System.nanoTime() - start;

두 함수가 나란히 있는데 이름만 봐서는 성격 차이가 안 보인다. nanoTime이 더 정밀한 버전이라고 오해하기 쉽지만, 정밀도가 아니라 종류가 다른 시계다.

단조 시계로는 할 수 없는 일

대신 잃는 것이 있다. 여기서 비유가 깨진다. 이 시계는 공항 벽에 걸 수 없다. 사람이 읽을 수가 없어서다.

  • 저장하면 안 된다. 재부팅하면 기준점이 사라져 값이 뜻을 잃는다
  • 다른 컴퓨터와 비교하면 안 된다. 기준점이 서로 다르다
  • 로그에 남겨도 쓸모가 없다. 언제 일어난 일인지 답하지 못한다

그래서 두 시계는 한쪽이 다른 쪽을 대체하지 못한다. 둘 다 있어야 한다.

무엇을 어느 시계로 재나

갈라 두면 판단이 쉽다.

하려는 일어느 시계
로그에 시각 남기기벽시계
만료 시각 저장하기벽시계
응답 시간 재기단조 시계
타임아웃 걸기단조 시계
재시도 간격 두기단조 시계
캐시 유효기간상황에 따라 다르다

마지막 줄이 애매한 이유가 있다. 캐시가 **“넣고 5분”**이면 경과 시간이니 단조 시계다. **“매일 자정까지”**면 달력 시각이니 벽시계다. 같은 캐시라도 유효기간을 어떻게 정했느냐로 갈린다.

요즘 언어는 대개 경과 시간을 담는 전용 타입을 따로 갖고 있다. 순간을 담는 타입과 섞이지 않게 만들어 둔 것이다. 수 하나로 들고 다니면 이 구분이 사라지므로, 타입이 있으면 쓰는 편이 낫다.

여러 대가 되면 더 어렵다

한 대 안에서는 단조 시계로 해결된다. 여러 대가 되면 그 수단이 사라진다.

서버 A와 서버 B의 벽시계는 늘 조금씩 다르다. 잘 맞춰 두어도 밀리초 단위 차이는 남는다. 그래서 이런 판단이 틀린다.

  • 두 서버의 로그 시각을 비교해 순서를 정한다. B가 먼저 찍혔는데 A가 먼저 일어난 일일 수 있다
  • “가장 최근 값이 이긴다”로 충돌을 푼다. 시계가 앞선 서버가 늘 이긴다
  • 시각으로 잠금 만료를 판단한다. 두 서버가 동시에 자기가 잠금을 쥐었다고 믿을 수 있다

그래서 분산 환경에서 순서가 중요하면 시계 말고 다른 것으로 정한다. 한 곳에서 번호를 발급하거나, 사건마다 앞선 사건을 가리키게 하거나, 순서 대신 순서가 틀려도 결과가 같은 성질을 만들어 두는 식이다.

시각은 사람이 보고 판단하기 위한 값으로 남기고, 기계의 판단 근거로는 쓰지 않는 것이 안전하다.

실무에서: 측정과 타임아웃부터 본다

기존 코드에서 확인할 자리는 좁고 분명하다.

  • 뺄셈을 찾는다. 시각끼리 빼서 걸린 시간을 구하는 자리가 벽시계를 쓰고 있으면 바꾼다
  • 타임아웃과 재시도 간격을 본다. 여기에 벽시계를 쓰면 시계 보정 때 타임아웃이 즉시 터지거나 영영 안 터진다
  • 음수 방어를 넣는다. 이미 벽시계로 재는 코드가 있고 당장 못 고치면, 음수가 나올 때 0으로 보고 경고를 남긴다. 값을 그냥 쓰면 통계가 무너진다
  • 시계를 바깥에서 주입한다. 시각을 코드가 직접 묻지 않고 주입받게 만들면 테스트에서 시각을 통제할 수 있다. 서머타임 전환이나 시계 역행처럼 재현이 어려운 상황을 시험할 유일한 방법이기도 하다
  • 시각을 읽는 것도 공짜가 아니다. 벽시계를 읽는 건 운영체제에 묻는 일이라 아주 촘촘한 반복문 안에서 매번 부르면 그 자체가 부담이 된다

정리

  • 컴퓨터 안에는 시계가 둘이다. 벽시계는 몇 시인지 알지만 뒤로 갈 수 있고, 단조 시계는 몇 시인지 모르지만 안 뒤로 간다.
  • 벽시계가 뒤로 가는 건 고장이 아니라 보정의 정상 동작이다.
  • 경과 시간을 벽시계로 재면 음수가 나오거나 몇 시간으로 부풀려진다.
  • 단조 시계는 저장·전송·비교에 쓸 수 없다. 둘은 서로를 대체하지 못한다.
  • 갈림길은 하나다. “몇 시인가”면 벽시계, “얼마나 지났나”면 단조 시계.
  • 여러 대 사이에서는 시계로 순서를 정하지 않는다. 시각은 사람이 볼 값으로 남긴다.

다음 글은 이 시리즈를 접는다. 시각이 이상할 때 증상만 보고 어디부터 볼지를 한 장으로 만든다.