KKflow

BUG · 버그 도감 #2

음력 생일이 한 달 밀렸다: 중국 음력과 한국 음력의 1,948일

사주 플로우의 음력 변환이 중국 음력 기준이었습니다. 1900~2049년을 한국 기준과 맞대어 보니 1,948일이 어긋났고, 2012년 음력 4월생은 양력이 한 달 통째로 밀렸습니다. 원인과 해결, 그리고 '안 쓰는 것을 검증하던 검증' 이야기.

· KKflow

버그 도감은 KKflow 프로젝트에서 실제로 만난 버그를 한 편에 하나씩 기록하는 시리즈입니다. 모든 내용은 해당 저장소의 커밋 기록과 코드를 근거로 합니다.

프로젝트 사주 플로우 · 시기 2026년 8월 17일 · 관련 기술 음력 변환, 라이브러리 검증, 테스트 설계

증상

사주 플로우는 생년월일을 양력이나 음력으로 받습니다. 음력으로 넣으면 먼저 양력으로 바꾼 뒤 사주를 계산합니다. 이 변환이 틀리면 날짜가 달라지고, 날짜가 달라지면 여덟 글자 중 일주가 바뀝니다. 심하면 월주까지 바뀝니다.

문제는 아무도 이걸 눈치채지 못했다는 점입니다. 오류 메시지도 없었고, 화면에는 그럴듯한 사주가 나왔습니다. 커밋 기록에는 처음에 이 부분을 "음력 변환이 미검증"인 약점 정도로 생각했다고 적혀 있습니다. 그래서 독립된 구현과 맞대어 보기로 했는데, 결과는 이랬습니다. "미검증이 아니라 틀려 있었다."

얼마나 틀렸나

음력 변환을 맡던 라이브러리(lunar-javascript)를, 한국천문연구원 자료로 만들어진 독립 구현(korean-lunar-calendar)과 1900년부터 2049년까지 하루도 빠짐없이 대조했습니다.

  • 어긋난 날: 1,948일
  • 그중 지금 살아 있는 사람의 출생년에 해당하는 해: 41개 연도

대부분은 하루 차이였지만, 몇몇 해는 차원이 달랐습니다.

음력 날짜중국식 양력한국식 양력
2012년 4월 1일4월 21일5월 21일
2017년 6월 1일6월 24일7월 23일
1990년 9월 1일10월 18일10월 19일

2012년과 2017년은 하루가 아니라 한 달이 통째로 다릅니다. 2012년 음력 4월에 태어난 사람이 음력으로 생일을 넣으면, 사주가 아예 다른 달에 태어난 사람의 것으로 나오고 있었습니다. 화면의 안내 문구도 틀려 있었습니다. "2012년에는 윤4월이 있습니다"라고 적고 있었지만, 한국 음력에서 그해의 윤달은 3월입니다.

원인: 초하루를 어느 나라 시계로 재는가

음력의 한 달은 달이 해와 같은 방향에 놓이는 순간, 즉 합삭이 들어 있는 날을 1일(초하루)로 삼아 시작합니다. 합삭은 우주에서 일어나는 한 순간이라 전 세계가 같은 순간을 봅니다. 그런데 그 순간이 "며칠"인지는 어느 나라 시계로 보느냐에 따라 달라집니다.

한국은 한국 표준시(UTC+9)로, 중국은 중국 표준시(UTC+8)로 날짜를 셉니다. 두 시계는 1시간 차이가 납니다. 예를 들어 합삭이 한국 시각으로 새벽 0시 30분에 일어나면, 중국 시각으로는 전날 밤 11시 30분입니다. 한국에서는 그날이 초하루지만 중국에서는 전날이 초하루가 됩니다. 이렇게 초하루가 하루 갈리면 그 달 전체가 하루씩 밀립니다.

더 드물게는 윤달을 어디에 두는지까지 갈립니다. 윤달 위치는 각 달에 어떤 절기(중기)가 들어 있는지로 정하는데, 달의 경계가 하루 갈리면 절기가 어느 달에 속하는지도 바뀔 수 있기 때문입니다. 2012년이 그런 해였습니다. 한국은 윤3월, 중국은 윤4월을 두었고, 그래서 음력 4월이 통째로 한 달 어긋났습니다. 2017년도 한국은 윤5월, 중국은 윤6월이었습니다.

라이브러리가 틀린 것은 아닙니다. 중국 음력을 정확하게 계산하고 있었습니다. 다만 한국 사용자를 위한 서비스에 필요한 건 한국 음력이었습니다.

해결 1: 음력 변환을 한 곳으로 모으다

