Skip to content

필사 모드: 시간에 대해 프로그래머가 믿는 거짓말

한국어
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.

들어가며 — 거짓말 목록이라는 장르

소프트웨어 개발에는 "프로그래머가 X에 대해 믿는 거짓말"이라는 유명한 글의 장르가 있습니다. 이름에 대한 거짓말, 주소에 대한 거짓말, 그리고 그중에서도 가장 악명 높은 것이 시간에 대한 거짓말입니다. 형식은 단순합니다. 우리가 "당연하다"고 여기는 명제를 나열하고, 그 하나하나가 사실은 틀렸음을 보여주는 것입니다.

이 장르가 인기 있는 이유는, 이 명제들이 너무나 그럴듯하기 때문입니다. "하루는 24시간이다"라는 말에 누가 이의를 달겠습니까? 그런데 바로 그 당연함이 함정입니다. 우리는 이런 가정을 의심 없이 코드에 새기고, 그 코드는 대부분의 경우 잘 돌다가, 어느 예외적인 순간에 조용히 무너집니다.

이 글은 시간에 관한 거짓말 중 특히 자주 개발자를 무는 네 가지를 골라, 각각에 실제로 일어난 버그를 하나씩 붙여 설명합니다. 이 글의 목적은 겁을 주려는 것이 아니라, "내 코드에 이런 가정이 숨어 있지 않은가"를 스스로 점검하게 하려는 것입니다. 시간을 다루는 도구가 필요하면 이 사이트의 세계 시계로 여러 지역의 시각을 나란히 비교해 볼 수 있습니다.

거짓말 1 — "하루는 24시간이다"

가장 근본적인 거짓말입니다. 하루가 정확히 24시간, 즉 86,400초라는 가정은 어긋나는 경우가 여럿 있습니다.

서머타임 때문입니다. 서머타임이 시작되는 봄의 어느 날, 시계는 한 시간 앞으로 점프합니다. 그날은 23시간짜리 하루입니다. 반대로 서머타임이 끝나는 가을의 어느 날은 25시간짜리 하루입니다. 그래서 "오늘 자정부터 내일 자정까지"를 무조건 86,400초로 계산하면 1년에 두 번 틀립니다.

여기에 더해, 나라가 표준시 자체를 바꾸거나 날짜변경선을 넘나드는 특수한 경우에는 하루의 길이가 더 극적으로 달라지기도 합니다. 실제로 어떤 지역은 정책 변경으로 특정 날짜를 통째로 건너뛴 역사가 있습니다.

실제 버그. 한 근태 관리 시스템이 근무 시간을 "출근 시각과 퇴근 시각의 차이"로 계산했습니다. 그런데 서머타임이 끝나 25시간이 된 날, 밤샘 근무를 한 직원의 근무 시간이 실제보다 한 시간 더 많게 집계되었습니다. 반대로 서머타임 시작일에는 한 시간 적게 집계되었습니다. "하루=24시간, 두 시각의 단순 뺄셈=경과 시간"이라는 가정이 서머타임 앞에서 무너진 것입니다. 올바른 방법은 두 순간을 UTC로 변환한 뒤 그 차이를 재는 것입니다. UTC에는 서머타임이 없으므로 경과 시간이 정확합니다.

거짓말 2 — "1분은 60초다"

이것도 거의 언제나 참이지만, 항상 참은 아닙니다. 앞서 언급한 윤초(leap second) 때문입니다.

지구의 자전 속도는 미세하게 불규칙해서, 원자시계 기준의 시간과 지구 자전 기준의 시간 사이에 오차가 쌓입니다. 이를 보정하려고 국제 기구가 이따금 하루의 끝에 1초를 더 끼워 넣습니다. 그런 날의 마지막 1분은 60초가 아니라 61초입니다.

윤초가 삽입된 날:
  23:59:59
  23:59:60   ← 정상적으로 존재하는 시각
  00:00:00

대부분의 애플리케이션은 이걸 신경 쓸 필요가 없습니다. 하지만 시각을 아주 정밀하게 다루거나, 초 단위 계산을 엄격히 하는 시스템에서는 문제가 됩니다.

실제 버그. 과거에 윤초가 삽입되던 순간, 여러 대형 서비스가 동시에 장애를 겪은 유명한 사건이 있었습니다. 일부 시스템의 커널과 애플리케이션이 "23:59:60"이라는 예상치 못한 값을 만나 무한 루프에 빠지거나 CPU를 100%로 태우며 멈췄습니다. "초는 0에서 59까지"라는 가정이 코드 깊숙이 박혀 있었던 것입니다. 이 사건 이후로 많은 대형 인프라가 윤초를 하루에 걸쳐 조금씩 나눠 흡수하는 "leap smearing" 방식을 도입했습니다. 61초짜리 1분을 아예 시스템에 노출하지 않는 전략입니다.

