테스트와 검증 — 정답이 여러 개인 도메인에서의 품질 보증
"맞다"의 기준이 하나가 아니다
일반적인 소프트웨어에서 테스트는 명확하다. 1 + 1의 결과가 2인지 확인하면 된다. 정답은 하나이고, 맞거나 틀리거나 둘 중 하나다. 사주 앱의 테스트는 이것과 근본적으로 다른 성격을 갖고 있었다.
같은 생년월일시를 입력해도, 진태양시 보정을 적용하느냐 안 하느냐, 야자시설을 채택하느냐 안 하느냐에 따라 "정답"이 달라진다. 전문 앱 A와 전문 앱 B의 결과가 다를 때, 둘 중 하나가 틀린 것이 아니라 서로 다른 설정을 적용하고 있는 것일 수 있다. 이 상황에서 "우리 앱이 정확하다"를 어떻게 증명할 것인가. 이 질문이 테스트 전략의 출발점이었다.
전문 앱들과의 교차 검증
가장 먼저 한 것은 기존 전문 앱들과 결과를 비교하는 것이었다. 1편에서 벤치마킹했던 앱들 — "만세력 천을귀인", "고전 사주", 네이버 만세력 — 에 동일한 생년월일시를 입력하고 우리 앱의 결과와 대조했다.
검증용 테스트 케이스는 세 가지 유형으로 나눠서 준비했다. 첫째, 경계가 아닌 "안전한" 케이스. 절기 경계에서 충분히 떨어진 날짜와 시각으로, 어떤 설정을 적용해도 결과가 같아야 하는 케이스다. 이것은 기본적인 만세력 정확도를 확인하는 용도다. 둘째, 절기 경계 케이스. 입춘 당일, 경칩 당일처럼 절기 경계에 가까운 출생일이다. 이 케이스에서는 절기 시각의 정밀도가 검증된다. 셋째, 보정 민감 케이스. 야자시 시간대, 서머타임 기간, 시주 경계 근처의 출생 시각이다. 설정에 따라 결과가 달라질 수 있는 케이스다.
첫 번째 유형에서 결과가 다르면 심각한 문제다. 기본적인 만세력 로직이 틀렸다는 뜻이기 때문이다. 실제 검증에서 이 유형에서는 모든 앱의 결과가 일치했고, 우리 앱도 일치했다. 기본 만세력 파이프라인이 올바르게 작동한다는 것을 확인한 순간이었다.
두 번째 유형에서 흥미로운 차이가 발견됐다. 입춘 당일 출생 케이스에서, 절기 시각을 분 단위로 처리하는 앱과 날짜 단위로만 처리하는 앱 사이에 결과 차이가 있었다. 우리 앱은 KASI 데이터를 기반으로 분 단위 판정을 하므로, 분 단위 처리를 하는 전문 앱들과 결과가 일치했다.
진태양시와 야자시 설정에 따른 결과 차이
세 번째 유형에서 가장 의미 있는 발견이 나왔다. 1987년 7월 15일 23시 30분 서울 출생이라는 테스트 케이스를 여러 앱에 입력했을 때, 앱마다 시주가 달랐다. 어떤 앱은 자시(子時), 어떤 앱은 해시(亥時)를 표시했다.
원인을 분석하자 답이 명확해졌다. 이 케이스는 세 가지 보정이 모두 관련된다. 1987년이므로 서머타임 기간이다(1시간 차감). 23시 30분이므로 야자시 영역이다. 서울 출생이므로 진태양시 보정이 필요하다. 각 앱이 이 세 가지 보정 중 어떤 것을 적용하느냐에 따라 결과가 갈린 것이다.
이 발견은 두 가지 중요한 교훈을 줬다. 첫째, "다른 앱과 결과가 다르다"가 반드시 "우리가 틀렸다"를 의미하지 않는다. 설정이 다르면 결과가 다른 것이 정상이다. 둘째, 사용자에게 "어떤 설정이 적용되어 있는지"를 투명하게 보여줘야 한다. 설정을 숨기면 사용자가 다른 앱과 비교했을 때 혼란을 느낀다.
결과적으로 앱에 "보정 설정 표시" 기능을 추가했다. 사주 결과 화면에 "진태양시 보정: 적용 / 서머타임: 자동 감지 / 야자시: 야자시설 적용"이라는 정보를 명시적으로 표시한다. 그리고 각 설정을 변경하면 즉시 결과가 재계산되어 차이를 직접 확인할 수 있게 했다.
Vitest로 단위 테스트 작성하기
교차 검증으로 전체적인 정확도를 확인한 후, 자동화된 단위 테스트를 Vitest로 작성했다. 수동 교차 검증은 개발 초기에 방향을 잡는 데는 유효하지만, 코드가 변경될 때마다 반복하기에는 비효율적이기 때문이다.
테스트 스위트는 네 개의 영역으로 구성했다.
첫째, 만세력 변환 테스트. 특정 양력 날짜를 입력했을 때 음력 변환이 정확한지, 해당 날짜의 절기가 올바른지를 검증한다. 윤달이 포함된 날짜, 연말 연시, 절기 경계일을 집중적으로 테스트했다.
둘째, 사주 기둥 계산 테스트. 이것이 가장 핵심적인 테스트다. 특정 생년월일시를 입력했을 때 연주, 월주, 일주, 시주가 정확히 기대값과 일치하는지를 검증한다. 기대값은 전문 만세력 사이트에서 직접 확인한 값이다. 최소 20개의 생년월일을 테스트 케이스로 포함했고, 각 케이스에 대해 네 기둥 모두를 검증한다.
셋째, 분석 결과 테스트. 특정 사주의 오행 비율 합계가 100%인지, 십신 판정이 이론적 규칙과 일치하는지, 용신 판단 로직이 신강/신약 분류를 올바르게 하는지를 검증한다. 신살 테스트는 알려진 사주(유명인의 공개된 사주 등)에 대해 기대되는 신살이 검출되는지를 확인했다.
넷째, 대운/세운 계산 테스트. 대운 시작 나이가 정확한지, 대운 간지의 순행/역행이 성별과 연간에 따라 올바르게 적용되는지를 검증한다. 이 테스트에서도 전문 앱의 결과를 기대값으로 활용했다.
"정답이 여러 개"일 때의 테스트 전략
가장 고민이 깊었던 부분은, 설정에 따라 결과가 달라지는 케이스의 테스트 전략이었다. "이 입력에 대해 이 결과가 나와야 한다"는 식의 단일 기대값 테스트로는 부족하다.
채택한 전략은 설정 조합별 기대값 매트릭스다. 하나의 테스트 케이스에 대해 가능한 설정 조합별로 기대 결과를 모두 정의한다. 예를 들어 특정 출생 정보에 대해, "진태양시 ON + 야자시설 ON"일 때의 기대 사주, "진태양시 ON + 야자시설 OFF"일 때의 기대 사주, "진태양시 OFF + 야자시설 ON"일 때의 기대 사주, "진태양시 OFF + 야자시설 OFF"일 때의 기대 사주를 각각 정의하고, 네 가지 설정 조합 모두에 대해 테스트를 실행한다.
이 방식의 장점은 "설정 X를 변경했을 때 결과가 올바르게 변하는지"까지 검증할 수 있다는 점이다. 단순히 "맞는 결과가 나온다"뿐 아니라 "설정 변경이 예상대로 반영된다"를 확인할 수 있다.
단점은 테스트 케이스의 양이 급격히 늘어난다는 것이다. 하나의 출생 정보에 대해 4개의 설정 조합, 각 조합에서 4개의 기둥을 검증하면 16개의 단언(assertion)이 필요하다. 이것을 20개 출생 정보에 대해 수행하면 320개의 단언이다. 하지만 이 비용은 "정확도"라는 앱의 핵심 가치를 보장하기 위해 감수해야 하는 투자다.
테스트 케이스 생성에서의 AI 활용
320개 이상의 단언을 수작업으로 만드는 것은 현실적으로 힘들다. 여기서 AI 협업이 효과적이었다. Claude에게 "다음 조건을 만족하는 테스트 케이스를 생성해줘: 입춘 경계, 야자시 시간대, 서머타임 기간, 시주 경계"라고 요청하면, 각 조건에 맞는 생년월일시를 제안해준다.
하지만 여기서 중요한 원칙이 있다. AI가 생성한 기대값을 그대로 테스트에 사용하면 안 된다. AI의 사주 계산이 맞는지를 검증하는 것이 목적인데, AI의 계산 결과를 기대값으로 쓰면 순환 논증이 된다. AI는 테스트 케이스의 "형태"(어떤 날짜, 어떤 시간대를 테스트해야 하는지)를 제안하는 역할만 하고, 기대값은 반드시 외부 소스(전문 만세력 사이트)에서 직접 확인하여 입력했다.
이 원칙은 AI 협업에서 테스트를 작성할 때 항상 의식해야 하는 부분이다. AI가 코드를 작성하고 AI가 테스트를 작성하면, 같은 오류가 코드와 테스트에 동시에 들어갈 수 있다. 테스트의 기대값만큼은 독립적인 소스에서 가져와야 한다.
만세력 정확도가 모든 것을 좌우한다
테스트 과정에서 가장 절실하게 느낀 것은, 만세력의 정확도가 앱 전체의 가치를 결정한다는 사실이다. 이것은 추상적인 원칙이 아니라, 실제 검증에서 체험한 현실이다.
초기 개발 단계에서 일주 계산에 미세한 오류가 있었던 적이 있다. 특정 날짜 범위에서 일주가 하루 밀려 있었다. 이 오류는 일주뿐 아니라 시주 판정(시주의 천간은 일간에 의존), 십신 분석(일간이 기준), 용신 판단(일간의 강약이 핵심), 대운 계산(월주가 기준)까지 연쇄적으로 영향을 미쳤다. 하나의 기둥이 틀리자 그 위에 쌓인 모든 분석이 무의미해진 것이다.
이 경험은 "만세력 테스트를 가장 먼저, 가장 철저하게 해야 한다"는 확신을 줬다. 분석 엔진의 오행 비율 계산이 1~2% 부정확한 것은 해석에 큰 영향을 미치지 않는다. 하지만 사주 기둥 자체가 틀리면 이후의 모든 것이 무너진다.
건물에 비유하면, 만세력은 기초 공사다. 기초가 1cm 어긋나면 10층에서는 수십 cm가 어긋난다. 분석 엔진은 1층부터 10층까지의 구조물이고, AI 해석은 인테리어다. 인테리어가 아무리 훌륭해도 건물이 기울어져 있으면 의미가 없다.
회귀 테스트의 가치
단위 테스트의 또 다른 가치는 회귀 방지다. 분석 엔진을 개선하거나, 새로운 신살을 추가하거나, 가중치를 조정할 때마다, 기존에 정확했던 결과가 깨지지 않았는지를 자동으로 확인할 수 있다.
실제로 용신 판단의 가중치를 미세 조정했을 때, 기존 테스트 케이스 중 하나가 실패한 적이 있었다. 가중치 변경으로 특정 사주의 신강/신약 판정이 바뀐 것이다. 이 실패가 없었다면 모르고 넘어갔을 것이고, 나중에 사용자가 "이 결과가 이상하다"고 보고했을 때 원인을 찾는 데 훨씬 오래 걸렸을 것이다.
CI(Continuous Integration) 파이프라인에 테스트를 통합하여, 코드가 변경될 때마다 자동으로 전체 테스트가 실행되게 설정했다. 사주 앱에서 "코드를 수정해도 기존 정확도가 유지된다"는 보장은, 개발 속도와 심리적 안정 모두에 크게 기여한다.
이 과정에서 배운 것
첫째, "정답이 여러 개인 도메인"에서의 테스트는 설정 조합별 기대값 매트릭스가 효과적이다. 단일 기대값 테스트로는 설정 간의 차이를 검증할 수 없다.
둘째, 교차 검증에서 "다른 앱과 결과가 다르다"는 것은 반드시 오류를 의미하지 않는다. 설정 차이를 먼저 의심하고, 같은 설정에서도 다르면 그때 오류를 의심해야 한다.
셋째, AI가 생성한 테스트의 기대값은 반드시 독립적인 외부 소스로 검증해야 한다. AI가 코드와 테스트를 모두 작성하면 같은 오류가 양쪽에 들어갈 수 있다.
넷째, 만세력 정확도는 앱 전체의 기초다. 기둥이 틀리면 분석이 무의미하고, 분석이 무의미하면 해석도 무의미하다. 테스트 우선순위는 만세력 > 기둥 계산 > 분석 > 해석 순이어야 한다.
다섯째, 회귀 테스트는 "기존 정확도를 유지하면서 개선한다"는 확신을 주는 안전망이다. 사주처럼 정확도가 핵심 가치인 앱에서는 이 안전망이 필수다.
다음 편 예고
만세력, 분석 엔진, 테스트까지 완성됐다. 이제 이 구조화된 분석 데이터를 "사람이 읽을 수 있는 해석"으로 바꿔야 한다. 룰 기반 분석과 AI 해석을 결합하는 하이브리드 아키텍처, 궁통보감 데이터를 프롬프트에 넣었을 때의 극적인 품질 향상, 그리고 "AI에게 계산을 시키지 않고 해석만 시키는" 전략을 11편에서 다룬다.
댓글
댓글 쓰기