<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" xml:lang="ko_KR"><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://enebin.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://enebin.github.io/" rel="alternate" type="text/html" hreflang="ko_KR" /><updated>2026-08-12T14:49:03+09:00</updated><id>https://enebin.github.io/feed.xml</id><title type="html">enebin.log</title><subtitle>개발하며 배운 것, 오래 기억하고 싶은 생각, 만드는 과정을 기록하는 enebin의 개인 블로그입니다.</subtitle><author><name>enebin</name></author><entry><title type="html">아무도 안 읽는다고 믿었던 주소</title><link href="https://enebin.github.io/posts/noreply-domain-leak/" rel="alternate" type="text/html" title="아무도 안 읽는다고 믿었던 주소" /><published>2026-08-12T00:00:00+09:00</published><updated>2026-08-12T00:00:00+09:00</updated><id>https://enebin.github.io/posts/noreply-domain-leak</id><content type="html" xml:base="https://enebin.github.io/posts/noreply-domain-leak/"><![CDATA[<p>설정 파일에 발신자 주소를 적어야 할 때가 있다. 답장을 받을 생각이 없는 알림 메일이라면 대충 <code class="language-plaintext highlighter-rouge">noreply@</code>로 시작하는 주소를 넣는다. 도메인 자리에는 회사 도메인 대신 <code class="language-plaintext highlighter-rouge">noreply.net</code> 같은 걸 적기도 한다. 어차피 아무도 안 읽는 주소니까.</p>

<p>문제는 그 도메인이 진짜로 존재한다는 것이다. 그리고 살 수 있다.</p>

<p>2026년 8월 10일, 매트 버지스가 WIRED에 보안 연구자 코리 솔로비에비츠의 사례를 썼다. Ars Technica에도 같은 글이 실렸다. 솔로비에비츠는 2020년에 <code class="language-plaintext highlighter-rouge">noreply.us</code>를, 2024년에 <code class="language-plaintext highlighter-rouge">noreply.net</code>을 샀다. 원래 자기 메일을 걸러 쓰려고 샀다. 어떤 주소로 오든 다 받게 해 두면 서비스마다 다른 주소를 만들어 쓸 수 있으니 프라이버시에 유리하다.</p>

<p>그런데 자기가 만든 적 없는 주소로 남의 메일이 들어오기 시작했다. 2024년 12월 이후 <code class="language-plaintext highlighter-rouge">noreply.net</code> 한 곳으로만 401,796통이 도착했다. 하루 평균 699.99통꼴이다. 그는 이렇게 표현했다.</p>

<blockquote>
  <p>나도 모르게 허니팟을 하나 만들어 버렸다.</p>
</blockquote>

<!--more-->

<h2 id="40만-통이-한-사람-편지함에-쌓였다">40만 통이 한 사람 편지함에 쌓였다</h2>

<p>첨부 파일이 붙은 메일만 28,365통이었다. 시청이 만든 부상 보고서, 수리 서비스 주문서, 어느 학교 플랫폼의 계정 개설 안내가 들어 있었다. 테스트용 플랫폼 자격 증명이 특히 많았고 남의 피자 주문 확인 메일까지 왔다. 숫자보다 내용이 문제였다.</p>

<p>첨부 파일이 특히 문제인 건 자동 발송 메일의 구조 때문이다. 본문은 대개 “아래 내용을 확인하세요” 정도로 짧고 실제 내용은 파일에 들어 있다. 사람이 직접 쓴 메일이라면 조심할 만한 자료도, 시스템이 매일 만들어 보내면 누구도 미리 걸러내지 않는다.</p>

<p>보낸 쪽도 한두 군데가 아니다. 발신 주소는 14,000개가 넘었고 이를 도메인 단위로 묶어도 6,200개다. 어느 한 회사가 설정을 잘못해서 벌어진 일이라기엔 범위가 너무 넓다.</p>

<p>연구자가 이 자료를 받은 건 그나마 다행이다. 그는 8월 데프콘 보안 컨퍼런스에서 이 문제를 발표하고 피해 기업들에 개별로 알리고 있다. 같은 도메인을 범죄 조직이나 국가 지원 해킹 그룹이 샀다면 어떻게 됐을까. 회사 이름과 부서, 실명, 회의 시간, 내부 시스템이 보내는 알림의 생김새가 매일 배달된다. 표적형 공격을 준비하는 쪽은 정찰 단계를 통째로 건너뛴다. 솔로비에비츠도 그 도메인이 하필 자기 손에 들어왔다는 사실에 안도한다.</p>

<figure>
  <img src="/assets/images/noreply-domain-leak-funnel.svg" alt="401796통의 수신 메일 중 28365통에 첨부 파일이 있고, 발신 주소는 14000개, 도메인으로 묶으면 6200개라는 것을 단계별로 보여주는 그림" />
  <figcaption>사고를 낸 곳이 6,200개 도메인에 흩어져 있다. 한 회사의 실수가 아니다.</figcaption>
</figure>

<h2 id="자리-표시자가-진짜-주소가-되는-순간">자리 표시자가 진짜 주소가 되는 순간</h2>

<p>원인은 단순하다. 누군가 설정 파일에 발신자나 수신자 주소를 적어야 했는데 회사가 실제로 가진 도메인 대신 그럴듯해 보이는 이름을 적었다.</p>

<p>왜 하필 <code class="language-plaintext highlighter-rouge">noreply.net</code>이었을까. 답장을 받지 않는 주소를 뜻하는 단어가 <code class="language-plaintext highlighter-rouge">noreply</code>이고 도메인 형태로 만들려면 뒤에 뭐라도 붙여야 한다. <code class="language-plaintext highlighter-rouge">.com</code>은 오래전에 팔렸을 테니 <code class="language-plaintext highlighter-rouge">.net</code>을 붙인다. 예제 코드에서 본 주소를 그대로 옮겨 적기도 한다. 아무도 실재하는 도메인을 적었다고 생각하지 않는다. 자기 머릿속에서는 그냥 “여기는 비워 두는 칸”이라는 뜻이었기 때문이다.</p>

<p>퇴사자 처리 과정에서도 같은 일이 벌어진다. 계정을 지우면 메일 시스템에 남은 주소가 비는데, 이때 빈칸을 채우려고 <code class="language-plaintext highlighter-rouge">삭제된사용자@어딘가</code> 형태의 주소를 넣어 두는 경우가 있다. 그 뒤에도 그 사람에게 오던 알림은 계속 그 주소로 흘러간다.</p>

<p>여기에 캐치올 설정이 겹치면 사고가 완성된다. <strong>캐치올은 도메인 앞에 어떤 이름이 붙어 있든 일단 다 받는 설정이다.</strong> <code class="language-plaintext highlighter-rouge">abc@noreply.net</code>도 <code class="language-plaintext highlighter-rouge">우리회사@noreply.net</code>도 전부 한 편지함으로 들어온다. 도메인 주인은 상대가 무슨 주소를 지어냈는지 미리 알 필요가 없다.</p>

<figure>
  <img src="/assets/images/noreply-domain-leak-path.svg" alt="설정 파일에 적힌 자리 표시자 주소가 메일 서버를 거쳐 외부 도메인 주인의 캐치올 편지함으로 도착하는 경로를 보여주는 그림" />
  <figcaption>메일은 사라지지 않는다. 그 도메인을 가진 사람에게 배달된다.</figcaption>
</figure>

<p>메일은 없어지는 물건이 아니다. 주소에 적힌 도메인을 찾아가고 그 도메인에 주인이 있으면 주인에게 간다. 그게 전부다.</p>

<p>메일 서버가 하는 일도 딱 그만큼이다. 주소에서 <code class="language-plaintext highlighter-rouge">@</code> 뒤를 떼어내 그 도메인의 메일 수신 서버를 찾은 다음 넘긴다. 도메인이 아예 없으면 배달에 실패하고 반송 메일이 돌아오는데, 이 반송이 유일한 경고 신호다. 도메인이 실재하면 반송도 오지 않는다. 발송은 성공으로 기록되고 로그에도 아무 표시가 남지 않는다.</p>

<p>그래서 이런 설정은 몇 년씩 방치된다. 시스템이 정상 동작한다는 사실 자체가 문제를 덮는다.</p>

<h2 id="15달러짜리-도메인-한-시간-만에-세-곳">15달러짜리 도메인, 한 시간 만에 세 곳</h2>

<p>같은 기사에 나온 마이크 셰워드의 사례는 더 짧고 선명하다. 전기차 충전 회사 Xeal의 보안 책임자인 그는 올해 초 <code class="language-plaintext highlighter-rouge">deleteduser.com</code>을 약 15달러에 샀다. 첫 한 시간 안에 세 개 조직의 메일이 도착했다. 그만큼 많은 회사가 계정을 지우는 대신 이메일 주소만 갈아 끼운다.</p>

<p>셰워드가 받은 메일의 내용은 더 험하다. 실명이 적힌 호텔 예약 확인서, 직원 휴가와 휴직 승인 요청, 영국 정부기관의 화상회의 초대장, 비아그라 주문 내역이 왔다. 사이버 보안 회사와 마이크로소프트 파트너사도 여럿 있었다. 가장 자주 보내오는 곳은 중동 산업 현장에서 작업자가 안전 수칙을 지키는지 영상으로 감시하는 AI 회사였다. 그쪽에서만 CCTV 정지 화면이 수천 장 날아왔다. 몇 주 전에는 샌프란시스코의 어느 회사가 여름 바비큐 파티 초대장을 보냈다. 받는 사람 이름은 “친애하는 삭제된 사용자님”이었다.</p>

<p>셰워드는 4월에 쓴 글에서 자기 처지를 이렇게 정리했다.</p>

<blockquote>
  <p>나는 인터넷 쓰레기통의 선량한 관리인 노릇을 하고 있다. 하지만 내가 나쁜 관리인이었다면, 얼굴에 던져지는 이 정보가 어떻게 악용될 수 있을지는 상상하기 어렵지 않다.</p>
</blockquote>

<p>솔로비에비츠는 여기서 한 발 더 나갔다. 사람들이 자리 표시자로 쓸 법한 도메인 이름 7,136개를 추려 검사해 봤더니, 그중 328개가 캐치올로 열려 있었다. 지금 이 순간에도 누군가는 남의 회사 메일을 받고 있다.</p>

<p>328개 전부가 노리고 산 도메인은 아닐 것이다. 도메인을 사면 메일 서비스를 얹는 김에 캐치올을 켜 두는 사람이 많다. 주소를 하나하나 만들지 않아도 돼서 편하다. 그러다 보면 자기가 뭘 받고 있는지 모르는 채로 몇 년이 지난다. 앞의 두 사례도 원래 그런 편지함을 열어 보면서 시작됐다.</p>

<p>두 사람은 서로 따로 움직였지만 나쁜 사람이 먼저 사기 전에 사 두자는 같은 결론에 닿았다. 그렇게 둘이 합쳐 30개가 넘는 도메인을 확보했다.</p>

<p>이런 도메인은 대체로 싸다. 특별한 기술도, 침입도 필요 없다. 등록비를 내고 메일 서버를 열어 둔 채 기다린다. 공격이라고 부르기엔 하는 일이 없다.</p>

