본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 06. 24. 23:26

퍼저(Fuzzer)의 지능은 오라클(Oracle)의 수준을 넘지 못한다

요약

AI 코딩 에이전트가 테스트를 수행할 때 발생하는 오라클(Oracle) 문제와 시맨틱 퍼징(Semantic Fuzzing)의 중요성을 다룹니다. AI는 테스트 하네스 구축 비용을 낮춰주지만, 무엇이 '올바른' 결과인지 정의하는 법칙(Law)을 설계하는 것은 여전히 인간의 영역임을 강조합니다.

핵심 포인트

  • AI 에이전트는 결과값은 맞출 수 있어도 과정의 오류를 잡아내는 오라클 역할에는 한계가 있음
  • 버그를 찾는 핵심은 무작위성이 아니라 검증하는 '오라클(법칙)'에 있음
  • 시맨틱 퍼징은 참조 출력 대신 도메인 규칙과 속성을 대상으로 실행되어 더 효과적임
  • AI 시대의 핵심 역량은 하네스 구축이 아닌 '검증할 법칙'을 파악하는 능력으로 이동함

내 스키마 도구가 생성한 마이그레이션은 내가 가진 모든 검사를 통과했습니다. 최종 스키마는 대상과 정확히 일치했고, convergence(수렴) 상태는 녹색(green)이었습니다. 그러다 그 결과에 도달하기 위해 취한 계획을 살펴보았습니다: DROP TABLE; CREATE TABLE. 데이터가 들어있는 테이블에 대해서 말이죠. 목적지는 맞았지만, 그 경로는 운영 데이터베이스를 지워버렸을 것입니다.

테스트가 통과(green)되었던 이유는 내가 잘못된 것을 검사하고 있었기 때문입니다. 나는 _결과가 어디에 도달했는지_에 대한 오라클(oracle)은 가지고 있었지만, _그곳에 어떻게 도달했는지_에 대한 오라클은 없었습니다.

그 간극이 이 포스트의 핵심 내용입니다. 나는 개발자 도구 — SDK, 컴파일러, 선언적 스키마 관리 시스템 — 를 만듭니다. 단 하나의 잘못된 엣지 케이스(edge case)가 하류의 모든 사용자에게 배포되는 종류의 소프트웨어 말입니다. 그리고 나는 코딩 에이전트(coding agents)가 나를 대신해 이런 것들을 테스트하도록 만드는 데 오랜 시간을 보냈습니다. Claude Code, Codex — 충분한 프롬프팅(prompting)과 적절한 기술이 있다면 테스트 스위트(test suite)처럼 보이는 무언가를 얻을 수 있습니다. 하지만 내가 결코 얻을 수 없었던 것은, 중요한 케이스를 조용히 건너뛰지 않으면서 실제 개발 흐름을 따르는 커버리지(coverage)였습니다. 그것은 실제로 무엇을 검사했는지 확인하기 전까지는 매우 설득력 있게 보입니다.

결국 더 효과적이었던 것은 **시맨틱 퍼징(semantic fuzzing)**이었습니다. 이는 참조 출력(reference output) 대신 _속성(properties)_을 대상으로 실행되며, 에이전트가 테스터 역할을 하는 대신 생성기(generators)와 도메인 규칙(domain rules)을 작성하는, 제약이 있고 결정론적인(deterministic) 무작위 생성 방식입니다. 이것은 많은 것을 잡아냅니다. 그리고 하네스(harness)를 더 많이 실행할수록 더 개선됩니다.

하지만 흥미로운 부분은 퍼징(fuzzing) 자체가 아닙니다. AI는 하네스를 구축하는 것 — 생성기, 어댑터(adapters), 방대한 도메인 규칙들 — 을 저렴하게 만들어 주었습니다. 하지만 AI는 하네스 자체가 올바르게 실행되도록 만들지는 않았으며, 비교할 수 있는 두 번째 구현체가 없을 때 무엇이 "올바른" 것인지 결정하는 부분을 작성해주지도 않았습니다. 그 부분이 언제나 진짜 작업이었고, 지금도 그렇습니다.