거짓말 3 — "시계는 앞으로만 간다"

시간이 단조롭게 앞으로만 흐른다는 것은 물리적 직관이지만, 컴퓨터의 시계에는 해당하지 않습니다. 컴퓨터의 벽시계(wall clock)는 뒤로 갈 수 있습니다.

가장 흔한 원인은 NTP(Network Time Protocol) 동기화입니다. 컴퓨터의 로컬 시계는 조금씩 어긋나는데, NTP는 이를 정확한 시각으로 맞춥니다. 만약 로컬 시계가 실제보다 앞서 있었다면, NTP는 시계를 뒤로 되돌립니다. 그 순간, 방금 전보다 지금이 더 과거가 됩니다. 서머타임 종료(가을에 시계를 한 시간 되돌리는 것)도 지역 시각을 되감습니다.

실제 버그. 어떤 서비스가 요청 처리 시간을 다음처럼 측정했습니다.

start = 현재_벽시계_시각()
... 요청 처리 ...
duration = 현재_벽시계_시각() - start

평소엔 잘 돌았습니다. 그런데 마침 처리 도중에 NTP가 시계를 뒤로 조정하자 duration이 음수가 되었습니다. 이 음수가 타임아웃 계산과 메트릭 집계로 흘러들어가, 어떤 요청은 즉시 타임아웃되고 어떤 통계는 말도 안 되는 값이 되었습니다. 원인을 찾는 데 며칠이 걸렸습니다. 재현이 거의 안 됐기 때문입니다. 교훈은 명확합니다. 경과 시간을 잴 때는 절대 뒤로 가지 않는 **단조 시계(monotonic clock)**를 써야 합니다. 벽시계는 "지금 몇 시"를 알 때만 쓰고, "얼마나 지났나"는 단조 시계로 재는 것이 원칙입니다.

거짓말 4 — "타임존은 고정된 오프셋이다"

Asia/Seoul은 언제나 UTC+9이고, America/New_York은 언제나 UTC-5라고 생각하기 쉽습니다. 하지만 타임존은 고정된 오프셋이 아니라, 시간에 따라 변하는 규칙의 집합입니다.

가장 큰 이유는 역시 서머타임입니다. 서머타임이 있는 지역은 1년에 두 번 오프셋이 바뀝니다. 뉴욕은 겨울에 UTC-5, 여름에 UTC-4입니다. 게다가 각국은 정치적·경제적 이유로 시간 정책을 실제로 바꿉니다. 서머타임을 새로 도입하거나 폐지하고, 심지어 표준시 오프셋 자체를 변경하기도 합니다. 이런 변경은 생각보다 자주, 때로는 몇 주 전 짧은 예고와 함께 일어납니다. 이 모든 변경 이력을 담아 두는 것이 IANA 타임존 데이터베이스입니다.

실제 버그. 한 글로벌 캘린더 앱이 회의 시각을 저장할 때, 타임존 이름 대신 그 순간의 오프셋만 저장했습니다. 사용자가 "3개월 뒤 오전 10시(뉴욕)"에 회의를 잡으면, 앱은 당시 오프셋인 UTC-4를 함께 굳혀 저장했습니다. 그런데 그 3개월 사이에 서머타임이 끝나 뉴욕이 UTC-5로 바뀌자, 저장된 회의가 실제 벽시계로 오전 9시에 표시되었습니다. 사용자들이 회의에 한 시간 일찍 나타나는 사고가 벌어졌습니다.

올바른 방법은 미래 이벤트를 오프셋이 아니라 타임존 이름(America/New_York)과 함께 저장하는 것입니다. 그러면 서머타임 규칙이 어떻게 바뀌든, 표시할 때 항상 올바른 벽시계 시각으로 재계산됩니다.

그 밖의 거짓말들 — 짧게 훑기

위 네 가지 외에도 "시간에 관한 거짓말" 목록에는 유명한 항목이 많습니다. 몇 가지만 짧게 짚어 봅니다.

  • "한 나라는 하나의 타임존을 쓴다." 아닙니다. 미국, 러시아, 호주처럼 여러 타임존을 가진 나라가 많습니다. 반대로 중국처럼 국토가 넓은데도 단일 타임존을 쓰는 나라도 있습니다.
  • "타임존 오프셋은 시간 단위다." 아닙니다. 인도는 UTC+5:30, 네팔은 UTC+5:45처럼 30분·45분 단위 오프셋이 존재합니다. 오프셋을 정수 시간으로 가정한 코드는 이런 지역에서 깨집니다.
  • "두 타임스탬프를 비교하면 시간 순서를 알 수 있다." 분산 시스템에서는 아닙니다. 서로 다른 서버의 시계가 어긋나 있으면, 나중에 일어난 일의 타임스탬프가 더 이르게 찍힐 수 있습니다.
  • "윤년은 4년마다 온다." 대체로 맞지만 예외가 있습니다. 100으로 나눠지는 해는 윤년이 아니고, 그중 400으로 나눠지는 해는 다시 윤년입니다. 2000년은 윤년이었지만 1900년은 아니었습니다.
  • "자정은 항상 존재한다." 아닙니다. 일부 지역은 서머타임 전환으로 특정 날짜의 00:00가 존재하지 않은 적이 있습니다.

