인용 검사기가 엉뚱한 문장을 인용이라고 잡았다 — 원인은 4자 미만 짧은 따옴표였다 (실측)

·

법률 콘텐츠의 인용문이 원문과 정확히 일치하는지 검사하는 도구를 쓰다가, 원문에 전혀 없는 긴 문장이 “인용문”으로 잡혀 실패 처리되는 걸 봤다. 그 문장은 인용문이 아니라 그냥 이어지는 본문이었다. 코드를 직접 추적해서 원인을 확인했다.

증상 — 있지도 않은 인용이 실패로 잡혔다

이 검사기는 답변 텍스트에서 큰따옴표로 감싼 구절을 뽑아 원문에 그대로 있는지 확인한다. 법령·판례를 인용할 때 실제로 쓴 것과 다르게 바뀌지 않았는지 보는 용도다.

법률 조문 하나를 인용하는 문장을 썼는데, 검사 결과에 이런 게 나왔다.

❌ 으로 바꿔 적용합니다. 그래서 임대인이 결정에 대해 이의신청을 할 수 있고, 취소신청도 가능합니다...
   └ 원문에 없다 (최고 유사도 0.14) — 지어낸 인용이거나, 인용이 아닌 따옴표(강조·지칭)다

이 문장은 큰따옴표로 감싼 적이 없는, 그냥 평범한 본문이었다. 그런데도 검사기는 이걸 “인용문”으로 뽑아냈다.

재현 — 원인이 된 문장

문제가 된 원본은 법 조문의 문언 치환 규정을 설명하는 문장이었다.

"가압류"는 "임차권등기"로, "채권자"는 "임차인"으로, "채무자"는 "임대인"으로 바꿔 적용합니다.
그래서 임대인이 결정에 대해 이의신청을 할 수 있고, 취소신청도 가능합니다.

짧은 법률 용어 6개를 각각 따옴표로 감싸 나열한 문장이다. 이 뒤에 이어지는 평범한 문장까지 검사기가 하나의 인용문으로 묶어버렸다.

원인 추적 — 정규식과 짧은 단어

검사기가 인용문을 뽑는 코드는 이렇다.

const patterns = [
  /"([^"]{4,400})"/g, // 곧은 큰따옴표
  // ...
];

여는 따옴표와 닫는 따옴표 사이에 4자에서 400자 사이의 텍스트가 있어야 매치된다. 4자 미만 인용은 우연히 원문과 일치할 수 있어서 애초에 걸러내려는 설계다. 문제는 “가압류”(3자), “채권자”(3자), “임차인”(3자), “채무자”(3자), “임대인”(3자)처럼 이 조건을 만족하지 못하는 짧은 단어가 연달아 나온다는 점이었다.

직접 최소 재현 코드로 실제 동작을 확인했다.

const text = `"가압류"는 "임차권등기"로, "채권자"는 "임차인"으로, "채무자"는 "임대인"으로 바꿔 적용합니다. 그래서 임대인이 결정에 대해 이의신청을 할 수 있고, 취소신청도 가능합니다. "이후 텍스트가 여기 있다고 치자"`;
const re = /"([^"]{4,400})"/g;
for (const m of text.matchAll(re)) console.log(JSON.stringify(m[1]));

출력은 이랬다.

"임차권등기"
"으로, "
"으로 바꿔 적용합니다. 그래서 임대인이 결정에 대해 이의신청을 할 수 있고, 취소신청도 가능합니다. "

“가압류”는 3자라 매치가 안 됐다. 정규식 엔진은 그 위치에서 실패하면 따옴표 문자를 소비하지 않고 다음 문자로 넘어가 다시 시도한다. 그렇게 한 칸씩 밀리다가 다음으로 만나는 “임차권등기”(5자)에서는 조건을 만족해 매치됐다. 그런데 그다음부터가 문제다 — “채권자”·“임차인”·“채무자” 모두 3자라 계속 실패하고, 그 실패들이 누적되면서 원래 의도와 다른 여닫음 짝이 만들어졌다. “으로, “(쉼표와 공백 포함 4자)에서 우연히 조건을 만족해 매치가 하나 더 나왔고, 마지막엔 “임대인” 뒤의 따옴표부터 훨씬 뒤에 있는 전혀 무관한 따옴표까지를 통째로 하나의 “인용문”으로 묶어버렸다.

왜 이렇게 되나 — 실패한 매치는 문자를 소비하지 않는다