모든 음력 변환을 klunar.ts라는 파일 하나로 모았습니다. 음력 1391년부터 2049년까지는 한국천문연구원 자료 기반의 한국 음력 표를 쓰고, 표 밖의 먼 미래만 어쩔 수 없이 중국식 계산으로 돌아가게 했습니다. 서비스가 받는 생년은 1900~2100년이라, 중국식이 쓰이는 건 먼 미래의 달력을 구경할 때뿐입니다.

그동안 음력을 직접 다루던 곳이 여섯 군데나 있었습니다. 생일 입력, 날짜 검증, 달력 표시, 명절 계산, 세시 명절, 그날의 안내 문구입니다. 이 여섯 곳을 모두 새 파일을 거치도록 바꿨습니다.

고친 결과는 이렇게 확인했습니다.

  • 설날과 추석을 저장소에 있는 한국천문연구원 공휴일 표(2004~2028년)와 맞대어 전부 일치했습니다.
  • 1950, 1995, 2012, 2026년은 1년 전체를 양력 → 음력 → 양력으로 왕복시켜, 모든 날짜가 제자리로 돌아오는지 확인했습니다.
  • 두 방식이 갈리는 날짜들을 실제 측정값 그대로 테스트에 넣었습니다.

해결 2: 안 쓰는 것을 검증하던 검증

여기서 끝났다면 평범한 버그 수정이었을 겁니다. 그런데 같은 날 12분 뒤 커밋에서 하나가 더 나왔습니다. 같은 종류의 문제가 더 있는지 전체를 훑어보다가 발견한 것입니다.

저장소에는 음력 변환이 맞는지 확인하는 별도의 검증 스크립트가 있었습니다. 그런데 이 스크립트가 여전히 옛 라이브러리, 즉 중국 음력을 정답으로 삼고 있었습니다. 앱은 한국 음력으로 바뀌었는데, 검증은 중국 음력과 비교하고 있었던 것입니다. 커밋 메시지에 이렇게 적었습니다.

"안 쓰는 것을 검증하는 검증은 통과해도 아무것도 말해 주지 않는다."

검증 스크립트가 앱이 실제로 쓰는 한국 음력 구현을 기준으로 삼도록 고쳤습니다.

해결 3: 다시 새지 않게 막다

마지막으로 같은 실수가 다시 들어오는 길을 막았습니다. 중국식 라이브러리를 꼭 써야 하는 곳은 두 군데뿐입니다. 한국식 표 밖을 맡는 klunar.ts, 그리고 절기를 계산하는 solarTerms.ts입니다. 절기는 태양의 위치로 정해지는 천문 현상이라 나라마다 갈리지 않고, 한국천문연구원 발표값이 우선합니다.

그래서 이 두 파일 밖에서 중국식 라이브러리를 가져다 쓰면 실패하는 테스트를 넣었습니다. 나중에 새 기능을 만들다 무심코 옛 라이브러리로 음력을 다루면, 배포 전에 그 자리에서 걸립니다. 실제 테스트 코드는 이렇게 시작합니다.

describe('중국식 라이브러리의 자리', () => {
  it('lunar-javascript는 klunar와 solarTerms만 가져다 쓴다', () => {
    const allowed = new Set(['lib/saju/klunar.ts', 'lib/saju/solarTerms.ts']);
    const dirs = ['lib', 'components', 'app'];
    // 세 폴더의 모든 파일을 훑어 허용 목록 밖에서 가져다 쓰면 실패

갈리는 날짜를 넣은 테스트에도 원칙이 하나 있습니다. 한국식이 맞는 답을 내는지만 보는 게 아니라, 중국식이 실제로 다른 답을 내는지도 함께 확인합니다. 코드 주석의 표현으로는 "다르지 않다면 그 검사는 아무것도 못 가른다." 두 방식이 같은 답을 내는 날짜로 테스트를 짜면, 누가 다시 중국식으로 되돌려도 테스트가 통과해 버리기 때문입니다.

교훈

  • 오류 없이 틀리는 것이 가장 위험하다. 1,948일이 어긋나는 동안 오류 메시지는 한 번도 뜨지 않았습니다.
  • "음력"이라는 같은 이름 아래 다른 기준이 있다. 라이브러리를 고를 때는 기능 이름이 아니라 어느 나라, 어느 기준인지를 확인해야 합니다.
  • 검증의 기준부터 검증한다. 검증 스크립트가 무엇과 비교하고 있는지 확인하지 않으면, 통과는 아무 의미가 없습니다.
  • 테스트는 틀린 답과 갈리는 지점에서 짠다. 맞는 구현과 틀린 구현이 같은 답을 내는 입력으로는 아무것도 지킬 수 없습니다.
  • 고친 뒤에는 같은 종류가 더 있는지 훑는다. 검증 스크립트의 문제는 고친 직후 전체를 다시 훑어서 찾았습니다.

사주 플로우를 만든 전체 과정은 사주 플로우 제작기에, 절기 시각 데이터는 24절기 절입 시각표에 정리했습니다.