따라서 이 포스트는 퍼징의 탈을 쓴 오라클(oracles)에 관한 글입니다. 평소와 마찬가지로, 조언이 아니라 내 생각이 어디로 흘러갔는지에 대한 기록입니다.

현재 제가 도달한 결론 (그리고 수정될 가능성이 있는 부분):

  • 무작위성(Randomness)이 버그를 찾는 것이 아닙니다. **오라클(oracle)**이 찾는 것이며, 무작위성은 그저 오라클을 그곳으로 안내할 뿐입니다.
  • 따라서 가치는 생성기(generator)에 있는 것이 아닙니다. 당신이 검증하는 **법칙(law)**에 있습니다.
  • 가치 있는 법칙은 참조 구현체(reference implementation)를 필요로 하지 않아야 합니다. 즉, 도구가 자체적인 논리에 의해 반드시 충족해야 하는 대수적 관계(algebraic relations)여야 합니다.
  • AI는 하네스(harness) 구축 비용을 낮추었습니다. 이제 희소한 기술은 _인력 규모(headcount)_에서 _법칙이 무엇인지 파악하는 능력_으로 이동했습니다.

"시맨틱(Semantic)"의 정의를 명확히 해야 한다

"나는 그것을 퍼징한다"라는 말은 그 자체로는 거의 아무런 의미도 없습니다. 다이얼의 한쪽 끝에는 크래시 퍼징(crash fuzzing)이 있습니다. 이는 파서(parser)에 균일한 노이즈(uniform noise)를 주입하며, 오직 "프로그램이 쓰러졌는가?"만을 묻습니다. 다른 한쪽 끝에는 참조 모델 테스트(reference-model testing)가 있습니다. 이는 당신이 직접 구축하고 신뢰해야 하는 완전한 두 번째 구현체와 모든 출력을 비교(diffing)하는 방식입니다. 시맨틱 퍼징(Semantic fuzzing)은 그 사이에 위치하며, 이 게임의 핵심은 그 중간 영역에서 무엇을 하느냐에 달려 있습니다.

세 가지 속성이 이를 작동하게 만듭니다. 생성(Generation)은 **분포 제약(distribution-constrained)**을 따릅니다 (균일한 노이즈가 아니라, 실제로 존재할 법한 구조—스키마 및 마이그레이션 시퀀스—를 가진 입력에 편향됨). 또한 **결정론적(deterministic)**입니다 (모든 케이스는 시드(seed)를 포함하므로, 실패 시 정확히 재현 가능함). 마지막으로 **축소 가능(shrinkable)**합니다 (실패한 케이스는 여전히 오류를 일으키는 가장 작은 단위로 스스로를 최소화함).

이것은 속성 기반 테스트(property-based testing)의 가계도이며, 저는 이것이 새로운 것처럼 꾸미기보다는 차라리 그렇게 말하는 편을 택하겠습니다. 목표는 최대치의 무작위성이 아니라, 생산적인 무작위성입니다. 즉, 재현 가능하고, 최소화 가능하며, 버그가 존재하는 곳을 겨냥하는 무작위성입니다. 이것이 거부하는 단 한 가지는 참조 모델 테스트가 하는 방식, 즉 정답을 확인하기 위해 두 번째 구현체를 만드는 것입니다. 대신 이것은 정답이 반드시 따라야 하는 _관계(relations)_를 검증하며, 이것이 이 글의 나머지 부분이 전개될 핵심적인 움직임입니다.

오라클이 게임의 전부다

퍼저는 결코 자신의 오라클보다 똑똑할 수 없습니다.

생성기는 단지 탐색(search)을 수행할 뿐입니다. 입력값을 생성하지만, 그중 무엇이 잘못되었는지는 전혀 알지 못합니다. _이것은 버그다_라고 결정하는 것은 오라클입니다. 그리고 오라클이 인식하지 못하는 것이라면, 당신이 아무리 많은 케이스를 소모하더라도 퍼저는 결코 찾아낼 수 없습니다.

