Unknown은 오류인가, 아니면 결과인가?
상호작용을 Human, Human + AI, Agent 또는 Unknown으로 분류하기 시작하면, Unknown 범주는 종종 사람들이 예상하는 것보다 큽니다. 이를 숨겨야 할 격차로 보고 Unknown을 Human에 합치거나, 반대로 의심스러운 것으로 간주하고 싶어지기도 합니다. 두 선택 모두 문제를 만듭니다.
Unknown은 이용 가능한 증거가 신뢰할 수 있는 판정을 뒷받침하지 못한다는 뜻입니다. JavaScript를 전혀 실행하지 않는 클라이언트의 요청, 신호를 수집하지 않는 endpoint로 들어오는 호출, 한 페이지만 보고 끝난 세션 같은 것은 모두 충분히 정상일 수 있으며, 이들만으로는 누가 무엇을 하고 있었는지 말할 수 없습니다.
Unknown을 솔직하게 보고하는 것이 다른 클래스의 신뢰성을 지키는 방법입니다. 시스템이 Unknown을 한 번도 말하지 않는다면, 모든 Human 결과는 일부 추측에 불과하고 어느 부분이 그런지도 알 수 없습니다. AI 트래픽을 분류할 때 Unknown이 유효한 결과인 이유에 대한 글에서 그 이유를 더 자세히 설명합니다.
이 가이드는 실제 환경에서 Unknown을 어떻게 다룰지 설명합니다. 명시적인 경로별 폴백, 다른 클래스로 새어 들어가지 않게 하는 규칙, 모니터링 방법, 그리고 중요한 지점에서 Unknown을 줄이는 방법을 다룹니다.
1단계: Human으로 기본값을 두지 마세요
가장 흔한 실수는 자동화 신호를 찾지 못하면 항상 Human을 반환하는 분류 함수입니다. 그럴듯해 보이지만 잘못된 방식입니다. 봇 신호가 없다는 것은 사람 신호가 있다는 뜻이 아닙니다.
// Wrong: silence becomes Human
function classifyWrong(e: Evidence) {
if (e.signatureValid) return { actor: "agent", basis: "verified" };
if (e.declaredAgent) return { actor: "agent", basis: "declared" };
return { actor: "human", basis: "inferred" };
}
// Right: Human requires positive evidence, otherwise Unknown
function classify(e: Evidence) {
if (e.signatureValid) return { actor: "agent", basis: "verified" };
if (e.declaredAgent) return { actor: "agent", basis: "declared" };
if (e.interactiveSession) return { actor: "human", basis: "inferred" };
return { actor: "unknown", basis: "unknown" };
}결과가 생성되는 모든 곳에 같은 규칙을 적용하세요: timeout, 오류, 오래된 cache 항목, 누락된 telemetry는 모두 Unknown을 반환해야 합니다. 이런 경로마다 테스트를 작성해 동작이 조용히 후퇴하지 않게 하세요. 아래 예시는 내장된 Node.js test runner를 사용합니다.
import { test } from "node:test";
import assert from "node:assert/strict";
test("missing evidence is unknown, not human", () => {
const r = classify({ signatureValid: false, declaredAgent: false, interactiveSession: false });
assert.equal(r.actor, "unknown");
});2단계: 모든 route에 명시적 폴백을 두세요
Unknown에는 정의된 동작이 필요하고, 올바른 동작은 route에 따라 다릅니다. route별로 결정하고, 문서화하고, 설정에 보관하세요. 이 폴백이 더 넓은 policy 모델에 어떻게 들어가는지는 사람과 AI 에이전트를 위한 앱 수준 policy 설계를 참고하세요.
| Route type | Example | Unknown에 대한 합리적인 fallback |
|---|---|---|
| Public content | Marketing pages, docs | Allow, standard response |
| Read-only data | Catalog, search API | 표준 rate limit으로 Allow |
| Account creation | Signup | 기존 검사 유지, 선택적 review queue |
| Sensitive change | Password, email, billing details | 기존 재인증 유지, 완화 없음 |
| High-value action | Bulk export, admin operations | 기존의 강한 authorization 유지 |
패턴은 단순합니다. 위험도가 낮은 route에서는 Unknown이 일반 트래픽처럼 동작합니다. 위험도가 높은 route에서는 Unknown에 이미 그 route가 갖고 있는 보호 수준이 적용됩니다. 어느 경우에도 actor 결과 때문에 분류되지 않았지만 합법적인 사용자가 더 불리해지지 않습니다.
type Fallback = "allow" | "standard_limit" | "existing_checks" | "review";
const unknownFallback: Record<string, Fallback> = {
"GET /docs/*": "allow",
"GET /api/catalog": "standard_limit",
"POST /signup": "existing_checks",
"POST /account/password": "existing_checks",
"POST /exports": "review",
};3단계: timeout과 오류를 같은 방식으로 다루세요
Actor 평가는 request 경로에 있으므로 엄격한 시간 예산이 필요합니다. 예산을 초과하면 Unknown을 반환하고 route 폴백을 적용하세요. 평가가 느렸다는 이유로 request를 막아서는 안 됩니다.
async function withBudget<T>(work: Promise<T>, ms: number, onTimeout: T): Promise<T> {
let timer: NodeJS.Timeout;
const timeout = new Promise<T>((resolve) => {
timer = setTimeout(() => resolve(onTimeout), ms);
});
try {
return await Promise.race([work, timeout]);
} finally {
clearTimeout(timer!);
}
}
const result = await withBudget(evaluateActor(req), 150, { actor: "unknown", basis: "unknown" });결과가 Unknown인 이유를 기록하세요: 증거 없음, timeout, 평가 오류 또는 오래된 증거. 그 이유가 무엇을 고쳐야 하는지 알려줍니다.
4단계: Unknown을 의심스러운 것으로 취급하지 마세요
Unknown은 risk score가 아닙니다. 많은 합법적인 클라이언트는 특정 route에서 항상 Unknown일 수 있습니다. command-line 도구, 서명이 없는 server-side integration, 접근성 도구, privacy 중심 browser, 단순 feed reader가 그 예입니다. 오늘날 큰 AI 운영자들조차도 주로 user agent와 공개된 IP 범위로 fetcher를 식별합니다. 예를 들어 OpenAI의 crawler 문서에는 2026년 11월 기준 각 bot에 대해 user agent 문자열과 공개된 IP 주소 파일이 나와 있습니다.
의심스러운 동작에 대응하고 싶다면, 요청 rate, 로그인 실패 패턴, abuse report처럼 설명 가능한 행동을 근거로 삼고, 그에 맞는 별도의 증거를 사용하세요. actor class와는 이 로직을 분리해서 둘 중 하나가 다른 하나를 왜곡하지 않게 하세요.
5단계: Unknown rate를 어떻게 모니터링하나요?
Unknown rate는 Unknown으로 분류된 상호작용의 비율입니다. route별, integration context별, 시간 경과별로 추적하세요. 이것은 가장 유용한 건강 지표 중 하나입니다.
type Row = { route: string; actor: string };
export function unknownRate(rows: Row[]) {
const byRoute = new Map<string, { total: number; unknown: number }>();
for (const r of rows) {
const s = byRoute.get(r.route) ?? { total: 0, unknown: 0 };
s.total += 1;
if (r.actor === "unknown") s.unknown += 1;
byRoute.set(r.route, s);
}
return [...byRoute].map(([route, s]) => ({ route, rate: s.unknown / s.total, total: s.total }));
}수치가 알려주는 것:
- 어떤 route에서 비율이 높고 안정적이라면 보통 그곳에 신호가 부족하다는 뜻입니다. browser integration이 전혀 로드되지 않는 페이지나 signed request를 전혀 보지 못하는 API는 대부분 Unknown으로 남습니다.
- 갑작스러운 상승은 대개 스택 어딘가가 바뀌었다는 뜻입니다. 예를 들어 header를 제거한 release, signature header를 지우는 CDN rule, app의 선언 전송을 멈춘 frontend 변경 등이 있습니다.
- 갑작스러운 하락도 확인할 가치가 있습니다. Unknown이 줄고 Human이 늘었다면, 새로운 증거가 들어온 것인지 아니면 fallback이 조용히 바뀐 것인지 확인하세요.
절대 수준이 아니라 급격한 변화에 대해 alert 하세요. 모든 application에는 고유한 정상 범위가 있습니다.
6단계: 중요한 지점에서 Unknown을 어떻게 줄이나요?
Unknown을 0으로 만들 필요는 없습니다. 중요한 decision이 있는 route에 충분한 증거가 있으면 됩니다. 영향도 순서대로 진행하세요:
- 영향이 큰 route부터 증거를 추가하세요. account 변경이 대부분 Unknown이라면, browser integration이나 server-side check가 가장 효과적인 곳입니다.
- 자동화된 클라이언트가 자신을 식별하도록 유도하세요. agent가 HTTP Message Signatures와 IETF Web Bot Auth conventions으로 request에 서명하는 방법을 설명하는 API 문서를 공개하고, verified 클라이언트에는 더 높은 limit이나 구조화된 response 같은 더 나은 경험을 제공하세요. 검증할 수 있는 클라이언트는 그렇게 하게 됩니다.
- 자체 application 지식을 활용하세요. 사용자가 내장 assistant와 상호작용한다면, 이를 declaration으로 기록하세요. app은 네트워크보다 더 많은 것을 알고 있는 경우가 많습니다.
- 기반 구성요소를 고치세요. proxy가 의존하는 header를 전달하는지, 평가가 끝날 만큼 충분한 time budget이 있는지 확인하세요.
Unknown에서 트래픽을 빼내는 신호를 실무적으로 보려면 website traffic이 AI agent에서 왔는지 구분하는 방법을 읽어보세요.
7단계: dashboard에서 Unknown을 솔직하게 보여주세요
Unknown을 다른 클래스와 같은 시각적 비중으로 독립된 series로 표시하세요. "other" bucket에 숨기지 말고, alert처럼 색을 칠하지도 마세요. 이해관계자가 큰 Unknown 비율을 보면, 올바른 반응은 "어디에 더 많은 증거가 필요한가"이지 "어디에 bot이 있는가"가 아닙니다.
8단계: 가정이 아니라 결과로 Unknown을 검토하세요
몇 주마다 가장 중요한 route에서 Unknown 상호작용 일부를 샘플링해 실제로 무슨 일이 있었는지 보세요. 세션이 구매를 완료했는지, project를 만들었는지, article 세 개를 읽었는지, 아니면 login에서 반복적으로 실패했는지 확인하세요. 결과는 너무 이른 label을 붙이지 않고도 그 bucket에 어떤 traffic이 있는지 알려줍니다.
발견한 내용을 세 가지 실용적 범주로 묶으세요:
- 합법적이고 위험도가 낮음. feed reader, link preview, monitoring tool, privacy 설정을 쓰는 사람들. route fallback은 그대로 두세요.
- 스스로 식별할 수 있는 합법적 자동화. 구조화된 경로를 사용하거나 요청에 서명할 수 있다면 기꺼이 그렇게 할 integrations와 agents. 안내 문서를 공개하고 가능한 곳에 연락하세요.
- 자체 control이 필요한 행동. credential stuffing이나 scraping 패턴으로, 둘 다 OWASP Automated Threats to Web Applications 프로젝트에 정리되어 있습니다. 로그인 실패율처럼 자체 증거를 가진 behavior-based control로 대응하고, actor class와는 분리해 두세요.
이 검토는 폴백을 실제 traffic에 맞춰 유지하게 해 주고, 다음에 어디에 증거를 추가해야 하는지 명확한 목록을 제공합니다.
자주 묻는 질문
Unknown을 rate limit에 포함해야 하나요? 표준 limit을 사용하세요. verified agent에는 더 높은 limit을 줄 수 있으므로 class별 rate limiting은 괜찮지만, Unknown에는 벌칙성 limit이 아니라 일반 limit이 적용되어야 합니다.
Unknown 사용자에게 challenge를 걸 수 있나요? 그 상황에서 route가 원래 모두에게 challenge를 거는 경우에만 가능합니다. 결과가 Unknown이라는 이유만으로 challenge를 추가하면, 더 적은 신호를 보내는 클라이언트를 가진 합법적 사용자가 불이익을 받습니다.
표준이 성숙하면 Unknown은 줄어드나요? 더 많은 자동화된 클라이언트가 request에 서명하게 되면, 더 많은 agent traffic이 Unknown이나 declared에서 verified로 이동합니다. 모니터링은 route별로 그 변화를 보여줄 것입니다.
Proof of Actor가 들어가는 위치
Proof of Actor는 Unknown을 이유와 함께 일급 결과로 보고하며, 이를 Human으로 바꾸지 않습니다. application이 각 route의 fallback을 선택하고, analytics는 어디까지가 coverage인지 보여 주므로 중요한 지점에 증거를 추가할 수 있습니다.
운영 중인 application에 이 구성을 우리 팀과 함께 설정하려면, early access를 신청하세요. 증거가 정확히 무엇을 입증할 수 있는지 이해하려면 Detection & Limitations을 읽어보세요.
출처
- RFC 9421: HTTP Message Signatures: 자동화된 클라이언트가 request에 서명하는 데 사용할 수 있는 standard
- draft-ietf-webbotauth-httpsig-protocol: 서명된 자동화 트래픽을 위한 IETF Web Bot Auth draft(2026년 11월 기준 진행 중)
- OpenAI: Overview of OpenAI Crawlers: OpenAI의 bot에 대한 user agent 문자열과 공개 IP 주소 파일
- OWASP Automated Threats to Web Applications: credential stuffing과 scraping 같은 자동화 위협의 taxonomy
- Node.js test runner: 예시에서 사용된
node:testmodule