
그 AI 개발, 할루시네이션(Hallucination)을 견딜 수 있습니까? ― 아무것도 모른 채 개발하면, 깨닫지 못한 채 망가져 간다
요약
LLM의 JIT(Just-In-Time)적 출력 구성 메커니즘과 할루시네이션의 상관관계를 분석합니다. 지식 없이 AI에 의존하는 개발의 위험성을 경고하며, 프롬프트의 구체성이 할루시네이션 감소에 미치는 영향을 설명합니다.
핵심 포인트
- LLM은 정보를 저장하는 것이 아니라 필요 시점에 의미 공간에서 구체화함
- 할루시네이션은 추상에서 구체로 변환할 때 정보 부족을 메우는 구조적 현상
- 프롬프트의 구체성과 형식성이 높을수록 할루시네이션 발생률이 감소함
- AI의 시야를 제어하기 위해서는 개발자의 명확한 도메인 지식이 필수적임
- 「AI가 있으면 지식이 없어도 서비스를 만들 수 있다」라는 주장에 대해, LLM이 출력을 구성하는 메커니즘(JIT적인 구체화)과 할루시네이션 (Hallucination)의 관계를 통해 반론한다.
- 할루시네이션 (Hallucination)은 추상에서 구체로의 변환 과정에서 정보가 부족한 부분을 「그럴듯함」으로 채워버리는 현상이며, 모델의 결함이라기보다 구조적인 귀결에 가깝다. 프롬프트의 구체성과 할루시네이션 (Hallucination) 발생률의 상관관계를 보여주는 연구와 OpenAI의 2025년 분석을 근거로 소개한다.
- 개인 사이트 구현 과정에서 실제로 발생했던, 「작동하는 것처럼 보이지만 아무것도 지키지 않았던」 Referer/Origin 체크 버그를 실례로, 도메인의 추상적 어휘를 갖지 못한 채 AI에게 구현을 맡기는 것의 위험성을 보여준다.
「그 AI 개발, 할루시네이션 (Hallucination)을 견딜 수 있습니까?」라고 물었을 때, 즉답할 수 있는 사람은 많지 않을 것이라 생각한다. 적어도 나는 이 글에서 쓰는 실례를 실제 운영 환경에서 마주하기 전까지 즉답할 수 없었다.
이전에, AI의 「시야」를 해킹하라.라는 글을 쓴 적이 있다. 요지는 AI에게 무엇이든 보여주는 것이 아니라 「지금 어디를 봐야 하는지」를 이쪽에서 명시적으로 구분해 주면, 추론의 노이즈가 줄어들어 정밀도가 올라간다는 것이었다.
이 글을 쓴 뒤에 한 가지 신경 쓰이는 점이 있었다. 그 「시야를 좁히는」 조작은 사실 상당히 고도의 전제 위에 성립되어 있다. 「로그인 기능의 버그니까 src/components/Auth/만 봐」라고 지시할 수 있는 것은, 지시하는 측이 「이것은 로그인 문제이며, 관계된 것은 인증 관련 코드다」라고 이미 판단하고 있기 때문이다. 이 판단을 할 수 없는 사람――즉, 자신이 지금 무엇을 만지고 있는지 언어화할 수 없는 사람에게는 애초에 「좁혀야 할 범위」 자체를 볼 수 없다.
즉 「AI의 시야를 제어한다」는 테크닉은 「지식 없이도 AI로 무엇이든 만들 수 있다」라는 주장과는 정반대의 위치에 서 있다. 이번에는 이 위화감을 LLM이 출력을 구성하는 메커니즘 그 자체로부터 설명해 보려 한다.
LLM은 지식을 데이터베이스처럼 그대로 저장했다가 꺼내는 것이 아니다. 오히려 필요해진 순간에 의미 공간(Semantic Space)에서 정보를 구성하는, Just-In-Time (JIT)적인 동작에 가깝다.
JIT (Just-In-Time)는 원래 도요타 생산 방식에서 유래한 용어로, 「필요한 것을, 필요한 때에, 필요한 만큼만」 만드는 방식을 가리킨다. 미리 대량으로 만들어 두는(재고) 것이 아니라, 필요해진 순간에 그 자리에서 만드는 사고방식이다. 프로그래밍 언어의 실행 환경(JavaScript 엔진이나 JVM 등)의 「JIT 컴파일러」도 같은 발상으로, 코드를 미리 전부 컴파일해 두는 것이 아니라 실행에 필요해진 부분만을 그 자리에서 컴파일한다. 이 글에서도 같은 발상의 비유로 사용하겠다. LLM은 구체적인 출력을 미리 전부 만들어 기억하고 있는 것이 아니라, 출력이 필요해진 순간에 그 자리에서 구체를 구성하고 있다.
대략적으로 말하면 다음과 같은 흐름이 된다.
입력
↓
추상화 (무엇에 관한 이야기인지, 구조·관계성을 파악함)
...
여기서 「추상」이란 구조·관계성·의미 그 자체를, 「구체」란 현실의 사실·고유명사·구현의 상세를 가리킨다. LLM 추론의 대부분은 추상 레벨에서 이루어지며, 마지막 마지막에 구체로 렌더링(Rendering)된다.
이러한 관점을 뒷받침하는 연구도 있다. 프롬프트의 「구체성 (concreteness)」, 「형식성 (formality)」이 할루시네이션 (Hallucination) 발생률과 상관관계가 있음을 검증한 연구에서는, 더 구체적이고 형식적인 프롬프트일수록 할루시네이션 (Hallucination)이 감소하는 경향이 있다고 보고되었다 (Rawte et al., 2023). 뒤집어 말하면, 프롬프트 측에 충분한 구체·추상의 발판이 없으면 모델은 스스로 그 빈틈을 채우러 간다는 의미이기도 하다.
할루시네이션 (Hallucination)은 종종 「AI가 거짓말을 한다」, 「추론을 틀린다」는 식으로 이야기된다. 하지만 실태에 조금 더 가까운 설명은 다음과 같다.
추상에서 구체로 변환할 때, 구체 정보가 부족하면 그럴듯한 내용으로 보완해 버리는 현상
OpenAI가 2025년에 공개한 연구에서는 이 현상을 더욱 심도 있게 설명하고 있다. 사전 학습 (Pre-training)은 '데이터의 분포를 재현하는 것'을 목적으로 하며, 진실성을 검증하도록 만들어지지 않았다. 어떤 사실에 대해 모델이 충분한 정보를 보지 못했다면, '그럴듯하지만 틀린' 문장을 생성해 버린다. 나아가 평가 및 벤치마크 (Benchmark) 측면에서도 '모르겠습니다'라고 답하기보다 자신만만하게 틀리는 편이 스코어가 더 높게 나오도록 설계되어 있으며, 이것이 구조적으로 할루시네이션 (Hallucination)을 조장하고 있다고 지적한다 (Kalai, Nachum & Vempala, "Why Language Models Hallucinate", OpenAI, 2025).
즉, 할루시네이션은 모델의 결함이라기보다 '구체적인 정보가 없는 부분을 추상적인 패턴(그럴듯함)만으로 채운 결과'라고 파악하는 것이 실태에 더 가깝다.
여기서 중요한 것은 다음과 같다.
추상은 정답이 아니라, 구체로 인도하는 '이정표'이다.
동물 → 개 → 일본 원산 → 서 있는 귀 → 말린 꼬리 → 시바견, 과 같이 추상에서 구체로 내려갈 때마다 후보는 좁혀진다. 이 이정표가 부족하거나 도중에 건너뛰게 되면, 모델은 부족한 부분을 자신이 가진 '그럴듯함'으로 채울 수밖에 없게 된다.
이를 역방향으로 읽으면 다음과 같다. 구체에 올바르게 접근하고 싶다면, 우선 충분히 좁은 추상(=도메인의 어휘·개념)이 필요하다. 추상은 구체보다 열등하고 모호한 것이 아니다. 구체로 가는 유일한 접근 경로 그 자체다. '지식 제로 상태에서 AI에게 맡기면 구체적인 구현이 완성된다'는 발상은 이 경로를 건너뛰려 하는 것이다.
이론만으로는 실감이 나기 어려우므로, 실제로 자신의 사이트에서 일어난 이야기를 하겠다.
개인 사이트 (motimotinotch.com, Astro + Cloudflare Workers + R2 구성)에는 이미지·동영상의 직링크를 방지하기 위해, 요청의 Referer / Origin 헤더가 자사 사이트의 도메인에서 오고 있는지 확인하는 코드가 있다. 이 방어 기제는 처음에 sec-fetch-dest에 의한 주소창 직접 입력 차단 · Referer/Origin의 엄격한 체크 · Vary 헤더의 3종 세트로 설계 및 검증을 마친 상태였다. 검증 항목도 한 차례 모두 통과한 구현이었다.
그런데 그 후, AI에게 구현을 맡긴 다른 수정 사항 중 어딘가에서 이 방어 기제가 조용히 망가져 있었다. 몇 달 후, 별건의 조사 중에 운영 환경을 실제로 확인해 보니 다음과 같은 상태가 되어 있었다.
// 의도: Referer/Origin 이 허용된 도메인의 것만 통과시킨다
const isAllowed = allowedDomains.some(domain => requestHost.includes(domain));
언뜻 보기에는 평범한 체크처럼 보인다. '허용 도메인 리스트에 포함되어 있는지'를 includes()로 확인하고 있다. 실제로 돌려봐도 에러는 발생하지 않는다. 403으로 차단되어야 하지 않는 케이스(사이트 내에서의 이미지 표시)는 문제없이 통과하며, 겉보기 동작 확인으로는 위화감이 느껴지지 않는다.
하지만 실제로는 이 체크는 Referer/Origin이 없는 요청까지 포함하여 항상 통과시키고 있었다. 원인은 requestHost가 '요청의 송신처 (Referer/Origin)'가 아니라 '요청의 목적지 = 자신의 사이트 호스트명'을 참조하고 있었기 때문이었다. 자신의 사이트 호스트명은 당연히 허용 도메인 리스트에 포함되어 있다 (즉, 포함되는 것이 당연한 문자열을 includes()로 확인하고 있었다). 즉, 이 체크는 누가 호출했는지와 관계없이 실질적으로 항상 true를 반환하는 조건식이 되어 있었다.
이는 '악의적인 제3자의 도메인명이 우연히 부분 문자열(Substring)로서 일치해 버리는' 고전적인 부분 일치 바이패스 (Partial match bypass)와도 조금 다르다. 훨씬 더 기본적인 부분에서, '어떤 변수를 검증해야 하는가'라는 추상적인 구분(= '요청의 목적지'와 '요청의 송신처'는 별개라는 이해)이 누락되었다는 이야기다. 코드의 외형은 '체크를 하고 있는' 형태를 띠고 있지만, 내용은 아무것도 검증하지 않고 있다. 한때는 올바르게 구성되었던 방어 기제가, AI가 작성한 수정 과정 중 어딘가에서 이 구분을 잃어버렸고, 작동하는 것처럼 보이지만 (즉, 코드로 존재하며 에러도 나지 않지만) 누구에게도 들키지 않은 채 운영 환경에 계속 남아 있었던 것이다.
수정 후의 코드는 Referer/Origin 헤더의 값 자체에서 호스트명을 추출하여, 허용 도메인과 엄격하게 일치하는지 확인하도록 고쳐져 있다.
let refererHost: string | null = null;
try { refererHost = referer ? new URL(referer).hostname : null; } catch { /* 不正한 값은 무시 */ }
const isAllowedHost = (hostname: string) =>
...
"Referer/Origin이란 무엇을 검증하기 위한 것인가", "부분 일치와 엄격 일치는 어떻게 다른가"라는 추상적인 이해가 없다면, 이 버그를 알아챌 수 없다. 테스트를 실행해도 일반적인 액세스 경로에서는 정상적으로 작동하는 것처럼 보이기 때문이다. AI에게 "직접 링크(Direct Link)를 방지해줘"라고만 요청한다면, 이러한 구분을 가지고 있지 않은 이상 리뷰하는 쪽에서도 AI가 출력하는 "그럴듯한 코드"를 가려낼 수 없다.
(이 구현을 둘러싼 9계층 방어의 전체 모습이나 다른 구현의 상세 내용은 「이미지·영상을 지키는 9계층 구현: Cloudflare Workers + R2로 만드는 다층 방어」에서 다루고 있다.)
이론은 이해하더라도 다음에 무엇을 해야 할지 보이지 않는다면 의미가 없다. 이번 실례를 통해 말할 수 있는 것은 그리 복잡하지 않다.
구현을 요청하기 전에, 추상을 한 문장으로 써 내려가기. "이 체크는 무엇을 검증하는 것인가"를 코드를 작성하기 전에 자신의 언어로 단정 지어 말한다. 이번 사례라면 "Referer/Origin은 '누가 호출했는가'를 검증하는 것이지, '어디에 도달했는가'가 아니다"라는 한 문장이 있었다면, requestHost라는 변수명을 본 순간 위화감을 느꼈을 것이다.
써 내려간 추상과 실제 코드가 정말로 연결되어 있는지, 변수 수준에서 추적하기. "구현해야 할 사양"과 "작성된 구체"가 일치하는지를 실행하기 전에 코드 그 자체로 확인한다. 이번 버그는 allowedDomains.some(domain => requestHost.includes(domain))라는, 겉보기에는 "그럴듯한" 로직 속에서 발생했다. 변수명이나 코드의 형태가 사양에 대응하는 것처럼 보여도, 실제로 검증하고 있는 대상(requestHost)이 사양의 주어(Referer/Origin의 송신처)와 어긋나 있지 않은지 한 줄씩 따라가며 확인하지 않으면 찾아낼 수 없다.
"동작하는가"가 아니라 "그 한 문장을 충족하는가"로 리뷰하기. 일반적인 브라우저 조작을 통한 동작 확인은 이런 종류의 버그를 그냥 지나치게 만든다. 의도적으로 그 추상적인 보장을 깨뜨리는 조작(Referer를 보내지 않는 curl을 통한 액세스, 다른 탭에서의 직접 입력 등)으로 확인할 필요가 있다.
AI에게 구현 의도를 설명하게 하고, 자신의 한 문장과 대조하기. 이전 기사에서 썼던 "좌표의 자기 참조"와 같은 발상이다. 구현 후에 "이 코드는 무엇을 검증하고 있습니까?"라고 AI 스스로 말하게 해보면, 작성된 코드와 본래 의도 사이의 괴리가 언어화되어 떠오를 때가 있다.
한 번 올바르게 구현된 것도 정기적으로 동일한 확인을 반복하기. 이번 방어도 처음에는 검증된 상태였다. 후속 수정으로 인해 그 전제가 조용히 깨지는 경우가 있는 이상, 구현한 시점을 골(Goal)로 삼아서는 안 된다.
- LLM은 지식을 그대로 꺼내는 것이 아니라, 추상 공간에서 추론하고 출력하는 순간 JIT(Just-In-Time) 방식으로 구체화(Rendering)한다.
- 할루시네이션(Hallucination)은 이 구체화 과정에서 정보가 부족한 부분을 "그럴듯함"으로 채워버리는 현상이며, 모델의 결함이라기보다 구조적인 귀결에 가깝다 (Rawte et al., 2023; Kalai et al., 2025).
- 추상은 구체보다 모호하고 열등한 것이 아니라, 구체로 올바르게 접근하기 위한 이정표이자 액세스 키(Access Key)이다.
- 도메인의 추상 어휘(이번 예시에서는 "Referer/Origin이 무엇을 의미하는가")를 갖지 않은 채 구현을 진행하면, AI는 동작하는 것처럼 보이지만 아무것도 지키지 못하는 코드를 그럴듯하게 출력하고, 리뷰하는 쪽도 이를 간파하지 못한다.
- "AI가 있으면 지식 없이도 서비스를 만들 수 있다"는 것은 이 이정표 없이 구체에 접근하려는 시도이며, 잘 되지 않는 경우가 많다. AI 시대에도 "무엇을 요구해야 하는지를 언어화하는 것 = 추상화하는 능력"은 본질적인 기술로 남을 것이다.
서두의 질문으로 돌아가면, 솔직히 말해 이번 실례를 깨닫기 전까지 나의 대답은 "아니오"였다. "동작한다"와 "안전하다"는 별개라고 말할 수 있게 된 것은, 추상이라는 이정표를 의식하게 된 이후부터다.
📘 이번 실례(Referer/Origin 체크)를 포함한 9계층 방어의 전체 모습과 구현 코드는 이 책에서 해설하고 있다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Qiita AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기