이 목록의 공통점은, 전부 "대부분의 경우 참이지만 예외가 있는" 명제라는 것입니다. 그리고 소프트웨어 버그는 언제나 그 예외에서 태어납니다.

왜 우리는 계속 속는가

이쯤에서 근본적인 질문을 던져 봅시다. 왜 이런 거짓말에 개발자들이 반복해서 속을까요? 몇 가지 이유가 있습니다.

첫째, 이 가정들은 일상 경험과 일치합니다. 우리 대부분은 서머타임이 없거나, 있어도 크게 신경 쓰지 않는 환경에서 살아갑니다. 그래서 하루가 24시간이 아닐 수 있다는 것이 직관에 반합니다.

둘째, 이 버그들은 평소에 잘 숨어 있습니다. 서머타임 전환은 1년에 두 번뿐이고, 윤초는 몇 년에 한 번이며, NTP의 시계 되감기는 예측하기 어렵습니다. 그래서 테스트에서 좀처럼 드러나지 않고, 운영 환경의 특정 순간에만 터집니다. 재현이 어려우니 디버깅도 어렵습니다.

셋째, 언어와 라이브러리가 함정을 방치합니다. 많은 언어의 기본 날짜 타입은 타임존 정보가 없는 naive 형태를 쉽게 허용하고, 벽시계와 단조 시계를 뚜렷이 구분해 주지 않습니다. 그래서 잘못된 선택이 기본값이 되기 쉽습니다.

이 세 가지가 겹쳐, 시간 버그는 "흔히 저지르지만 드물게 발현하고 잡기 어려운" 특유의 성질을 갖습니다.

거짓말에 속지 않는 습관

목록을 외우는 것만으로는 부족합니다. 실무에서 이 함정을 피하는 습관으로 옮겨야 합니다.

  • 저장은 UTC로. 서머타임과 오프셋 변경의 영향을 받지 않는 단일 기준으로 저장하면, "하루=24시간"이나 "고정 오프셋" 같은 가정에 덜 의존하게 됩니다.
  • 경과 시간은 단조 시계로. "시계는 앞으로만 간다"는 거짓말에 대한 방어입니다.
  • 미래 이벤트는 타임존 이름과 함께. "타임존은 고정 오프셋"이라는 거짓말에 대한 방어입니다.
  • 타임존 규칙을 하드코딩하지 말고 IANA 데이터베이스를 쓰기. 그리고 최신으로 유지하기.
  • 테스트에 극단적 케이스를 넣기. 서머타임 전환일, 30분·45분 오프셋 지역, 윤년 경계, 날짜변경선 부근을 일부러 테스트합니다.
  • "이 값은 절대 순간인가, 달력의 날짜인가"를 항상 묻기. 이 질문 하나가 많은 타입 선택 실수를 막습니다.

마치며

"프로그래머가 시간에 대해 믿는 거짓말"이라는 목록이 오래도록 회자되는 이유는, 그것이 단순한 트리비아가 아니라 실제 버그의 지도이기 때문입니다. 하루가 24시간이라는 것도, 1분이 60초라는 것도, 시계가 앞으로만 간다는 것도, 타임존이 고정 오프셋이라는 것도, 전부 "대부분의 경우 참"일 뿐 "항상 참"은 아니었습니다.

핵심은 이 거짓말들을 하나하나 외우는 것이 아니라, 그 뒤에 있는 원리를 이해하는 것입니다. 시간은 순수한 물리량이 아니라 인간의 약속이 겹겹이 쌓인 개념이고, 그 약속은 지역마다 다르며 시간이 지나면 바뀝니다. 이 사실을 받아들이고 UTC 저장, 단조 시계, IANA 데이터베이스 같은 방어 습관을 들이면, 이 거짓말들 대부분이 더 이상 여러분의 코드를 물지 못합니다. 여러 지역의 시각을 직접 비교해 보고 싶다면 세계 시계를 열어 이 거짓말들이 실제로 어떻게 드러나는지 확인해 보세요.

참고 자료

현재 단락 (1/53)

소프트웨어 개발에는 "프로그래머가 X에 대해 믿는 거짓말"이라는 유명한 글의 장르가 있습니다. 이름에 대한 거짓말, 주소에 대한 거짓말, 그리고 그중에서도 가장 악명 높은 것이 시...

작성 글자: 0원문 글자: 4,847작성 단락: 0/53