당신의 툴체인은 문법을 검사할 뿐, 사실을 검사하지 않습니다
요약
코드 툴체인이 문법적 유효성은 검증하지만, 도메인 소유권과 같은 실제 사실(fact)은 검증하지 못해 발생하는 오류를 다룹니다. 특히 LLM이 생성한 코드가 문맥상 그럴듯한 잘못된 정보를 포함할 때 발생하는 위험성을 경고합니다.
핵심 포인트
- 타입 체커와 번들러는 값의 형태만 검사할 뿐 실제 사실 여부는 판단하지 못함
- CI 환경의 모킹 테스트는 실제 외부 도메인 연결 문제를 발견하기 어려움
- LLM 생성 코드는 문맥상 가장 그럴듯한(probabilistic) 잘못된 값을 생성할 위험이 있음
- 코드의 형식적 완결성과 실제 비즈니스 로직의 사실 관계를 구분해야 함
저는 잘못된 이메일 주소 하나를 찾으러 갔다가 여섯 개의 잘못된 도메인을 발견했습니다.
그것들은 6개의 서로 다른 서버 기능(server functions)에 걸쳐 몇 달 동안 운영되고 있었습니다. 그 어떤 것도 문제를 제기한 적이 없었는데, 스택(stack) 내에 문제를 제기할 수 있는 것이 아무것도 없었기 때문입니다. 그것들은 모두 유효한 코드였습니다.
실제로 존재했던 것들
제가 소유하지 않은 세 개의 서로 다른 호스트(hosts)였습니다.
첫 번째는 제가 한 번도 소유한 적 없는 제 브랜드의 .com 버전이었습니다. 해당 사이트는 다른 TLD(Top-Level Domain)에서 운영됩니다. 과정 중 어디에선가, 생성된 코드(generated code)가 .com이 당연한 주소라고 판단하여 그대로 사용해 버렸습니다. 사용자에게 그곳으로 지원 이메일을 보내라고 안내하는 연락 양식(contact-form) 답장과 보안 경고 내부의 관리자 링크에 사용되었습니다.
두 번째는 .io 버전이었습니다. 같은 논리였지만 다른 추측이었습니다. 이것은 단순한 링크가 아니었기에 더 심각했습니다:
from: "Alerts <alerts@brand.io>",
세 번째는 제공업체의 자체 공유 테스트 도메인(shared test domain)이었습니다. 모든 튜토리얼과 퀵스타트(quickstart)에서 사용되는 주소였습니다:
from: "App <noreply@resend.dev>",
그것은 비밀번호 재설정 이메일에 포함되어 있었습니다. 공유 테스트 도메인은 보통 사용자의 계정 주소로 엄격하게 제한됩니다. 따라서 계정이 잠긴 사용자가 의존해야 하는 시스템 내의 유일한 이메일이, 전달 가능성이 가장 낮은 발신자로부터 나가고 있었던 것입니다.
왜 아무것도 걸러내지 못했는가
일반적인 파이프라인(pipeline)의 각 단계가 실제로 무엇을 알고 있는지 생각해 보십시오.
타입 체커(typechecker)는 "alerts@brand.io"가 문자열(string)이라는 것을 알고 있으며, 해당 필드가 원하는 것도 문자열입니다. 맞습니다, 그리고 끝입니다.
번들러(bundler)는 파일이 파싱(parse)된다는 것을 알고 있습니다. 맞습니다, 그리고 끝입니다.
테스트(tests)는 함수가 자신이 단언(assert)한 형태(shape)를 반환한다는 것을 알고 있습니다. 만약 테스트가 메일 클라이언트를 모킹(mock)한다면 — 그리고 CI에서는 아무도 실제 이메일을 보내지 않으므로 실제로 모킹을 합니다 — 그들은 발신자를 전혀 볼 수 없습니다.
배포(deploy)는 함수가 컴파일(compile)되고 업로드되었다는 것을 알고 있습니다.
이 단계들 중 그 어느 것도 제가 어떤 도메인을 소유하고 있는지에 대한 사실(fact)을 보유하고 있지 않습니다. 이것은 어느 단계의 실수도 아닙니다. 타입 시스템(Type systems)은 값의 형태(shape)를 검사하는 것이지, 그것이 세상과 일치하는지를 검사하는 것이 아닙니다. "이 조직이 제어하는 도메인"을 의미하는 타입은 존재하지 않습니다.
따라서 전체 장치(apparatus)는 문법을 검사하고 있는 것입니다. 문장은 형식적으로 잘 구성되어 있습니다. 하지만 그것이 사실인지 확인하는 사람은 아무도 없습니다.
생성된 코드는 그럴듯한 답변이 틀렸을 때 실패합니다
이 부분이 저에게 변화를 준 지점입니다.
제가 직접 도메인을 작성할 때는 대시보드, DNS 레코드, 송장(invoice) 같은 무언가를 보고 읽어옵니다. 반면 모델이 작성할 때는 주어진 문맥(context)에서 가장 그럴듯한 토큰 시퀀스(token sequence)를 생성합니다. X라는 브랜드의 경우, x.com은 압도적으로 가장 그럴듯한 도메인입니다. 이메일 제공업체의 발신자(sender) 필드의 경우, 문서에 나온 주소가 가장 그럴듯한 값입니다.
두 가지 추측 모두 매우 훌륭한 추측입니다. 상식적인 사람이라면 당연히 그렇게 가정할 것입니다. 하지만 그 추측들은 틀렸으며, 리뷰 과정에서 보이지 않는 방식으로 틀립니다. 왜냐하면 정답을 이미 알고 있는 것이 아니라면 페이지상에서 support@brand.com은 올바르게 보이기 때문입니다.
이것은 어디를 살펴봐야 할지를 예측해 줍니다. 이러한 오류들은 올바른 값을 추측할 수 있는(guessable) 곳마다 모여 있습니다. 브랜드 도메인, 고객 지원 주소, 제휴 및 파트너 ID, API 발신자, 대시보드 URL 등이 그러합니다. 겉보기에 명백해 보이는 답이 있는 곳이라면 어디든, 그 명백해 보이는 답을 얻게 되며, 그것이 충분히 자주 맞기 때문에 예외 상황은 결코 검사되지 않습니다.
이것은 극적인 의미에서의 환각(hallucination)은 아닙니다. 아무것도 발명되지 않았습니다. 단지 진실된 무언가가 실제 존재하는 무언가로 대체되었을 뿐입니다.
외부 접점(external surface)을 선언하세요
해결책을 찾는 데 약 20분이 걸렸습니다. 소스에서 모든 URL 호스트(host)와 이메일 도메인을 추출하여, 여러분이 선언한 호스트 목록과 비교하고, 그 외의 모든 것에 대해 실패(fail)하도록 만드는 것입니다.
✗ git ref abc1234에 선언되지 않은 도메인 참조가 12개 있습니다:
resend.dev (5개 참조)
...
제가 수동으로 수정을 시작하기 전의 커밋을 대상으로 실행해 보니, 제가 찾아냈던 11개의 참조에 더해 제가 완전히 놓쳤던 하나를 더 보고했습니다. 한 페이지가 제가 소유하지 않은 도메인으로 자신의 캐노니컬 URL(canonical URL)을 설정하고 있었는데, 이는 해당 페이지의 권위 있는 복사본(authoritative copy)이 다른 사람의 자산에 존재한다고 검색 엔진에 알려왔음을 의미했습니다.
저는 읽으면서 6개를 찾아냈습니다. 스크립트는 약 1초 만에 7개를 찾아냈으며, 앞으로도 계속 찾아낼 것입니다.
정규 표현식 (regex)보다 더 중요한 두 가지 세부 사항이 있습니다.
허용 목록 (allowlist)은 설정 (configuration)이 아니라 저장소 (repository)에 있어야 합니다. 실패 모드는 생성된 코드가 그럴듯한 호스트를 도입할 때 발생합니다. 만약 허용 목록이 어시스턴트 (assistant)가 조용히 확장할 수 있는 어딘가에 존재한다면, 그것은 검사 (check)가 아니라 형식적인 절차 (formality)가 되어버립니다. 저장소에 있다면, 목록을 넓히는 것은 디프 (diff)가 되며, 디프는 검토의 대상이 됩니다.
실행되는 위치가 명확하지 않습니다. 프리푸시 훅 (pre-push hook)은 타이트 루프 (tight loop)이며, 사용자의 머신을 통과하는 커밋 (commit)만을 볼 수 있습니다. 만약 커밋과 푸시를 스스로 수행하는 호스팅된 에이전트 (hosted agent)를 사용한다면 — 제 것도 그렇습니다 — 로컬 훅 (local hook)은 대부분의 생성된 코드가 유입되는 바로 그 지점에서 눈이 멀게 됩니다. 따라서 동일한 스크립트가 원격 브랜치 (remote branch)를 대상으로 스케줄에 따라 실행되도록 하며, 문제가 되는 호스트 집합이 변경될 때만 경고를 보냅니다. 그래야 이미 알려진 미해결 탐지 결과가 노이즈 (noise)로 전락하지 않습니다.
이것이 하지 못하는 것
이것은 잘못된 상수 (constants)를 잡아냅니다. 잘못된 동작 (behaviour)을 잡아내지는 못합니다.
같은 주에, 저는 몇 달 동안 모든 호출에서 실패해 온 조회 (lookup) 로직을 수정했습니다. API 레이어 (API layer)가 노출하지 않는 스키마 (schema)를 쿼리하고 있었기 때문입니다. 유효한 코드, 실제 기능, 올바른 인자 (arguments)를 갖추고 있었지만, 에러가 버려지고 있었기에 실패가 일반적인 결과처럼 읽혔던 것입니다. 어떤 도메인 검사 (domain check)도 이를 찾아낼 수 없었을 것입니다. 이것은 다른 범주이며 다른 도구가 필요합니다.
스크립트가 가진 것보다 더 많은 커버리지 (coverage)를 암시하게 두느니, 차라리 솔직하게 말하겠습니다. 조용히 기대에 미치지 못하는 가드 (guard)야말로 우리가 벗어나고자 하는 대상입니다.
일반적인 버전
생성된 코드가 수기로 작성된 코드보다 훨씬 더 쉽게 만들어내며, 기존의 툴링 (tooling)으로는 구조적으로 탐지할 수 없는 오류의 범주가 있습니다: 형식은 올바르지만 사실이 아닌 값들 (values that are well-formed and untrue).
값들이 올바르게 보이기 때문에, 리뷰만으로는 이 문제에서 벗어날 수 없습니다. 값들이 올바른 타입이기 때문에, 타입 체크 (typecheck)만으로도 이 문제에서 벗어날 수 없습니다. 당신이 할 수 있는 일은 프로젝트가 의존하는 작은 사실들의 집합 — 어떤 도메인이 당신의 것인지, 어떤 계정인지, 어떤 엔드포인트 (endpoints)인지 — 을 기록하고, 코드가 실행될 때마다 기계가 그 목록과 코드를 비교하도록 만드는 것입니다.
이 목록은 흥미로운 산출물 (artifact)입니다. 그것이 영리해서가 아니라, 당신이 직접 앉아서 작성하기 전까지는 아무도 당신의 코드베이스 (codebase)가 무엇과 통신할 수 있도록 허용되는지를 실제로 열거해 본 적이 없기 때문입니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기