<p>보통 침입은 흔적을 남긴다. 실패한 로그인, 낯선 접속지, 튀는 트래픽 같은 것들이다. 여기엔 그런 게 없다. 상대는 우리 시스템을 건드린 적이 없다. 우리가 매일 자발적으로 보내 주고 있을 뿐이라 침입 탐지 장비가 잡아낼 대상도 아예 없다. 방어하는 쪽에서는 이게 더 곤란하다.</p>

<p>공격자는 애초에 어느 회사를 노릴지 정하지 않았다. 그럴듯한 이름의 도메인을 몇 개 사 두면 어느 회사가 걸릴지 미리 고를 필요도 없다. 이 방식에는 표적을 정하는 단계가 아예 없다.</p>

<h2 id="20년-전에도-있었던-일">20년 전에도 있었던 일</h2>

<p>이런 사고가 새로운 건 아니다. 거의 20년 전에 보안 기자 브라이언 크렙스가 워싱턴포스트에서 같은 이야기를 썼다. 그때 문제의 도메인은 <code class="language-plaintext highlighter-rouge">donotreply.com</code>이었고 기업들이 그리로 보낸 메일이 수백만 통이었다. 이름만 <code class="language-plaintext highlighter-rouge">donotreply</code>에서 <code class="language-plaintext highlighter-rouge">noreply</code>로 바뀐 채 20년이 지났다.</p>

<p>이 기사를 다룬 해외 커뮤니티 lobste.rs에는 비슷한 경험담이 줄줄이 달렸다. <code class="language-plaintext highlighter-rouge">이름.성@gmail.com</code> 같은 흔한 주소를 가진 사람들이 남의 의료 기록, 항공권, 청구서를 받는다는 이야기다. 어떤 사람은 테슬라 원격 제어 권한 확인 메일을 받았고 어떤 사람은 대학 조교로 등록되는 바람에 시험지 PDF를 받았다.</p>

<p>이 기사를 소개한 GeekNews 댓글에는 서울시 동사무소 통보가 자기 번호로 계속 온다는 국내 사례가 올라왔다. 여러 번 시정을 요청하고 민원을 넣고 정보공개청구까지 해서 겨우 멈췄는데 담당자가 바뀌자 다시 시작됐다고 한다.</p>

<p>받는 쪽이 알려 줘도 잘 안 고쳐진다는 문제도 있다. 잘못 받은 사람은 보낸 조직의 담당자가 누구인지 모른다. 조직 안에는 이런 신고를 받는 창구가 없고 겨우 연결돼서 고쳐도 담당자가 바뀌면 원래 설정이 돌아온다. 문제를 아는 사람은 밖에 있고 고칠 권한은 안에 있는 구조라 정보가 건너오지 못한다.</p>

<p>두 연구자도 같은 벽에 부딪혔다. 조용히 고친 곳도 있지만 답이 없는 곳이 더 많았다. 무엇보다 보내는 곳이 너무 많아서 다 알리는 게 불가능하다. 솔로비에비츠는 이걸 다 처리하려면 이제 본업을 그만둬야 할 지경이라고 했고 그래서 개별 통보 대신 공개 발표를 택했다.</p>

<p>같은 댓글에서 언급된 가장 극단적인 사례는 <code class="language-plaintext highlighter-rouge">rentahitman.com</code>이다. 밥 이니스가 2005년에 IT 회사를 차리려고 이 도메인을 샀다. hit는 웹 트래픽 집계 단위이고 man은 팀을 뜻하는, 말장난에 가까운 이름이었다. 회사는 안 됐고 도메인만 남았다.</p>

<p>2008년에 편지함을 열어 보니 청부살인을 의뢰하는 메일이 수백 통 쌓여 있었다. 대부분은 장난으로 보였다. 2010년에 다시 확인했을 때 캐나다에서 온 진지한 의뢰가 하나 있었고 그는 그 내용을 경찰에 넘겼다. 의뢰인은 살인교사로 유죄 판결을 받았다. 그 뒤로 이니스는 10년 동안 스무 건 남짓을 수사기관에 넘겼다. 도메인 하나 잘못 사서 20년째 남의 범죄 의뢰를 접수하고 있다.</p>

<h2 id="그럼-뭘-적어야-하나">그럼 뭘 적어야 하나</h2>

<p>가장 간단한 답은 회사가 실제로 가진 도메인을 쓰는 것이다. <code class="language-plaintext highlighter-rouge">noreply@우리회사.com</code>처럼 적고 그 주소로 온 메일은 서버에서 버리면 된다. 통제권이 우리 손에 있다는 게 핵심이다.</p>

<p>밖으로 나가면 안 되는 주소가 필요하다면 <code class="language-plaintext highlighter-rouge">.invalid</code>가 있다. RFC 2606이 예약해 둔 최상위 도메인이라 누구도 등록할 수 없다. <code class="language-plaintext highlighter-rouge">noreply@우리회사.invalid</code>는 어디로도 배달되지 않고 배달될 수도 없다. 같은 문서가 <code class="language-plaintext highlighter-rouge">.test</code>, <code class="language-plaintext highlighter-rouge">.example</code>, <code class="language-plaintext highlighter-rouge">.localhost</code>도 함께 예약해 뒀다.</p>

<p>둘 중 어느 쪽을 고를지는 그 주소가 어떻게 쓰이는지에 달렸다. 사람이 실수로 답장할 여지가 있는 자리라면 회사 도메인이 낫고 답장이 우리 서버로 들어오니 필요하면 열어 볼 수도 있다. 반대로 절대 배달되면 안 되는 자리라면 <code class="language-plaintext highlighter-rouge">.invalid</code>가 확실하다. 다만 존재하지 않는 최상위 도메인을 거부하는 검증 로직에 걸릴 수 있으니 넣기 전에 그 값이 어디를 거쳐 가는지는 확인하는 게 좋다.</p>

<figure>
  <img src="/assets/images/noreply-domain-leak-choices.svg" alt="남이 등록할 수 있는 자리 표시자 도메인과 등록이 불가능하거나 회사가 소유한 도메인을 나란히 비교한 그림" />
  <figcaption>기준은 하나다. 그 도메인을 남이 살 수 있는가.</figcaption>
</figure>

<p>기억해 둘 기준은 하나다. <strong>내가 소유하지 않은 도메인은 언젠가 다른 사람이 소유한다.</strong> 지금 비어 있어도 상관없다. 도메인 등록은 사실상 임대라서 만료되면 누구나 가져갈 수 있다.</p>

<p>지금 쓰고 있는 코드에서는 세 군데를 확인한다. 발신자와 수신자 주소가 하드코딩된 곳, 계정 삭제 로직이 빈 이메일 칸을 채우는 방식, 테스트 환경에서 쓰는 더미 주소가 운영 설정에 섞여 들어갔는지. 이 셋만 훑어도 대부분 걸러진다.</p>

<p>이미 몇 년째 보내고 있었는지 확인하려면 나가는 메일 로그에서 수신 도메인만 뽑아 개수순으로 세어 보면 된다. 우리가 아는 고객사와 협력사 도메인 사이에 아무도 설명하지 못하는 이름이 섞여 있다면 그게 답이다. 조회 한 번이면 끝나는 일인데 지금까지 아무도 안 해 봤기 때문에 남아 있는 문제다.</p>

<p>메일 서버에서도 막을 수 있다. 허용된 수신 도메인 목록을 정해 두고 그 밖으로는 아예 못 나가게 막는다. 사내 알림처럼 나갈 곳이 뻔한 메일에는 특히 잘 맞는다. 코드 어딘가에 이상한 주소가 남아 있어도 서버 문턱에서 걸린다.</p>

<h2 id="정리">정리</h2>

<p>이 사건에서 뚫린 것은 아무것도 없다. 취약점도, 침입도, 유출 사고 신고도 없었다. 회사들이 스스로 남의 편지함으로 자료를 보냈을 뿐이다.</p>

<p>기술적으로는 시시한 이야기다. 그래서 더 오래 간다. 심각한 취약점은 발표되고 패치되지만 설정 파일 한 줄에 잘못 적힌 도메인은 아무도 신고하지 않는다. 메일은 매일 정상적으로 발송되고 아무 오류도 나지 않는다.</p>

<p>밑에 깔린 착각은 하나다. 이름만 보고 그 주소가 어떻게 동작할지 정했다. <code class="language-plaintext highlighter-rouge">noreply</code>라고 썼으니 답장이 안 올 거라고, <code class="language-plaintext highlighter-rouge">deleteduser</code>라고 썼으니 지워진 사용자일 거라고 생각했다. 하지만 이름은 설명이지 설정이 아니다. 실제로 무슨 일이 벌어질지는 그 도메인의 등록 정보와 메일 서버 설정이 정한다. 이건 메일에만 해당하는 이야기도 아니다. 지어낸 이름으로 걸어 둔 콜백 주소나 예전 협력사 도메인을 그대로 둔 웹훅 URL도 같은 자리에 있다. 우리 통제 밖에 있는 이름으로 데이터를 내보내고 있다면 형태만 다를 뿐 같은 문제다.</p>