충돌 오라클(crash oracle)을 향해 10억 개의 입력을 쏟아부으면 충돌(crash)을 얻을 수 있습니다. 하지만 파멸적인 경로를 통해 올바른 상태로 수렴하는 마이그레이션(migration)이나, 제약 조건(constraint)을 조용히 누락시키는 롤백(rollback)을 얻을 수는 없습니다. 이런 것들은 충돌을 일으키지 않습니다. 그것들은 _의미론적(semantically)_으로 틀린 것이며, 충돌 오라클은 바로 그 지점에 대해 눈이 멀어 있습니다.

여기서 빠지기 쉬운 함정은 참조 구현체(reference implementation)를 찾는 것입니다. "올바른 모델을 구축한 뒤 그것과 차이(diff)를 비교하겠다"라고 생각하는 것이죠. 하지만 스키마 엔진(schema engine)의 완전한 참조 모델을 만드는 것 자체가 또 다른 스키마 엔진을 만드는 것이며, 그 엔진 또한 유지보수해야 할 자체적인 버그를 갖게 됩니다. 작업량은 두 배로 늘어났지만, 신뢰할 수 없는 대상을 하나 더 얻었을 뿐입니다.

해결책은 다음과 같습니다: 참조(reference)를 요구하는 것을 멈추고, 법칙(laws) — 즉, "정답"과 관계없이 도구가 반드시 만족해야 하는 관계를 요구하십시오.

법칙 (Law)확인되는 관계 (Relation checked)참조 필요 여부?축 (Axis)
수렴 (convergence)apply(spec) → 내성(introspect) → 잔류 드리프트(residual drift) == 0아니오정확성 (correctness)
...

그 어떤 것도 사전에 올바른 스키마를 필요로 하지 않습니다. 각 법칙은 연산의 의미에 따라 그 자체로 참이기 때문입니다. convergence는 "이 스키마가 맞는가?"라고 묻는 것이 아니라, "당신이 주장한 상태에 도달했는가?"라고 묻습니다. 이것이 바로 변형적(metamorphic) 접근법입니다. 정답을 검증하지 말고, 정답이 반드시 따라야 하는 _관계(relation)_를 검증하십시오.

safety(안전성)를 각주가 아닌 동등한 요소로 주목하십시오. "올바른 상태에 도달했는가"와 "안전한 경로를 택했는가"는 서로 다른 축입니다. 서두에서 언급한 DROP TABLE; CREATE TABLE은 수렴(convergence)을 만족하지만 여전히 데이터를 삭제합니다. 오라클은 목적지뿐만 아니라 경로도 확인해야 합니다. 이것이 바로 제 사례에서 나타난 '겉보기에는 정상적이지만 파멸적인' 마이그레이션을 잡아낼 수 있었을 법칙입니다.

실제 사례: 스키마, 확장, Docker 루프

하나의 생성기(generator)가 파이프라인에 입력을 공급하며, 모든 단계는 법칙을 적용할 수 있는 지점이 됩니다.

 스키마 생성 + 변이 시퀀스(mutation sequence)
            │
            ▼
...

수천 개의 시드(seed)에 대해 이를 실행하면, 단순히 "ADD COLUMN이 작동하는가"를 테스트하는 것이 아닙니다. 인간이 만든 테스트 스위트(test suite)가 결코 도달할 수 없는 조합, 특히 **확장 기능 조합(extension combinations)**을 타격하게 됩니다. 기본 PostgreSQL/MySQL은 쉬운 부분입니다. 버그는 citext가 생성된 열(generated column)과 만나고, 그것이 부분 인덱스(partial index) 및 오래된 트리거(trigger)와 결합되는 지점에 숨어 있습니다. 과거에는 이를 수동으로 열거하기 위해 팀 단위의 인력이 필요했습니다. 하지만 생성된 방식에서는 여러분이 조정하는 하나의 분포(distribution)가 됩니다.

하지만 _어느 계층(layer)이 답하는지_에 대해 솔직해져야 합니다:

계층 (Layer)런타임 (Runtime)답변 가능 내용답변 불가능 내용
인프로세스 (In-process)PGlite / node:sqlite수렴성 (convergence), 멱등성 (idempotency), 실제 SQL 실행멀티 커넥션 (multi-connection), 잠금 충실도 (lock fidelity), 전체 확장 기능 카탈로그
...

동일한 생성 사례가 **확산(fan out)**됩니다. 비용이 저렴한 규칙(laws)은 수천 개씩 인프로세스(in-process)에서 실행되며, 실제 엔진이 필요한 비용이 큰 규칙들은 Docker에서 샘플링되어 실행됩니다. 계층은 파이프라인 기준이 아니라 _속성(property)_에 따라 선택됩니다. 잠금 경합(lock-contention) 규칙을 인프로세스 계층에 배치한다고 해서 빨라지는 것이 아니라, 결과가 거짓이 될 뿐입니다.

플라이휠(Flywheel)은 실재하지만, 함정이 있습니다

"더 많이 실행할수록 더 좋아진다"는 말은 사실입니다. 하지만 이는 통과(pass) 결과가 쌓이기 때문이 아닙니다. 100% 통과하는 테스트 스위트는 무언가를 잡아낼 수 있는지에 대해 아무것도 증명하지 못합니다. 플라이휠은 모든 실패 상황에서 다음 조건이 충족될 때만 가치를 향해 회전합니다:

  1. 실패가 최소화되고 회귀 테스트 케이스(regression case)로 재진입해야 합니다. 모든 실제 버그는 영구적이고 작으며 결정론적인(deterministic) 테스트가 됩니다. 코퍼스(corpus)는 여러분의 실수 모양을 따라 성장합니다.
  2. 오라클(oracle) 자체를 주기적으로 테스트해야 합니다. 알려진 결함(fault)을 주입하십시오. 위험 규칙을 삭제하거나, 가드(guard)를 반전시키거나, 인트로스펙터(introspector)에서 제약 조건(constraint)을 삭제한 뒤, 특정 규칙이 이를 _차단(kill)_하는지 확인하십시오.
결함 주입 (fault injected) ──▶ 규칙이 실패하는가?
                     ├─ 예 → 오라클이 제대로 작동함. 통과(green) 결과를 신뢰하십시오.
                     └─ 아니오 → 통과(green)는 장식일 뿐입니다. 오라클을 수정하십시오.

그 두 번째 단계는 제가 보기에 가장 많이 생략되는 단계이며, 이 전체 과정이 단순한 보여주기식 행위(theater)로 전락하는 것을 막아주는 단계입니다. 테스트하지 않는 오라클(Oracle)은 검증이 아니라 주장일 뿐입니다. 정직한 플라이휠(flywheel)은 다음과 같습니다: 실패 사례를 코퍼스(corpus)로 최소화하고, 검증기(verifier)를 검증하십시오. 그렇게 하면 진정으로 복리 효과가 발생합니다.

무엇이 저렴해졌는가 (그리고 무엇은 그렇지 않은가)

몇 년 전만 해도 이 파이프라인은 인력 충원의 문제였습니다. 생성기(generators), 섀도우 DB(shadow DB), 마이그레이션 플래너(migration planner), Docker 오케스트레이션(orchestration), 방언별 어댑터(per-dialect adapters) 등 — 팀 하나와 그 이상의 인력이 필요했습니다. 장벽은 아이디어가 아니라 구현 노동이었으며, 이는 대부분의 사람들이 진지한 시뮬레이션 테스트를 포기하게 만드는 가격 요인이었습니다.

그 장벽은 무너졌고, 저는 그 지점을 지목할 수 있습니다. 저의 IR(Intermediate Representation)을 특정 데이터베이스의 특이성에 매핑하는 계층인 방언별 어댑터(per-dialect adapter)는, 예전에는 문서를 읽고 직접 시행착오를 겪으며 엣지 케이스(edge case)를 발견하느라 며칠씩 걸리는 고된 작업이었습니다. 제가 마지막으로 추가한 어댑터는 오후 한나절 만에 완성되었습니다. 제가 IR과 대상의 인트로스펙션(introspection) 형식을 설명하면, 에이전트(agent)가 어댑터와 특이 규칙(quirk-rules)의 초안을 작성했고, 저는 타이핑하는 대신 검토하는 데 시간을 보냈습니다. 파싱(Parsing)은 범용화(commodity)되었고, 로컬 실행은 저렴해졌으며, 방대한 양의 상용구(boilerplate) 작성은 제가 명세(spec)를 작성하는 속도보다 더 빠르게 스스로 이루어집니다.

