CS · Time · Backend

시각이 틀어졌을 때 - 밀린 폭만 보고 어디부터 볼지 고르기

9시간이 밀렸는지 1시간이 밀렸는지 하루가 밀렸는지는 아무렇게나 정해지지 않는다. 밀린 폭 자체가 어디가 어긋났다는 신호다. 이 시리즈를 한 장의 판단표로 접는다.

·시간이라는 데이터 9편
목차
  1. 밀린 폭이 원인을 말해 준다
  2. 딱 오프셋만큼 밀렸다
  3. 한 시간만 밀렸다
  4. 1970년이 보인다
  5. 하루가 밀렸다
  6. 걸린 시간이 음수이거나 터무니없다
  7. 순서가 뒤집혔다
  8. 되짚는 순서
  9. 한 장으로
  10. 시리즈를 한 줄씩
  11. 여기서 다루지 않은 것
  12. 정리

시간 버그는 대개 “값이 이상하다”는 한 줄로 신고된다. 그 한 줄에서 범위를 좁히는 법으로 시리즈를 닫는다.

밀린 폭이 원인을 말해 준다

시간 사고에는 다른 버그에 없는 성질이 하나 있다. 틀린 값이 아무렇게나 나오지 않는다.

글자가 깨질 때 깨진 모양이 어디가 어긋났는지 말해 주던 것과 같다. 시간은 밀린 폭이 그 역할을 한다. 9시간과 1시간과 하루는 각각 다른 곳이 어긋났다는 신호다.

← 시계가 거꾸로 갔다 밀린 폭이 커진다 →
음수
시계 종류를 잘못 골랐다 경과 시간을 벽시계로 쟀다. 드물게 몇 개만 튄다
0
1시간
서머타임 · 낡은 규칙 특정 날짜를 경계로 갈린다. 그날을 찾으면 원인이 잡힌다
오프셋만큼
팻말을 안 씌웠거나 두 번 씌웠다 어느 방향으로 밀렸는지가 단서다
하루
날짜 값에 팻말을 씌웠다 자정 근처만 틀리고 낮 값은 멀쩡하다
.
수십 년
단위 착각 · 빈 값 1970년이나 먼 미래가 보인다. 자릿수를 세면 갈린다

틀린 값은 아무렇게나 나오지 않는다. 밀린 폭을 재면 볼 곳이 좁혀진다. (눈금은 순서만 맞고 간격은 실제 비율이 아니다)

딱 오프셋만큼 밀렸다

한국이라면 9시간이다. 가장 흔한 증상이고 원인도 좁다.

어딘가에서 팻말을 안 씌웠거나 두 번 씌웠다.

  • UTC로 저장된 값을 지역 시각으로 안 바꾸고 그대로 보여 줬다 (9시간 늦게 보인다)
  • 이미 지역 시각인 값에 팻말을 한 번 더 씌웠다 (9시간 빠르게 보인다)
  • 서버의 기본 지역 설정이 개발 장비와 다르다

어느 방향으로 밀렸는지가 단서다. 늦게 보이면 안 씌운 것이고 빠르게 보이면 두 번 씌운 것이다.

확인 순서는 저장된 값부터다. DB에 든 값을 UTC로 읽어 봐서 그게 맞으면 저장은 정상이고 보여 주는 자리만 보면 된다. 저장된 값이 이미 9시간 밀려 있으면 들어올 때 어긋난 것이라 범위가 완전히 다르다.

한 시간만 밀렸다

서머타임이거나 시간대 규칙이다. 오프셋 착각으로는 한 시간이 안 나온다.

  • 그 지역이 서머타임을 쓰는데 전환을 반영 안 했다
  • 서버나 컨테이너의 시간대 데이터가 낡았다
  • 미래 일정을 오프셋으로 굳혀 저장했는데 그 사이 규칙이 바뀌었다

언제부터 틀렸는지가 결정적인 단서다. 특정 날짜를 경계로 갈리면 서머타임 전환일이거나 규칙이 바뀐 날이다. 그날을 찾으면 원인이 거의 특정된다.

증상이 새벽 시간대에만 나타난다면 전환 구간 자체를 밟은 것이다. 1년에 두 번만 나타나서 재현이 안 되고 오래 산다.

  • 배치가 그날만 두 번 돌았다 (가을, 같은 시각이 두 번 왔다)
  • 그날만 안 돌았다 (봄, 그 시각이 존재하지 않았다)
  • 그날 집계의 하루 길이가 23시간이거나 25시간이었다

1970년이 보인다

값이 비었거나 단위를 착각했다.

  • 값이 0이다. 초기화가 안 됐거나 널이 0으로 떨어졌다
  • 밀리초 값을 초로 읽었다. 1970년 1월 중순쯤이 나온다

두 번째가 지독한 이유는 그럴듯한 날짜가 나오기 때문이다. 1970-01-01이면 빈 값이라고 바로 알겠는데 1970-01-21이 나오면 한참 헤맨다. 자릿수를 세 보면 곧장 갈린다. 초는 10자리, 밀리초는 13자리다.

반대 방향도 있다. 먼 미래 날짜가 보이면 초 값을 밀리초로 읽은 것이다.

하루가 밀렸다

날짜 자체가 뜻인 값에 팻말을 씌웠다.