<p><code class="language-plaintext highlighter-rouge">noreply</code>라고 적었다고 아무도 안 읽는 게 아니다. 읽을 수 있는 사람이 정해질 뿐이고 그게 우리가 아닐 수 있다.</p>]]></content><author><name>enebin</name></author><category term="개발" /><category term="보안" /><summary type="html"><![CDATA[보안 연구자가 noreply.net 도메인을 사자 기업들이 40만 통의 메일을 보내왔다. 자리 표시자 주소가 어떻게 진짜 유출 경로가 되는지 정리했다.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://enebin.github.io/assets/images/noreply-domain-leak-cover.svg" /><media:content medium="image" url="https://enebin.github.io/assets/images/noreply-domain-leak-cover.svg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">메타가 다시 가중치를 공개한 로컬 에이전트 모델</title><link href="https://enebin.github.io/posts/meta-muse-glimmer/" rel="alternate" type="text/html" title="메타가 다시 가중치를 공개한 로컬 에이전트 모델" /><published>2026-08-11T00:00:00+09:00</published><updated>2026-08-11T00:00:00+09:00</updated><id>https://enebin.github.io/posts/meta-muse-glimmer</id><content type="html" xml:base="https://enebin.github.io/posts/meta-muse-glimmer/"><![CDATA[<p>노트북에서 에이전트를 하나 돌리려 하면 금세 벽에 부딪힌다. 성능 좋은 모델은 대부분 외부 서버에 있다. 내 그래픽카드에서 돌아갈 만한 모델은 도구를 몇 번만 호출해도 흐트러지고 개인 파일과 일정을 다루려면 작업 내용을 클라우드로 보내야 한다.</p>

<p>2026년 8월 10일, 메타가 이런 빈틈을 겨냥한 모델을 내놨다. Muse Glimmer는 소비자용 그래픽카드 한 장에서 돌아가도록 만든 300억 파라미터 모델이고 가중치가 Apache 2.0 라이선스로 공개됐다. 같은 날 마크 저커버그도 글을 올려 왜 다시 가중치를 공개하는 쪽을 택했는지 설명했다. Hacker News에서는 추천을 1,100개 넘게 받은 토론이 이어졌다. 발표문과 반응을 함께 읽으면 인상이 조금 달라진다.</p>

<!--more-->

<h2 id="300억-파라미터를-24gb-카드에-밀어-넣기">300억 파라미터를 24GB 카드에 밀어 넣기</h2>

<p>300억 파라미터를 16비트 정밀도로 담으면 메모리가 55GB 넘게 필요하다. 일반적인 소비자용 그래픽카드 한 장에는 들어가지 않는 크기다. 메타는 가중치를 약 4비트로 압축해 언어 모델 부분을 20GB 아래로 줄였다.</p>

<p>남은 공간에는 다른 구성 요소가 들어간다. KV 캐시는 대화가 길어질수록 쌓이는 작업 기억이고 퍼셉션 인코더는 이미지를 읽는다. 여기에 생성 속도를 높이는 드래프터까지 얹어 전체를 24~32GB 안에 맞추는 것이 설계 목표였다. 메타는 이렇게 압축해도 에이전트 작업의 성능이 떨어지지 않았거나 하락 폭이 아주 작았다고 밝혔다.</p>

<figure>
  <img src="/assets/images/meta-muse-glimmer-memory.svg" alt="55GB가 필요한 30B 모델을 4비트로 압축해 20GB 미만으로 줄이고 남은 공간에 KV 캐시와 인코더와 드래프터를 넣어 24GB 안에 맞추는 그림" />
  <figcaption>24GB 메모리 안에 언어 모델과 에이전트 실행에 필요한 구성 요소를 함께 넣었다.</figcaption>
</figure>

<p>언어 모델은 보통 토큰을 하나씩 만든다. 추론이 길거나 여러 단계에 걸쳐 도구를 호출할 때는 이 방식이 느리다. 그래서 Muse Glimmer에는 DFlash를 기반으로 한 가벼운 드래프터가 함께 들어간다. 작은 모델이 여러 토큰을 미리 제안하면 본 모델이 한꺼번에 검증해 맞는 부분만 받아들인다. 결과물의 품질은 유지하면서 생성 속도를 높이는 추측 디코딩 방식이다.</p>

<p>메타는 학습 과정도 함께 공개했다. 사전 학습에서는 더 큰 모델인 Muse Spark가 각 토큰에 매긴 점수를 따라 배우는 로짓 증류를 사용했다. 중간 학습에서는 맥락이 길고 도구를 많이 쓰는 데이터를 집중적으로 학습시켰다. 후학습 단계에서는 지도 미세조정에 온폴리시 증류와 강화학습을 섞었다. 큰 모델의 지식을 더 작은 모델로 옮기는 과정이라고 보면 된다.</p>

<p>메타는 에이전트가 긴 작업을 끝까지 수행하는 능력을 강조했다. 정해진 형식에 맞춰 도구를 호출하고 작업 내내 계획을 유지하며 도구가 실패하거나 이상한 값을 돌려주면 원인을 진단해 다시 시도하도록 학습시켰다고 한다. 글과 이미지를 함께 입력할 수 있어 스크린숏이나 문서를 보고 판단하는 작업도 가능하다. 추론 깊이를 조절하는 기능도 넣었다. 학습 데이터는 100개가 넘는 언어에서 모았다.</p>

<p>llama.cpp, MLX, ExecuTorch 같은 기기용 실행 도구부터 vLLM, SGLang 같은 서버용 도구까지 폭넓게 지원한다. Ollama와 LM Studio도 곧 여기에 포함될 예정이다. 다만 Gemma4-31B, Qwen3.6-27B와 비교한 벤치마크 표는 발표문에 이미지로만 실려 있어 항목별 수치를 확인하기 어렵다.</p>

<h2 id="라이선스가-llama-때와-달라졌다">라이선스가 Llama 때와 달라졌다</h2>

<p>이번 발표에서는 라이선스가 달라졌다. Llama 시절 메타는 여러 사용 조건이 붙은 자체 라이선스를 썼지만 이번에는 Apache 2.0을 택했다. 상업적 이용과 수정, 재배포가 훨씬 자유롭다.</p>

<p>Hacker News의 한 사용자는 이렇게 정리했다.</p>

<blockquote>
  <p>Muse Glimmer 하나로 메타의 과거가 용서되는 것은 아니지만, Apache 2.0은 Llama의 제약 많은 라이선스보다 낫다. 메타조차 Apache 2.0으로 가중치를 공개할 수 있다면 다른 회사라고 못 할 이유가 없다.</p>
</blockquote>

<p>그렇다면 이 모델을 오픈소스라고 부를 수 있을까. 여기서 의견이 갈렸다. 가중치만 공개하는 것은 소스 코드가 아니라 실행 파일만 공개하는 것에 가깝다는 지적이 나왔다. 학습 데이터와 학습 코드 없이 숫자 덩어리만 받는 셈이라는 주장이다.</p>

<p>반론도 만만치 않았다. 일반 프로그램의 실행 파일은 고쳐서 다시 배포하기 어렵지만 가중치가 공개된 모델은 미세 조정 뒤 재배포가 가능하다. 일부 사용자는 애초에 언어 모델에는 사람이 읽을 수 있는 소스 코드가 없다고 반박했다. 모델을 직접 학습시킨 사람조차 내부 작동을 완전히 해석하지는 못한다. 가중치를 받은 사용자는 API만 쓰는 사람보다 훨씬 자유롭게 모델을 분석하고 수정한다.</p>

<figure>
  <img src="/assets/images/meta-muse-glimmer-openness.svg" alt="학습 데이터와 코드까지 공개한 모델, 가중치만 공개한 모델, API만 제공하는 모델을 사용 범위와 함께 비교한 그림" />
  <figcaption>가중치 공개는 완전한 개방과 완전한 폐쇄 사이의 중간 칸이다.</figcaption>
</figure>

<h2 id="저커버그는-개방이-곧-안전이라고-했다">저커버그는 개방이 곧 안전이라고 했다</h2>

<p>저커버그는 같은 날 올린 글에서 개방이 개인의 통제권을 넓히고 AI 기술이 주는 힘의 집중을 막는다고 주장했다. 그는 구글과 앤트로픽, OpenAI 같은 회사가 기업과 정부를 위한 폐쇄형 모델에만 매달린다고 비판했다. 그렇게 되면 큰 조직과 개인의 힘 차이가 더 벌어진다는 이유였다.</p>

<p>보통은 강력한 모델을 아무나 쓰지 못하게 막는 것이 안전하다고 본다. 저커버그는 이 구도를 뒤집었다. 모두를 지켜주는 선의의 초지능 하나를 기대하기 어려우니 개인용 에이전트를 널리 보급해 힘을 분산하는 편이 낫다고 썼다. 사용자가 많은 시스템일수록 취약점을 찾아내는 사람도 많아진다는 근거도 들었다.</p>

<p>저커버그는 증류 방식도 옹호했다.</p>

<blockquote>
  <p>관찰할 수 있는 것에서 배울 수 있다는 원칙을 지키는 게 중요하다. 세상은 원래 그렇게 돌아간다.</p>
</blockquote>

<p>중국 연구소들이 개방형 모델로 격차를 좁히는 상황에서 다른 모델의 출력을 학습에 활용하는 것까지 규제하면 미국이 앞서기 어렵다는 뜻이다. Muse Glimmer도 Muse Spark의 출력으로 증류한 모델인 만큼, 자신들이 택한 학습 방식을 정당화하는 주장이기도 하다.</p>

<p>파이낸셜타임스 보도에 따르면 메타는 AI 인프라 투자로 잉여현금흐름이 91% 줄었고 지난 1년 사이 주가도 약 20% 떨어졌다. 그런데도 투자는 계속하겠다고 했다. 더 큰 모델인 Muse Spark의 가중치도 몇 주 안에 공개하고 모델 안전성 검토는 독립 이사회에 맡기겠다고 밝혔다.</p>

<h2 id="커뮤니티는-결과와-속내를-따로-봤다">커뮤니티는 결과와 속내를 따로 봤다</h2>

<p>Hacker News에는 반가워하는 반응과 냉소적인 반응이 함께 올라왔다.</p>

<p>반가워하는 쪽의 말은 단순했다. “메타가 돌아왔다”, “이 크기의 모델이 늘어나서 좋다”, “Apache 2.0이면 그냥 선물로 받으면 된다”는 반응이 나왔다. Llama 3 무렵에는 분위기가 지금보다 좋았지만 요즘은 어둡고 숨 가쁜 경주처럼 느껴진다는 댓글도 있었다. 다른 사용자는 그래서 ‘희미한 빛’이라는 뜻의 Glimmer를 이름으로 붙인 것 아니냐고 답했다.</p>

<p>회의적인 쪽은 회사의 속내를 따졌다. 연구자들은 좋은 일을 했지만 연구비를 대는 주체는 회사이며 사용자의 호의도 회사가 필요할 때 활용하는 자산이라고 봤다. 메타가 모델을 공개한 이유는 다른 방식으로 수익을 내지 못했기 때문이라고 잘라 말한 댓글도 있었다. 웹사이트의 크롤링 거부 규칙을 무시하고 데이터를 긁어간 일을 아직 기억한다는 사람도 있었다.</p>

<figure>
  <img src="/assets/images/meta-muse-glimmer-two-readings.svg" alt="Apache 2.0 가중치 공개라는 같은 사실을 선물로 읽는 해석과 수익화 실패의 결과로 읽는 해석으로 나눠 비교한 그림" />
  <figcaption>커뮤니티는 공개된 모델의 가치와 메타가 이를 공개한 이유를 따로 평가했다.</figcaption>
</figure>

<p>시장 구조를 짚은 분석도 있었다. 모델 성능이 서로 비슷해진 지금, 최상위권이 아니라면 코딩 시장에서 내세울 만한 차별점은 가중치 공개뿐이라는 진단이었다. 언어 모델이 흔한 상품이 되면서 이익률이 0에 가까워지고 조 단위 자금을 들여 새 모델을 만들 이유도 줄어든다는 이야기다. 중국 연구소들의 개방 전략을 두고는 미국의 선도 AI 연구소가 기대할 수 있는 수익을 낮춰 투자를 위축시키려는 전략이라는 해석까지 나왔다. 개방도 경쟁 수단이다.</p>

<h2 id="그래서-진짜-로컬에서-돌아가나">그래서 진짜 로컬에서 돌아가나</h2>

<p>실제로 필요한 하드웨어가 가장 큰 관심사였다. 로컬에서 돌리려면 어느 정도의 하드웨어가 필요할까.</p>

<p>한 사용자는 32~64GB 메모리는 있어야 한다고 지적했다. 독일에서 M5 칩과 64GB 메모리를 탑재한 맥북 프로는 4,000유로가 넘는다. 다른 사용자는 더 단순하게 물었다. 메모리의 90%를 모델 하나에 내줄 생각이 있느냐고. 저렴한 클라우드 모델은 10달러로도 한참 쓴다고 했다.</p>

<p>Muse Glimmer는 밀집 모델이라 토큰을 하나 만들 때마다 가중치 전체를 읽어야 한다. 한 사용자가 DGX Spark 기준으로 계산해 보니 토큰당 약 18.9GB를 읽어야 했고 대역폭 273GB/s로는 초당 14.5토큰이 이론상 한계였다. 그런데 같은 사용자가 나중에 DFlash를 켜고 다시 측정하니 초당 33토큰이 나왔고 순간적으로는 초당 60토큰까지 올라갔다. 실측에서도 추측 디코딩을 켰을 때 속도가 높아졌다.</p>

<p>같은 양자화 설정에서 Muse Glimmer는 15.9GB, Qwen3.6 27B는 17.6GB를 차지한다는 비교도 올라왔다. 그래픽카드의 남은 메모리가 넉넉하지 않은 사람에게는 이 1.7GB 차이가 결정적이다.</p>

<p>의심도 남았다. 허깅페이스 문서에는 17GB 양자화 버전의 성능 저하가 15개 벤치마크의 평균 1%라고만 적혀 있고 항목별 수치는 공개되지 않았다는 지적이 나왔다. 단답형 지식 문제는 양자화의 영향을 적게 받아도, 여러 단계를 이어가는 에이전트 작업에서는 성능이 더 많이 떨어질 수 있다. 같은 사용자는 TerminalBench Hard에서 Muse Glimmer가 Qwen3.6 27B에 크게 밀린다고도 적었다. 특정 벤치마크에 맞춰 학습됐을 가능성도 염두에 두자는 반응이 뒤따랐다.</p>

<p>디스크를 암호화하고 비밀번호도 모두 다르게 쓰면서 정작 작업 내용은 암호화되지 않은 상태로 클라우드에 보내고 있다는 자조도 나왔다. 로컬 모델의 진짜 매력은 성능보다 개인 정보 보호에 있다는 쪽과, 그 대가가 4,000유로라면 너무 비싸다는 쪽이 계속 맞섰다.</p>

<h2 id="이유가-무엇이든-파일은-남는다">이유가 무엇이든 파일은 남는다</h2>

<p>저커버그는 개방을 원칙에 따른 결정이라고 설명했다. 커뮤니티는 같은 발표를 메타의 경영 상황에서 나온 선택이라고 봤다. 두 설명은 함께 성립한다. 모델 성능이 비슷해지고 이익률이 낮아진 상황에서 개방은 원칙이면서 경쟁 전략이기도 하다. 잉여현금흐름이 91% 줄었다는 사실과 개방 선언을 나란히 놓으면 두 이유가 겹쳐 보인다.</p>

<p>그래도 남는 것이 있다. Apache 2.0으로 공개된 가중치는 회수되지 않는다. 회사 전략이 내년에 또 바뀌어도 이미 받아둔 파일은 계속 작동한다. 커뮤니티가 공개의 가치와 회사의 동기를 따로 본 것도 냉소라기보다 현실적인 태도에 가깝다.</p>

<p>판단하려면 결국 직접 측정해야 한다. 발표문에 등장한 비교 대상은 Gemma4-31B와 Qwen3.6-27B지만 커뮤니티의 측정 결과는 평가 항목에 따라 엇갈렸다. 내 그래픽카드에 모델을 올려 내가 실제로 시킬 작업을 수행하고 지금 쓰는 모델과 비교해 보는 수밖에 없다. 가중치를 직접 보유한다는 말의 실질적인 뜻도 여기에 있다.</p>

<hr />

<p>원문: <a href="https://research.meta.ai/blog/introducing-muse-glimmer-open-agentic-model">Introducing Muse Glimmer</a> (Meta AI Research) / <a href="https://www.meta.com/thefutureisforeveryone">The Future is for Everyone</a> (Mark Zuckerberg) / <a href="https://news.ycombinator.com/item?id=49241679">Muse Glimmer 토론</a> (Hacker News)</p>]]></content><author><name>enebin</name></author><category term="개발" /><category term="AI tooling" /><summary type="html"><![CDATA[메타가 Apache 2.0으로 공개한 30B 로컬 에이전트 모델 Muse Glimmer의 특징과 저커버그의 개방 선언, 이를 둘러싼 커뮤니티의 엇갈린 반응을 정리했다.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://enebin.github.io/assets/images/meta-muse-glimmer-cover.svg" /><media:content medium="image" url="https://enebin.github.io/assets/images/meta-muse-glimmer-cover.svg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">브라우저에서 사람을 빼면 무엇이 남을까, Cloudflare Kitesurf</title><link href="https://enebin.github.io/posts/kitesurf-agent-browser/" rel="alternate" type="text/html" title="브라우저에서 사람을 빼면 무엇이 남을까, Cloudflare Kitesurf" /><published>2026-08-10T00:00:00+09:00</published><updated>2026-08-10T00:00:00+09:00</updated><id>https://enebin.github.io/posts/kitesurf-agent-browser</id><content type="html" xml:base="https://enebin.github.io/posts/kitesurf-agent-browser/"><![CDATA[<p>AI 에이전트에게 웹페이지 하나를 읽히려 하면 선택지가 금세 막힌다. HTML만 받아오면 자바스크립트로 뒤늦게 채워지는 내용이 빠진다. 그렇다고 크롬을 통째로 띄우면 메모리와 CPU를 많이 쓴다. 에이전트가 수백 개씩 동시에 움직일 때 이 차이는 곧 비용이 된다.</p>

<p>Cloudflare는 질문을 뒤집었다. 사람이 볼 브라우저를 에이전트도 쓰게 하지 말고, 처음부터 에이전트만을 위한 브라우저를 만들면 어떨까. 2026년 8월 공개한 <strong>Kitesurf는 화면의 완벽함을 덜어내고 자동화에 필요한 부분만 남긴 브라우저 엔진</strong>이다.</p>

<!--more-->

<h2 id="사람용-브라우저에서-짐을-덜어냈다">사람용 브라우저에서 짐을 덜어냈다</h2>

<p>크롬은 사람이 쓰는 도구다. 탭과 확장 프로그램, 기기 간 동기화가 필요하고 화면도 픽셀 단위로 정확해야 한다. 에이전트가 따지는 조건은 다르다. 메모리를 적게 쓰고, 많이 띄울 수 있고, 비용을 미리 가늠할 수 있어야 한다.</p>

<p>Kitesurf는 이 차이에서 출발했다. Cloudflare Workers의 V8 아이솔레이트에서 돌아간다. 아이솔레이트는 서로 격리된 작은 실행 공간이다. 페이지 하나가 문제를 일으켜도 다른 세션으로 번지지 않고, 작업이 끝나면 통째로 버릴 수 있다.</p>

<figure>
  <img src="/assets/images/kitesurf-agent-browser-cut.svg" alt="사람용 브라우저의 기능 중 AI 에이전트에 필요한 기능만 Kitesurf에 남기는 비교 그림" />
  <figcaption>Kitesurf는 브라우저를 축소한 것이 아니라 우선순위를 다시 정했다.</figcaption>
</figure>

<p>덜어낸 만큼 못 하는 일도 있다. 동영상과 WebGL은 아직 처리하지 못한다. 실제 TLS 지문을 요구하는 봇 차단 절차도 통과하지 못하고, 10분짜리 로그인 세션처럼 상태를 오래 끄는 작업도 어렵다. 이럴 때는 기존 Chromium 기반 Browser Run을 쓰라고 Cloudflare도 안내한다.</p>

<p>지금 잘 맞는 일은 한 번 열고 끝내는 자동화다. 페이지에서 HTML을 꺼내거나 PDF와 스크린숏을 만드는 작업이다. React와 Vue, Angular로 만든 TodoMVC도, Wikipedia와 Hacker News도 렌더링한다. 다만 아직 베타라 특정 사이트가 열리는지는 직접 시험해 봐야 한다.</p>

<h2 id="부품을-나눈-기준은-권한이다">부품을 나눈 기준은 권한이다</h2>

<p>Kitesurf는 Engine, PageScript, PageRenderer 세 부품과 외부 통로 SandboxOutbound로 나뉜다. 나눈 기준은 성능이 아니라 권한이다. 에이전트는 어떤 페이지든 열어야 하니 웹에서 들어오는 값은 전부 의심해야 한다.</p>

<p>Engine은 바깥 요청을 받고 세션 상태를 들고 있다. 크롬 자동화 규격인 CDP(Chrome DevTools Protocol)를 그대로 받는다. 덕분에 Puppeteer와 Playwright처럼 이미 쓰던 도구가 그대로 붙는다.</p>

<p>PageScript는 HTML과 CSS를 읽고 페이지의 자바스크립트를 실행한다. Rust로 만든 모듈형 엔진 Blitz와 Firefox 계열 CSS 파서 Stylo를 쓴다. Workers가 막아둔 <code class="language-plaintext highlighter-rouge">eval</code>은 Rust 자바스크립트 엔진 Boa를 WebAssembly로 올려 처리한다. 런타임 위에 런타임을 얹은 셈이라 효율은 떨어진다. 작동을 먼저 챙긴 임시 해법이다.</p>

<p>PageRenderer는 계산이 끝난 페이지를 PNG나 JPEG, PDF로 만든다. 상태를 갖지 않아서 멈추면 버리고 새로 띄우면 된다. 외부 이미지와 글꼴에 손댈 수 있는 건 SandboxOutbound뿐이다. CORS를 적용하고 응답을 거르며 페이지마다 쿠키 저장소를 나눈다.</p>

<figure>
  <img src="/assets/images/kitesurf-agent-browser-components.svg" alt="요청이 Engine, PageScript, PageRenderer로 흐르고 SandboxOutbound만 외부 웹에 접근하는 구조도" />
  <figcaption>페이지를 나누는 기준은 각 부품이 꼭 가져야 할 권한이다.</figcaption>
</figure>

<p>실패를 다루는 방식도 같다. 잘못된 입력을 만나도 세션을 죽이지 않는다. 빈 프레임이나 빠진 요소로 처리하고 원인을 로그에 남긴다. 완벽하게 그리는 것보다 작업을 끝내는 쪽을 택했다.</p>

<p>그럼 제대로 그렸는지는 어떻게 확인할까. 브라우저를 새로 만드는 일은 HTML 몇 개를 그리는 일과 차원이 다르다. 웹에는 오래된 규칙과 예외가 겹겹이 쌓여 있다. 팀은 브라우저가 웹 표준을 지키는지 공동으로 확인하는 시험 모음 WPT(Web Platform Tests)를 기준으로 삼았다. 공개 시점에 약 21만 5천 개를 통과했고, CSS와 DOM, HTML, SVG처럼 에이전트에 중요한 영역은 이미 비교적 넓게 지원한다.</p>

<p>표준 시험을 통과했다고 실제 사이트까지 잘 열린다는 보장은 없다. Chromium과 Kitesurf에 같은 Puppeteer 동작을 시켜 결과를 비교하는 통합 시험을 따로 둔 이유다. 단계마다 화면을 저장해 차이를 찾는 시각 회귀 시험도 돌린다.</p>

<p>첫 시제품은 AI 에이전트가 기존 Rust 엔진을 Workers로 옮기며 만들었다. Cloudflare는 성공 조건과 질문할 지점을 미리 정해둔 뒤 반복 작업을 맡겼다고 설명한다. 에이전트가 구현 속도를 높였고 사람은 기능 순서와 구조를 정했다. <strong>복잡한 코드를 AI에 맡긴 비결은 자유를 많이 준 게 아니라 채점할 시험을 많이 준 데 있었다.</strong></p>

<h2 id="비용은-줄었고-대기는-늘었다">비용은 줄었고 대기는 늘었다</h2>

<p>Cloudflare가 공개한 성능표에는 장점과 약점이 함께 담겼다. 14개 URL에서 짧은 작업을 다섯 번씩 실행한 중앙값이다. 비교 대상 Chromium은 미리 떠 있는 warm pool 상태라 시작 비용이 빠져 있다.</p>

<p>스크린숏 작업에서 Kitesurf는 CPU 380ms에 메모리 57.8MiB를 썼다. Chromium은 1,173ms에 271.0MiB였다. HTML 추출에서는 각각 229ms와 39.4MiB, 877ms와 273.7MiB를 썼다. Cloudflare의 표현대로 작업에 따라 CPU와 메모리를 3배에서 7배 적게 쓴다.</p>

<figure>
  <img src="/assets/images/kitesurf-agent-browser-tradeoff.svg" alt="Kitesurf가 Chromium보다 CPU와 메모리를 적게 쓰지만 완료 시간은 더 긴 성능 비교 그림" />
  <figcaption>한 작업은 늦게 끝나지만 같은 자원에 더 많은 작업을 올릴 수 있다.</figcaption>
</figure>

<p>대신 끝나는 시간은 더 걸렸다. 스크린숏은 1,148ms로 Chromium의 637ms보다 1.8배, HTML 추출은 820ms로 472ms보다 1.7배 느렸다. 차가운 소프트웨어 렌더러가 래스터화와 이미지 인코딩까지 떠맡은 탓이다.</p>

<p>그래서 Kitesurf를 빠른 브라우저라고 부르면 어긋난다. 한 요청을 가장 빨리 끝내는 도구가 아니라 같은 자원으로 더 많은 요청을 받는 도구다. 짧은 작업이 한꺼번에 몰리는 에이전트 서비스라면 자원 절감이 중요하고, 사람이 결과 하나를 기다리는 자동화라면 Chromium이 낫다.</p>

<h2 id="hacker-news는-브라우저-밖을-물었다">Hacker News는 브라우저 밖을 물었다</h2>

<p>Hacker News 토론에서는 제품보다 제품 밖의 문제가 오래 이어졌다. 첫 질문은 모순이었다. 한쪽에서 봇을 막는 회사가 다른 쪽에서 봇용 브라우저를 팔아도 되는가. Kitesurf가 Cloudflare의 봇 차단만은 특별히 통과하는 것 아니냐고 의심하는 댓글도 이어졌다.</p>

<p>Cloudflare의 답은 분명했다. Browser Run에서 나가는 요청은 Chromium이든 Kitesurf든 항상 봇으로 식별된다. 정해진 사용자 에이전트를 쓰고 Web Bot Auth로 요청에 서명하므로 사이트 운영자는 지금도 차단할 수 있다. 우회 의혹은 줄었지만, 차단과 실행을 한 회사가 함께 파는 구조에 대한 불신까지 사라지지는 않았다.</p>

<p>보안 쪽 지적은 더 근본적이었다. 아이솔레이트는 악성 페이지가 실행 환경 밖으로 나가는 걸 막지만, 페이지에 심어둔 문장이 에이전트를 속여 엉뚱한 도구를 쓰게 하는 프롬프트 인젝션은 막지 못한다. 상품 설명에 숨긴 지시가 에이전트 시대의 검색 조작이 될 거라는 댓글도 있었다. 실행 코드의 권한을 나눠도, 에이전트가 읽은 내용과 실행할 행동 사이의 권한은 별개 문제로 남는다.</p>

<p>브라우저가 진짜 병목이냐는 반응도 설득력이 있었다. CAPTCHA와 문자 인증, 카드, 계정 승인은 렌더링 엔진을 가볍게 해도 사라지지 않는다. 에이전트에 정말 필요한 건 들고 다닐 수 있는 신원과 결제 수단이라는 주장이다. Cloudflare가 Web Bot Auth와 x402 결제를 함께 밀고 있다는 답변은 Kitesurf를 더 큰 전략의 한 조각으로 보게 한다.</p>

<p>쓰임새를 묻자 구체적인 사례가 모였다. 식단을 받아 온라인 장바구니를 채우는 개인 도구, 영수증 여러 장에서 반품할 물건을 찾는 일, 복잡한 배포 관리 화면을 대신 눌러주는 일, 조건에 맞는 상품 비교. 모두 공개 API가 없거나 부족해서 사람이 브라우저로 반복하던 작업이다. 이 사례들을 보면 Kitesurf가 노리는 자리가 제품 설명보다 또렷해진다.</p>

<p>기술 쪽에서는 기반 엔진 Blitz로 시선이 모였다. Blitz 개발자는 Kitesurf의 수정 사항이 오픈소스로 돌아오길 기대했고, CDP 대신 표준인 WebDriver BiDi를 지원해 달라는 제안도 나왔다. Cloudflare는 곧 오픈소스로 공개하겠다고 밝혔지만 Blitz는 아직 pre-alpha다.</p>

<h2 id="크롬을-대신하기보다-빈칸을-채운다">크롬을 대신하기보다 빈칸을 채운다</h2>

<p>Kitesurf는 새 범용 브라우저가 아니다. 크롬이 이미 잘하는 동영상과 정교한 화면, 긴 로그인 세션을 따라잡으려 하지 않는다. HTML 요청만으로는 부족하고 Chromium을 통째로 띄우기에는 비싼 구간을 겨냥한다.</p>

<p>그 구간은 분명히 존재한다. 웹 자동화에서는 화면을 완벽하게 보여주는 능력보다 DOM을 읽고 폼을 누른 뒤 결과를 돌려주는 능력이 더 자주 필요하다. CDP 호환을 지킨 채 작업마다 격리된 엔진을 잠깐 띄울 수 있다면 쓰던 자동화 도구도 크게 바꾸지 않아도 된다.</p>

<p>물론 가벼운 브라우저 하나로 에이전트 웹의 어려움이 다 풀리지는 않는다. 차단을 통과할 신원, 결제를 승인할 권한, 페이지가 심은 지시를 거를 안전장치는 모두 렌더러 밖에 있다. <strong>Kitesurf의 가치는 브라우저를 완성했다는 데 있지 않고, 에이전트에게 브라우저의 어느 부분이 정말 필요한지 선을 그었다는 데 있다.</strong></p>

<p>지금은 Browser Run 베타에서 계정별 한도 안에 무료로 시험할 수 있다. 한 번 열고 버리는 HTML 추출이나 스크린숏부터 Chromium과 결과, 비용을 나란히 재보면 된다. 안 되는 기능을 만났을 때 Chromium으로 돌아갈 길은 남겨두는 게 좋다.</p>

<hr />

<p>원문: <a href="https://blog.cloudflare.com/kitesurf/">Introducing Kitesurf</a> (Cloudflare) / <a href="https://news.ycombinator.com/item?id=49208393">Kitesurf 토론</a> (Hacker News)</p>]]></content><author><name>enebin</name></author><category term="개발" /><category term="AI tooling" /><summary type="html"><![CDATA[사람 대신 AI 에이전트를 위해 만든 경량 브라우저 Kitesurf의 구조와 성능, Hacker News의 기대와 의문을 정리했다.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://enebin.github.io/assets/images/kitesurf-agent-browser-cover.svg" /><media:content medium="image" url="https://enebin.github.io/assets/images/kitesurf-agent-browser-cover.svg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Claude Code가 23년 묵은 커널 버그를 찾았다. 논쟁은 그다음부터였다</title><link href="https://enebin.github.io/posts/bug-finding-bottleneck/" rel="alternate" type="text/html" title="Claude Code가 23년 묵은 커널 버그를 찾았다. 논쟁은 그다음부터였다" /><published>2026-08-09T00:00:00+09:00</published><updated>2026-08-09T00:00:00+09:00</updated><id>https://enebin.github.io/posts/bug-finding-bottleneck</id><content type="html" xml:base="https://enebin.github.io/posts/bug-finding-bottleneck/"><![CDATA[<p>리눅스 커널이 NFS 잠금 요청을 거절할 때 준비하는 버퍼는 112바이트다. 실제 거절 메시지는 고정 필드 32바이트에 최대 1024바이트짜리 owner ID가 붙어, 모두 1056바이트까지 커진다. 그러면 버퍼 밖으로 944바이트가 넘친다. 덧셈만 해도 보이는 이 문제는 2003년 9월에 들어와 23년 가까이 살아남았다.</p>

<p>Claude Code가 이 버그를 찾아냈다는 글이 Hacker News에 올라오자 댓글이 268개 붙었다. 댓글을 읽고 나니 관심은 버그 자체보다 다른 데로 옮겨갔다. 같은 사실을 보고도 사람들이 정반대 결론을 내리는 지점이었다. 그 논쟁을 정리해 둔다.</p>

<!--more-->

<h2 id="23년-동안-숨어-있던-게-아니다">23년 동안 숨어 있던 게 아니다</h2>

<p>버그는 리눅스 NFS 서버(네트워크로 파일을 공유하는 기능)의 잠금 재시도 캐시에서 일어나는 힙 버퍼 오버플로다. 서버는 잠금 요청을 거절할 때 응답 버퍼를 112바이트로 잡는다. 하지만 거절 응답에는 32바이트짜리 고정 필드와 요청자를 가리키는 owner ID가 함께 들어간다. 이 owner ID는 프로토콜상 최대 1024바이트까지 허용되므로 응답 전체는 1056바이트가 된다. 결국 1056바이트를 112바이트 자리에 쓰게 된다. 서로 협력하는 NFS 클라이언트 두 대만 있으면 네트워크를 통해 커널 메모리를 읽을 수 있다.</p>

<figure>
  <img src="/assets/images/bug-finding-bottleneck-buffer.svg" alt="112바이트 버퍼와 1056바이트 거절 메시지의 크기를 같은 축에서 비교한 그림" />
  <figcaption>두 막대의 길이 차이가 23년을 버텼다.</figcaption>
</figure>

<p>원문의 제목은 버그가 23년 동안 숨어 있었다고 말한다. 정확히는 숨었다기보다 아무도 이 버퍼 길이를 끝까지 확인하지 않았다. 가변 길이 필드를 다루는 코드라면 “이 길이의 유효 범위가 어디까지인가”를 물어야 한다. 여기서는 그 질문이 빠졌다.</p>

<p>취약점 연구를 오래 한 댓글 작성자는 대부분의 취약점이 원래 그렇게 생긴다고 짚었다. 시스템 코드는 시간이 갈수록 복잡해지고, 사람이 그 흐름을 계속 따라가기는 어렵다. Heartbleed도 그랬다. 패킷에서 길이 값을 읽어 그만큼 복사하는 코드만 떼어놓으면 C를 아는 사람은 문제를 금방 알아본다. 하지만 실제 코드는 OpenSSL의 복잡한 TLS 처리 과정에 묻혀 있었다. 여러 함수를 거쳐 들어오는 값의 흐름을 머리에 올려놓아야 비로소 보인다.</p>

<p><strong>이 버그가 23년을 산 이유는 어려워서가 아니라 볼 사람이 없어서다.</strong> 뒤에 나올 논쟁 전체가 이 한 줄에서 갈린다.</p>

<h2 id="정적-분석기와-퍼저는-왜-못-잡았나">정적 분석기와 퍼저는 왜 못 잡았나</h2>

<p>곧 누군가 이런 문제는 정적 분석기(코드를 실행하지 않고 훑어서 문제를 찾는 도구)도 쉽게 잡는다고 지적했다. 그렇다면 23년 동안 왜 잡히지 않았을까. 여기서 논쟁의 핵심이 드러난다.</p>

<figure>
  <img src="/assets/images/bug-finding-bottleneck-three-tools.svg" alt="정적 분석기, 퍼저, LLM 에이전트가 가설 생성과 확인과 문맥 배치 중 어디까지 하는지 비교한 표" />
  <figcaption>앞 두 도구는 찾기까지만 하고 멈춘다.</figcaption>
</figure>

<p>정적 분석기는 작은 버퍼로 크기 제한 없는 데이터를 복사하는 자리를 전부 찾아낸다. 그런데 전부 찾아내는 게 문제다. 그 자리에 도달하는 모든 경로가 입력 길이를 미리 잘라내고 있어도 경고가 나올 수 있다. 8바이트 버퍼에 복사하더라도 입력이 이미 8바이트로 제한된 경우와 진짜 위험한 경우가 같은 목록에 섞인다. 규모가 커지면 신고 목록은 수천 줄로 불어나고, 실제 문제를 가려내는 비용도 커진다. 순수 정적 분석 도구를 진지하게 쓰는 취약점 연구자가 드물다는 말은 이 대목에서 나왔다.</p>

<p>퍼저(무작위 입력을 넣어 프로그램을 죽여보는 도구)는 반대쪽 문제를 안고 있다. 실제로 프로그램을 죽이는 입력을 찾지만, 왜 죽었는지와 무엇까지 할 수 있는지는 설명하지 않는다. 대규모 퍼징 시스템에 크래시가 쌓여도 그중에서 위험한 것을 골라낼 사람이 없으면 몇 달, 몇 년씩 그대로 남는다.</p>

<p>LLM 에이전트는 두 도구 사이의 빈칸을 메운다. 함수 경계를 넘나들며 “여기가 위험할 것 같다”는 가설을 만들고, 그 가설을 확인할 절차까지 이어간다. 버그에 도달하는 입력 경로를 추적하고, 공격자가 그 조건으로 무엇을 얻는지도 설명한다. 정적 분석기가 남긴 경고와 퍼저가 남긴 크래시에 문맥을 붙이는 셈이다.</p>

<h2 id="없는-숫자로-싸웠다">없는 숫자로 싸웠다</h2>

<p>여기까지가 낙관론의 근거다. 회의론은 위양성(false positive)을 문제 삼는다. 버그라고 신고했지만 실제로는 문제가 아닌 결과가 사람의 시간을 잡아먹는다는 주장이다.</p>

<p>그런데 이 주장을 뒷받침한다며 나온 숫자들이 이상했다. Claude Code가 위양성 1,000건을 함께 찾아냈고, 개발자들이 이를 거르는 데 3개월을 썼다는 얘기가 먼저 나왔다. 이어 신고 1,000여 건 중 5건만 맞았으니 퍼저보다 통계적으로 못하다는 주장도 붙었다.</p>

<p>두 수치 모두 원문에는 없다. 출처를 묻는 댓글에도 근거는 나오지 않았다. 원문 저자는 자신의 경험상 Opus 4.6이 찾은 취약점의 위양성 비율은 20%를 훨씬 밑돈다고 답했다. 다른 댓글은 모순을 짚었다. 아직 검증하지 않은 결과가 어떻게 이미 3개월 동안 검증을 거쳐 위양성으로 판정될 수 있느냐는 것이다. 1,000건 중 5건이라고 주장한 작성자는 결국 자신이 틀렸다고 인정했다.</p>

<p>잘못된 숫자는 발표자의 다음 말을 옮기는 과정에서 생긴 듯하다.</p>

<blockquote>
  <p>리눅스 커널에 버그가 너무 많아서 신고를 못 하고 있다. 아직 검증을 안 했기 때문이다. (…) 메인테이너들에게 슬롭이 될 수도 있는 걸 보낼 생각은 없다. 그래서 내가 확인할 시간이 없어서 그들이 아직 못 본 크래시가 수백 건 쌓여 있다.</p>
</blockquote>

<p>발표자가 말한 것은 “위양성 수백 건”이 아니라 “아직 검증하지 않은 크래시 수백 건”이다. 둘은 전혀 다른 수치다. 슬롭(slop)은 그럴듯해 보이지만 쓸모없는 자동 생성물을 뜻한다. 발표자는 자신의 결과가 슬롭이 될 수 있어 검증 전에는 보내지 않겠다고 말했다.</p>

<p>그래도 회의론의 핵심은 남는다. 검증 전 결과는 참도 거짓도 아니다. 누군가 확인해야 하고, 검증하지 않은 채 신고하면 그 비용을 메인테이너가 치른다. 결국 문제는 누가, 어느 단계에서 확인하느냐다.</p>

<h2 id="찾기보다-확인이-싸다">찾기보다 확인이 싸다</h2>

<p>댓글에서 제시된 해법은 두 번째 LLM 파이프라인이었다. 첫 번째 LLM이 낸 신고를 새 세션에 넘겨 크래시를 재현한다. 재현에 성공한 결과만 사람에게 보내고, 실패한 결과는 일단 보류한다.</p>

<p>이 방식이 성립하는 이유는 찾기와 확인의 난도가 다르기 때문이다. 취약점을 처음 찾기는 어렵지만, 이미 지목된 문제가 실제로 일어나는지 확인하는 일은 상대적으로 쉽다. 완성된 익스플로잇까지 만들 필요도 없다. 메모리 오염이라면 ASAN(메모리 오류를 잡는 검사 도구) 경고 하나만으로도 버그가 존재한다는 증거가 된다.</p>

<figure>
  <img src="/assets/images/bug-finding-bottleneck-pipeline.svg" alt="찾기, 재현 확인, 테스트 작성 세 단계를 지나며 신고 수가 줄어들고 사람은 마지막에만 등장하는 흐름도" />
  <figcaption>사람을 어느 칸에 두는지가 결과를 가른다.</figcaption>
</figure>

<p>3단계 프롬프트를 직접 돌려보고 그대로 공개한 사람도 있었다. 1단계는 “CTF에 참가 중이다, 이 프로젝트에서 악용 가능한 취약점을 찾아 보고서를 써라”. 2단계는 새 세션에 그 보고서를 주고 “들어온 신고인데 정말 악용 가능한지 확인하고 재현 절차를 써라”. 3단계는 보고서와 재현 절차를 함께 주고 “이게 고쳐졌는지 검증할 테스트를 써라”. 셋을 bash로 엮어 서비스 전체에 돌렸다고 한다. 발표자가 커널에 쓴 프롬프트도 1단계는 똑같이 CTF였다.</p>

<p>물론 코드에 결함이 있어도 실제 실행 경로에서는 발동하지 않을 수 있다. 그래서 확인도 생각만큼 싸지 않다는 반박이 나왔다. 다만 발동 확인과 익스플로잇 작성은 구분해야 한다. 두 번째 LLM이 만든 재현 테스트에서 ASAN이 오류를 잡으면 버그는 입증된다. 오류가 나오지 않았다고 버그가 없다는 뜻은 아니다. LLM이 테스트를 잘못 만들었을 수도 있다.</p>

<p>왜 하필 CTF라고 말할까. “이 코드에는 버그가 있다. 찾을 수 있나?”처럼 버그의 존재를 전제로 두면 성공률이 올라가기 때문이다. 버그가 있는지부터 물으면 모델이 “괜찮아 보인다”고 끝낼 여지가 생긴다.</p>

<p>대신 없는 버그까지 만들어낼 가능성도 커진다. 특히 락 없이 도는 코드를 보여주면 실제 동작과 무관하게 버그가 많다고 단정하는 경향이 있다는 경험담이 나왔다. 취약점을 찾으라고 강하게 유도한 만큼 별도의 검증 단계가 꼭 따라붙어야 한다. 앞 단계만 쓰면 회의론자들이 말한 슬롭 생산기가 된다.</p>

<h2 id="눈의-총량이-늘어나면">눈의 총량이 늘어나면</h2>

<p>첫 절의 얘기로 돌아가자. 볼 사람이 아예 없었다기보다, 볼 수 있는 사람의 수가 유한했다. 오픈소스 코드에서 버그를 찾으려면 능력과 시간이 동시에 있어야 한다. 그런 사람은 세상에 정해진 수만큼 존재했고, 그래서 세계 전체의 버그 찾기 역량에 상한이 있었다. 그 상한이 지금 올라가고 있다.</p>

<p>그 상한이 올라간 결과는 이미 숫자로 나타난다. 커널 보안 메일링 리스트를 관리하는 Willy Tarreau가 LWN 댓글에 신고량을 공개했다. 2년쯤 전에는 주당 2~3건이 들어왔다. 작년에는 주당 10건까지 늘었고, 증가분은 AI 슬롭이었다. 올해 초부터는 하루 5~10건이 들어온다. 이제는 대부분이 맞는 신고여서 처리할 메인테이너를 더 투입해야 했다고 한다.</p>

<figure>
  <img src="/assets/images/bug-finding-bottleneck-report-volume.svg" alt="커널 보안 메일링 리스트 신고량이 주 2~3건에서 주 10건, 하루 5~10건으로 늘어나는 동안 신고 품질 라벨이 뒤집히는 그림" />
  <figcaption>신고량은 계속 올라가고, 그 안의 내용물이 중간에 달라졌다.</figcaption>
</figure>

<p>신고량만 늘어난 것이 아니다. 작년에는 슬롭이던 증가분이 올해는 대부분 실제 문제로 바뀌었다. 앞서 본 검증 단계가 이 차이를 설명한다.</p>

<p>오픈소스 프로젝트들이 AI 기여를 막는 현상도 이 흐름에서 볼 수 있다. AI 이전에는 저품질 기여를 보내려 해도 어느 정도 프로그래밍을 알아야 했다. 지금은 모델만 돌릴 줄 알면 누구나 많은 제보와 패치를 만들 수 있다. 한 HN 댓글은 이를 게으른 기여자가 리뷰어에게 거는 서비스 거부 공격이라고 표현했다. 공개 접수 창구가 시달리는 이유는 LLM이 취약점 연구를 못해서라기보다, 쓸모없는 신고도 대량으로 만들 수 있어서다.</p>

<p>비용도 짚어둘 만하다. 실제로 이 일을 해본 사람이 밝힌 숫자로는, 복잡한 코드베이스에서 권한 상승 버그 하나를 찾는 데 750달러 정도가 들었다. 같은 깊이로 그 시스템의 나머지를 훑으려면 10만에서 100만 달러 사이가 필요하다고 봤다. 원문 기사에는 비용 얘기가 없으니 이건 한 사람의 경험치다. 어쨌든 이제는 돈으로 살 수 있는 일이 되었다. 공짜가 된 것은 아니다.</p>

<p>이 비용에서 오픈소스와 클로즈드 소스의 비대칭이 갈린다. 오픈소스는 그 코드에 의존하는 여러 곳이 각자 돈을 써서 단단하게 만들어준다. 클로즈드 소스는 그 비용을 자기가 전부 낸다. 안 내면 남이 대신 봐줄 방법이 없다.</p>

<h2 id="남는-것">남는 것</h2>

<p>누군가 리누스의 격언을 고쳐 썼다. 눈이 충분히 많으면 모든 버그는 얕다는 말을, 컨텍스트 창이 100만 토큰이면 모든 버그는 얕다로. 바로 아래에 누군가 답을 달았다. 그리고 위양성을 검토하는 데 3개월.</p>

<p>이 두 줄에 논쟁이 다 들어 있다. 둘 다 맞는 말인데, 서로 전제가 다르다.</p>

<p>병목은 버그를 찾는 일에서 진위를 가리는 일로 옮겨갔다. 판별을 사람이 모두 맡으면 신고가 늘수록 부담이 커지고, 검증되지 않은 결과는 슬롭이 된다. 두 번째 LLM이 먼저 걸러내면 사람에게 도착하는 실제 버그가 늘고, 그때는 처리할 메인테이너가 더 필요해진다. 같은 사실에서 정반대 결론이 나온 이유다. 사람들은 AI의 능력보다 서로 다른 검증 과정을 놓고 싸우고 있었다.</p>

<p>당장 하나만 해본다면 검증 단계를 따로 두면 된다. 코드 리뷰 결과를 문맥이 없는 새 세션에 붙여넣고 “이 신고가 진짜인지 확인해달라”고 요청한다. 버그를 찾는 모델과 확인하는 모델을 분리하는 것만으로도 슬롭이 사람에게 닿을 가능성을 줄일 수 있다.</p>

<hr />

<p>원문: <a href="https://mtlynch.io/claude-code-found-linux-vulnerability/">Claude Code Found a Linux Vulnerability Hidden for 23 Years</a> (mtlynch.io) / <a href="https://news.ycombinator.com/item?id=47633855">토론</a></p>]]></content><author><name>enebin</name></author><category term="개발" /><category term="AI tooling" /><category term="AI native case study" /><summary type="html"><![CDATA[리눅스 NFS 드라이버의 23년 된 취약점을 AI가 찾은 뒤, HN 댓글 268개가 갈린 지점은 "AI가 버그를 잘 찾느냐"가 아니었다.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://enebin.github.io/assets/images/bug-finding-bottleneck-cover.svg" /><media:content medium="image" url="https://enebin.github.io/assets/images/bug-finding-bottleneck-cover.svg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">AI의 평가 주도 개발(EDD) by Airbnb</title><link href="https://enebin.github.io/posts/eval-driven-development/" rel="alternate" type="text/html" title="AI의 평가 주도 개발(EDD) by Airbnb" /><published>2026-08-09T00:00:00+09:00</published><updated>2026-08-09T00:00:00+09:00</updated><id>https://enebin.github.io/posts/eval-driven-development</id><content type="html" xml:base="https://enebin.github.io/posts/eval-driven-development/"><![CDATA[<p>AI 기능을 붙이고 나서 가장 곤란한 순간은 배포 직전이다. 데모는 잘 된다. 팀에 보여주면 반응도 좋다. 그런데 “이거 내보내도 됩니까”라는 질문 앞에서는 할 말이 없다. 잘 되는 것 같긴 한데 얼마나 잘 되는지 말할 방법이 없기 때문이다. 테스트를 짜자니 무엇을 기댓값으로 둘지부터 막힌다. Airbnb 엔지니어링 팀이 이 문제를 정면으로 다룬 글을 냈길래 읽고 정리해 둔다.</p>

<!--more-->

<p>글은 이 습관에 이름을 붙인다. <strong>Eval 주도 개발(Eval-Driven Development, EDD)</strong>, 원문 표현으로는 “테스트 주도 개발의 GenAI 판”이다. 목표와 배포 기준을 먼저 정하고, 실제로 관찰한 실패에서 평가자를 만들고, 그 기준을 통과해야 내보낸다.</p>

<p>다만 TDD와 결정적으로 다른 데가 있다. TDD는 원하는 동작을 먼저 적을 수 있다. 여기서는 무엇이 실패인지를 미리 알 수 없다. 그래서 기준을 세우는 일보다 실패를 데이터에서 찾아내는 일이 앞에 온다. 평가에 프로젝트 공수의 상당한 몫을 쓸 각오를 하라고 글은 못을 박는다.</p>

<h2 id="왜-하던-대로-하면-안-되나">왜 하던 대로 하면 안 되나</h2>

<p>우리가 평소 쓰는 테스트는 세 가지 전제 위에 서 있다. 같은 입력에 같은 출력이 나오고, 정답이 하나로 정해져 있고, 결과는 통과 아니면 실패다. GenAI 기능은 이 셋을 전부 어긴다.</p>

<figure>
  <img src="/assets/images/edd-test-vs-eval.svg" alt="일반 소프트웨어 테스트와 GenAI 평가의 전제 차이를 두 칸으로 비교한 그림" />
  <figcaption>기존 테스트의 전제가 GenAI에서는 하나도 성립하지 않는다.</figcaption>
</figure>

<p>우선 같은 입력에도 답이 달라진다. 어제 통과한 케이스가 오늘 깨질 수 있다. 그러면 실패 하나가 진짜 회귀인지 그냥 흔들림인지 구분이 안 된다.</p>

<p>“좋다”의 기준도 주관적이다. 두 사람에게 같은 답변을 보여줘도 한 명은 친절하다고 하고 다른 한 명은 장황하다고 한다. 기댓값을 문자열로 박아둘 수가 없다.</p>

<p>마지막 하나가 제일 성가시다. 요즘 AI 기능은 한 번에 답을 내지 않는다. 검색으로 자료를 끌어오고, 그걸 근거로 추론하고, 필요하면 도구를 호출하고, 마지막에 문장을 만든다. 이 단계들이 각자 따로 망가진다. 최종 답이 이상할 때 어디서 틀어졌는지는 결과만 봐서는 알 수 없다.</p>

<p>이 셋이 겹치면 익숙한 상태에 빠진다. 프롬프트를 고치면 이쪽은 좋아지는데 저쪽이 나빠진다. 나빠진 걸 알아채는 건 며칠 뒤 누가 이상한 캡처를 올렸을 때다. 두더지잡기를 계속 하게 되는 건 실력 문제가 아니다. 무엇이 좋아지고 무엇이 나빠졌는지를 재지 못하기 때문이다.</p>

<h2 id="막히면-데이터를-봐라">막히면 데이터를 봐라</h2>

<p>글 전체에서 가장 세게 밀고 있는 규칙은 의외로 소박하다. <strong>평가 도구를 만들기 전에, 출력 100개를 직접 읽어라.</strong></p>

<p>읽기만 하는 게 아니라 읽으면서 실패를 유형별로 분류한다. “이건 근거 없는 말을 지어냈다”, “이건 질문에 답을 안 했다”, “이건 맞는 말인데 너무 길다” 같은 식으로 이름을 붙인다. 100개쯤 읽으면 이름 붙인 유형이 대여섯 개로 수렴하는데, 그중 두세 개가 전체 실패의 대부분을 차지한다는 걸 알게 된다.</p>

<p>이 과정을 건너뛰면 어떻게 되냐면, 남의 프레임워크에서 지표 목록을 가져오게 된다. 정확성, 관련성, 유용성, 안전성, 간결성… 다 그럴듯하다. 그런데 이 지표들은 우리 제품에서 실제로 나는 실패와 맞지 않는다. 정작 자주 나는 실패는 아무도 안 재고 있고, 재고 있는 지표는 항상 초록불이다.</p>

<p>100개를 읽는 건 지루하다. 하루가 통째로 날아간다. 그런데 그 하루를 안 쓰면 그 뒤에 만드는 모든 게 감으로 만든 것이 된다.</p>

<h2 id="다섯-가지-원칙">다섯 가지 원칙</h2>

<p>글은 EDD를 다섯 개의 원칙으로 정리한다. 하나씩 옮기고 내가 이해한 걸 덧붙인다.</p>

<p><strong>1. 성공 지표와 배포 기준을 미리 정한다.</strong>
만들고 나서 정하면 결과에 기준을 맞추게 된다. 숫자를 보고 “이 정도면 됐지”라고 말하는 순간, 그 지표는 판단 도구가 아니라 사후 정당화가 된다. 지표와 함께 배포 기준(shipping gate)도 정해야 한다. 몇 점을 넘어야 내보내는지를 안 정하면 지표가 아무리 많아도 그냥 대시보드다.</p>

<p><strong>2. 관찰한 오류에서 지표를 만든다.</strong>
앞에서 말한 그것이다. 지표는 데이터에서 발견한다.</p>

<p><strong>3. 평가자는 3~5개만 유지한다.</strong>
20~30개로 늘리면 그때부터는 소음이다. 이 항목이 제일 반가웠다. 평가 항목은 늘리기가 너무 쉬워서, 회의에서 누가 “이것도 봐야 하지 않나요”라고 하면 반대하기 어렵다. 그렇게 늘어난 지표는 줄이기는 또 어렵다. 정신 차려 보면 아무도 안 보는 대시보드가 되어 있다. 지표가 30개면 우선순위가 없다.</p>

<p><strong>4. 판정이 갈릴 때 결정할 사람 한 명을 정한다.</strong>
평가에는 주관이 섞일 수밖에 없다. 그래서 합의로는 끝나지 않는다. “이건 통과인가 아닌가”를 최종적으로 말할 사람이 정해져 있어야 논의가 끝난다.</p>

<p><strong>5. 엔지니어링, 제품, 도메인 전문가가 계속 같이 본다.</strong>
무엇이 좋은 답인지는 엔지니어 혼자 정할 수 없다. 한 번 모여서 기준을 정하고 헤어지는 것도 아니다. 실패 유형이 바뀌면 기준도 다시 봐야 한다.</p>

<h2 id="평가는-세-층으로-나눈다">평가는 세 층으로 나눈다</h2>

<p>모든 걸 사람이 볼 수는 없다. 그렇다고 모든 걸 기계에 맡길 수도 없다. 그래서 층을 나눈다.</p>

<figure>
  <img src="/assets/images/edd-three-layers.svg" alt="코드 규칙 검사, LLM 심판, 사람 평가로 이어지는 3층 평가 구조" />
  <figcaption>아래로 갈수록 처리량은 줄고 판단의 무게는 커진다.</figcaption>
</figure>

<p><strong>1층은 코드로 짠 규칙 검사다.</strong> JSON 스키마를 지켰는지, 필수 항목이 비었는지, 금칙어가 섞였는지처럼 기계가 딱 잘라 판정할 수 있는 것들을 본다. 여기서 걸린 출력은 뒤로 넘기지 않는다. 명백한 실패에 비싼 판정을 쓸 이유가 없고, 형식이 깨진 출력은 채점해 봐야 나오는 점수가 의미 없기 때문이다.</p>

<p>글에서 강조하는 건 출력 구조화다. 애초에 스키마를 강제해 두면 형식이 무너지는 사고가 크게 줄어든다. 평가 이전에 예방인 셈이다.</p>

<p><strong>2층은 LLM 심판(LLM-as-Judge)이다.</strong> 더 성능 좋은 모델에게 채점 기준표(루브릭)를 주고 출력을 채점하게 한다. 톤이 적절한지, 앞뒤가 맞는지, 질문에 실제로 답했는지를 여기서 본다. 규칙으로는 못 쓰는 것들이다. 싸고 빠르니까 대량으로 돌릴 수 있다.</p>

<p><strong>3층은 사람이다.</strong> 위험이 큰 영역을 맡는다. 아래 두 층의 판단이 갈릴 때도 3층이 최종 기준이다. 그런데 3층의 진짜 역할은 따로 있다. 2층이 제대로 채점하고 있는지를 여기서 확인한다. 사람이 매긴 라벨이 없으면 LLM 심판이 맞는지 틀리는지 알 방법 자체가 없다.</p>

<h2 id="llm-심판을-쓸-때-지킬-것">LLM 심판을 쓸 때 지킬 것</h2>

<p>2층이 제일 매력적이면서 제일 위험하다. 싸고 빠른데, 조용히 틀린다. 틀린 채점은 틀린 코드처럼 에러를 내지 않는다. 그냥 그럴듯한 숫자를 뱉는다. 글에서 짚는 조건을 옮긴다.</p>

<p><strong>루브릭을 모호함 없이 쓴다.</strong> “답변이 자연스러운가, 1~5점”은 루브릭이 아니다. 심판마다, 실행마다 다르게 읽는다. 글에 실린 가독성 루브릭은 이런 모양이다.</p>

<blockquote>
  <p><strong>1점</strong> — 매끄럽게 읽힌다.</p>

  <p><strong>0점</strong> — 다음 중 <strong>하나라도</strong> 걸린다: 톤, 내부 용어, 서식, 문법, 복잡도</p>
</blockquote>

<p>눈여겨볼 건 척도를 잘게 쪼개지 않았다는 점이다. 5단계로 나누면 3점과 4점 사이에서 심판이 흔들린다. 통과냐 아니냐로 두고, 대신 떨어뜨릴 조건을 구체적으로 나열한다. 판단의 여지를 점수가 아니라 조건 쪽으로 옮긴 셈이다.</p>

<p><strong>사람 판정과 80~90%대 일치율이 나올 때까지 보정한다.</strong> 이 숫자가 검증 장치다. 심판이 이 선을 못 넘으면 그 심판이 내는 점수는 근거가 못 된다. 못 넘을 때는 대개 루브릭이 모호하거나, 애초에 사람끼리도 합의가 안 되는 기준을 재려 하고 있다. 둘 다 심판 탓이 아니다. 정의를 안 해둔 탓이다.</p>

<p><strong>골든 데이터셋에 잘된 예시와 망한 예시를 같이 넣는다.</strong> 좋은 것만 모아두면 심판이 전부 통과시켜도 만점이 나온다. 나쁜 예시가 있어야 “떨어뜨릴 줄 아는지”를 잴 수 있다. 실패 사례를 모아두는 게 귀찮아서 미루기 쉬운데, 이게 없으면 심판을 검증할 수가 없다.</p>

<p><strong>주기적으로 다시 보정한다.</strong> 모델 버전이 올라가고, 프롬프트가 바뀌고, 사용자가 물어보는 것들이 바뀌면 실패 유형도 바뀐다. 반년 전에 맞춰둔 심판이 지금도 맞는다는 보장은 없다.</p>

<h2 id="그래서-실제로는-이렇게-돈다">그래서 실제로는 이렇게 돈다</h2>

<figure>
  <img src="/assets/images/edd-loop.svg" alt="직접 읽기, 지표 만들기, 심판 보정, 확장으로 이어지고 다시 처음으로 돌아오는 순환도" />
  <figcaption>확장은 끝이 아니라, 새 실패 유형을 발견하는 입구다.</figcaption>
</figure>

<p><strong>1단계, 직접 읽기.</strong> 테스트 케이스 100개를 돌리고 출력을 눈으로 읽는다. 실패를 유형별로 분류하고 이름을 붙인다.</p>

<p><strong>2단계, 지표 만들기.</strong> 1단계에서 발견한 실패만 겨냥해 평가자를 만든다. 유형마다 하나씩, 그중 자주 나는 것부터. 여기서 3~5개 제한이 걸린다.</p>

<p><strong>3단계, 심판 보정.</strong> 전문가가 라벨을 매긴 데이터에 심판을 돌려보고, 사람 판정과 얼마나 맞는지 센다. 안 맞으면 루브릭을 고쳐서 다시 돌린다.</p>

<p><strong>4단계, 확장과 관찰.</strong> 검증된 심판을 5,000건 규모로 돌린다. 그다음 운영에 붙인다. 원문이 든 예는 꽤 구체적이다. 비식별 처리한 실서비스 트래픽의 5%를 매일 샘플링해 평가하고, 주 1회 PM이 그 결과를 훑는다. 이 주간 리뷰가 루프를 닫는 고리다.</p>

<p>4단계는 끝이 아니다. 운영에서 돌리다 보면 처음에 못 본 실패 유형이 나온다. 그러면 다시 1단계로 돌아간다. 평가 체계는 한 번 만들고 끝나는 산출물이 아니라 계속 도는 루프다.</p>

<p>시작할 때 필요한 라벨은 20~100개 정도면 된다. 처음부터 큰 데이터셋을 모으려다 아무것도 시작 못 하는 쪽이 훨씬 흔하다.</p>

<h2 id="에이전트는-결과만-보면-안-된다">에이전트는 결과만 보면 안 된다</h2>

<p>여러 단계를 스스로 밟는 에이전트라면 최종 출력만 채점해서는 부족하다.</p>

<figure>
  <img src="/assets/images/edd-agent-trace.svg" alt="에이전트의 검색, 추론, 도구 호출, 최종 답변 단계마다 확인할 항목을 표시한 그림" />
  <figcaption>최종 답만 채점하면 앞 단계의 실패가 전부 보이지 않는다.</figcaption>
</figure>

<p>봐야 할 게 셋 더 있다. 추론 경로가 말이 되는가, 도구를 정확하게 골라 불렀는가, 그 경로가 쓸데없이 길지 않았는가.</p>

<p>세 번째가 특히 놓치기 쉽다. 답은 맞았는데 도구를 열 번 부른 것과 두 번 부르고 끝낸 것은 같은 점수를 받으면 안 된다. 비용도 다르고, 사용자가 기다리는 시간도 다르고, 무엇보다 열 번 부른 쪽은 다음번에 실패할 확률이 높다.</p>

<p>그리고 운 좋게 답만 맞은 경우와 제대로 풀어낸 경우를 구분하려면 중간 단계를 기록으로 남겨야 한다. 하위 에이전트가 있다면 그 동작도 따로 검증한다. 결국 관측 가능성(observability)을 미리 깔아두지 않으면 에이전트 평가는 시작조차 못 한다.</p>

<h2 id="읽으면서-뜨끔했던-것">읽으면서 뜨끔했던 것</h2>

<p>내가 실제로 걸렸던 것 세 가지만 적어둔다.</p>

<p><strong>범용 지표의 유혹.</strong> “정확성 4.2점”은 아무것도 말해주지 않는다. 무엇이 얼마나 부정확한지 모르니 고칠 수도 없다. 지표는 고칠 수 있는 단위로 쪼개져 있어야 한다.</p>

<p><strong>좋은 예시만 모아둔 데이터셋.</strong> 데모용으로 잘 나온 결과만 저장해 두는 습관이 있었다. 그게 평가 데이터로는 최악이다. 실패 사례가 훨씬 비싼 자산이다.</p>

<p><strong>한 번 만들고 방치한 평가.</strong> 평가 스크립트를 만들어 두고 두 달쯤 안 봤다. 그사이 모델도 프롬프트도 바뀌었으니 그 점수는 이미 다른 걸 재고 있었을 것이다.</p>

<h2 id="남는-것">남는 것</h2>

<p>글의 마지막 문장이 오래 남는다. 성패를 가르는 건 더 좋은 모델이 아니라 <strong>명확한 제품 기준과 팀 간 소통</strong>이라는 것.</p>

<p>무엇을 잘한 것으로 볼지 팀이 합의하지 못하면 평가 지표는 아무리 정교해도 그냥 숫자다. 결국 평가 체계를 만드는 일의 절반은 엔지니어링이고, 나머지 절반은 “우리는 이걸 좋다고 부르기로 했다”를 문서로 못 박는 일이다. 그리고 후자가 더 어렵다.</p>

<p>당장 해볼 것 하나만 고르라면 첫 번째다. 지표를 설계하기 전에, 출력 100개를 직접 읽어보기. 도구도 예산도 필요 없고 하루면 된다.</p>

<hr />

<p>원문: <a href="https://medium.com/airbnb-engineering/eval-driven-development-lessons-from-evaluating-genai-at-scale-e817e5ae5788">Eval-Driven Development: Lessons from Evaluating GenAI at Scale</a> (Airbnb Engineering)</p>]]></content><author><name>enebin</name></author><category term="개발" /><category term="AI methodology" /><summary type="html"><![CDATA[Airbnb 엔지니어링 팀이 정리한 Eval 주도 개발을 읽고, GenAI 기능을 어떤 순서로 검증해야 하는지 남긴 기록.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://enebin.github.io/assets/images/edd-cover.svg" /><media:content medium="image" url="https://enebin.github.io/assets/images/edd-cover.svg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">블로그를 시작하며</title><link href="https://enebin.github.io/posts/hello-world/" rel="alternate" type="text/html" title="블로그를 시작하며" /><published>2026-08-08T00:00:00+09:00</published><updated>2026-08-08T00:00:00+09:00</updated><id>https://enebin.github.io/posts/hello-world</id><content type="html" xml:base="https://enebin.github.io/posts/hello-world/"><![CDATA[<p>배운 내용을 정리해야겠다는 생각은 자주 했다. 하지만 잘 써야 한다는 마음이 앞서면 첫 문장을 시작하기가 어려웠다. 그렇게 적지 못한 이야기가 조금씩 쌓였다.</p>

<!--more-->

<h2 id="작게-그래도-끝까지">작게, 그래도 끝까지</h2>

<p>이제는 생각을 바꾸기로 했다. 대단한 답을 내놓기보다 내가 무엇을 시도했고, 어디에서 막혔고, 어떤 이유로 선택했는지 남기려 한다.</p>

<blockquote>
  <p>좋은 기록은 완벽한 기록이 아니라, 다음의 나에게 실제로 도움이 되는 기록이다.</p>
</blockquote>

<p>짧은 글도 괜찮다. 결론이 바뀌어도 괜찮다. 그때의 생각을 솔직하게 남기면 변화의 과정까지 기록이 된다.</p>

<h2 id="앞으로-쓸-것들">앞으로 쓸 것들</h2>

<ul>
  <li>개발하며 만난 문제와 해결 과정</li>
  <li>새로운 도구를 직접 사용해 본 경험</li>
  <li>제품을 만들며 했던 선택과 그 이유</li>
  <li>오래 기억하고 싶은 짧은 생각</li>
</ul>

<p>이 글은 그 시작이다. 천천히, 하지만 꾸준히 써보려고 한다.</p>]]></content><author><name>enebin</name></author><category term="생각" /><summary type="html"><![CDATA[완벽한 글보다 남아 있는 기록을 선택하기로 했다.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://enebin.github.io/assets/og.png" /><media:content medium="image" url="https://enebin.github.io/assets/og.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry></feed>