하지만 정확히 무엇이 저렴해졌는지 주목하십시오. 테스트 하네스(harness)를 구축하는 것은 저렴해졌습니다. 하지만 어떤 관계(relation)가 실제로 불변(invariant)하는지 아는 것은 그렇지 않았습니다. 각 법칙을 정직하게 답변할 수 있는 계층으로 라우팅(routing)하는 것도, 검증기(checker)가 여전히 제대로 작동하는지 확인하는 것도 그렇지 않았습니다. 에이전트는 어댑터를 작성했지만, safetyconvergence와 동일한 축에 속하는지, 혹은 저의 '통과(green)' 결과가 어떤 의미를 갖는지 저에게 말해줄 수는 없었습니다. 희소한 기술은 "하네스를 구축할 수 있는가"에서 "법칙이 무엇인지 파악할 수 있는가"로 옮겨갔습니다. 구현은 민주화되었지만, 판단(judgment)은 그렇지 않았습니다.

어디에서 무너지는가

로컬 시뮬레이션(Local simulation)은 거의 비용을 들이지 않고도 놀라울 정도로 많은 버그를 잡아냅니다. 하지만 그것이 원격 환경(remote environment)을 대체하지는 못합니다:

  • Provider parity (제공자 동등성) — 실제 클라우드 API는 어떤 에뮬레이터도 재현할 수 없는 동작을 수행합니다.
  • Real enforcement (실제 강제 적용) — IAM 거부, KMS 정책, VPC 격리, 감사 로그(audit-log)의 불변성: 이러한 것들은 오직 실제 환경에서만 증명 가능합니다.
  • Hosted-backend quirks (호스팅된 백엔드의 특이점) — 관리형 Postgres, 풀러(pooler) 모드, 호스팅된 인증 스키마 등입니다. 제가 로컬에서 실행한 Docker Postgres는 마이그레이션을 문제없이 수행했지만, 트랜잭션 모드 풀러(transaction-mode pooler) 뒤에 있는 관리형 인스턴스에서는 데드락(deadlock)이 발생했습니다. 이는 풀러가 세션과 잠금(lock)이 얽히는 방식을 변경하기 때문이며, 로컬 컨테이너에는 이러한 동작이 전혀 없습니다.
  • Genuine concurrency at scale (대규모 환경에서의 진정한 동시성) — 레이스 프로브(race probes)가 많은 것을 잡아내긴 하지만, 그것이 프로덕션 부하(production load)를 인증해 주지는 않습니다.

이러한 요소들은 대량 테스트용이 아니라, 보정 앵커(calibration anchors)로 사용되는 희귀한 **라이브 티어(live tier)**에서 작동합니다. 로컬 환경은 로직이 맞다고 말해주지만, 오직 라이브 환경만이 플랫폼이 동의한다고 말해줍니다.

