Skip to Content

1장. 하늘이 정하던 시간 — 해시계에서 그레고리력까지

만화로 보는 요약 — 먼저 읽어보세요

만화로 보는 요약 — 먼저 읽어보세요

365.2422일의 남는 조각 → 율리우스력의 누적 오차 → 열흘 삭제와 그레고리력이라는 더 나은 근사.

면접 실전 질문: ① 그레고리력 윤년 규칙에 100년·400년 예외가 있는 이유는? ② 1582년 10월의 열흘은 왜 사라졌는가? ③ java.time과 GregorianCalendar의 컷오버 동작은 왜 다른가?


한 해는 정수 개의 날로 떨어지지 않는다 — 모든 달력은 365.2422라는 불편한 상수를 정수로 근사한 결과이고, 그 근사 오차를 메우려다 인류는 열흘을 통째로 지워버렸다.

면접 실전 질문: ① 윤년 규칙이 “4의 배수, 단 100의 배수는 제외, 400의 배수는 다시 포함”인 이유는? ② 1582년 10월 5일부터 14일까지는 왜 존재하지 않는가? ③ java.time이 그 열흘의 공백을 재현하지 않는 이유는?


배경 — 하늘은 정수로 떨어지지 않는다

아주 오랫동안 시간을 정하는 건 하늘이었습니다. 해가 뜨면 아침, 그림자가 제일 짧으면 정오, 해가 지면 저녁. 해시계는 그 관계를 돌에 새긴 물건이었죠. 막대 하나 꽂아두고 그림자가 어디를 가리키는지 읽으면 끝. 시계가 틀릴 걱정은 없었어요. 하늘이 곧 원본이었으니까요.

그런데 여기서 아무도 원치 않은 문제가 하나 생깁니다. 하늘이 주는 주기는 하나가 아니라 둘이었어요.

  • 하루 — 지구가 스스로 한 바퀴 도는 시간(자전).
  • 한 해 — 지구가 태양을 한 바퀴 도는 시간(공전).

이 둘은 서로 아무 관계가 없는 운동입니다. 하나가 다른 하나의 배수일 이유가 전혀 없어요. 그리고 실제로 아니었습니다. 계절이 정확히 제자리로 돌아오는 데 걸리는 시간, 즉 회귀년(tropical year)은 약 365.2422일입니다. 소수점 뒤가 딱 떨어지질 않아요. 0.2422일은 대략 5시간 48분 45초쯤 되고요.

문제는 달력이 정수로만 만들어진다는 겁니다. “올해는 365.2422일입니다”라고 적힌 달력을 걸어놓을 수는 없잖아요. 사람은 0.2422일을 살 수가 없습니다. 날짜는 세는 단위고, 세는 건 정수니까요.

그래서 모든 달력은 태생적으로 같은 물건입니다. 정수가 아닌 천문 상수를, 정수로 근사한 결과물. 근사는 반드시 오차를 남기고, 오차는 반드시 쌓입니다. 이 책에서 앞으로 볼 시간 버그들 — 윤초, DST, 클록 드리프트 — 은 전부 이 첫 번째 미스매치의 후손이에요.

스토리 — 사라진 열흘

기원전 45년, 카이사르가 도입한 율리우스력의 답은 단순하고 우아했습니다. 0.25일씩 모아서 4년에 한 번 하루를 끼워 넣자. 평균 1년 = 365.25일. 소수점을 정수의 리듬으로 바꾼 최초의 성공적인 해법이었죠.

그런데 실제 회귀년은 365.2422일입니다. 율리우스력은 매년 0.0078일 = 약 11분 14초씩 더 길게 셌어요. 1년에 11분. 살면서 체감할 수 있는 크기가 아닙니다. 아무도 신경 쓰지 않았고, 신경 쓸 이유도 없었어요.

하지만 오차는 누적됩니다. 11분씩 128년을 모으면 정확히 하루가 됩니다.