생일이 하루 당겨지거나 정산 기준일이 하루 밀리는 사고다. 원인은 거의 하나다. 날짜에 자정을 붙여 순간으로 만들었고, 그걸 다른 지역에서 펼치면서 전날로 넘어갔다.

자정 근처의 값만 틀리고 낮 시간대 값은 멀쩡하다는 게 특징이다. 이 패턴이 보이면 날짜 전용 타입을 안 쓴 것이다.

걸린 시간이 음수이거나 터무니없다

시계 종류를 잘못 골랐다.

응답 시간이 음수로 찍히거나 몇 시간으로 나오면 벽시계로 경과 시간을 잰 것이다. 시계 보정이 그 사이에 일어났다.

드물게 튀는 값 몇 개로 나타나는 게 특징이다. 대부분의 측정은 정상이고 가끔 하나가 이상하다. 그래서 통계의 평균은 멀쩡한데 최대값만 이상한 꼴로 온다.

순서가 뒤집혔다

팻말 없는 값으로 정렬했거나 여러 서버의 시각을 비교했다.

  • 지역 시각으로 정렬했는데 사용자들이 서로 다른 지역에 있다
  • 서머타임 되돌리는 날의 겹침 구간이다
  • 서버 A와 B의 시계 차이만큼 순서가 뒤집혔다

마지막 경우는 고칠 수 없다. 시계를 아무리 잘 맞춰도 차이는 남는다. 순서가 정말 중요하면 시각 말고 다른 것으로 정해야 한다.

되짚는 순서

증상과 상관없이 먼저 할 일이 있다. 범위를 절반으로 자르는 것이다.

  1. 저장된 값을 직접 본다. DB를 UTC로 읽어 원본이 맞는지 확인한다. 여기서 이미 틀렸으면 들어오는 길을, 맞으면 나가는 길을 본다
  2. 경계를 하나씩 짚는다. 값이 지나온 자리는 대개 넷이다. 브라우저 → API → 애플리케이션 → DB. 각 지점에서 값을 찍어 보면 어느 구간에서 바뀌는지 보인다
  3. 밀린 폭을 잰다. 정확히 몇 시간 몇 분인지 센다. 위 표가 여기서 쓰인다
  4. 서버 설정을 확인한다. 애플리케이션·DB 세션·컨테이너의 시간대가 각각 무엇인지 본다. 셋이 다른 경우가 흔하다

한 장으로

무엇을 하려는 참이면 무엇을 확인할지 정리하면 이렇다.

하려는 일확인할 것
시각을 저장한다순간인가 지역 시각인가 날짜인가. 컬럼 타입이 그걸 담나
시각을 주고받는다오프셋을 붙였나. 미래 시각이면 지역 이름도 실었나
시각을 보여 준다사용자의 지역을 알고 있나. 어떻게 알아냈나
미래 일정을 잡는다오프셋을 굳히지 않았나. 지역 이름으로 저장했나
반복 작업을 건다지역 시각 기준인가 주기 기준인가. 새벽 2~3시대인가
걸린 시간을 잰다단조 시계인가
두 사건의 순서를 정한다같은 컴퓨터에서 잰 값인가
날짜만 다룬다시각과 팻말을 안 붙였나
컨테이너에 올린다시간대 데이터가 이미지에 들어 있나

시리즈를 한 줄씩

각 항목의 근거는 시리즈에 흩어져 있다. 필요한 것만 골라 보면 된다.

여기서 다루지 않은 것

이 시리즈는 시각을 담고 되돌리는 일만 다뤘다. 가까이 있지만 뺀 것들이 있다.

  • 달력 자체의 규칙 - 윤년 계산, 그레고리력 이전 날짜, 음력·이슬람력 같은 다른 달력. 규칙이 문화마다 달라 이야기가 따로 선다.
  • 업무 시간 계산 - “영업일 3일 뒤”처럼 휴일과 근무 시간을 반영하는 계산. 나라와 회사마다 규칙이 달라 데이터 문제에 가깝다.
  • 시계 동기화 프로토콜의 내부 - 여러 대의 시계를 어떻게 맞추는지, 얼마나 정확한지. 네트워크 지연을 다루는 이야기라 결이 다르다.
  • 분산 환경의 순서 정하기 - 시계 대신 무엇으로 순서를 정하는지. 이 시리즈는 “시계로는 안 된다”까지만 짚었다.

정리

  • 시간 버그는 밀린 폭이 원인을 말해 준다. 아무렇게나 틀리지 않는다.
  • 오프셋만큼 밀렸으면 팻말을 안 씌웠거나 두 번 씌운 것이다. 방향이 어느 쪽인지가 단서다.
  • 한 시간이면 서머타임이거나 시간대 데이터가 낡은 것이다. 언제부터 틀렸는지를 찾는다.
  • 1970년이면 값이 비었거나 단위를 착각한 것이다. 자릿수를 세면 갈린다.
  • 하루가 밀렸으면 날짜 전용 값에 팻말을 씌운 것이다. 자정 근처만 틀린다.
  • 음수 시간이면 경과 시간을 벽시계로 잰 것이다.
  • 되짚을 때는 저장된 값이 이미 틀렸는지부터 확인한다. 범위가 절반으로 준다.
  • 결국 이 시리즈 전체가 한 문장이다. 순간과 달력 시각을 갈라 두고, 경계마다 어느 쪽인지 밝힌다.