한줄 요약
AI에게 세법을 그냥 묻지 않으려고 공식 법령 원문을 직접 옮겨 위키로 만들었습니다. 옮긴 결과가 원문과 같은지는 기계가 검사하도록 했고, 검사를 통과한 것만 정본으로 올렸습니다.
그런데 그 “통과”가 네 번 배신했습니다. 조문 개수도 파일 지문도 맞는데 문장이 통째로 빠져 있었고, 그걸 잡으라고 만든 검 사기가 같은 실수를 같이 하고 있었고, 근거로 딸려 나온 계산식은 원문과 뜻이 달랐고, AI 코드 리뷰를 여덟 번 통과한 코드가 다른 법 62건에서 6건을 틀렸습니다.
공통점이 하나 있었습니다. 네 번 다 검사기가 아니라 검사기 바깥에서 걸렸습니다.
이런 분께
AI에게 규정·약관·법령처럼 “틀리면 곤란한” 자료를 물어보고 싶은데, 답을 어디까지 믿어야 할지 모르겠는 분
자료를 옮기거나 정리하는 작업을 AI에게 맡겨봤고, “검사 통과” 메시지를 받아본 적 있는 분
테스트가 다 통과했는데 왜 여전히 불안한지 설명이 안 되던 분
반대로, 이미 검증 설계를 오래 해오신 분께는 새로운 내용이 없을 수 있습니다
왜 원문을 직접 옮기기로 했나
세법은 일상적인 질문에서 시작됩니다. 부모님께 얼마까지 증여받으면 세금이 없는지, 집을 팔면 뭘 내야 하는지 같은 것들입니다. 검색보다 AI에게 묻는 쪽이 빠르니 저도 그렇게 했습니다.
문제는 답이 그럴듯하다는 것이었습니다. 틀렸을 때도 그럴듯했습니다. 특히 최근에 바뀐 조항일수록 그랬습니다. AI는 “모르겠다”고 물러나는 대신, 예전에 학습한 내용과 그럴듯한 근거를 섞어서 빈칸을 채웠습니다. 더 최신 모델로 바꿔도 그때만 해결될 뿐, 다음 개정이 오면 같은 일이 반복될 구조였습니다.
그래서 방향을 바꿨습니다. AI에게 세법을 묻지 않고, AI가 답할 때 되돌아볼 수 있는 공식 원문을 먼저 만들자. 법제처가 운영하는 국가법령정보 시스템에는 법령 원문을 기계가 받아갈 수 있는 공식 통로(API, 외부 서비스에서 데이터를 자동으로 받아오는 창구)가 열려 있었습니다. 사람이 웹페이지에서 복사·붙여넣기 하는 대신 그 통로로 받기로 했습니다.
여기까지는 흔한 판단입니다. 진짜 이야기는 그다음부터입니다.
“통과한 것만 올린다”는 규칙
받아온 원문을 그대로 쌓으면 위키가 아니라 덩어리입니다. 조문 하나하나를 따로 찾을 수 있어야 하니 쪼개야 했고, 쪼개는 과정에서 내용이 상하면 원문 위키라는 말이 무색해집니다.
그래서 흐름을 이렇게 잡았습니다.
받아오기 → 조문 단위로 쪼개기 → 검사 → 통과한 것만 정본 폴더로이걸 말로만 정해두면 지키다 말게 되므로, 폴더를 아예 셋으로 나눴습니다.
inbox/ # 공식 통로로 막 받아온 원문. 아직 아무것도 검사하지 않은 상태
staging/ # 조문 단위로 쪼갠 결과. 검사 대기 중이고, 여기서 떨어지면 버려짐
source/ # 검사를 통과한 것만. AI가 답할 때 근거로 삼는 유일한 폴더핵심은 마지막 폴더입니다. 검사를 통과하지 못한 결과물은 source에 못 들어옵니다. 거꾸로 말하면, 거기 들어 있다는 사실 자체가 “검사를 통과했다”는 뜻입니다.
이 구조가 마음에 들었습니다. 규칙을 지키려고 노력하는 게 아니라, 안 지키면 파일이 물리적으로 다음 칸에 못 가니까요.
검사는 세 종류로 나눴습니다. 하나에 다 맡기지 않은 이유는, 검사마다 잘 잡는 문제가 다르기 때문입니다.
검사
쉽게 말하면
맡은 일
1단
서류 형식 확인
필요한 항목이 다 있고 형식이 맞는지
2단
파일 지문 확인
내용이 중간에 바뀌거나 깨지지 않았는지
3단
원문 대조
원본에 있던 내용이 옮기는 과정에서 빠지지 않았는지
2단의 “파일 지문”은 해시(파일 내용을 요약한 짧은 문자열, 한 글자만 바뀌어도 완전히 달라짐)를 말합니다. 정본에 올릴 때 지문을 같이 기록해두면, 나중에 누가 6억원을 60억원으로 바꿔놓아도 지문이 안 맞아 걸립니다. 다만 이건 올린 이후의 변조를 잡는 장치입니다. 애초에 잘못 옮긴 상태로 지문을 뜨면 그 지문은 잘못된 내용과 사이좋게 일치합니다. 이 구멍이 바로 다음 이야기입니다.
첫 결과는 좋았습니다. 상속세 및 증여세법 111개 조문, 소득세법 326개 조문을 옮겼고, 반드시 맞아야 한다고 미리 정해둔 대표 테스트 11개도 전부 통과했습니다. 화면에는 초록불이 떴습니다.
이 글은 그 초록불이 네 번 틀렸던 기록입니다.
첫 번째 — 개수도 지문도 맞는데, 문장이 없었습니다
작업 중에 습관처럼 하는 확인이 있습니다. 결과물이 정말 실제 조문처럼 생겼는지 대표 조문 md 파일 몇 개를 그냥 열어보는 것입니다. 검사가 다 통과했으니 형식적인 확인이라고 생각했습니다.
그런데 상속세 및 증여세법 제53조에서 이상한 게 보였습니다. 증여재산 공제, 그러니까 흔히 말하는 “배우자한테는 6억까지, 부모한테는 5천까지”가 들어 있는 조문입니다.
파일에는 이런 것들이 남아 있었습니다.
1. 배우자로부터 증여를 받은 경우: 6억원
2. 직계존속으로부터 증여를 받은 경우: 5천만원
3. 직계비속으로부터 증여를 받은 경우: 5천만원
4. 그 밖의 친족으로부터 증여를 받은 경우: 1천만원숫자는 다 맞습니다. 그런데 원문에서 이 목록 바로 앞에 있어야 할 문장이 통째로 사라져 있었습니다.
거주자가 다음 각 호의 어느 하나에 해당하는 사람으로부터
증여를 받은 경우에는 다음 각 호의 구분에 따른 금액을
증여세 과세가액에서 공제한다.이 문장이 없으면 아래 숫자들이 독립적으로 무엇을 의미하는 것인지 알 수 없습니다. 6억원이 세금인지 공제액인지, 누구에게 적용되는지, 무엇에서 빼는 금액인지가 전부 앞문장에 들어 있었습니다.
왜 빠졌냐면, 받아온 원본 데이터에서 이 둘이 서로 다른 칸에 담겨 있었기 때문입니다.
원본에서
담긴 내용
쪼갠 결과에
조문 내용 칸
“거주자가 다음 각 호의 어느 하나에…공제한다” (앞문장)
안 실림
호 목록 칸
1. 배우자 6억원 / 2. 직계존속 5천만원 / …
실림
쪼개는 코드가 목록 칸만 보고 있었던 것입니다. 조문 하나가 통째로 빠졌으면 개수가 안 맞아서 바로 걸렸을 텐데, 조문은 있고 그 안의 한 칸만 비어서 아무 검사에도 안 걸렸습니다.
여기서 중요한 건 검사가 이걸 못 잡았다는 사실입니다. 조문 111개가 들어와야 하는데 111개가 들어왔으니 개수 검사는 통과합니다. 들어온 내용은 손상되지 않았으니 지문 검사도 통과합니다. “각 조문 안에서 뭐가 빠졌는지”는 아무도 안 보고 있었습니다.
그래서 3단 검사, 그러니까 원본과 대조해서 빠진 내용을 잡는 검사를 새로 만들었습니다. 이걸로 닫혔다고 생각했습니다.
두 번째 — 그 검사기가 같은 실수를 같이 하고 있었습니다
구현을 한번 한 뒤, 코드를 다른 AI에게 넘겨 독립적으로 검토받았습니다. 제가 만든 흐름 안에서는 안 보이는 걸 보라는 취지였습니다.
두 리뷰어가 같은 문제를 1순위로 지적했습니다. 새로 만든 3단 검사가 자기 자신을 확인하는 꼴에 가깝다는 것이었습니다.
이유를 듣고 나니 명백했습니다. 조문을 쪼개는 코드와 그 결과를 검사하는 코드가 원본을 읽는 부품을 공유하고 있었습니다. 그러면 쪼개는 코드가 어떤 칸을 안 보고 지나쳤을 때, 검사하는 코드도 똑같이 그 칸을 안 보고 지나갑니다. 시험 문제를 낸 사람이 자기 답안지로 채점하는 상황이었습니다. 겉으로는 대조를 하는데, 같은 전제를 공유하니 같이 속습니다.
제53조 사고를 잡으라고 만든 검사기가, 제53조 사고와 정확히 같은 이유로 눈이 멀어 있었던 셈입니다.
고치는 방향은 단순했습니다. 검사 코드가 쪼개는 코드의 방식에 기대지 않고, 원본 데이터에서 필요한 항목을 따로 모아 비교하도록 다시 짰습니다. 검사는 같은 코드를 한 번 더 돌리는 일이 아니라 다른 길로 확인하는 일이라는 걸 이때 배웠습니다.
여기까지 오면 검사에 대한 감각이 조금 생겼다고 느끼게 됩니다. 다음 사고는 그 감각이 닿지 않는 곳에서 나왔습니다.
세 번째 — 근거로 딸려 나온 계산식이 원문과 달랐습니다
위키가 어느 정도 쌓인 뒤로는, 질문을 던지면 관련 조문을 찾아 근거와 함께 답하는 구조를 만들었습니다. 답이 맞았는지 사람이 확인하는 화면도 따로 뒀습니다. 저는 그걸 검수대라고 부릅니다. 질문, AI의 답, 그리고 그 답이 근거로 들고 온 조문 발췌가 나란히 뜹니다.
어느 날 검수대에서 답을 넘겨보다가, 근거로 딸려온 계산식이 눈에 걸렸습니다. 실제 법조문의 계산식과 모양이 달랐습니다.
“종합소득세를 기한 내에 신고 안 하면 가산세가 얼마나 붙나요?”라는 질문에 달려온 근거가 일곱 개인데, 일곱 개 전부에 초록색으로 “원문 일치”가 찍혀 있습니다. 아래에서 여섯 번째를 보시면 됩니다.
원문은 이렇게 생겼습니다. 법령 사이트가 계산식을 그림처럼 문자로 그려 넣기 때문입니다.
┌────────────────────────────┐
│ 가산세 = A × B × 100분의 5 │
│ ─── │
│ C │
│ A: 종합소득산출세액 │
│ B: 사업소득금액 │
│ C: 종합소득금액 │
└────────────────────────────┘가운데 짧은 가로줄이 분수선입니다. B가 분자, C가 분모입니다. 그런데 근거로 실려 온 발췌는 가산세 = A × B × 100분의 5 이 한 줄뿐이었습니다.
이 한 줄은 원문에 글자 그대로 존재합니다. 그래서 “발췌가 원문에 있는가”를 확인하는 검사는 통과합니다. 통과하는데, 분모 C가 사라졌습니다.
읽히는 식
원문의 뜻
A × (B ÷ C) × 5%
잘린 발췌의 뜻
A × B × 5%
세금 계산식에서 분모가 증발한 것입니다. 화면에서 그 발췌의 “원문 펼치기”를 눌러보면 이렇게 보입니다.
한국사이트 스크린샷
노란색으로 표시된 부분이 근거로 실려 온 한 줄이고, 그 바로 아래에 ───와 C가 있습니다. 화면 위쪽은 여전히 “원문 일치”라고 말하고 있습니다.
검사는 글자를 봤고, 사람은 뜻을 봤습니다. 그 사이가 벌어질 수 있다는 걸 그때 알았습니다.
이 경우는 원문을 다시 받아와도 해결되지 않습니다. 원문이 원래 그렇게 조판되어 있기 때문입니다. 그래서 발췌를 쓰는 시점에 막기로 했습니다. 조문에서 박스 문자로 둘러싸인 구간을 찾고, 발췌가 그 구간을 일부만 잘라 왔는지 보고, 세 갈래로 판정합니다.
분수식을 일부만 잘라 온 경우 → 차단. 이후 작업이 진행되지 않습니다
표 형태거나 판단이 애매한 경우 → 사람 검토 대기로 표시
박스 전체를 인용했거나 애초에 박스와 무관한 경우 → 통과
이 가드를 만들면서 숫자가 셋 나왔는데, 서로 다른 것을 세고 있어서 나눠 적 습니다.
숫자
무엇을 센 것
결과
8개
가드와 함께 새로 만든 시험 케이스
분수식을 일부만 자른 가짜 발췌를 일부러 넣어, 실제로 차단되는지 확인 (전체 테스트 254 → 262)
97건
기존에 쓰이던 근거 발췌 전수
차단 대상 0건
9건
정답지와 후보 발췌를 따로 전수 스캔
표 형태라 판단이 애매해 사람 검토 대기로 넘김 (정답지 6 + 후보 3)
가운데 줄의 0건은 자랑이 아닙니다. 문제가 없어서 0이 아니라, 이미 사람이 두 건을 찾아내 고쳐놓은 뒤라서 0이었습니다. 가드는 지난 사고를 잡은 게 아니라 다음 사고를 막으려고 만든 것이고, 그게 실제로 막히는지는 위의 8개 시험 케이스가 확인했습니다. 검사를 새로 만들고 그 검사로 0건을 얻는 건 아무것도 증명하지 않습니다. 막혀야 할 것을 일부러 넣어봐야 압니다.
그런데 이때까지의 사고 세 건은 전부 제 위키 안에서 일어난 일이었습니다. 마지막 배신은 위키 밖에서 왔습니다.
네 번째 — 여덟 번 통과했는데, 여덟 번 다 같은 방 안이었습니다
앞의 세 번은 기계가 짠 검사가 통과시킨 경우였습니다. 네 번째는 조금 다릅니다. 이번에 통과 도장을 찍은 건 사람이 만든 검사가 아니라 AI 리뷰어였습니다.
문제가 된 건 인용 표기를 정리하는 코드였습니다. 위키의 답변은 본문에 조문을 인용합니다. “상속세 및 증여세법 제19조에 따라”처럼요. 그런데 표기가 지저분했습니다. 정식 명칭과 줄임말이 섞이고, “시행령 제31조”처럼 어느 법의 시행령인지 빠진 것도 있었습니다. 이걸 기계가 알아듣게 정리해서, 각 인용이 어느 법 몇 조를 가리키는지 확정하는 코드였습니다.
구현은 금방 끝났는데 검토가 길었습니다. 고친 코드를 외부 AI 리뷰어에게 넘기면 “이런 입력이면 엉뚱한 법에 붙는다”는 반례가 나왔습니다. 고치면 그 수리가 만든 새 반례가 나왔고, 또 고치면 또 나왔습니다.
세 번 연속 같은 자리에서 터졌는데, 매번 반대 방향이었습니다. 처음엔 “영리법인”의 ’법인’을 법령 이름으로 봤고, 그걸 막았더니 이번엔 진짜 법령 이름인 “법인세법인지를”을 걸러냈고, 또 고쳤더니 “…법인 경우”를 엉뚱한 법에 붙였습니다. 사람은 이 셋을 구분하는 데 1초도 안 걸리지만 코드는 글자만 봅니다.
세 번째에 같은 자리가 또 터지자 저는 손 들 준비를 했습니다. 무한정 땜질하는 대신 멈출 기준을 먼저 정해두려고, “이 다음에도 안 되면 문제를 정리해서 조사부터 하는 게 낫지 않을까”라고 물었습니다. 다행히 네 번째 라운드에서는 반례가 나오지 않았습니다.
커밋하기 전에 하나를 더 붙였습니다. 네 라운드를 함께 돈 리뷰어는 자기가 제안한 수리 안에서만 보게 되니, 그 바깥은 아무 맥락 없는 새 리뷰어가 봐야 한다고 생각했습니다. 네 라운드를 도는 동안 리뷰가 여섯 번, 맥락 없는 리뷰어가 두 번, 합쳐서 여덟 번이었습니다.
여기까지가 제가 “충분히 봤다”고 느낀 지점입니다. 그런데 이 여덟 번이 본 것은 전부 ’법’이라는 글자를 어떻게 자를 것인가였습니다. 정작 뒤에 터진 사고는 그 문제가 아니었습니다.
작업을 정리하다가 하나가 걸렸습니다. 지적받은 반례가 전부 세법 문서에서만 나왔다는 것. 그렇다면 반례가 더 안 나온 것도 세법 문서 안에서 안 나온 것뿐 아닌가. 여덟 번을 돌렸다는 사실이, 여덟 번 다 같은 방 안에서 돌렸다는 사실을 덮고 있었습니다.
마침 시험대가 있었습니다. 같은 방식으로 만들던 원자력안전법 위키였습니다. 완전히 다른 분야, 다른 문체의 법령 문서입니다. 새 AI 창을 열어 그 문서들에 인용 정리 코드를 돌리고, “이 인용은 이 법 이 조문”이라고 확정한 62건 전부를 원문과 대조하게 했습니다.
62건 중 6건이 엉뚱한 법에 붙어 있었습니다. 9.7%입니다.
원인은 전부 하나였습니다. 법령 원문에는 「원자력안전법 시행규칙」(이하 “규칙”이라 한다)라고 한 번 정의해두고, 그 뒤로는 그냥 “규칙 제95조”라고만 쓰는 관행이 있습니다. 코드는 이 “규칙”을 독립된 법 이름으로 착각했고, 그 착각이 같은 문단의 다른 인용까지 줄줄이 오염시켰습니다.
여덟 번의 리뷰가 이걸 못 잡은 건 리뷰어가 부실해서가 아닙니다. 세법 위키 문서에는 그 표기가 한 건도 없었습니다. 리뷰어가 볼 수 있는 반례는 눈앞의 데이터에 있는 반례뿐이고, 데이터에 없는 함정은 몇 번을 들여다봐도 나오지 않습니다.
나란히 놓으면 이렇습니다.
같은 방에서 여덟 번
옆방 문을 한 번
무엇을 봤나
세법 문서 (리뷰 8회)
원자력안전법 문서 62건
나온 지적
“법”이라는 글자를 어디서 자를 것인가 — 3건
「…시행규칙」을 “규칙”으로 줄여 쓰는 관행
결과
통과
6건이 엉뚱한 법에 붙음
여덟 번과 한 번의 차이는 횟수가 아니었습니다. 방을 옮겼느냐였습니다.
숫자로 정리하면 이렇습니다.
다른 법에 돌려보기 전
돌려본 뒤
세법 문서 기준 판정
리뷰 8회 통과
그대로 유효
다른 분야 법령 62건 기준
재본 적 없음
6건이 엉뚱한 법에 붙음 (9.7%)
내가 아는 것
“이 코드는 검토가 끝났다”
“이 코드는 세법 문서에서만 검토가 끝났다”
마지막 줄이 이 사건에서 실제로 바뀐 전부입니다. 코드는 그날 안 고쳤습니다. 급하게 고치면 또 세법 문서만 보고 고치게 되기 때문입니다. 대신 틀린 6건을 포함한 62건의 정답 목록을 그대로 남겨두고, 다음에 고칠 때 “세법 밖에서도 0건”을 통과 조건으로 걸기로 했습니다. 세법 위키 쪽은 그 표기가 아예 없으니 지금 답이 틀리고 있는 건 아니고, 다른 분야로 이 방식을 옮길 때 반드시 통과해야 할 관문이 하나 생긴 셈입니다.
그래서 지금은 어떻게 하고 있나
네 번을 늘어놓고 보니 공통점이 보였습니다.
검사기는 뭐라고 했나
실제로 누가 잡았나
문장 누락
통과 (개수·지문 다 맞음)
사람이 대표 조문을 열어봄
검사기의 자기참조
통과 (검사기 자신이 눈이 멀었음)
외부 AI 리뷰어
계산식 왜곡
통과 (발췌가 원문에 글자 그대로 존재)
사람이 검수대에서 원문과 나란히 봄
인용이 엉뚱한 법에 붙음
통과 (리뷰 8회)
다른 분야 법령 62건
네 번 다 검사기 바깥입니다. 사람의 눈, 다른 AI, 다른 데이터. 검사기 안에서 처음 걸린 건 한 건도 없었습니다.
여기서 오해가 생기기 쉬운데, 그렇다고 검사가 쓸모없었다는 뜻은 아닙니다. 발견은 매번 바깥에서 했지만, 발견한 뒤의 방어는 전부 검사기 안에 집어넣었습니다.
사건
바깥에서 발견한 뒤, 안에 넣은 것
문장 누락
원본과 대조해 빠진 내용을 잡는 검사를 신설
검사기의 자기참조
검사 코드가 원본을 따로 읽도록 경로를 분리
계산식 왜곡
분수식 일부만 잘라 온 발췌는 차단, 애매하면 사람 검토로 표시
인용이 엉뚱한 법에 붙음
다른 분야 62건을 정답 목록으로 확보, “세법 밖에서도 0건”을 다음 관문으로
그래서 같은 사고가 다시 나면 이번엔 검사기가 잡습니다. 바뀌지 않은 건 아직 안 본 종류의 사고를 처음 발견하는 일입니다. 그건 여전히 바깥 몫입니다.
당연한 말처럼 들리지만 저는 이걸 네 번 겪고 나서야 알았습니다. 검사는 “옳은가”를 보지 않습니다. “내가 확인하기로 정한 것이 맞는가”를 볼 뿐입니다. 개수를 세기로 정했으면 개수만 맞으면 통과고, 발췌가 원문에 있는지 보기로 정했으면 뜻이 뒤집혀도 통과입니다. 통과 도장은 그 도장을 찍는 방식 안에서만 참입니다.
그러면 새 사고를 어떻게 발견하느냐. 여기는 솔직히 답을 못 찾았습니다. 조문이 수천 개라 전부 들여다보는 건 불가능합니다. 생각날 때 한 번씩 파일을 직접 열어보고 그때그때 지적하는 정도입니다. 체계라고 부르기 민망한 수준입니다.
그런데 위 표를 다시 보면, 네 건 중 두 건을 잡은 게 정확히 그 “생각날 때 열어보기”였습니다. 자동화된 검사가 아니라 무작위로 하나 열어본 사람이었습니다. 그렇다면 그건 대충 하는 일이 아니라, 지금 제 시스템에서 실제로 작동하는 검출 경로 중 하나인 셈입니다. 그래서 당분간은 계속 적은 샘플이라도 사람이 열어서 검수하는 쪽을 택하기로 했는데, 추후에는 조금더 편하게 할수 없을지 고민이긴 합니다.
비슷하게 따라해볼 수 있는 프롬프트
AI에게 자료 정리나 옮기는 작업을 맡기고 “검사 통과” 답을 받았을 때, 그대로 믿기 전에 아래를 그대로 붙여넣어 보시면 됩니다. 답이 아니라 검사의 사각지대 목록을 받는 프롬프트입니다.
방금 만든 검사에 대해 다음 세 가지를 답해줘. 코드를 고치지는 말고 목록만.
1. 이 검사가 확인하는 것을 정확히 나열해줘.
2. 그리고 확인하지 '않는' 것을 나열해줘. 특히 이 검사를 통과하면서도
결과물이 틀릴 수 있는 경우를 구체적인 예로 3개 만들어줘.
3. 검사 코드가 원본을 읽는 방식이 만드는 코드와 부품을 공유하는지 확인해줘.
공유한다면 같이 놓칠 수 있는 항목이 뭔지 말해줘.
4. 이 검사를 지금까지 어떤 자료로만 시험해봤는지 정리하고,
그 자료에 한 번도 안 나온 형태의 입력을 3개 만들어줘.
<여기에 당신의 검사 코드나 검사 절차 설명을 붙여넣으세요>