핵심은 정규식이 특정 위치에서 매치에 실패해도 그 위치의 문자(따옴표)를 “처리 완료”로 치지 않는다는 점이다. 실패한 여는 따옴표는 무효 처리되는 게 아니라, 단지 다음 문자부터 다시 매치를 시도할 때 후보에서 제외될 뿐이다. 그 결과 문서 전체를 놓고 보면 “짝이 맞다고 생각한 여는 따옴표”와 “실제로 매치에 쓰인 여는 따옴표”가 달라질 수 있다. 사람이 눈으로 볼 때는 따옴표 6쌍이 각각 독립적으로 보이지만, 정규식 엔진 입장에서는 실패한 시도들 때문에 그중 일부만 진짜 매치로 쓰이고 나머지는 다음 순번으로 밀려난다.

대응 — 도구를 고치는 대신 글쓰기로 피했다

이 검사기의 최소 길이 제한(4~400자) 자체는 합리적인 설계다. 짧은 인용은 우연의 일치일 수 있으니 걸러야 한다. 문제는 그 설계가 “여러 개의 짧은 따옴표 쌍이 한 문장에 연달아 나오는” 특정 패턴과 만났을 때 부작용을 낸다는 점이다. 정규식을 고쳐서 짧은 쌍까지 다 잡게 하면, 이번엔 원래 걸러야 했던 우연의 일치까지 다시 새어 들어올 위험이 생긴다.

그래서 도구 코드는 그대로 두고, 글쓰기 쪽에서 이 패턴을 피했다. 법률 조문의 문언 치환 표현(“A는 B로 본다” 같은 형식)을 설명할 때, 짧은 용어 여러 개를 따옴표로 나열하는 대신 따옴표 없이 풀어 쓰는 방식으로 바꿨다. 검사기가 하는 일(진짜 인용 검증)은 그대로 유지하면서, 이 검사기와 궁합이 안 맞는 특정 문장 구조 하나만 피한 것이다.

정리

인용 검사가 실패했을 때 처음엔 “원문을 잘못 옮겨 적었나”부터 의심했다. 실제 원인은 검사 대상 문장이 아니라, 그 앞에 있던 전혀 다른 문장의 짧은 따옴표들이었다. 정규식 매치 실패가 그 자리에서 조용히 끝나지 않고 뒤쪽 매치의 짝짓기까지 흔든다는 걸, 최소 재현 코드로 직접 돌려보고 나서야 확인할 수 있었다.

자주 묻는 질문

왜 4자 미만 따옴표가 문제가 되나요?

검사기의 정규식이 `/"([^"]{4,400})"/g`로 짜여 있습니다. 여는 따옴표와 닫는 따옴표 사이에 4자에서 400자 사이의 텍스트가 있어야만 매치됩니다. '채권자'처럼 3자짜리 단어를 따옴표로 감싸면, 그 사이 텍스트가 3자뿐이라 이 조건을 만족하지 못해 정규식 엔진이 그 위치에서 매치를 포기합니다.

매치가 안 되면 그냥 무시되는 거 아닌가요? 왜 뒤에까지 영향을 주나요?

정규식 엔진은 특정 위치에서 매치에 실패하면 그 따옴표 문자를 소비하지 않고 다음 문자로 넘어가 다시 시도합니다. 즉 실패한 여는 따옴표는 '건너뛴 것'으로 취급되지 않고, 다음으로 만나는 아무 따옴표나 새로운 여는 따옴표 후보가 됩니다. 이 과정이 반복되면서 원래 의도한 여닫음 짝과는 다른 조합으로 짝이 맞춰지고, 그 결과 훨씬 뒤에 있는 무관한 따옴표까지 하나의 매치로 묶일 수 있습니다.

이 버그는 실제로 고쳤나요?

도구 코드는 그대로 두고, 글 쪽에서 짧은 따옴표 나열을 피하는 방식으로 우회했습니다. 법률 조문의 문언 치환 표현(예: 'A는 B로 본다' 형식)처럼 짧은 용어를 여러 개 따옴표로 나열해야 하는 경우, 따옴표 없이 풀어 쓰는 게 이 검사기와 궁합이 맞습니다.

왜 도구를 고치지 않고 글을 고쳤나요?

정규식을 짧은 따옴표까지 다 잡게 고치면, 이번엔 반대로 원래 의도대로 걸러야 했던 진짜 우연의 일치(4자 미만 인용은 우연히 맞을 수 있어 애초에 걸러내도록 설계됨)까지 다시 잡힐 위험이 있습니다. 검사기의 최소 길이 제한 자체는 합리적인 설계이고, 문제는 '매치 실패 시 문자를 소비하지 않는' 정규식 엔진의 기본 동작과 맞물려 생긴 부작용입니다. 이번 한 번의 사례로 정규식을 복잡하게 고치는 것보다, 글쓰기 규칙으로 이 패턴을 피하는 쪽이 더 간단했습니다.

#정규식#regex#파서#인용검증#실측