그리고 이 하루가 하필 교회의 일정을 건드렸습니다. 서기 325년 니케아 공의회는 부활절을 계산하는 기준점으로 춘분을 3월 21일에 못 박아뒀거든요. 부활절은 “춘분 이후 첫 보름달 다음 일요일”로 정해지니까, 춘분 날짜가 흔들리면 부활절 전체가 흔들립니다.

1582년, 실제 춘분은 3월 11일에 오고 있었습니다. 열흘이 밀린 거예요. 계산도 맞습니다. 325년부터 1582년까지 1257년, 128년당 하루씩이면 약 9.8일. 달력이 하늘을 앞질러 가고 있었습니다.

여기서 짚어둘 게 있어요. 달력 개혁의 진짜 동기는 농사도 항해도 아니었습니다. 부활절 날짜 계산(computus)이 깨졌다는 것, 그게 전부였어요. 시간 표준은 언제나 “그게 틀리면 누가 곤란해지는가”가 정합니다. 이건 3장의 원자시계까지 그대로 반복되는 패턴이에요.

1582년 2월 24일, 교황 그레고리우스 13세가 칙서 Inter gravissimas를 냅니다. 내용은 두 가지였습니다.

① 앞으로의 오차를 줄인다 — 윤년 규칙 수정.

4로 나누어떨어지면 윤년. 단, 100으로 나누어떨어지면 윤년이 아니다. 단, 400으로 나누어떨어지면 다시 윤년이다.

400년에 윤일을 100번이 아니라 97번 넣는 규칙입니다. 400년 = 146,097일, 평균 1년 = 365.2425일. 율리우스력의 365.25보다 회귀년에 훨씬 가까워졌죠.

② 이미 쌓인 오차는 그냥 지운다.

새 규칙은 앞으로의 오차만 막습니다. 이미 밀린 열흘은요? 지웠습니다. 그냥 없앴어요.

1582년 10월 4일 목요일 다음 날은, 10월 15일 금요일이었습니다.

10월 5일부터 14일까지 열흘은 존재한 적이 없습니다. 그 날짜에 태어난 사람도, 서명된 계약서도, 기록된 사건도 없어요. 참고로 요일은 끊지 않았습니다 — 목요일 다음은 금요일, 7일 주기는 그대로 이어졌죠. 10월을 고른 이유도 실용적이었습니다. 교회 축일이 가장 적은 달이었거든요.

그리고 여기서 이 책의 주제가 처음 모습을 드러냅니다. 저건 교황의 칙서였습니다.

가톨릭 국가들은 1582년에 바로 따랐습니다. 개신교 국가와 정교회권은 안 따랐어요. 영국과 그 식민지는 1752년에야 넘어옵니다. 그때는 오차가 더 벌어져서 지워야 할 날이 11일로 늘어 있었고요(1700년이 율리우스력에선 윤년, 그레고리력에선 평년이라 하루가 더 벌어졌습니다). 1752년 9월 2일 다음 날은 9월 14일이었습니다.

그 170년 동안, “같은 날짜”가 서로 다른 순간을 가리켰습니다. 런던에서 쓴 3월 1일과 파리에서 쓴 3월 1일은 열흘 떨어진 다른 날이었어요. 그래서 당시 문서에는 O.S.(Old Style, 율리우스) / N.S.(New Style, 그레고리) 병기 관습이 생깁니다.

가장 유명한 사례가 뉴턴입니다. 뉴턴은 1642년 12월 25일(영국 O.S.) 에 태어났고, 같은 순간이 유럽 대륙에서는 1643년 1월 4일(N.S.) 이었습니다. 날짜만 다른 게 아니라 연도까지 다릅니다.

날짜 문자열에 어느 체계인지 적어두지 않으면 같은 값이 다른 시점을 의미한다 — 이게 이 책이 21장 내내 쫓아다닐 버그의, 아마 최초의 판본입니다.

핵심 — 근사는 끝나지 않았고, 코드는 역사를 기억하지 않는다

달력 이야기를 백엔드 개발자의 눈으로 보면 세 가지가 남습니다.

① 그레고리력도 여전히 근사다. 계산해보면 바로 보입니다.