제가 제기할 법한 반론들

  • "이것은 그저 속성 기반 테스트(property-based testing)일 뿐이다." 생성(generation)과 축소(shrinking) 측면에서는 대체로 그렇습니다. 하지만 핵심적인 기여는 프레임워크(framing)에 있습니다. 즉, *오라클(oracle)*이 산출물(artifact)이며, 법칙(laws)은 참조가 필요 없고, 안전성(safety)이 일급 시민(first-class) 축이라는 점입니다.

  • "왜 에이전트가 직접 테스트를 작성하고 실행하게 하지 않는가?" 저도 정말 열심히 시도해 보았습니다. 에이전트는 그럴듯한 테스트 스위트(suite)를 만들어내겠지만, 자신이 방금 작성한 해피 패스(happy path)를 테스트하는 경향이 있으며 실제로 문제를 일으키는 케이스는 건너뛰곤 합니다. 그리고 테스트가 통과(green)되어도 무엇이 문제인지 알 수 없습니다. 퍼즈 하네스(fuzz harness)는 이를 뒤집습니다. 에이전트는 생성기(generators)와 법칙(laws)(내가 검토할 수 있는 것들)을 제공하고, 생성(generation) 과정이 우리 둘 다 생각하지 못한 케이스를 찾아냅니다. 에이전트는 규칙을 제안하는 데는 능숙하지만, 포괄적(exhaustive)인 데는 신뢰할 수 없습니다.

  • "AI가 작성한 오라클은 미묘하게 부정확하다." 제가 가장 두려워하는 실패 모드입니다. 오라클의 허점이 안전함으로 읽히는 것입니다. 따라서, 이미 검증된 알고리즘 코어를 빌려 쓰고, 맞춤형 부분은 내가 검토 시 눈으로 확인할 수 있는 도메인 규칙으로 유지하며, 하네스가 실제 버그를 "예상된 결과"로 격하시키지 않도록 해야 합니다. 검증기(verifier)는 AI에게 감독 없이 맡겨서는 안 될 마지막 요소입니다.

  • "통과(Green) 결과도 여전히 거짓말을 한다." 오라클에 대해 변이 테스트(mutation-test)를 수행하기 전까지는 말입니다. 그것은 단순한 다듬기가 아니라, 통과(green) 결과가 의미를 갖게 만드는 핵심입니다.

  • "이것은 유지 관리 불가능한 매트릭스로 커집니다." 모든 법칙이 모든 레이어에서 실행될 때만 그렇습니다. 이에 대한 대응 원칙(Counter-discipline)은 각 속성(property)을 가장 비용이 적게 드는 정직한 레이어로 라우팅하고, 더 이상 아무것도 잡아내지 못하는 법칙은 폐기하는 것입니다.

이 논의가 향하는 곳

저는 실용적인 문을 통해 들어왔습니다. 시맨틱 퍼징(semantic fuzzing)은 많은 버그를 저렴하게 잡아내니까요. 하지만 걸어 나올 때는 훨씬 더 좁고 명확한 지점에 도달해 있었습니다. 무작위성(randomness)은 결코 핵심이 아니었습니다. 핵심은 오라클(oracle)이었고, AI는 오라클을 제외한 모든 것을 마침내 깨달을 수 있을 만큼 충분히 저렴하게 만들었습니다.

AI 시대에는 엄밀함(rigor)이 보상을 가져다줍니다. 엄밀함은 모호한 상황을 에이전트(agent)가 행동할 수 있는 몇 가지 확고한 사실로 압축합니다. 여기서 그것은 가장 날카로운 형태를 띱니다. 엄밀함은 맹목적인 무작위성을 버그 탐지 기계로 바꾸는 힘입니다. 법칙이 탐지를 수행하고, 노이즈는 그저 움직일 뿐입니다.

구현 장벽은 낮아졌습니다. 이제 당신이 만든 대상에 대해 "정확함(correct)"이 무엇을 의미하는지 아는 것이 업무의 전부입니다.

이는 다시 DROP TABLE이 경로에 숨어 있던 그 녹색 마이그레이션(green migration) 이야기로 저를 되돌려 놓습니다. 퍼저(fuzzer)가 그것을 놓친 것은 무작위성이 약했기 때문이 아닙니다. 데이터 삭제를 버그라고 부르는 법칙을 제가 아직 작성하지 않았기 때문입니다. 제가 그 법칙을 작성한 날, 생성 모델은 단 몇 분 만에 해당 케이스를 찾아냈습니다. 그것은 제가 그것이 중요하다라고 말해주기를 기다리며 내내 그 옆을 지나쳐 가고 있었던 것입니다. 그것이 이제의 업무입니다. 더 많은 케이스를 실행하는 것이 아니라, 한 번에 하나의 법칙씩, 잡아낼 가치가 있는 실패를 명명하는 법을 배우는 것입니다.

AI 자동 생성 콘텐츠

본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0