사주 앱마다 결과가 다른 이유 — 사주·자미두수·별자리 계산 엔진을 Go로 다시 쓰며
퀴즈로 시작하겠습니다. 2024년 2월 4일 오후 5시 20분(17시 20분), 서울에서 아이가 태어났습니다. 이 아이는 무슨 띠일까요?
- 해가 1월 1일에 바뀐다고 보면 2024년 갑진년, 용띠입니다.
- 설날(2024년 2월 10일)에 바뀐다고 보면 아직 계묘년, 토끼띠입니다.
- 사주1로 보면 토끼띠입니다. 사주에서는 해가 입춘에 바뀌는데, 2024년 입춘은 2월 4일 17시 27분이었습니다.
입춘보다 7분 일찍 태어나 토끼띠가 됐습니다. 바뀌는 건 띠만이 아닙니다. 같은 날 10분 늦게, 17시 30분에 태어난 아이와 사주 여덟 글자를 나란히 놓으면 이렇습니다.
| 출생 시각 | 년주 | 월주 | 일주 | 시주 |
|---|---|---|---|---|
| 17:20 | 癸卯 | 乙丑 | 戊戌 | 庚申 |
| 17:30 | 甲辰 | 丙寅 | 戊戌 | 辛酉 |
10분 사이에 여덟 글자 중 여섯 글자가 바뀌었습니다. 입춘을 지나며 년주와 월주2가 바뀌었고, 마침 시진3 경계까지 지나 시주도 바뀌었습니다. 거꾸로 말하면, 입춘 시각을 몇 분만 틀리게 계산해도 그 근처에 태어난 사람의 사주는 남의 사주가 됩니다. 같은 생년월일시를 넣었는데 사주 앱마다 결과가 다른 이유는 대개 이런 곳에 있습니다.
저는 사주·자미두수4·별자리5를 한 번에 읽어 주는 사주사이트(saju.site)를 혼자 만들고 있습니다. 지난 9월 25일, 계산을 맡던 Node 라이브러리를 걷어내고 Go로 직접 쓴 엔진으로 바꿨습니다. 바꾸기 전에 두 엔진에 같은 출생 정보 12,120건을 넣고, 결과가 다른 곳마다 이유를 빠짐없이 가려냈습니다. 이 글은 그렇게 모은 "사주 앱마다 결과가 다른 이유"이자, 수천 건의 차이를 사람이 일일이 보지 않고 분류한 방법에 대한 기록입니다.
요약하면 이렇습니다.
- 두 엔진을 비교해 보니 차이는 대부분 버그가 아니라 규칙과 계산 방식의 선택에서 나왔습니다. 사주 앱마다 결과가 다른 이유도 대개 여기에 있습니다. 이번에 확인한 갈림길은 여섯 가지입니다. 절입 시각을 어떻게 구하는지, 출생 시각을 어떻게 보정하는지, 밤 11시대 출생의 날짜를 언제 넘기는지, 한국 음력인지 중국 음력인지, 대운 나이를 어떻게 세는지, 자미두수 별 밝기 표가 어느 유파인지.
- 새 엔진에 예전 규칙을 다시 켤 수 있는 스위치를 달아, 두 엔진 결과의 차이를 "규칙 차이"와 "버그"로 자동으로 나눴습니다. 설명되지 않는 차이가 0건이 될 때까지 새 엔진을 고쳤습니다.
- 교체 뒤 한국 출생의 사주 네 기둥은 0.1%만 바뀌었습니다. 대운 나이는 넷 중 한 명꼴로 한 살 달라졌고, 자미두수는 30% 가까이 바뀌었습니다. 계산 시간은 100ms 이상에서 10~15ms로 줄었습니다.
사주사이트가 계산 엔진을 직접 만든 이유
서버는 Go인데 차트 계산만 Node 라이브러리 @orrery/core에 맡기고 있었습니다. 요청마다 Node 프로세스를 띄워 결과 JSON을 받아 오는 구조라 한 번에 100ms가 넘게 걸렸고, 운영 이미지에 Node 런타임을 따로 넣어야 했습니다. 사주·자미두수·출생 차트를 한 번에 계산해 주는 고마운 라이브러리였고, 덕분에 사이트를 빨리 열 수 있었습니다. 다만 AGPL6이라 서비스 코드와 어떻게 엮을지는 계속 신경이 쓰였습니다.
더 큰 이유는 규칙을 직접 정하고 설명하고 싶었다는 점입니다. 사주는 여덟 글자 중 하나만 틀려도 그 뒤 풀이가 전부 남의 이야기가 됩니다. 그런데 "이 사람 월주가 왜 이렇게 나왔나", "이 밝기 표는 어느 계열인가"를 물으면 저는 답할 수 없었습니다. 다른 앱과 결과가 다를 때 어느 쪽이 어떤 규칙을 쓴 건지 설명하려면 계산을 직접 쥐고 있어야 했습니다.
목표는 세 가지였습니다.
- 서버 안에서 Go로 계산한다. 외부 프로세스는 없다.
- 해석 코드가 읽는 결과 JSON 모양은 그대로 둔다.
- 예전 엔진과 결과가 다른 곳은 하나도 빠짐없이 어떤 규칙 때문인지 설명할 수 있어야 한다.
사주·자미두수·별자리는 모두 해와 달의 위치에서 나옵니다
엔진을 짜기 전에 세 체계가 무엇을 재는지부터 정리했습니다. 겉보기엔 전혀 다른 점술이지만, 입력은 셋 다 "태어난 순간과 장소"이고, 셋 다 하늘의 시계를 읽습니다.
- 사주는 태양 시계입니다. 년과 월은 태양이 황도7 위 어디까지 왔는지(절기8)로 바뀌고, 날과 시는 태어난 곳에서 해가 지금 하늘 어디쯤 있는지로 정해집니다.
- 별자리도 태양 시계입니다. 태어난 순간 태양이 황도를 30°씩 나눈 열두 칸 중 어디에 있었는지가 그 사람의 별자리입니다. 출생 차트는 여기에 달과 행성의 자리까지 그린 것입니다.
- 자미두수는 달 시계입니다. 음력 생월·생일과 태어난 시진으로 열두 궁의 판을 짭니다.
재미있는 건 사주와 별자리가 같은 눈금자를 쓴다는 점입니다. 둘 다 춘분점에서 출발해 황도를 나누는데, 24절기는 15°마다, 별자리는 30°마다 바뀝니다. 그래서 절기의 절반은 별자리 경계와 정확히 겹칩니다. 춘분(0°)에 양자리가, 곡우(30°)에 황소자리가, 대한(300°)에 물병자리가 시작됩니다. 별자리 경계와 겹치는 이 열두 절기를 중기라고 합니다.
나머지 열두 절기, 입춘(315°)·경칩(345°)·청명(15°) 같은 절은 별자리 칸의 한가운데에 떨어집니다. 그리고 사주의 달은 바로 이 절에서 바뀝니다. 사주의 한 달은 별자리 한 칸을 반 칸 밀어 놓은 셈입니다. 퀴즈에서 새해를 연 입춘도 물병자리의 한가운데였습니다.
중기는 음력의 윤달도 정합니다. 음력 한 달(평균 29.5일)은 태양이 별자리 한 칸을 지나는 시간(평균 30.4일)보다 조금 짧아서, 가끔 태양이 다음 별자리로 한 번도 넘어가지 않는 음력 달이 생깁니다. 그런 달이 윤달9 후보가 됩니다. 새 엔진의 윤달 판정 코드도 말 그대로 "이 달에 태양이 별자리를 갈아탔나"를 묻습니다.
// sector 는 그날 0시의 태양 황경이 몇 번째 30° 구간(= 별자리)인지입니다.
func (c *Calendar) sector(jdn int) int {
return int(math.Floor(astro.SunApparentLongitude(c.midnightJDE(jdn)) / 30))
}
// 동지에서 다음 동지까지 달이 13개인 해에는, 중기가 들지 않은 첫 달이 윤달입니다.
hasZhongqi := c.sector(s) != c.sector(next) // s: 이 달 초하루, next: 다음 달 초하루
if leapYear && !leapUsed && !hasZhongqi {
m.leap = true
}
정리하면, 엔진의 바닥은 "태양과 달이 언제 하늘 어디에 있는가"를 계산하는 패키지(astro) 하나이고, 사주·자미두수·별자리는 그 위에 얹은 규칙입니다. 세 점술이 천문 계산기 하나를 나눠 쓰는 셈입니다. 그러니 앱마다 결과가 다르다면 원인은 둘 중 하나입니다. 하늘을 다르게 계산했거나, 같은 하늘을 다른 규칙으로 읽었거나. 사용자 화면에서 보이는 차이는 대부분 뒤쪽이었습니다.
수천 건의 차이를 "규칙"과 "버그"로 나누기
새 엔진을 다 쓴 뒤 바로 갈아 끼우지 않았습니다. 두 엔진에 같은 출생 정보를 넣으면 결과가 수천 건씩 달랐는데, 여기에는 두 종류가 섞여 있습니다.
- 일부러 바꾼 차이. 한국 음력으로 바꾸고 30분 보정 방식을 바꿨으니, 달라지는 게 당연한 경우입니다.
- 실수로 생긴 차이. 새 엔진의 버그 때문에 달라진 경우입니다.
결과만 봐서는 둘을 구분할 수 없습니다. 둘 다 그냥 "예전과 다름"입니다. 그렇다고 수천 건을 사람이 하나씩 들여다볼 수도 없었습니다.
두 사람이 같은 문제를 풀었는데 답이 다르다고 해 보겠습니다. 문제를 다르게 읽었을 수도 있고(규칙), 계산을 틀렸을 수도 있습니다(버그). 가려내는 방법은 간단합니다. 한 사람에게 상대방처럼 문제를 읽고 다시 풀게 하면 됩니다. 그래도 답이 다르면 계산이 틀린 겁니다.
그래서 새 엔진에 예전 규칙 스위치(WithLegacy)를 달았습니다. 스위치를 켜면 계산은 새 엔진이 그대로 하고, 문제를 읽는 방식만 예전 엔진처럼 바꿉니다. 중국 음력, 표준시 그대로 가르는 자미두수 시진, 예전 밝기 표, 해외 출생의 30분 차감 같은 것들입니다. 채점 테스트(TestGrade)는 이 스위치로 차이를 가렸습니다. 예전 엔진의 코드는 보지 않고, 결과만 받아 비교했습니다.
- 두 엔진 결과가 같으면 통과입니다.
- 다르면 스위치를 켜고 새 엔진으로 다시 계산합니다. 이번에 예전 엔진과 똑같이 나오면, 차이는 전부 규칙 때문입니다.
- 스위치를 켜도 다르면 규칙만으로는 설명되지 않는 차이입니다. 그중 세 경우만 따로 확인해 분류했고, 그래도 남는 것은 "설명 안 됨"으로 세어 0이 될 때까지 새 엔진을 고쳤습니다.
따로 확인한 세 경우는 스위치로 재현하지 않은 차이입니다. 예전 엔진의 근사식과 시간대 자료까지 똑같이 옮기지는 않았기 때문에, 대신 차이의 모양을 봤습니다.
- 절입 시각 차이. 다른 곳이 년주·월주와 대운뿐이고, 절입 시각 차이로 설명될 때입니다. 절입 시각은 일주와 시주를 바꾸지 못하니, 그 둘까지 다르면 절입 탓이 아닙니다. 년주·월주가 다르면 태어난 순간이 절입에서 2시간 반 안쪽이어야 하고, 대운만 다르면 시작일 차이를 "사흘이 1년" 비율로 되돌려 절입 시각 차이가 2시간 반 안쪽인지 봤습니다. 절입이 조금만 달라도 거의 모든 사람의 대운 시작일이 며칠씩 움직여서, 아래 표에서 사주의 '절기' 칸이 큽니다.
- 균시차 차이(해외 출생). 시진 경계 90초 안쪽이라 시진이 갈리거나, 자미두수 명반에 적힌 진태양시만 1분 안쪽으로 다른 경우입니다.
- 시간대 자료 차이. 1970년 이전 일부 해외 시간대에서 예전 엔진(Node·ICU)과 서버의 tzdata 기록이 다른 경우입니다.
예를 들면 이렇습니다. 서울 오전 1시 10분 출생의 자미두수 시진이 예전 엔진은 丑시, 새 엔진은 子시로 달랐습니다. 스위치를 켜서 시진을 표준시 그대로 가르게 하면 새 엔진도 丑시가 나옵니다. 그러면 이 차이는 "자미두수 시계 규칙을 바꾼 것"으로 설명됩니다. 스위치를 켰는데도 丑시가 안 나왔다면 새 엔진 어딘가가 틀린 것이고, 그런 사례가 바로 고쳐야 할 대상이었습니다.
분류 코드를 줄여 보면 이렇습니다.
// 사주 결과가 예전 엔진과 다를 때, 그 차이가 어디서 왔는지 가립니다(요약).
// 시간대 자료가 갈린 경우는 이보다 먼저 따로 셉니다.
legacy, _ := saju.Calculate(in.WithLegacy()) // 계산은 새 엔진, 규칙만 예전 것
diffs := compare(old, legacy)
switch {
case len(diffs) == 0:
return "convention" // 규칙만 맞추면 같아짐 → 일부러 바꾼 규칙 때문
case termExplainedAt(at, diffs):
return "term" // 년주·월주·대운만 다르고, 절입 시각 차이(2시간 반 안)로 설명됨
case abroad && nearHourBoundary(in, 90*time.Second):
return "eot" // 해외 출생, 시진 경계 90초 안 → 균시차 차이
}
return "other" // 설명 안 됨 → 새 엔진을 고친다
입력은 무작위 출생 5,368명과 가장자리 출생(엣지 케이스) 6,752명입니다. 가장자리는 무작위로는 거의 뽑히지 않는 입력입니다. 서머타임 전환과 건너뛴 시각, 한국 표준시가 바뀐 순간, 절입 앞뒤 1~3분, 고위도, 30·45분 시간대, 날짜변경선, 계산 범위의 양 끝을 넣었습니다. 표의 각 칸은 차이가 어느 원인으로 설명됐는지를 센 것입니다.
| 묶음 | 사주 | 자미두수 | 출생 차트 | 설명 안 됨 |
|---|---|---|---|---|
| 무작위 5,368명 | 절기 4,297 · 해외 1,034 · 균시차 1 | 바꾼 규칙 4,428 · 균시차 413 | 위치 보정 5,327 · 시간대 자료 5 | 0 |
| 가장자리 6,752명 | 절기 1,578 · 해외 5,126 · 균시차 32 · 시간대 자료 11 | 바꾼 규칙 5,095 · 균시차 1,641 · 시간대 자료 11 | 위치 보정 6,711 · 시간대 자료 36 | 0 |
설명 안 되는 차이가 0건이 된 뒤 엔진을 바꿨고, 채점 코드와 예전 엔진은 저장소에서 지웠습니다. 지금은 180명의 결과 전체를 고정해 둔 스냅숏 테스트10(TestSnapshot)로 결과가 뜻하지 않게 바뀌지 않았는지 확인합니다.
새 코드와 예전 코드를 나란히 돌려 결과를 비교하는 방법(차등 테스트, differential testing) 자체는 흔합니다. 이번에 배운 건, 차이를 자동으로 분류하려면 예전 규칙을 새 코드 안에서 다시 켤 수 있어야 한다는 점입니다. 이 스위치가 이번 작업에서 가장 쓸모 있던 코드였습니다.
이유 ① 절입 시각: 사주의 새해는 1월 1일도 설날도 아닌 입춘에 옵니다
사주의 년주와 월주는 1월 1일이나 음력 초하루가 아니라 절기로 바뀝니다. 입춘 순간에 해가 바뀌고, 달마다 절입 순간에 월주가 바뀝니다. 퀴즈처럼 절입 근처에 태어난 사람은 절입 시각이 몇 분만 어긋나도 월주가, 입춘이면 년주까지 바뀝니다.
예전 엔진은 절입을 근사식으로 구했고, 실제 절입과 평균 26분, 최대 2시간 가까이 차이가 났습니다. 근사식은 가볍고 빠르지만, 절입 근처에 태어난 사람에게는 이 차이가 그대로 기둥 차이가 됩니다.
새 엔진은 태양의 겉보기 황경이 입춘 315°, 경칩 345°, 청명 15°처럼 30°마다 있는 절을 지나는 순간을 직접 찾습니다. 태양 위치는 VSOP87D11 계수로, 지구 자전이 고르지 않아 생기는 시간 차이(ΔT12)는 Espenak·Meeus 식으로 계산하고, Meeus 『Astronomical Algorithms』의 예제로 검증했습니다. 기준점은 한국천문연구원이 발표한 2024년 입춘, 2월 4일 17시 27분입니다.
// 2024년 입춘: 2월 4일 17:27 (한국천문연구원). 1분 앞뒤로 년주와 월주가 함께 바뀝니다.
before := pillarsOf(t, seoul(2024, 2, 4, 17, 26)) // 癸卯년 乙丑월
after := pillarsOf(t, seoul(2024, 2, 4, 17, 28)) // 甲辰년 丙寅월
대량 대조는 lunar-javascript(MIT)와 했습니다. 절기 4,824개를 맞춰 보니 최대 27초 차이였습니다. 두 계산이 서로 맞는다는 뜻이고, 정답의 기준은 앞의 천문연 발표 시각입니다.
절입 시각은 대운13으로도 번집니다. 대운은 태어난 때부터 절까지의 날수를 3으로 나눠 정하는데(사흘이 한 해), 이 비율 때문에 절입이 1분 어긋나면 대운 시작일은 2시간쯤 어긋납니다(365.2422 ÷ 3 ≈ 122배). 예전 근사식의 평균 오차 26분이면 이틀쯤입니다. 실제로 대운 시작일이 평균 2일, 최대 10일쯤 달라졌습니다.
대운 나이를 세는 방식도 바꿨습니다. 예전 엔진은 "대운이 시작되는 해 − 태어난 해"로 셌고, 새 엔진은 대운수(날수 ÷ 3을 반올림한 값)를 그대로 씁니다. 이 차이로 한국 출생의 넷 중 한 명꼴(24.6%)이 대운 나이가 한 살 달라졌고, 풀이에 나오는 "지금 대운"이 바뀐 사람은 12.4%였습니다.
해외 출생은 절입과 비교하는 기준 시각도 바뀌었습니다. 예전 엔진은 태어난 곳의 진태양시14를 한국 표준시 숫자로 읽어 절입과 비교했고, 뉴욕 출생이면 실제 순간과 14시간쯤 차이가 났습니다. 새 엔진은 년주·월주·대운을 정할 때 태어난 실제 순간을 절입 순간과 비교합니다.
이유 ② 시간 보정: 서울의 해는 시계보다 30분쯤 늦습니다
시주는 두 시간 단위의 시진으로 바뀝니다(23~01시 子, 01~03시 丑 …). 기준은 시계가 아니라 해의 위치입니다. 그런데 한국 표준시는 일본과 같은 동경 135°15 기준이고 서울은 약 127°라, 서울에서 해가 가장 높이 뜨는 때는 평균 낮 12시 반쯤입니다. 그래서 사주에서는 흔히 한국 출생의 시계에서 30분을 빼고 시주를 가립니다. 퀴즈의 17시 30분생도 30분을 빼면 정확히 17시, 酉시가 시작되는 순간이라 시주가 바뀐 것입니다.
여기에도 갈림길이 있습니다. 서울의 실제 경도로 계산하면 32분이고, 부산은 24분쯤입니다. 도시마다 경도대로 빼는 방식도 있고, 동경 127.5°를 기준으로 일괄 30분을 빼는 방식도 있습니다. 사주사이트는 한국 출생이면 도시와 상관없이 30분을 뺍니다.
"30분을 뺀다"를 코드로 옮길 때도 갈림길이 하나 더 있습니다.
- 출생 기록에 적힌 시각에서 30분을 뺀다.
- 태어난 순간을 UTC+8:30 시계로 읽는다.
대부분은 답이 같습니다. 그런데 한국 표준시는 생각보다 자주 바뀌었습니다. 1954~1961년에는 표준시 자체가 UTC+8:30이었고, 1987·88년 여름처럼 서머타임을 하던 때도 있습니다. 이런 시기에 태어난 사람은 두 방법의 답이 갈립니다.
| 출생(서울, 기록된 시각) | 그때 시계 | ① 30분 빼기 | ② UTC+8:30으로 읽기 |
|---|---|---|---|
| 1960년 3월 1일 07:10 | UTC+8:30 | 06:40 卯시 | 07:10 辰시 |
| 1988년 7월 1일 08:00 | UTC+10(서머타임) | 07:30 辰시 | 06:30 卯시 |
1960년의 시계는 이미 동경 127.5° 기준이라 뺄 것이 없고, 1988년 여름의 시계는 한 시간 앞당겨져 있었으니 한 시간 반을 빼야 맞습니다. 새 엔진은 두 번째 방법을 씁니다. 기록된 시각을 IANA 시간대 자료16에 따라 실제 순간(UTC)으로 바꾼 뒤 고정 오프셋(+8:30)을 더하면, 이런 역사는 시간대 자료가 알아서 처리합니다.
// RefClock 은 시주·일주(그리고 자미두수 시진)를 가르는 시계입니다.
// - 한국: UTC+8:30 고정(한국 표준시 − 30분).
// - 해외: 태어난 곳의 진태양시(경도 + 균시차). 경도로 이미 보정하므로 30분을 따로 빼지 않습니다.
func RefClock(birth time.Time, tz string, longitude float64) time.Time {
if IsKorea(tz) {
return birth.UTC().Add(8*time.Hour + 30*time.Minute)
}
if loc, err := time.LoadLocation(tz); err == nil {
birth = birth.In(loc) // 날짜변경선 접기는 그 순간의 표준시 차이를 씁니다.
}
return birth.UTC().Add(time.Duration(SolarOffsetMinutes(birth, longitude) * float64(time.Minute)))
}
해외 출생은 30분 대신 진태양시를 씁니다. 태어난 곳의 경도와 균시차17로 그 자리의 해 시각을 직접 구하는 방식입니다. 시주가 가장 크게 바뀐 곳이 여기입니다. 예전 엔진은 경도로 진태양시를 구한 뒤 한국 출생처럼 30분을 한 번 더 뺐습니다. 진태양시는 이미 경도로 보정한 시각이라, 새 엔진은 30분을 더 빼지 않습니다.
뉴욕 2000년 6월 1일 10시(EDT) 출생이면 진태양시로 약 9시 6분, 巳시입니다. 30분을 더 빼면 8시 36분, 辰시가 됩니다(TestAbroadHourUsesSolarTimeWithoutKoreanOffset). 해외 출생은 네 명 중 한 명꼴(25.8%)로 이 차이 때문에 시주가 바뀌었습니다.
밤 11시생의 일주: 야자시와 조자시
子시는 밤 11시에 시작해 새벽 1시에 끝나서 자정을 가로지릅니다. 그래서 子시가 시작될 때 날을 바꿀지, 자정에 바꿀지가 학파마다 다릅니다. 밤 11시~자정을 야자시18, 자정~새벽 1시를 조자시로 나눠 보는 관점이 대표적입니다. 만세력 앱마다 밤 11시대 출생의 일주가 다르게 나오는 흔한 이유입니다.
사주사이트의 사주는 子시가 시작되는 순간, 즉 30분 보정한 시계로 23시(한국 시각 23시 30분)에 날을 바꿉니다. 2000년 1월 1일 23:29는 戊午일 亥시, 23:30은 己未일 子시입니다(TestKoreaDayChangesAt2330KST).
그 밖에 시각을 다루며 정한 규칙입니다.
- 애매한 시각은 거절하지 않습니다. 서머타임이 끝나며 두 번 오는 시각은 앞의 것으로, 시작되며 건너뛴(없는) 시각은 바뀌기 전 시계로 읽습니다. 예전 엔진에서는 건너뛴 시각을 계산할 수 없었습니다.
- 날짜변경선 근처는 출생 기록의 날짜를 지킵니다. 키리바시 라인 제도(키리티마티)는 UTC+14인데 서경 157°라, 경도만으로 진태양시를 구하면 하루가 밀립니다. 경도 차이를 표준시 기준 ±12시간 안으로 접어 이를 막습니다(
TestDateLineZonesKeepTheCivilDate). - 균시차도 근사식 대신 천문 계산으로 구합니다. 예전 근사식은 참값과 평균 0.49분, 최대 1.4분 차이가 났습니다.
이유 ③ 음력: 2027년 설날, 한국은 2월 7일이고 중국은 2월 6일입니다
음력 초하루는 삭(달과 해가 같은 방향에 겹치는 순간)이 든 날입니다. 그런데 그 "날"을 어느 자오선19의 시계로 가르느냐에 따라 초하루가 하루 갈릴 수 있습니다. 중국은 동경 120°(UTC+8), 한국은 1912년부터 동경 135°(UTC+9) 기준으로 음력을 만듭니다. 삭이 한국 시각 0~1시에 들면 한국에서는 이미 그날이지만 중국에서는 아직 전날이라, 초하루가 하루 갈립니다.
2027년 설날이 그렇습니다. 정월을 여는 삭이 한국 시각으로 2월 7일 0시 56분, 중국 시각으로는 2월 6일 23시 56분에 듭니다. 그래서 한국 설날은 2월 7일, 중국 춘절은 2월 6일입니다. 2028년도 하루 갈립니다(한국 1월 27일, 중국 1월 26일).
새 엔진의 lunar 패키지는 한국천문연구원 역서와 같은 자오선을 씁니다(근거: 계승혁, 「보름달과 음력」).
- 1911년까지: 동경 120°. 1908년에 표준시를 바꿨지만 음력은 중국 것을 그대로 썼습니다.
- 1912년부터: 동경 135°.
- 1954~61년(표준시 127.5°): 127.5°로 날을 갈라도 135°로 가른 것과 초하루가 모두 같아 따로 다루지 않습니다. 이것도 테스트로 고정해 두었습니다(
TestKorean1954To1961MeridianDoesNotMatter).
두 달력이 갈리는 경우는 생각보다 많습니다.
- 설날(1900~2050년): 1916·1944·1954·1958·1966·1988·1997·2027·2028년
- 윤달(1900~2050년): 2012년(한국 윤3월, 중국 윤4월), 2017년(한국 윤5월, 중국 윤6월). 초하루가 하루 갈리면 어느 달에 중기가 드는지도 달라져, 윤달 자리까지 바뀝니다. 2012년 5월 1일은 한국 음력으로 윤3월 11일, 중국 음력으로 4월 11일입니다.
- 1950~2050년 날짜의 3.7%가 하루 다릅니다.
1900~1960년 윤달 23개와 설날 60개(61년 가운데 1947년은 대조 자료를 옮겨 적다 틀려 제외)를 공개 자료와 대조해 모두 같았고, 계승혁 글에 나오는 설날이 갈리는 해도 모두 재현했습니다. 중국 음력 달 시작 2,486개는 lunar-javascript와 모두 같았습니다.
사주는 절기로 년주와 월주를 가르기 때문에, 양력 생일로 넣으면 음력 달력이 바뀌어도 사주는 그대로입니다. 하지만 생일을 음력으로 입력하면 양력으로 바꾸는 단계에서 달력이 쓰입니다. 1960~2008년 음력 날짜의 2.9%가 양력으로 하루 다른 날이 됐고, 이런 사람은 일주(와 시주의 천간)도 바뀝니다. 한국 음력으로 바로잡힌 것입니다. 음력 날짜를 직접 쓰는 자미두수는 영향이 훨씬 큽니다.
이유 ④ 자미두수: 같은 시계, 한국 음력, 다른 밝기 표
자미두수 명반20은 음력 생월과 생시로 명궁 자리를 정하고, 그 위에 별을 배치합니다. 사주와 같은 출생 정보를 쓰지만, 예전 엔진과는 세 가지 규칙이 다릅니다.
- 시계. 예전 엔진은 사주 시주를 가를 때는 한국 출생의 30분을 빼고, 자미두수 시진은 한국 표준시 그대로 갈랐습니다. 표준시 그대로 가르는 것도 자미두수에서 쓰는 방식이지만, 사주사이트의 리딩은 두 체계를 한 글 안에서 함께 읽기 때문에 같은 시각을 봐야 했습니다. 서울 01:10 출생이면 사주는 00:40으로 子시인데, 예전 자미두수는 丑시였습니다.
- 달력. 예전 엔진은 중국 음력 기준이었습니다. 한국 서비스라 한국 음력이 필요했고, 두 달력이 갈리는 날에는 생월이나 생일이 달라집니다.
- 밝기 표. 14주성이 어느 궁에서 얼마나 밝은지21(廟旺得利平不陷)를 정한 표는 유파마다 다릅니다. 예전 엔진의 표가 어느 계열인지 확인하지 못해, 근거를 댈 수 있는 표로 바꿨습니다. 두 표는 168칸 중 101칸이 다릅니다.
새 엔진에서는 이렇게 정했습니다.
- 시진을 사주와 같은
RefClock으로 가릅니다. 무작위 600명(서울·부산·뉴욕·런던·도쿄·키리바시)으로 사주 시주와 자미두수 시진의 지지가 늘 같은지 봅니다(TestHourBranchMatchesSaju). - 한국 음력을 씁니다(
TestUsesKoreanLunarCalendar). - 밤 11시~자정 출생은 날을 넘기지 않고 그날로 봅니다(보정한 시계 기준). 23시에 날을 바꾸는 사주와는 다른 규칙입니다.
- 밝기는 『자미두수전서』 계열 7단계 표로 바꿨습니다. 요즘 명반 프로그램들이 널리 쓰는 표와 같고, 고전 격국22 이름이 가리키는 칸과도 맞는지 테스트합니다(
TestBrightnessTable).
| 격국 | 별과 자리 | 밝기 |
|---|---|---|
| 極向離明格 | 紫微 午 | 廟 |
| 英星入廟格 | 破軍 子·午 | 廟 |
| 巨機同臨格 | 天機 卯·酉 / 巨門 卯·酉 | 旺 / 廟 |
| 梁同巳亥格 | 天梁 巳·亥 | 陷 |
그 결과 한국 출생의 25.1%는 시진이, 29.4%는 별 위치가 하나 이상 바뀌었습니다. 밝기 표기를 빼면 이번 교체에서 가장 많이 바뀐 부분입니다. 25.1%는 손으로도 어림할 수 있습니다. 시진은 두 시간(120분)짜리 칸인데 시계를 30분 옮겼으니, 칸마다 경계 바로 뒤 30분에 태어난 사람, 곧 넷 중 한 명이 옆 칸으로 넘어갑니다.
별자리: 날짜표가 아니라 태양 위치로 정합니다
별자리 운세 표에는 흔히 물고기자리가 2월 19일부터라고 적혀 있습니다. 하지만 태양이 물고기자리(황경 330°)에 들어가는 순간은 해마다 조금씩 움직입니다. 2025년에는 2월 18일 19시 6분(한국 시각)이었습니다. 이날 저녁 8시에 태어난 사람은 표로 보면 물병자리, 태양 위치로 보면 물고기자리입니다. 별자리 앱마다 경계일 생일의 별자리가 다른 이유입니다. 사주사이트는 절기를 구하는 것과 같은 태양 계산으로 별자리를 정합니다. 황경 330°는 절기로 치면 우수, 곧 물고기자리가 시작되는 중기입니다.
출생 차트는 이번 교체에서 바뀐 게 가장 적습니다. 예전 엔진은 해·달·행성 위치를 빛이 오는 데 걸리는 시간(광행시)과 광행차, ΔT 없이 계산했고, 새 엔진은 이것들을 모두 넣은 겉보기 위치를 씁니다. 차이는 해 20″, 달 35~100″, 행성 3~30″입니다. 1″는 1°의 3,600분의 1이라, 행성의 별자리가 바뀐 사람은 0.1%였습니다. 하우스는 플라시두스 방식으로, 극지방에서는 포르피리 방식으로 나눕니다.
키론23은 라이선스 걱정 없이 서버에 넣을 수 있는 위치표를 찾지 못해 직접 만들었습니다. JPL 소천체 데이터베이스의 궤도 요소에서 출발해 태양과 여덟 행성의 중력을 넣은 적분기를 짰고, 1899~2101년 위치표(10일 간격)를 만드는 데 2분이 걸립니다. 키론이 별자리를 옮겨 가는 날짜를 공개 자료와 맞춰 확인했습니다.
결과: 한국 출생 사주 네 기둥은 0.1%, 자미두수는 30% 가까이 바뀌었습니다
1960~2008년생 한국 출생 6,000명과 해외 출생 2,000명을 두 엔진에 넣어, 사용자 화면에 보이는 값이 얼마나 바뀌는지 재 봤습니다(2026년 9월 25일에 한 번 실행).
한국 출생 사주 네 기둥이 바뀐 0.1%(양력 입력 기준)는 절입 근처의 월주이고, 해외 출생 26.6%는 대부분 시주(25.8%)입니다. 자미두수 음력 날짜가 바뀐 5.2%는 대부분 달력 차이(3.1%)와 자시 경계(2.0%)에서 나왔습니다. 자시 경계는 한국 시각 0시~0시 30분 출생이 보정한 시계로는 아직 전날 23시대라 음력 날짜가 하루 앞당겨진 경우입니다. 주성 밝기 표기는 모든 명반에서 바뀌었습니다(주성 14개 중 평균 9개).
한마디로, 한국 출생 사주의 네 기둥은 두 엔진이 거의 같았습니다. 차이는 대운 나이, 절입 근처, 해외 출생, 그리고 규칙을 바꾼 자미두수에 몰려 있습니다.
속도는 Node 프로세스 기동을 포함해 100ms 이상이던 것이 10~15ms가 됐습니다. 운영 이미지에서 Node를 뺐고, DB는 건드리지 않아 되돌리기는 이전 릴리스 재배포로 끝납니다.
이미 결제한 사용자도 챙겨야 했습니다. 풀이 캐시 버전을 올려, 같은 입력으로 새로 요청하면 새 차트로 새 리딩을 쓰게 했습니다. 예전 차트로 쓴 글은 "지난 이야기"에 그대로 두었고, 같은 입력으로 전체 리딩을 이미 받은 사람은 잠긴 맛보기를 열 때 다시 결제하지 않습니다.
남은 것과 배운 것
아직 못 한 것도 있습니다.
- 행성 위치를 JPL Horizons24와 대조하지 못했습니다. 지금은 Meeus 예제와 공개된 1961년 출생 차트까지만 맞춰 봤습니다.
- 2050년 뒤, 삭이 자정 1분 안에 드는 날은 아직 확인하지 못했습니다. 1912~2050년에는 2005년 12월 2일(한국 시각 00:00:54) 하나로, 발표된 역과 같습니다. 그 뒤로 2051·2074·2097년에 더 있는데, 달 이론과 ΔT의 오차 범위 안이라 발표된 역이 나오면 확인해야 합니다.
- 명왕성 식은 1885~2099년용입니다. 계산 범위를 2100년 이후로 넓히려면 바꿔야 합니다.
배운 것은 세 가지입니다.
- "예전과 같다"는 정답의 기준이 아닙니다. 맞는지는 바깥 기준으로만 말할 수 있었습니다. 천문연 발표 시각, 발표된 역, Meeus 예제, 고전 격국 이름 같은 것들입니다.
- 목표는 차이를 0으로 만드는 게 아니라, 모든 차이를 설명할 수 있게 만드는 것이었습니다. 예전 규칙 스위치 덕분에 12,120건을 비교하며 나온 차이를 사람이 하나씩 보지 않고도 원인별로 나눌 수 있었습니다.
- 가장자리 입력은 일부러 만들어야 합니다. 무작위 입력으로는 서머타임에 건너뛴 시각이나 날짜변경선 출생이 거의 나오지 않습니다. 지금은 이런 입력 300개 이상을 일반 테스트(
TestEdgeCasesCalculate)에 넣어 두었습니다.
같은 생일인데 사주 앱(어플)마다 결과가 다르게 나왔다면, 대개 이 글의 갈림길 중 하나가 다른 것입니다. 절입 시각, 시간 보정, 밤 11시대의 날짜, 음력 기준, 대운 나이 세는 법, 그리고 자미두수라면 밝기 표입니다. 퀴즈의 2024년 2월 4일 17시 20분과 17시 30분은 사주사이트의 무료 만세력에 직접 넣어 볼 수 있습니다. 사주·자미두수·별자리를 함께 읽는 리딩은 사주사이트에 있습니다. 틀린 곳이 보이면 알려 주세요.
각주
-
사주(四柱). 태어난 해·달·날·시를 각각 두 글자(천간과 지지)로 적은 네 기둥입니다. 모두 여덟 글자라 '팔자'라고도 합니다. ↩
-
년주·월주·일주·시주는 사주의 네 기둥으로, 각각 태어난 해·달·날·시를 나타냅니다. 년주의 아래 글자(지지)가 띠입니다. 卯는 토끼, 辰은 용입니다. ↩
-
하루를 두 시간씩 열두 칸으로 나눈 옛 시간 단위입니다. 子시(23~01시)부터 亥시(21~23시)까지 십이지 이름을 붙입니다. ↩
-
자미두수(紫微斗數). 음력 생년월일시로 열두 궁의 판을 세우고, 자미성을 비롯한 별들을 배치해 읽는 동양 점성술입니다. ↩
-
여기서 별자리는 서양 점성술을 가리킵니다. 흔히 말하는 별자리(태어난 순간 태양이 있던 칸)와, 해·달·행성의 자리를 모두 그린 출생 차트를 함께 다룹니다. 이 별자리는 춘분점에서 황도를 30°씩 나눈 황도 12궁이라, 세차 운동 때문에 지금은 하늘의 실제 별자리와 한 칸 가까이 어긋나 있습니다. ↩
-
오픈소스 라이선스의 하나입니다. 이 라이선스의 코드를 고쳐 네트워크 서비스로 제공하면, 사용자에게 그 소스 코드를 공개해야 합니다. ↩
-
황도는 지구에서 볼 때 태양이 1년 동안 하늘에서 지나가는 길입니다. 그 길 위에서 태양이 몇 도 지점에 있는지를 황경이라 하고, 춘분점이 0°입니다. ↩
-
1년을 태양의 위치로 24등분한 것이 24절기입니다. 그중 입춘·경칩처럼 사주의 달이 시작되는 12개를 '절', 춘분·곡우처럼 그 사이에 있는 12개를 '중기'라 하고, 절에 들어서는 순간을 절입이라 합니다. ↩
-
음력 열두 달은 태양의 1년보다 11일쯤 짧습니다. 이 차이를 메우려고 2~3년에 한 번 넣는 달이 윤달입니다. ↩
-
지금 결과 전체를 파일로 저장해 두고, 코드를 바꿀 때마다 결과가 그대로인지 비교하는 테스트입니다. ↩
-
프랑스 경도국(Bureau des longitudes)이 만든 행성 위치 이론입니다. 태양과 행성의 위치를 아주 정밀하게 계산할 수 있는 계수 표로 공개되어 있습니다. ↩
-
지구 자전이 조금씩 불규칙해서 생기는, 천문 계산용 균일한 시간과 실제 시계 시간의 차이입니다. 2020년대에는 약 70초입니다. ↩
-
대운은 사주에서 10년 단위로 바뀌는 운의 흐름이고, 대운수는 첫 대운이 시작되는 나이입니다. 태어난 때부터 다음(또는 지난) 절까지의 날수를 3으로 나눠 정합니다. ↩
-
시계가 아니라 그 자리에서 해가 실제로 가장 높이 뜨는 때를 정오로 삼는 시간입니다. 경도와 균시차로 계산합니다. ↩
-
한국 표준시는 일본과 같은 동경 135° 자오선 기준(UTC+9)입니다. 경도 15°가 1시간이라, 동경 약 127°인 서울은 해의 시각이 30분 남짓 늦습니다. ↩
-
나라·지역별 표준시와 서머타임의 변경 이력을 모은 공용 데이터베이스(tzdata)입니다. 대부분의 운영체제와 프로그래밍 언어가 이 자료를 씁니다. ↩
-
지구 궤도가 타원이고 자전축이 기울어 있어서, 해의 실제 시각이 1년 동안 평균보다 최대 16분쯤 앞서고 14분쯤 늦는 차이입니다. ↩
-
야자시(夜子時)는 밤 11시~자정, 조자시(朝子時)는 자정~새벽 1시를 가리킵니다. 둘 다 子시지만 야자시 출생을 그날로 볼지 다음 날로 볼지가 학파마다 다릅니다. 자미두수에서는 이 구간을 晩子라고도 부릅니다. ↩
-
같은 경도를 잇는 선입니다. 어느 경도의 시각으로 날짜를 가르느냐에 따라 자정이 달라집니다. ↩
-
명반은 자미두수에서 열두 궁에 별을 배치한 판이고, 명궁은 그중 그 사람 자신을 나타내는 궁입니다. ↩
-
14주성은 자미성을 비롯해 명반의 중심이 되는 별 14개입니다. 밝기는 별이 놓인 궁에 따라 그 힘이 얼마나 잘 드러나는지를 7단계(廟·旺·得·利·平·不·陷)로 매긴 것입니다. ↩
-
별이나 글자가 특정하게 짜인 구성에 붙인 고전의 이름입니다. 예를 들어 極向離明格은 자미성이 午궁에 있는 구성입니다. ↩
-
토성과 천왕성 사이를 도는 소천체로, 서양 점성술 출생 차트에서 쓰입니다. 적분기는 궤도의 시작 상태에서 출발해, 중력에 따른 움직임을 짧은 시간 단위로 쪼개 이어서 계산하는 프로그램입니다. ↩
-
미국 NASA 제트추진연구소(JPL)가 제공하는 천체 위치 계산 서비스로, 행성 위치를 확인할 때 표준처럼 쓰입니다. ↩