import java.time.LocalDate; import java.time.temporal.ChronoUnit; public class LeapRule { public static void main(String[] args) { for (int y : new int[]{1700, 1800, 1900, 2000, 2024, 2100}) { System.out.printf("%d : isLeapYear=%-5b 2월 %d일%n", y, LocalDate.of(y, 1, 1).isLeapYear(), LocalDate.of(y, 2, 1).lengthOfMonth()); } long days = ChronoUnit.DAYS.between(LocalDate.of(2000, 1, 1), LocalDate.of(2400, 1, 1)); System.out.println(); System.out.println("400년 총 일수 = " + days); System.out.printf("그레고리력 평균 1년 = %.5f일%n", days / 400.0); System.out.printf("율리우스력 평균 1년 = %.5f일%n", 365.25); System.out.printf("실제 회귀년 = %.5f일%n", 365.24219); } }
1700 : isLeapYear=false 2월 28일 1800 : isLeapYear=false 2월 28일 1900 : isLeapYear=false 2월 28일 2000 : isLeapYear=true 2월 29일 2024 : isLeapYear=true 2월 29일 2100 : isLeapYear=false 2월 28일 400년 총 일수 = 146097 그레고리력 평균 1년 = 365.24250일 율리우스력 평균 1년 = 365.25000일 실제 회귀년 = 365.24219일

365.24250 대 365.24219. 아직 0.00031일, 약 27초가 남아 있습니다. 3천 년쯤 지나면 또 하루가 밀려요. 그레고리력은 문제를 해결한 게 아니라 더 나은 근사로 갈아탄 겁니다. 시간 API를 다룰 때 계속 기억해두면 좋은 사실이에요. 정답은 없고, 합의된 근사만 있습니다.

② 윤년 규칙은 규칙이 아니라 예외의 3층 구조다. 위 출력에서 1700·1800·1900은 평년, 2000만 윤년이죠. year % 4 == 0만 구현한 코드는 1900과 2100에서 깨집니다. 2000년은 400 규칙 덕분에 어차피 윤년이라, 버그가 있어도 티가 안 났어요. 그래서 더 오래 살아남았습니다.

가장 유명한 화석이 엑셀입니다. 엑셀은 지금도 1900년 2월 29일이 존재한다고 믿습니다. 없는 날짜인데도요. Lotus 1-2-3가 처음에 그렇게 만들었고, 마이크로소프트가 호환성을 위해 버그인 줄 알면서 그대로 복사했거든요. 그 결과 이 오류는 Office Open XML 명세에까지 박제됐습니다. 시간 관련 버그는 이렇게 삽니다 — 고쳐지지 않고, 호환성이라는 이름으로 상속돼요.

③ 그리고 java.time은 1582년을 기억하지 않습니다. 이건 좀 놀랍습니다.

import java.time.LocalDate; import java.util.Calendar; import java.util.GregorianCalendar; import java.util.TimeZone; public class Cutover { public static void main(String[] args) { LocalDate d = LocalDate.of(1582, 10, 4); System.out.println("java.time : 1582-10-04 다음 날 = " + d.plusDays(1)); GregorianCalendar cal = new GregorianCalendar(TimeZone.getTimeZone("UTC")); cal.clear(); cal.set(1582, Calendar.OCTOBER, 4); cal.add(Calendar.DAY_OF_MONTH, 1); System.out.printf("GregorianCalendar : 1582-10-04 다음 날 = %d-%02d-%02d%n", cal.get(Calendar.YEAR), cal.get(Calendar.MONTH) + 1, cal.get(Calendar.DAY_OF_MONTH)); // 율리우스력엔 있었지만, 예변 그레고리력엔 없는 날 try { LocalDate.of(1500, 2, 29); } catch (Exception e) { System.out.println("java.time 1500-02-29 : " + e.getMessage()); } } }
java.time : 1582-10-04 다음 날 = 1582-10-05 GregorianCalendar : 1582-10-04 다음 날 = 1582-10-15 java.time 1500-02-29 : Invalid date 'February 29' as '1500' is not a leap year

같은 JDK 안에서 두 API가 다른 답을 냅니다. 레거시 GregorianCalendar1582년 컷오버를 실제로 재현해요. 그래서 열흘을 건너뜁니다. 반면 java.timeLocalDate예변 그레고리력(proleptic Gregorian) 을 씁니다. 그레고리 규칙이 생기기 전 과거에까지 규칙을 그대로 소급 적용해요. 그래서 사라진 열흘이 java.time에는 아예 존재하지 않습니다. 율리우스력에 멀쩡히 있었던 1500년 2월 29일도, java.time은 없는 날짜라며 거부하고요.

왜 이렇게 만들었을까요? java.time은 “역사적 사실”이 아니라 “계산 가능한 규칙”을 택했기 때문입니다. 컷오버를 재현하면 plusDays(1) 하나가 국가별 채택 시점에 따라 다른 답을 내야 합니다 — 이탈리아는 1582년, 영국은 1752년, 러시아는 1918년. 날짜 산술을 신뢰할 수 없게 되는 거죠. 그래서 역사적 정확성을 버리고 규칙의 일관성을 샀습니다.

실무에서 이게 문제가 되는 경우는 드뭅니다. 1582년 데이터를 다루는 서비스는 거의 없으니까요. 하지만 계보·고문서·역사 아카이브를 다룬다면 얘기가 다릅니다. 그럴 땐 java.time이 아니라 java.time.chrono.JulianChronology를 쓰거나, 컷오버 날짜를 명시적으로 관리해야 해요. (레거시 쪽은 GregorianCalendar.setGregorianChange()로 컷오버 시점을 바꿀 수 있습니다.)

기억할 건 트레이드오프 자체입니다. 시간 API는 늘 “실제로 일어난 일”과 “계산하기 좋은 모델” 사이에서 하나를 고릅니다. 이 선택은 앞으로 윤초에서도, DST에서도, 단조시계에서도 똑같은 얼굴로 다시 나타납니다.

정리

  • 하루(자전)와 한 해(공전)는 무관한 두 운동이라, 회귀년 ≈ 365.2422일 이라는 정수가 아닌 값이 나온다. 달력은 이 값을 정수로 근사한 결과물이고, 근사 오차는 반드시 쌓인다.
  • 율리우스력(365.25일)은 매년 약 11분 14초를 더 셌고, 128년에 하루씩 밀렸다. 1582년에는 춘분이 3월 21일에서 3월 11일로 밀려 부활절 계산이 깨졌다 — 이게 개혁의 실제 동기.
  • 그레고리력 개혁은 두 가지였다. ① 윤년 규칙 수정(4의 배수, 100의 배수 제외, 400의 배수는 포함 → 평균 365.2425일) ② 밀린 열흘 삭제 — 1582년 10월 4일 다음 날이 10월 15일.
  • 하지만 그건 교황의 칙서였다. 영국과 식민지는 1752년에야 11일을 지우며 따라왔고, 그 170년 동안 두 나라의 “같은 날짜”는 다른 순간이었다(뉴턴: 1642-12-25 O.S. = 1643-01-04 N.S.).
  • 코드에서: 그레고리력도 3천 년에 하루 오차가 남는 근사이고, java.time예변 그레고리력을 써서 1582년의 공백을 재현하지 않는다. 역사적 정확성 대신 산술의 일관성을 택한 것.

생각해볼 질문: 어쨌든 달력은 합의됐습니다. 세계가 “오늘이 며칠인지”에는 대체로 같은 답을 하게 됐어요. 그런데 “지금 몇 시인지” 는 여전히 마을마다 달랐습니다. 각 마을이 자기 머리 위의 해를 보고 정오를 정했으니까, 30km 떨어진 옆 도시와는 몇 분씩 차이가 났죠. 걸어서 사흘 걸리는 거리라면 아무 문제가 없습니다. 그런데 그 거리를 한 시간에 지나가는 쇳덩이가 등장하면 어떻게 될까요? 하늘에게서 시간을 빼앗아 간 건 원자시계가 아니라, 기차였습니다.

2장 · 기차가 시간을 통일하다 — 표준시와 타임존의 탄생

Last updated on