<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <author>
    <name>어썸블로그</name>
  </author>
  <id>국내의 좋은 블로그 글들을 매일 배달해줍니다.</id>
  <title>테크회사 어썸블로그</title>
  <updated>2026-07-15T09:00:00+09:00</updated>
  <entry>
    <author>
      <name>멋쟁이사자처럼</name>
    </author>
    <content type="html">&lt;!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd"&gt;
&lt;html&gt;&lt;body&gt;&lt;p&gt;부트캠프를 수료해도 인턴 연계나 정규직 전환까지 이어질 수 있을지, 막막하게 느껴질 때가 있죠.  멋쟁이사자처럼 그로스 마케팅 부트캠프 1기를 수료한 성진님은 인턴 연계로 입사해 정규직 전환까지 성공하며, 지금은 그로스마케터 6기 운영까지 함께 맡고 있어요.  디지털경영을 전공하며 데이터 분석 역량을 쌓아온 성진님의 이야기를 통해, 인턴십에서 무엇이 달라지&lt;img src="https://img1.kakaocdn.net/thumb/R1280x0/?fname=http%3A%2F%2Ft1.kakaocdn.net%2Fbrunch%2Fservice%2Fuser%2F2YH6%2Fimage%2FJiKcJMEl7ScM2Na3Avsqh_DVraA.png" width="500"&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;
</content>
    <id>https://brunch.co.kr/@@2YH6/172</id>
    <link href="https://brunch.co.kr/@@2YH6/172"/>
    <summary type="html">부트캠프를 수료해도 인턴 연계나 정규직 전환까지 이어질 수 있을지, 막막하게 느껴질 때가 있죠.  멋쟁이사자처럼 그로스 마케팅 부트캠프 1기를 수료한 성진님은 인턴 연계로 입사해 정규직 전환까지 성공하며, 지금은 그로스마케터 6기 운영까지 함께 맡고 있어요.  디지털경영을 전공하며 데이터 분석 역량을 쌓아온 성진님의 이야기를 통해, 인턴십에서 무엇이 달라지&lt;img src= "https://img1.kakaocdn.net/thumb/R1280x0/?fname=http%3A%2F%2Ft1.kakaocdn.net%2Fbrunch%2Fservice%2Fuser%2F2YH6%2Fimage%2FJiKcJMEl7ScM2Na3Avsqh_DVraA.png" width="500" /&gt;</summary>
    <title>[그로스 마케팅 부트캠프 후기]인턴 연계로 정규직 전환 - 그로스마케터 인턴 연계로 시작해 정규직 전환까지 이어진 실무 성장기</title>
    <updated>2026-07-14T11:07:09+09:00</updated>
    <dc:date>2026-07-14T11:07:09+09:00</dc:date>
  </entry>
  <entry>
    <author>
      <name>라인</name>
    </author>
    <content type="html">&lt;!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd"&gt;
&lt;html&gt;&lt;body&gt;&lt;p&gt;들어가며: 왜 Kafka 종단 간 암호화인가?LINE 메신저에서는 매일 수십억 건의 메시지가 오갑니다. 이 방대한 데이터는 다양한 시스템으로 전달되며, 그중에는 개인 정보와 같이 ...&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;
</content>
    <id>https://techblog.lycorp.co.jp/ko/applying-e2ee-to-apache-kafka-in-line-app</id>
    <link href="https://techblog.lycorp.co.jp/ko/applying-e2ee-to-apache-kafka-in-line-app"/>
    <summary type="html">들어가며: 왜 Kafka 종단 간 암호화인가?LINE 메신저에서는 매일 수십억 건의 메시지가 오갑니다. 이 방대한 데이터는 다양한 시스템으로 전달되며, 그중에는 개인 정보와 같이 ...</summary>
    <title>초당 100만 건, LINE 앱에 Apache Kafka 종단 간 암호화 적용기</title>
    <updated>2026-07-10T11:00:00+09:00</updated>
    <dc:date>2026-07-10T11:00:00+09:00</dc:date>
  </entry>
  <entry>
    <author>
      <name>네이버</name>
    </author>
    <content type="html">&lt;!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd"&gt;
&lt;html&gt;&lt;body&gt;
&lt;blockquote&gt;
  &lt;p&gt;모두가 같은 주제로, 같은 수준의 AI 도구를 들고 시작한 해커톤. 그런데 결과물은 팀마다 전혀 달랐습니다. 같은 도구를 썼는데 왜 결과는 달라졌을까요?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;네이버가 전사 차원에서 처음으로 개발자와 비개발자의 구분 없이 연 &lt;strong&gt;모두의 Engineering Day AI 해커톤&lt;/strong&gt;에서 가장 흥미로웠던 질문입니다.&lt;/p&gt;

&lt;p&gt;이 글은 그 질문에 대한 회고로, AI를 어떻게 활용했는지, 그리고 끝까지 사람의 몫으로 남은 일은 무엇이었는지 정리했습니다. 결론부터 말하면 AI는 계획(planning)에 강했고, 끝까지 사람의 몫으로 남은 일은 전략(strategy)이었습니다.&lt;/p&gt;

&lt;h2 id=""&gt;해커톤의 배경&lt;/h2&gt;

&lt;p&gt;이번 해커톤은 사내에서 실제로 겪는 문제를 해결하는 방식으로 진행됐습니다. 출제자(problem holder)가 문제와 요구 사항을 제시했고, 팀은 선발된 세 가지 문제 중 하나를 배정받았습니다. 요구 사항은 반드시 충족해야 하는 &lt;code&gt;Must&lt;/code&gt; 항목과 추가로 해결하면 좋은 &lt;code&gt;Nice&lt;/code&gt; 항목으로 나뉘었습니다.&lt;/p&gt;

&lt;p&gt;제약은 분명했습니다. 짧은 시간 안에 사내 인프라와 보안 환경에서 실제로 동작하는 결과물을 만들어야 했습니다. 단순 데모로는 충분하지 않고 사내 시스템과 실제로 연동되어야 한다는 점이 가장 어려웠습니다. 평가는 코드 실행 없이 결과물 문서(&lt;code&gt;readme.html&lt;/code&gt;)와 저장소 정적 분석으로 이뤄졌고, 사람과 LLM이 함께 심사했습니다.&lt;/p&gt;

&lt;p&gt;저희 팀이 맡은 과제는 사내 경비 정산 자동화였습니다. 목표는 사내 메신저인 WORKS로 영수증 사진 한 장만 보내면 정산을 끝낼 수 있게 만드는 것이었습니다.&lt;/p&gt;

&lt;h2 id=""&gt;무엇을 만들었나&lt;/h2&gt;

&lt;p&gt;저희 팀은 WORKS로 영수증 사진을 보내면 OCR, 비용 분류, 카드 내역 매칭, 임시 저장까지 처리하는 정산 봇과 PC 인증을 위한 크롬 익스텐션을 만들었습니다.&lt;/p&gt;

&lt;p&gt;사내 인증 세션의 SSO 쿠키가 PC 웹에서만 전달된다는 제약 때문에 WORKS 챗봇만으로는 정산 API를 호출할 수 없었습니다. 그래서 크롬 익스텐션이 최초 1회 인증 세션을 수행하고, 이후 WORKS 챗봇이 영수증 정산 흐름을 처리하는 2단계 구조를 선택했습니다. 봇 서버는 사내 PaaS에 배포했습니다.&lt;/p&gt;

&lt;p&gt;그 결과, 매월 1시간 이상 걸리던 정산 작업을 영수증 10장 기준 약 30초, 사용자 액션 2번으로 줄일 수 있었습니다.&lt;/p&gt;

&lt;h3 id=""&gt;시스템 구성&lt;/h3&gt;

&lt;p&gt;전체 구조는 크롬 익스텐션, WORKS 봇 플랫폼, FastAPI 기반 봇 콜백 서버, 외부 연동 API군으로 이어집니다. 크롬 익스텐션이 인증 세션을 한 번 확보하면, 이후 영수증 처리는 콜백 서버가 비동기로 진행합니다.&lt;/p&gt;

&lt;p&gt;&lt;img src="https://d2.naver.com/content/images/2026/07/01_architecture.png" alt="해커톤_산출물_아키텍처"&gt;&lt;/p&gt;

&lt;h2 id="1"&gt;1. 해커톤 자체에 대한 회고&lt;/h2&gt;

&lt;p&gt;이번 해커톤에서 가장 흥미로웠던 점은 모두가 같은 주제로 같은 수준의 AI 도구를 썼는데도 접근법, UI/UX, 활용 방식까지 산출물이 제각각이었다는 것입니다. AI가 항상 최적의 답을 준다면 '같은 주제'의 산출물은 하나로 수렴해야 할 텐데 실제 결과는 달랐습니다. 그 차이를 만든 것은 완성도를 더 끌어올리고자 하는 &lt;strong&gt;사람의 의지&lt;/strong&gt;였습니다.&lt;/p&gt;

&lt;h3 id=""&gt;개발 그 이상의 영역 — 사람의 개입이 필요한 순간&lt;/h3&gt;

&lt;p&gt;실제 병목은 코드 작성이 아니라 WORKS 봇 권한 신청과 타 부서가 관리하는 경비 API 명세 확보 과정에서 발생하는 '불확실성'이었습니다. 이 문제는 AI에게 더 많은 컨텍스트를 준다고 해결되지 않았습니다. 인증 방식이 바뀌었는데 사내 문서는 최신 상태가 아니었습니다. 외부 문서는 저희 환경과 달라 AI도 혼란스러웠고, 경비 API는 공개 문서가 없어 담당자부터 찾아야 했습니다.&lt;/p&gt;

&lt;p&gt;그래서 &lt;strong&gt;휴먼 인 더 루프&lt;/strong&gt;를 적용해 역할을 나눴습니다. 사람은 아웃라인, 부서 소통, 업무 분장을 맡고, AI는 세부 구현, 코드 생성을 맡았습니다. 모듈 통합에는 AI를 오케스트레이터로 활용했습니다.&lt;/p&gt;

&lt;h3 id=""&gt;계획과 전략의 차이&lt;/h3&gt;

&lt;p&gt;계획은 목표를 작은 작업으로 쪼개고, 구현 순서를 정하는 일입니다. 이 영역에서 AI는 탁월했습니다. 반면, 전략은 불확실성 속에서 이기기 위한 가설과 방향을 세우는 일입니다. 사내 인프라의 제약을 어떻게 넘을지, 어떤 기능을 먼저 완성해야 평가단의 문제 의식을 정확히 건드릴지와 같은 결정은 AI가 대신하지 못했습니다. AI가 '어떻게 만들지'는 정할 수 있어도, '이기기 위해서는 무엇을 해야 할지'는 사람의 몫이었습니다.&lt;/p&gt;

&lt;h3 id=""&gt;프로세스 민첩성을 위한 바이브 코딩&lt;/h3&gt;

&lt;p&gt;주어진 시간이 짧았으므로 저희는 오버헤드가 큰 문서 중심 개발(SDD)은 과감히 배제하고 도메인 지식을 바탕으로 AI와 빠르게 주고받는 &lt;strong&gt;바이브 코딩&lt;/strong&gt;을 택했습니다. 일부 팀이 &lt;code&gt;plan.md&lt;/code&gt; 셋업에 리소스를 쏟다 속도가 느려지기도 한 것을 보면, 단기 스프린트에서는 형식적 프로토콜보다 휴리스틱과 유연한 애자일이 더 잘 맞았습니다.&lt;/p&gt;

&lt;h3 id=""&gt;인프라/보안과 '책임 있는 투명성'&lt;/h3&gt;

&lt;p&gt;구현을 서두르는 과정에서 사내 보안과 권한 프로세스의 사각지대도 발견했습니다. 저희 팀은 이를 편법으로 이용하는 데 그치지 않고, 해커톤 종료 직후 관련 부서에 내용을 투명하게 공유했습니다. 덕분에 단순히 해커톤 산출물을 내는 것을 넘어 전사 공용 플랫폼 보안에 선제적으로 기여하고, 동시에 정산 자동화가 실제 업무에도 충분히 적용 가능하다는 것을 확인해 &lt;strong&gt;정식 기능 출시 검토&lt;/strong&gt;까지 이어진 것이 가장 값진 성과였습니다.&lt;/p&gt;

&lt;h2 id="2"&gt;2. 개발 파트 회고: 속도와 안정성의 균형을 찾아서&lt;/h2&gt;

&lt;p&gt;개발 파트는 각자의 도메인을 책임지되, AI를 공통 언어로 삼아 조립해 나가는 과정이었습니다.&lt;/p&gt;

&lt;h3 id=""&gt;작은 블록으로 나누기&lt;/h3&gt;

&lt;p&gt;저희 팀은 레고식 개발을 적용해 전체 시스템을 인증 캡처, 경비 API 연동, 봇 콜백 서버, 메시지 송신, OCR, LLM 분류라는 6개의 독립 블록으로 나눴습니다. 각자 AI를 사용해 각 블록을 빠르게 구현한 뒤 AI에게 인터페이스 포팅과 머지를 맡겼습니다.&lt;/p&gt;

&lt;p&gt;&lt;img src="https://d2.naver.com/content/images/2026/07/02_lego.png" alt="레고식 개발 — 출처: OpenAI Harness Engineering"&gt;&lt;/p&gt;

&lt;p&gt;&lt;span class="caption"&gt;출처: &lt;a href="https://openai.com/index/harness-engineering/"&gt;OpenAI Harness engineering&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;

&lt;h3 id=""&gt;검색, 리서치, 로그를 컨텍스트로 제공&lt;/h3&gt;

&lt;p&gt;AWS, GCP 등의 퍼블릭 클라우드와는 달리 사내 배포 환경은 LLM이 학습하지 못한 영역입니다. 따라서 'AI가 아는 것'에 기대기보다, 사내 정보에 연결해 '잘 찾게 만드는 것'이 관건이었습니다.&lt;/p&gt;

&lt;p&gt;저희 팀은 사내 코드 저장소, 이슈 트래커, 위키를 사내 MCP로 연결해, AI가 직접 검색하며 사내 배포 플랫폼, 로드밸런서, 분산 DB 환경에 맞게 설정하도록 했습니다. 막힐 때 던지는 질문은 크게 세 종류였습니다.&lt;/p&gt;

&lt;table&gt;  
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;구분&lt;/th&gt;
      &lt;th&gt;질문&lt;/th&gt;
      &lt;th&gt;활용 방식&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;WHAT&lt;/td&gt;
      &lt;td&gt;무엇을 써야 하는가&lt;/td&gt;
      &lt;td&gt;사내에서 사용할 수 있는 플랫폼과 유사 사례를 먼저 검색&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;HOW&lt;/td&gt;
      &lt;td&gt;어떻게 써야 하는가&lt;/td&gt;
      &lt;td&gt;설정 방법과 예시를 찾아 적용(예: DDL 분석, 마이그레이션, 쿼리 테스트 순서로 분산 DB 이관)&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;WHY&lt;/td&gt;
      &lt;td&gt;왜 실패하는가&lt;/td&gt;
      &lt;td&gt;빌드, 배포, 오류 로그를 제공하고 원인과 다음 조치를 확인&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;h3 id="327"&gt;속도와 품질을 높이기 위한 327개의 테스트&lt;/h3&gt;

&lt;p&gt;빠르게 바꾸는 만큼 회귀도 빠르게 생깁니다. 저희 팀은 실패한 케이스를 지나치지 않고 테스트로 고정해 327개의 테스트를 만들었습니다. 정보가 부족하면 문서를 보강했고, 동작이 깨지면 테스트를 추가했습니다. 같은 실수가 반복될 것 같으면 가드레일로 검증 로직을 넣음으로써 속도에만 치중하지 않고 품질을 함께 지킬 수 있었습니다.&lt;/p&gt;

&lt;p&gt;&lt;img src="https://d2.naver.com/content/images/2026/07/03_test.png" alt="테스트 코드"&gt;&lt;/p&gt;

&lt;h2 id="3ai"&gt;3. 비개발 파트 회고: AI라는 징검다리&lt;/h2&gt;

&lt;p&gt;해커톤에서 AI는 코드를 만드는 도구일 뿐만 아니라, 비개발자가 개발 상황을 이해하고 기획과 디테일에서 팀의 협업 속도를 높일 수 있게 해주는 징검다리이기도 했습니다.&lt;/p&gt;

&lt;h3 id=""&gt;개발 상황을 따라잡기&lt;/h3&gt;

&lt;p&gt;해커톤 중 팀 개발 채널에서 빠르게 흐르는 기술 대화를 비개발자가 이해하기는 어려웠습니다. 그래서 Playwright와 로그인된 Chrome 브라우저를 활용해 AI가 팀 채팅, 발표 자료, 사내 코드 저장소를 읽고 진행 상황 문서로 정리하게 했습니다. 그 결과 '지금 무슨 개발이 진행 중인지'를 파악해 담당 개발자에게 더 정확하게 질문할 수 있었습니다.&lt;/p&gt;

&lt;p&gt;&lt;img src="https://d2.naver.com/content/images/2026/07/04_devsync.png" alt="해커톤_개발팀싱크_모자이크"&gt;&lt;/p&gt;

&lt;h3 id=""&gt;사용자 흐름을 구체화하기&lt;/h3&gt;

&lt;p&gt;서비스가 사용자에게 닿기까지의 흐름을 도입 페이지, 챗봇 플로우, 발표 자료 세 영역으로 나눠 AI와 함께 설계했습니다.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;도입 페이지: 2단계 인증 구조로 인해 크롬 익스텐션만으로는 봇 사용법을 알기 어려워, 설치, 튜토리얼, 유의 사항을 모아놓은 페이지를 AI와 함께 기획했습니다.&lt;/li&gt;
&lt;li&gt;챗봇 플로우: '언제 실패하는가'를 AI와 브레인스토밍해 분류 모호, 매칭 실패, 결제 익일 반영, 개인카드 오등록과 같은 오류 케이스를 설계하고, 이상적인 흐름과 오류 케이스를 HTML로 시각화해 개발 우선순위를 논의했습니다.&lt;/li&gt;
&lt;li&gt;발표 자료: 코드 실행 없이 평가되는 만큼, 평가자를 LLM 평가단, 출제자, 청중으로 정의하고 첫 페이지에 &lt;code&gt;Must&lt;/code&gt;, &lt;code&gt;Nice&lt;/code&gt;, &lt;code&gt;Beyond&lt;/code&gt; 충족도를 두괄식으로 배치했습니다.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src="https://d2.naver.com/content/images/2026/07/05_planning.png" alt="비개발파트_기획섹션"&gt;&lt;/p&gt;

&lt;h3 id=""&gt;디자인 톤을 하나로 맞추기&lt;/h3&gt;

&lt;p&gt;크롬 익스텐션, 도입 페이지, 발표 자료가 서로 다른 제품처럼 보이면 완성도가 낮아 보일 수 있습니다. 그래서 민트색(&lt;code&gt;#0d9373&lt;/code&gt;), Pretendard 글꼴, 캐릭터, 아이보리색 알림 창과 같은 디자인 토큰을 정하고 팝업, 도입 페이지, 슬라이드에 일관되게 적용했습니다.&lt;/p&gt;

&lt;p&gt;발표 덱은 출제자가 공유한 HTML 형식을 AI가 참고하게 해 기본 골격을 만들었습니다. 이후 내용을 이식해 약 1시간 만에 11장 분량의 초안을 완성했습니다. HTML 단일 파일이라는 특성 덕분에 지시 한 줄로 문구와 레이아웃을 빠르게 고칠 수 있었고, 실제로 마감 직전까지 20번 넘게 수정해 완성도를 높였습니다.&lt;/p&gt;

&lt;p&gt;&lt;img src="https://d2.naver.com/content/images/2026/07/06_design.png" alt="비개발파트_디자인섹션"&gt;&lt;/p&gt;

&lt;h2 id=""&gt;마치며&lt;/h2&gt;

&lt;p&gt;이번 해커톤에서 가장 분명하게 남은 것은 처음 질문에 대한 답이었습니다. 같은 AI를 쥐여 줘도 결과를 가르는 것은 사람의 선택과 전략입니다.&lt;/p&gt;

&lt;p&gt;AI는 '계획과 구현'의 영역에서 압도적인 속도를 냈습니다. 하지만 불확실성 속에서 가설을 세우고, 조직 안의 의존성을 풀고, 보안 사각지대를 책임 있게 공유하는 '전략'의 영역은 사람의 몫으로 남았습니다.&lt;/p&gt;

&lt;p&gt;개발 파트에서는 사람이 AI 에이전트를 오케스트레이션하며 큰 작업을 작은 블록으로 나눠 병렬로 진행한 전략이 중요했습니다. 비개발 파트에서는 AI를 소통과 기획의 징검다리로 삼아 협업 속도를 높인 전략이 중요했습니다.&lt;/p&gt;

&lt;p&gt;AI 시대의 협업은 사람이 AI로 대체되는 그림이 아니라, 사람이 전략을 쥐고 AI에게 계획과 실행을 위임하는 그림에 가깝습니다. AI는 매일 더 똑똑해지지만, '만들었다'를 넘어 시장에서 선택받는 프로덕트를 만들어내는 것은 여전히 사람의 역할이 아닐까요?&lt;/p&gt;

&lt;p&gt;이것이 이번 해커톤이 저희 팀에 남긴 가장 큰 배움이었습니다.&lt;/p&gt;

&lt;p&gt;&lt;img src="https://d2.naver.com/content/images/2026/07/07_closing.png" alt="스크린샷 2026-05-27 오전 5 12 37"&gt;&lt;/p&gt;
&lt;/body&gt;&lt;/html&gt;
</content>
    <id>https://d2.naver.com/helloworld/1883072</id>
    <link href="https://d2.naver.com/helloworld/1883072"/>
    <title>[AI 해커톤 후기] AI 시대의 해커톤과 인간의 역할: AI의 계획과 사람의 전략</title>
    <updated>2026-07-13T21:43:28+09:00</updated>
    <dc:date>2026-07-13T21:43:28+09:00</dc:date>
  </entry>
  <entry>
    <author>
      <name>네이버</name>
    </author>
    <content type="html">&lt;!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd"&gt;
&lt;html&gt;&lt;body&gt;
&lt;p&gt;&lt;img src="https://d2.naver.com/content/images/2023/07/-----------2023-07-06------4-16-49.png"&gt;&lt;/p&gt;

&lt;h2 id=""&gt;주요소식&lt;/h2&gt;

&lt;p&gt;&lt;img src="https://d2.naver.com/content/images/2026/07/-----------2026-07-08------5-49-05.png" alt=""&gt;&lt;/p&gt;

&lt;p&gt;26년 6월 소식에서는 다음과 같은 유용한 정보들을 만나보실 수 있습니다.&lt;/p&gt;

&lt;h4 id="typescript70rchttpsdevblogsmicrosoftcomtypescriptannouncingtypescript70rc"&gt;&lt;strong&gt;&lt;a href="https://devblogs.microsoft.com/typescript/announcing-typescript-7-0-rc"&gt;TypeScript 7.0 RC&lt;/a&gt;&lt;/strong&gt;&lt;/h4&gt;

&lt;p&gt;Go로 재작성된 네이티브 컴파일러를 탑재한 TypeScript 7.0의 릴리스 후보(RC)가 공개되었다. 기존 타입 체크 로직과 구조를 그대로 유지한 채 포팅해 6.0보다 약 10배 빠르며, Bloomberg, Figma, Google, Slack, Vercel 등 여러 기업의 팀과 프리릴리스 빌드를 테스트해 왔다. 향후 한 달 내 정식 릴리스될 예정으로, 대규모 코드베이스에서 타입 체크와 빌드 속도로 고생해 온 팀이라면 미리 검증해 볼 가치가 있다.&lt;/p&gt;

&lt;h4 id="shipthepolicynotthecodehttpswwwjayfreestonecomwritingsharethepolicynotthecode"&gt;&lt;strong&gt;&lt;a href="https://www.jayfreestone.com/writing/share-the-policy-not-the-code/"&gt;Ship the policy, not the code&lt;/a&gt;&lt;/strong&gt;&lt;/h4&gt;

&lt;p&gt;프런트엔드와 백엔드처럼 별도로 배포되는 두 시스템에 동일한 비즈니스 규칙을 구현할 때 발생하는 코드 중복과 불일치 문제를 다룬다. 백엔드가 "allowedActions": ["CANCEL"] 같은 결정값을 내려보내거나, CASL이나 JSON Schema 같은 표준 형식으로 정책 자체를 데이터로 직렬화해 전송하는 방안을 제시한다. 마이크로서비스나 BFF 구조에서 규칙 일관성을 고민하는 개발자에게 실용적인 관점을 제공한다.&lt;/p&gt;

&lt;h4 id="gettingstartedwithloopshttpsxcomclaudedevsstatus2074208949205881033"&gt;&lt;strong&gt;&lt;a href="https://x.com/ClaudeDevs/status/2074208949205881033"&gt;Getting started with loops&lt;/a&gt;&lt;/strong&gt;&lt;/h4&gt;

&lt;p&gt;Claude Code 팀이 코딩 에이전트에게 프롬프트를 반복 입력하는 대신 "루프를 설계"하는 접근을 정리한 글이다. 트리거 방식과 종료 기준에 따라 턴 기반, 목표 기반, 시간 기반, 프로액티브 4가지 유형으로 분류하고, 검증 절차를 스킬(SKILL.md)로 인코딩해 자가 검증 능력을 높이는 방법을 소개한다. 명확한 종료 기준 설정과 대규모 실행 전 파일럿 테스트 등 토큰 사용량 관리 팁도 함께 제안한다.&lt;/p&gt;

&lt;h4 id="reactdoctorhttpsgithubcommillioncoreactdoctor"&gt;&lt;strong&gt;&lt;a href="https://github.com/millionco/react-doctor"&gt;React Doctor&lt;/a&gt;&lt;/strong&gt;&lt;/h4&gt;

&lt;p&gt;AI 에이전트가 작성한 React 코드의 품질 문제를 자동으로 잡아내는 도구다. 상태 관리, 이펙트, 성능, 아키텍처, 보안, 접근성 전반을 검사하며 Next.js, Vite, TanStack, React Native, Expo 등 대부분의 React 환경을 지원한다. Claude Code, Cursor, Codex 같은 AI 에이전트와 연동되고, GitHub Actions를 통한 PR 자동 리뷰도 가능하다.&lt;/p&gt;

&lt;h4 id="vercelevehttpsgithubcomverceleve"&gt;&lt;strong&gt;&lt;a href="https://github.com/vercel/eve"&gt;vercel/eve&lt;/a&gt;&lt;/strong&gt;&lt;/h4&gt;

&lt;p&gt;Vercel이 공개한 파일시스템 기반의 지속형(persistent) AI 에이전트 프레임워크다. 시스템 프롬프트는 instructions.md, 도구는 tools/, 온디맨드 절차는 skills/ 디렉터리에 두는 식으로 에이전트의 구성 요소를 코드가 아닌 디렉터리 구조로 관리해 검사와 확장이 쉽다. 마크다운과 타입 정의된 함수만으로 에이전트를 구성하는 접근은 코드 중심 프레임워크 대비 낮은 진입 장벽을 제공한다.&lt;/p&gt;

&lt;h2 id="fenews267httpsgithubcomnaverfenewsblobmasterissues202607md"&gt;&lt;a href="https://github.com/naver/fe-news/blob/master/issues/2026-07.md"&gt;&amp;gt;&amp;gt; FE News 26년 7월 소식 보러가기&lt;/a&gt;&lt;/h2&gt;

&lt;p&gt;&lt;br&gt;  &lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;◎ FE News란?&lt;/strong&gt;&lt;br&gt;
  네이버 FE 엔지니어들이 엄선한 양질의 FE 및 주요한 기술 소식들을 큐레이션해 공유하는 것을 목표로 하며, 이를 통해 국내 개발자들에게 지식 공유에 대한 가치 인식과 성장에 도움을 주고자 하는 기술소식 공유 프로젝트 입니다.&lt;/p&gt;
  
  &lt;p&gt;매월 첫째 주 수요일, 월 1회 발행 되고 있으니 많은 관심 부탁드립니다.&lt;br&gt;
  &lt;a href="https://fenews.substack.com/embed"&gt;▷ 구독하기&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/body&gt;&lt;/html&gt;
</content>
    <id>https://d2.naver.com/news/7560502</id>
    <link href="https://d2.naver.com/news/7560502"/>
    <title>FE News 26년 7월 소식을 전해드립니다.</title>
    <updated>2026-07-09T02:52:20+09:00</updated>
    <dc:date>2026-07-09T02:52:20+09:00</dc:date>
  </entry>
  <entry>
    <author>
      <name>네이버</name>
    </author>
    <content type="html">&lt;!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd"&gt;
&lt;html&gt;&lt;body&gt;
&lt;p&gt;2026년 봄, 네이버는 개발 직군 중심으로 열리던 Engineering Day를 모든 직군이 함께하는 '모두의 Engineering Day'로 넓혔습니다. 행사의 마지막 순서는 AI 해커톤이었습니다. 목표는 단순히 AI를 써보는 것이 아니라, 사람이 하던 반복 업무를 AI로 줄이고 그 과정에서 'AI native로 일해 보는 경험'을 만드는 것이었습니다.&lt;/p&gt;

&lt;p&gt;실제 업무 문제를 겪는 현업 담당자(problem holder)가 세 가지 문제를 냈고, 문제를 푸는 사람(solver)은 직군 제한 없이 네 명씩 팀을 이뤘습니다. 모두 25개 팀, 100여 명이 현장에서 문제와 매칭되었습니다. 각 팀은 바로 사용할 수 있는 수준의 최소 기능 제품(minimum viable product, MVP)을 하루 안에 만들어야 했습니다. 주제는 신규 서비스가 아니라 사람이 손으로 처리하던 반복 업무를 줄이는 사내 문제로 좁혔습니다.&lt;/p&gt;

&lt;p&gt;행사를 준비하면서 마지막까지 남은 고민은 평가였습니다. 25팀의 제출물을 짧은 시간 안에 보고, 문제별 상위 팀을 공정하게 가려야 했습니다. 모든 코드를 실행해 볼 수 있다면 가장 좋았겠지만 시간이 부족했고, 제출된 코드와 문서를 일관된 기준으로 빠르게 읽을 방법이 필요했습니다. 마침 LLM의 성능이 좋아지면서 LLM을 평가자로 활용하는 방식(LLM as a judge)이 외부에서도 활발히 시도되고 있었습니다. 공통 문제를 두고 여러 팀이 경쟁하는 이번 행사에도 적용해 볼 만하다고 판단했습니다.&lt;/p&gt;

&lt;p&gt;이 글은 코드와 문서만 읽는 평가를 어떻게 설계했는지, 그 결과가 사람의 평가와 얼마나 일치했는지, 평가를 반복해도 비슷한 결과가 나오는지를 정리한 기록입니다. 다만 평가 대상은 세 문제, 25팀, 100여 명이 하루 만에 만든 MVP였습니다. 실제 서비스 코드처럼 방대하고 복잡한 환경에 이 방식을 그대로 적용하기는 어렵지만, 'LLM이 코드와 문서만 읽고도 사람과 비슷한 판단을 내릴 수 있는지'를 가늠해 볼 수 있었습니다.&lt;/p&gt;

&lt;h2&gt; 세 개의 문제와 평가 단계 &lt;/h2&gt;

&lt;p&gt;세 개의 문제는 다음과 같습니다.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;문제 A(출자·투자 현황 관리)&lt;/strong&gt;: 수기 엑셀로 취합하던 출자·투자 지분 현황을 입력하고 검증할 수 있는 플랫폼으로 전환&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;문제 B(영수증 경비 정산)&lt;/strong&gt;: 영수증을 일일이 모아 처리하던 정산을 사내 메신저 봇으로 자동화&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;문제 C(업무 시간 관리)&lt;/strong&gt;: 흩어진 업무 소스를 모아 하루 일정을 만들고 실제 쓴 시간까지 돌아보는 어시스턴트 구현&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;심사는 1차와 2차로 나눴습니다. 1차는 문제별 후보군을 좁히는 예선이었고, 2차는 최종 시상을 정하는 본선이었습니다. 1차 안에는 두 트랙이 있었습니다. LLM 평가단은 문제당 상위 2팀을 점수와 근거로 추렸고, 문제 출제자는 본인 문제에서 꼭 살리고 싶은 1팀을 SUPER(슈퍼패스)로 본선에 올렸습니다.&lt;/p&gt;

&lt;table&gt;
&lt;thead&gt;&lt;tr&gt;
&lt;th colspan="2"&gt;단계&lt;/th&gt;
&lt;th&gt;평가 주체&lt;/th&gt;
&lt;th&gt;결과&lt;/th&gt;
&lt;/tr&gt;&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td rowspan="2"&gt;1차&lt;/td&gt;
&lt;td&gt;트랙 A — LLM 자동 평가&lt;/td&gt;
&lt;td&gt;LLM 평가단&lt;/td&gt;
&lt;td&gt;문제당 상위 2팀 선발&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;트랙 B — SUPER(슈퍼패스)&lt;/td&gt;
&lt;td&gt;문제 출제자&lt;/td&gt;
&lt;td&gt;본인 문제에서 1팀 구제&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="2"&gt;2차&lt;/td&gt;
&lt;td&gt;사내 AI 커뮤니티 30% + 출제자 30% + 참가자 40%&lt;/td&gt;
&lt;td&gt;최종 수상자 결정&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;

LLM 평가는 사내에서 제공하는 코딩 에이전트로 진행했습니다. 평가 절차와 루브릭(채점 기준표)을 하나의 '스킬'로 정의하고, 에이전트가 그 스킬에 따라 코드와 문서를 읽게 했습니다. 사내에는 Claude Code와 Codex가 제공되었는데, 두 에이전트의 결과를 얻고 맞춰 볼 시간이 부족했습니다. 그래서 이번 LLM 평가는 Codex 하나로 진행했습니다.  
&lt;br&gt;&lt;br&gt;

&lt;h2&gt; 평가 기준은 '일을 얼마나 줄였는가' &lt;/h2&gt;

이 평가가 던진 질문은 하나, '실제로 사용자의 일을 얼마나 줄여줬는가'였습니다. 단순히 AI나 기술을 사용했다는 사실은 중요하지 않았습니다.

같은 기능이라도 어디까지 이어지는지에 따라 점수가 달라졌습니다. 예를 들어 RAG(문서를 검색해 답을 보강하는 기법)가 정책을 찾아줘도 그 답이 경비 분류나 한도 판단으로 이어지지 않으면 검색을 돕는 데 그칩니다. 업무 화면으로 이동시키기만 하고 입력은 사람에게 맡기는 브라우저 확장 프로그램도 마찬가지입니다. 반대로 화면 입력을 대신 채우고, 실제 저장·상신(결재 요청)에 필요한 데이터를 만들고, 실패했을 때 다시 처리하는 흐름까지 구현했다면 사용자의 일을 대신했다고 볼 수 있습니다. 정적 대시보드도 정해진 상황만 보여주는지, 실제 수집 결과를 계속 반영하는지에 따라 다르게 평가했습니다. 그래서 발표 자료의 설명보다는 입력을 받고, 판단하고, 결과를 만드는 흐름이 실제 코드 안에서 이어지는지를 봤습니다.

기준을 납득 가능하게 만드는 것만으로는 충분하지 않았습니다. 문제당 상위 2팀을 가려야 했기 때문에, 비슷하게 좋아 보이는 팀들 사이에서도 차이가 드러나야 했습니다. 그래서 기능 완성도(functionality) 40점, 기술·구현(technical) 20점, 차별성·문제해결(differentiation) 20점, 문서·제출자료(documentation) 20점의 네 가지 평가 축을 정했습니다. 발표 자료만 보면 비슷해 보이는 팀도 코드에서 확인되는 구현 범위, 예외 처리, 업무 흐름의 연결 정도를 기준으로 보면 차이가 났습니다.
&lt;br&gt;&lt;br&gt;

&lt;h2&gt; 평가 시스템의 동작 &lt;/h2&gt;

평가 파이프라인은 단순했습니다. 각 팀의 저장소 주소, 커밋, 제출물 경로를 고정한 매니페스트를 준비하면 에이전트가 코드와 문서를 읽고 점수와 한 줄 평을 생성했습니다. 사람이 개입한 곳은 매니페스트를 준비하는 사전 단계와 결과 공개 시점을 정하는 단계뿐이었습니다.&lt;br&gt;&lt;br&gt;

&lt;img src="https://d2.naver.com/content/images/2026/07/eval-pipeline.png" alt="eval-pipeline"&gt;
&lt;br&gt;  
가장 신경 쓴 부분은 공정성이었습니다. 화면을 직접 실행하지 않고 코드와 문서만으로 구현 수준을 판단해야 했기 때문에, 참가자와 운영자가 납득할 수 있는 기준이 필요했습니다. 발표 자료에는 완성됐다고 적혀 있어도 코드로 확인되지 않으면 구현 완료로 보지 않았습니다. TODO로 남은 기능, 틀만 있고 동작하지 않는 코드, 발표 자료에만 있는 기능도 마찬가지였습니다. 다만 같은 이유로 감점을 반복하지는 않고, 구현된 만큼만 점수를 주고 확인되지 않은 부분은 점수를 주지 않았습니다.

채점은 팀별로 분리했습니다. 팀별 채점 에이전트(team-evaluator)는 매니페스트에서 팀명, 문제, 커밋, 저장소, 제출물 경로를 확인한 뒤 문제별 루브릭에 따라 코드와 문서를 읽었습니다. 점수만 남기면 이후 조율과 검수가 어려우므로 내부 JSON에는 점수, 강점, 근거, 판단 리스크를 함께 남겼습니다.

세 문제는 동일한 네 가지 평가 축을 사용했지만, 문제별 스킬에서 중점적으로 본 부분은 달랐습니다.&lt;br&gt;&lt;br&gt;

&lt;table&gt;
&lt;thead&gt;&lt;tr&gt;
&lt;th&gt;문제&lt;/th&gt;
&lt;th&gt;중점적으로 본 부분&lt;/th&gt;
&lt;/tr&gt;&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;문제 A(출자·투자 현황 관리)&lt;/td&gt;
&lt;td&gt;수기 엑셀 기반 출자 현황을 기준 데이터(master DB)로 옮기는 입력, 검증, 불일치 발견·수정 경로&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;문제 B(영수증 경비 정산)&lt;/td&gt;
&lt;td&gt;영수증 인식부터 카드 매칭, 분류, 한도 확인, 임시 저장까지 이어지는 경비 처리 흐름&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;문제 C(업무 시간 관리)&lt;/td&gt;
&lt;td&gt;흩어진 업무 소스를 모아 하루 일정으로 바꾸는 핵심 흐름, 실제 연동, 상태 관리, 문서 근거&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;

예를 들어 문제 B에서는 단순 OCR 데모와 실제 경비 처리 자동화를 나눠 보는 기준을 촘촘하게 뒀습니다. 영수증을 읽는 데서 끝나는지, 카드 매칭, 분류, 한도 확인, 예외 회복, 임시 저장까지 업무 흐름이 이어지는지를 따로 확인했습니다.

채점이 끝나면 문제별 취합 에이전트(topic-lead)가 같은 문제의 팀별 결과를 모았습니다. 이 단계에서는 새 점수를 만들지는 않고, 개별 채점 결과의 점수, 근거, 위험을 같은 문제 안에서 비교하고 점수 스케일과 상위권 후보가 갈리는 지점을 확인했습니다.

평가를 조율할 때는 책임을 나누는 데 신경을 썼습니다. 팀별 채점, 문제 내 조율, 한 줄 평 작성을 한 컨텍스트에 몰아넣으면 앞선 팀의 인상이나 문장의 톤이 다음 판단에 섞일 수 있고 평가 집중도가 떨어질 수도 있습니다. 그래서 채점 에이전트는 한 팀만 보고, 취합 에이전트는 같은 문제의 결과만 모으고, 한 줄 평은 작성(copywriter)과 검수(reviewer) 에이전트로 분리했습니다. 운영 관점에서는 네 가지 공통 평가 축을 먼저 고정했다는 점도 도움이 됐습니다. 문제별 세부 루브릭은 달라도 같은 평가 축을 공유했기 때문에 결과를 하나의 파이프라인으로 모으기 쉬웠습니다.
&lt;br&gt;&lt;br&gt;

&lt;h2&gt; 사람의 판단과 만난 지점 &lt;/h2&gt;

LLM 평가는 문제당 상위 2팀을 뽑는 1차 관문이었습니다. 따라서 공식 우승팀이 LLM 후보군 안에 들어왔다는 사실만으로는 큰 의미를 부여하기 어렵습니다. 더 흥미로웠던 지점은 문제 A와 문제 B에서 서로 다른 세 판단이 같은 팀을 가리켰다는 점입니다. LLM은 8~9개 팀(문제 A는 9개 팀, 문제 B와 C는 각 8개 팀) 중 해당 팀을 1위로 평가했습니다. 문제 출제자도 같은 팀을 SUPER로 골랐고, 최종 심사에서 사람들이 고른 우승 팀도 같았습니다.&lt;br&gt;&lt;br&gt;

&lt;img src="https://d2.naver.com/content/images/2026/07/image-2026-6-5_15-52-18.png" alt="image-2026-6-5_15-52-18"&gt;

&lt;img src="https://d2.naver.com/content/images/2026/07/-----------2026-07-06-------10-32-09.png" alt="image-2026-6-5_15-52-31"&gt;

&lt;br&gt;  
&lt;table&gt;  
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;문제&lt;/th&gt;
      &lt;th&gt;LLM 1위&lt;/th&gt;
      &lt;th&gt;SUPER&lt;/th&gt;
      &lt;th&gt;최종 1위&lt;/th&gt;
      &lt;th&gt;일치&lt;/th&gt;
      &lt;th&gt;상위권 양상&lt;/th&gt;
      &lt;th&gt;우수한 지점&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;문제 A (출자·투자 현황 관리)&lt;/td&gt;
      &lt;td&gt;팀 NStake&lt;/td&gt;
      &lt;td&gt;팀 NStake&lt;/td&gt;
      &lt;td&gt;팀 NStake&lt;/td&gt;
      &lt;td&gt;일치&lt;/td&gt;
      &lt;td&gt;1~3위 초접전&lt;/td&gt;
      &lt;td&gt;기능 완성도&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;문제 B (영수증 경비 정산)&lt;/td&gt;
      &lt;td&gt;팀 youngsoo&lt;/td&gt;
      &lt;td&gt;팀 youngsoo&lt;/td&gt;
      &lt;td&gt;팀 youngsoo&lt;/td&gt;
      &lt;td&gt;일치&lt;/td&gt;
      &lt;td&gt;1위가 비교적 뚜렷&lt;/td&gt;
      &lt;td&gt;차별성, 문제 해결&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;문제 C (업무 시간 관리)&lt;/td&gt;
      &lt;td&gt;팀 daylog&lt;/td&gt;
      &lt;td&gt;팀 TeamNaver_Assistant&lt;/td&gt;
      &lt;td&gt;팀 timamanager&lt;/td&gt;
      &lt;td&gt;불일치&lt;/td&gt;
      &lt;td&gt;1~3위 접전&lt;/td&gt;
      &lt;td&gt;문서, 제출 자료&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;문제 A는 상위권 점수 차가 거의 없는 초접전이었습니다. 눈에 띄는 것은 LLM 평가에서 접전 구간의 맨 앞에 놓인 팀이 출제자의 SUPER 선택과 같았다는 사실입니다. 팀 NStake를 고른 SUPER 선택도 실제 사용 가능성과 동작 완성도를 중요시한 결과라면, 두 평가는 같은 축에 무게를 둔 셈입니다.&lt;/p&gt;

&lt;p&gt;문제 B에서는 팀 youngsoo가 차별성과 문제 해결에서 비교적 뚜렷하게 앞섰습니다. 이 문제의 스킬은 사내 메신저 봇(WORKS Bot), OCR, 예외 회복, 한도 확인, 사내 경비 시스템(neon-ess), 상신 안전성 같은 경계를 자세히 봤습니다. 루브릭이 영수증을 읽기만 하는 수준과 실제 경비 처리를 자동화하는 수준을 나눠 보도록 설계돼 있었기 때문에, 차별성이 코드 근거로 확인되는 팀을 찾아낼 수 있었습니다.&lt;/p&gt;

&lt;p&gt;문제 C에서는 LLM 평가, SUPER, 최종 심사가 서로 다른 팀을 가리켰습니다. 당일 결과만 보면 문서와 제출 자료 근거가 확실한 팀 daylog가 LLM 평가에서 앞섰고, 공식 우승 팀 timamanager는 실제 동작 완성도에서 강했습니다. 특정 팀이 맞고 틀렸다는 뜻은 아닙니다. 문제 C처럼 상위권 점수 차가 작을 때는 한 번의 평가 총점만으로 1위를 단정하기 어렵다는 사실을 알 수 있었습니다.&lt;/p&gt;

&lt;h2&gt; 반복 평가와 안정성 &lt;/h2&gt;

&lt;p&gt;문제 C의 불일치를 보고 나니 한 가지를 더 확인하고 싶었습니다. 같은 제출물을 LLM이 다시 채점하면 비슷한 결과가 나올까, 아니면 실행할 때마다 전혀 다른 결과가 나올까 하는 것이었습니다. 해커톤이 끝난 뒤 각 문제를 5회씩 다시 평가해 봤습니다. 점수 자체는 예상보다 흔들렸지만, 상위권을 유지하는 팀은 비교적 안정적이었습니다.&lt;/p&gt;

&lt;p&gt;반복 평가에서 1위가 나온 횟수는 다음과 같았습니다.&lt;/p&gt;

&lt;table&gt;  
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;문제&lt;/th&gt;
      &lt;th&gt;5회 중 1위 횟수&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;문제 A&lt;/td&gt;
      &lt;td&gt;팀 NStake 4회, 팀 team22 1회&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;문제 B&lt;/td&gt;
      &lt;td&gt;팀 youngsoo 5회&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;문제 C&lt;/td&gt;
      &lt;td&gt;팀 timamanager 4회, 팀 Sherpa 1회&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;여기서 보고 싶었던 것은 문제별 순위를 다시 매기는 것이 아니라, 반복 평가에서도 상위권을 유지하는 팀에는 어떤 공통점이 있는가였습니다.&lt;/p&gt;

&lt;p&gt;팀 NStake는 출자 현황 입력부터 기준 데이터 대조와 불일치 수정까지 이어졌습니다. 팀 youngsoo는 영수증 인식부터 카드 매칭, 분류, 임시 저장과 상신까지 이어졌습니다. 팀 timamanager는 업무 소스 수집부터 일정 생성, 상태 갱신, 퇴근 리뷰까지 이어졌습니다. 세부 구현은 달랐지만 점수를 만든 이유는 비슷했습니다. 입력부터 판단, 결과 생성까지 이어지는 문제 해결 흐름이 코드에서 보였다는 것입니다.&lt;/p&gt;

&lt;p&gt;반대로 점수가 크게 흔들린 제출물은 기술이 부족했기 때문은 아니었습니다. 그 기술이 실제 업무를 어디까지 대신하는지가 애매한 경우가 많았습니다.&lt;/p&gt;

&lt;p&gt;그래서 문제 C처럼 점수 차가 작은 경우에는 평가 한 번의 총점뿐 아니라 같은 평가가 반복해서 유지되는지도 함께 봐야 한다고 느꼈습니다. 반복해도 같은 팀이 상위에 남는다면, 그 평가는 운이 아니라 기준에 따라 어느 정도 일관되게 작동했다고 볼 수 있습니다.&lt;/p&gt;

&lt;h2&gt; 회고 &lt;/h2&gt;

&lt;p&gt;평가를 맡은 팀이 해커톤 뒤 잠깐 모여 회고했을 때 가장 아쉬워했던 점은 당일에 정해진 시간 안에 결과를 내야 하기 때문에 반복 평균을 사용하지 못했다는 것입니다. 다음에는 가능한 범위에서 평가를 병렬로 3~5회 반복하고, 평균 점수와 함께 반복 평가에서도 순위가 유지되는지를 보려고 합니다.&lt;/p&gt;

&lt;p&gt;실제 실행 검증까지 가지 못한 것도 한계였지만, 모든 제출물을 직접 실행해 보는 것이 다음 과제라고 생각하지는 않습니다. 낯선 저장소를 안정적으로 실행하는 일에는 평가와는 또 다른 어려움이 있고, 사후 반복 평가에서 확인했듯, 코드와 문서만 읽어도 실제 업무 흐름이 잘 이어진 팀은 반복해서 상위권에 남았기 때문입니다.&lt;/p&gt;

&lt;p&gt;반면, 다음에도 공통 평가 축을 먼저 정하고 문제별 세부 루브릭으로 구체화하는 방식은 유지하려고 합니다. 문제별 루브릭이 달라도 같은 평가 축을 공유했기 때문에 결과를 한 방향으로 모을 수 있었습니다.&lt;/p&gt;

&lt;h2&gt; 마치며 &lt;/h2&gt;

&lt;p&gt;이번 평가에서 가장 분명하게 확인한 것은, 채점 기준이 충분히 구체적이면 코드와 문서만 읽은 LLM도 사람의 판단에 가까운 결론에 도달할 수 있다는 점이었습니다. LLM은 단순히 기능 구현 여부만 보는 것이 아니라, 실제 문제를 해결하는 흐름이 코드 안에서 처음부터 끝까지 이어지는지까지 확인했습니다.&lt;/p&gt;

&lt;p&gt;명확한 루브릭과 평가 범위가 있다면, 더 복잡한 코드에서도 제품이 의도한 업무 흐름을 갖추었는지 점검하는 보조 평가로 LLM 평가를 활용할 수 있겠다는 아이디어를 얻었습니다. 작은 코드베이스에서 본 가능성이지만, 이번 해커톤에서 여러 팀의 코드를 같은 기준으로 살펴보고, 그 판단을 사람의 평가와 나란히 비교해 본 덕분에, 그 가능성을 작게나마 실제 사례로 확인할 수 있었습니다.&lt;/p&gt;
&lt;/body&gt;&lt;/html&gt;
</content>
    <id>https://d2.naver.com/helloworld/2541696</id>
    <link href="https://d2.naver.com/helloworld/2541696"/>
    <title>[AI 해커톤 후기] 코드와 문서만 읽은 LLM은 어떻게 사람과 같은 팀을 1위로 골랐을까</title>
    <updated>2026-07-06T23:44:16+09:00</updated>
    <dc:date>2026-07-06T23:44:16+09:00</dc:date>
  </entry>
  <entry>
    <author>
      <name>김상국</name>
    </author>
    <content type="html">&lt;!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd"&gt;
&lt;html&gt;&lt;body&gt;
&lt;h2&gt;분명 가이드에 다 적었는데, 왜 안 될까요?&lt;/h2&gt;
&lt;p&gt;분명 가이드에 다 적어놨습니다. 그런데 AI는 또 엉뚱한 코드를 내놓았습니다.&lt;/p&gt;
&lt;p&gt;저는 디자인 시스템 안에서 MCP(Model Context Protocol) 서버 개발을 주도하고 있었습니다. AI가 우리 디자인 시스템 규칙에 맞는 코드를 생성하도록 가이드하는 게 목적이었고, 그러려면 우리가 원하는 코드가 무엇인지를 모델에게 정확히 전달해야 했습니다.&lt;/p&gt;
&lt;p&gt;가이드라인을 짜고 실제 데이터를 넣어가며 테스트했지만 원하는 결과가 잘 나오지 않았습니다. 처음엔 프롬프트 문구나 가이드 양을 탓했습니다. 더 자세히, 더 많이 넣으면 나아지겠지 싶었죠. 그런데 증상이 묘하게 일관되지 않았습니다. 같은 입력인데 어떤 날은 멀쩡하고, 어떤 날은 가이드를 통째로 무시한 코드가 나왔습니다.&lt;/p&gt;
&lt;p&gt;아래 두 코드를 비교해 보면 차이가 분명합니다. 첫 번째 코드는 기본값을 명시하지 않은 것은 좋았으나 스페이싱, 디자인시스템 컴포넌트 미사용, 커스텀 스타일 사용 등 규칙을 무시했고, 두 번째 코드는 대체로 규칙을 지켰지만, 기본값을 명시한 예외가 발생했습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//생성 예제 1
@Composable
private fun FavoriteStoreProductCard(
    item: FavoriteStoreProductItem,
    onClick: () -&amp;gt; Unit,
    onFavoriteClick: () -&amp;gt; Unit,
    modifier: Modifier = Modifier,
) {
    Column(
        modifier = modifier.width(102.dp),
        verticalArrangement = Arrangement.spacedBy(10.dp), // 1. 정의된 스페이싱 안 씀
    ) {
        Thumbnail(
            ...
            onClicked = onClick,
            //2. 기본값 명시 안 함
        )

        Text( //3. 디자인시스템 내 텍스트 사용 안 함
            text = item.title,
            modifier = Modifier.fillMaxWidth(),
            style = ItemTitleTextStyle, //4. 커스텀 스타일 선언
            maxLines = 2,
            overflow = TextOverflow.Ellipsis,
        )
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;//생성 예제 2
@Composable
private fun FavoriteItemCard(
    item: FavoriteSectionItem,
    itemWidth: Dp,
    onClick: () -&amp;gt; Unit,
    favoriteIcon: @Composable () -&amp;gt; Unit,
) {
    Column(
        modifier = Modifier.width(itemWidth),
        verticalArrangement = Arrangement.spacedBy(ClayMintTheme.spacings.spacing125),
        //1. 정의된 스페이싱 사용
    ) {
        Thumbnail(
            ...
            pressedEffect = PressedEffect.None, // 2. 기본값 명시
            onClicked = onClick,
        )
        ClayText(//3. 디자인시스템 내 텍스트 사용
            text = item.title,
            modifier = Modifier.fillMaxWidth(),
            color = ClayMintTheme.colorScheme.foregroundPrimary,
            style = ClayMintTheme.typography.descriptionMedium, //4. 디자인시스템 내 스타일 사용
            maxLines = 2,
            overflow = TextOverflow.Ellipsis,
        )
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;도구를 더 만지기 전에, 의심의 방향을 틀었습니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;가이드를 못 써서가 아니라, 이 모델이 글을 어떻게 읽는지를 내가 모르고 있는 것 아닐까.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt; &lt;/p&gt;
&lt;h2&gt;팁을 더 찾기 전에, 원리를 먼저 봤습니다&lt;/h2&gt;
&lt;p&gt;검색하면 프롬프트 팁은 많이 나옵니다. 하지만 팁은 왜 되는지를 알려주지 않아서, 안 될 때 어디를 고쳐야 하는지도 알 수 없었습니다.&lt;/p&gt;
&lt;p&gt;그래서 두 가지를 직접 파봤습니다. 하나는 LLM이 입력을 어떻게 처리하는지(원리), 다른 하나는 Claude, GPT, Gemini 같은 최신 모델에서 프롬프팅 권고가 어떻게 바뀌었는지(경향)입니다.&lt;/p&gt;
&lt;p&gt;그 전에 하나만 짚고 가자면, LLM은 문장을 사람처럼 이해하는 게 아니라, 다음에 올 확률에 따라 단어(토큰)를 계속 이어 붙입니다. 출력이 확률 분포에서 하나를 고르는 일이다 보니, 같은 입력에도 답이 매번 조금씩 달라질 수 있습니다. 제가 겪은 "어떤 날은 되고 어떤 날은 안 되는" 현상의 뿌리가 여기였습니다. 가이드를 따르는 것조차 확정이 아니라 확률이었던 거죠.&lt;/p&gt;
&lt;p&gt;LLM을 깊이는 모르고 활용만 해왔다면, 다음 세 단어만 잡고 기억하고 가면 됩니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;컨텍스트 윈도우(Context Window): 모델이 한 번에 보는 작업 공간&lt;/li&gt;
&lt;li&gt;어텐션(Attention): 그 공간에서 무엇에 주목할지 정하는 방식&lt;/li&gt;
&lt;li&gt;컨텍스트 엔지니어링: 그 공간에 무엇을 담을지 설계하는 일&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;이 세 가지를 축으로 두 개의 질문을 따라갑니다. 먼저 모델이 글을 어떻게 읽는지를 봅니다. 순서대로 읽지 않고 관련도를 한꺼번에 따지는 어텐션, 무한하지 않은 컨텍스트 윈도우, 그리고 나에게 동의하려는 아첨 성향까지요.&lt;/p&gt;
&lt;p&gt; &lt;/p&gt;
&lt;h2&gt;첫 번째 질문: LLM은 글을 어떻게 읽을까요?&lt;/h2&gt;
&lt;p&gt;LLM 모델이 사람처럼 글을 왼쪽에서 오른쪽으로 순서대로 읽는다고 막연히 생각했는데, 아니었습니다. LLM 모델은 어텐션기반으로 컨텍스트 윈도우에 있는 필요한 정보를 한 번에 가져옵니다.&lt;/p&gt;
&lt;p&gt; &lt;/p&gt;
&lt;h3&gt;LLM은 순서가 아니라 관련도로 읽습니다&lt;/h3&gt;
&lt;p&gt;먼저, 어텐션은 입력 안의 각 단어가 다른 모든 단어와 얼마나 관련 있는지를 동시에 따져 의미를 만드는 방식입니다. 순서대로 훑는 게 아니라 한 번에 펼쳐놓고 관련도를 가중치로 계산합니다. 다만 이때 각 단어의 위치(순서) 정보도 함께 반영되며, 특히 앞이나 끝에 놓인 내용일수록 더 큰 가중치를 받는 경향이 있습니다. 모델이 어디에 주의를 두느냐가 곧 결과를 좌우한다는 뜻입니다.&lt;/p&gt;
&lt;p&gt;여기서 실제로 도움이 되는 점 두 가지가 나옵니다. 먼저, 관련된 지시와 자료끼리 관계를 분명히 드러내야 한다는 것입니다. 모델은 무엇이 무엇과 얼마나 관련 있는지를 스스로 따지는데, 정작 이어져야 할 지시와 자료 사이에 상관없는 내용이 잔뜩 끼면 그 관련도가 희석됩니다. 그래서 가이드에 줄 글이 아닌 양식이 있으면 도움이 되는데, 그 방법으로 가장 널리 쓰이고 양식이 무겁지 않은 MarkDown 파일이 쓰입니다.&lt;br&gt;
반면, 정보가 많을수록 오히려 정확도가 떨어질 수 있다는 것입니다. 모든 단어 쌍의 관련도를 계산하니 입력이 길어질수록 계산량이 제곱으로 늘고 주의가 묽어집니다. 가이드를 더 많이 넣으면 나아지겠지 했던 게 정확히 반대로 작용하던 이유입니다.&lt;br&gt;
다음은 어텐션이 문장 내 단어 간 관련도를 가중치로 계산하는 방식을 보여줍니다.&lt;/p&gt;
&lt;p&gt;&lt;img decoding="async" src="https://techblog.woowa.in/wp-content/uploads/2026/07/01-attentionV2.png" alt="attention"&gt;&lt;/p&gt;
&lt;p&gt;가이드 방법 중 하나인 few-shot 즉, 원하는 형식을 몇 개 보여주는 것이 먹히는 이유도 여기서 설명됩니다. 모델 안에는 앞선 패턴을 그대로 복사해 잇는 메커니즘(induction heads)이 있어서, 예시를 본 뒤 같은 형식을 이어가려는 경향이 생깁니다. 예시는 논리를 가르치는 도구가 아니라 형식을 복사하게 만드는 도구인 셈인데, 이 점이 뒤에서 다시 중요해집니다.&lt;/p&gt;
&lt;p&gt; &lt;/p&gt;
&lt;h3&gt;컨텍스트 윈도우는 무한한 메모리가 아니라 유한한 책상입니다&lt;/h3&gt;
&lt;p&gt;컨텍스트 윈도우는 모델이 한 번에 올려놓고 보는 작업 공간입니다. 여기 올라가는 건 제 질문만이 아닙니다. 시스템 프롬프트, 그동안의 대화 이력, 도구와 검색 결과, 지금 생성 중인 답이 전부 같은 공간을 나눠 씁니다.&lt;/p&gt;
&lt;p&gt;MCP나 긴 세션에서 대답이 들쑥날쑥했던 체감이 컸던 이유가 이거였습니다. 도구가 돌려준 결과와 누적된 대화가 알게 모르게 쌓여, 정작 중요한 가이드를 책상 밖으로 보이지 않게 밀어내고 있었습니다.&lt;/p&gt;
&lt;p&gt;게다가 책상이 넓다고 다 똑같이 잘 보는 것도 아닙니다. 정보가 입력의 한가운데 있으면 활용도가 떨어지는 현상이 관측됩니다(lost in the middle). 중요한 지시는 앞이나 끝에 둘 때 가장 알아듣습니다. 그래서 시스템프롬프트나 사용자의 명령에는 잘 따른다고 느껴질 수 있습니다.&lt;br&gt;
다음은 lost-in-the-middle이 발생할 수 있는 상황에 대한 예시를 보여줍니다.&lt;/p&gt;
&lt;p&gt;&lt;img decoding="async" src="https://techblog.woowa.in/wp-content/uploads/2026/07/02-lost-in-the-middle.png" alt="lost-in-the-middle" title="lost-in-the-middle"&gt;&lt;/p&gt;
&lt;p&gt;이를 해소할 수 있는 방안으로 XML 태그나 Markdown 제목처럼 구조를 갖춘 문서가 유리합니다. 제목과 태그가 위치 표지 역할을 해 주면, 모델이 필요한 대목을 더 잘 찾아내 한가운데 묻혀 유실되던 정보가 줄어듭니다.&lt;/p&gt;
&lt;p&gt; &lt;/p&gt;
&lt;h3&gt;모델은 사용자에게 동의하고 싶어합니다&lt;/h3&gt;
&lt;p&gt;토큰 예측만으로 설명되지 않는 동작이 하나 더 있습니다. 모델은 왜 이렇게 고분고분할까요.&lt;/p&gt;
&lt;p&gt;사실, 사전학습만 끝낸 모델은 원래 지시를 따르지 않습니다. 지금의 채팅 모델은 그 뒤에 사람의 선호로 보상을 주는 학습(RLHF 등 후처리)을 거쳐, 사람이 좋아하는 답을 내도록 정렬된 결과물입니다. 시스템 프롬프트를 따르는 힘조차 지능이 아니라 이 정렬에서 나옵니다.&lt;/p&gt;
&lt;p&gt;문제는 부작용입니다. 보상 과정이 동의는 곧 사용자 만족이라고 함께 학습해버려서, 모델은 사용자에게 동의하고 칭찬하려는 경향이 있습니다(sycophancy, 아첨). 실제로 2025년 4월 OpenAI는 GPT-4o 업데이트가 과도하게 아첨적이라는 이유로 &lt;a href="https://openai.com/index/sycophancy-in-gpt-4ohttp://" title="롤백"&gt;롤백&lt;/a&gt;하기도 했습니다.&lt;/p&gt;
&lt;p&gt;그래서 "이 설계 괜찮지?"라고 물으면 모델은 "네 좋습니다" 쪽으로 기웁니다. 코드 리뷰에서 "좋은 접근이에요"는 거의 의미없는 멘트입니다. 내가 한 거니까 칭찬하는 것에 가깝죠. 물어보는 방식을 바꿔야 합니다. "이 설계의 약점과 반례부터 찾아줘", "내 결론에 동의하지 말고 반박해줘"처럼요. 사용자의 생각을 드러낸 질문 즉, 유도신문을 피하는 것만으로 답의 정직함이 달라집니다.&lt;/p&gt;
&lt;p&gt; &lt;/p&gt;
&lt;h2&gt;두 번째 질문: 그래서 지금은 어떻게 쓸까요?&lt;/h2&gt;
&lt;p&gt;LLM이 글을 어떻게 읽는지 구조를 들여다보고 나니, 다음 질문이 자연스레 따라왔습니다. 지금 우리가 쓰는 최신 모델은 예전과 무엇이 다를까. AI는 눈에 띄게 좋아졌고, 갈수록 더 복잡한 일을 스스로 처리하도록 설계되고 있습니다. 도구가 이렇게 달라지는데 쓰는 방식만 그대로일 수는 없었죠. 원리는 그대로여도 그 위에서 권장되는 사용법은 모델 세대에 따라 바뀝니다. 그래서 이번엔 최신 모델들의 경향을 따라가 봤습니다.&lt;/p&gt;
&lt;p&gt; &lt;/p&gt;
&lt;h3&gt;프롬프트 엔지니어링에서 컨텍스트 엔지니어링으로&lt;/h3&gt;
&lt;p&gt;원리를 알고 나니, 업계가 왜 프롬프트 엔지니어링에서 컨텍스트 엔지니어링으로 무게중심을 옮겼는지 이해됐습니다. 프롬프트 엔지니어링이 한 줄을 영리하게 쓰는 법이라면, 컨텍스트 엔지니어링은 모델이 볼 토큰 전체를 무엇으로, 얼마나, 어떤 순서로 채울지 설계하는 일입니다. 어텐션과 컨텍스트 윈도우의 한계가 그대로 근거가 됩니다.&lt;/p&gt;
&lt;p&gt;프롬프트 기법이 한물갔다는 뜻은 아닙니다. 기법은 그대로 살아 있고, 사고의 단위가 한 줄에서 컨텍스트 전체로 넓어졌을 뿐입니다. 핵심 원칙은 세 가지입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;다 넣지 말고 관련된 것만 넣습니다. 정보가 많으면 어텐션이 묽어지니까요.&lt;/li&gt;
&lt;li&gt;핵심 지시와 자료는 앞이나 끝에 둡니다. 한가운데 묻으면 놓치기 쉽습니다.&lt;/li&gt;
&lt;li&gt;길이는 비용이자 노이즈입니다. 충분한 만큼만 최소로 넣습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;특히 마지막이 중요합니다. 관련 없는 텍스트는 그냥 쓸모없는 정도가 아니라 적극적으로 해롭습니다. 어텐션을 분산시켜 틀린 답을 유도하니까요. 혹시 모르니 다 붙여두기가 가장 흔한 안티패턴이고, 제가 바로 그러고 있었습니다.&lt;/p&gt;
&lt;p&gt;예를 들어, 이 원리가 잘 드러나는 예가 하위 에이전트(sub-agent) 오케스트레이션입니다. 상위 에이전트, 즉 슈퍼바이저의 작업 공간은 일이 진행될수록 어질러집니다. A안과 B안을 비교하다 A안으로 정해도 기각된 B안과 그동안 쌓인 도구 결과가 그대로 남는데, 정작 "A안을 구현하라"는 작업에는 대부분 노이즈입니다. 그래서 슈퍼바이저가 그 어질러진 공간에서 직접 구현하는 것보다, 정제된 목표만 들려 하위 에이전트를 깨끗한 새 공간에서 돌리는 편이 그 작업에는 더 낫습니다.&lt;br&gt;
다음은 하위 에이전트에게 전달하는 문맥에 따라 발생하는 컨텍스트 윈도우의 차이에 대해 보여줍니다.&lt;/p&gt;
&lt;p&gt;&lt;img decoding="async" src="https://techblog.woowa.in/wp-content/uploads/2026/07/03-subagent.png" alt="sub-agent" title="sub-agent"&gt;&lt;/p&gt;
&lt;p&gt;하위 에이전트가 더 똑똑해서가 아닙니다. 대개 같은 모델이고, 차이는 일꾼이 아니라 깨끗해진 작업 공간에서 옵니다. 다만 공짜는 아닙니다. 슈퍼바이저가 목표를 제대로 정제해 넘겨야 하고, 버린 맥락에 진짜 제약이 숨어 있었다면 그걸 빼먹고 넘기는 순간 하위 에이전트는 같은 함정에 걸립니다. 결국 어려움은 사라지는 게 아니라 무엇을 넘기고 무엇을 뺄지 고르는 일로 옮겨갈 뿐입니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;이 흐름은 여기서 멈추지 않습니다. 프롬프트, 컨텍스트, 하네스, 루프 엔지니어링 순으로 한 칸씩 올라갑니다. 매번 손으로 시키는 대신 에이전트가 도는 루프(성공 기준, 멈출 조건, 자가 수정)를 설계하는 쪽으로 무게가 옮겨가고, 가치의 단위도 응답 한 번에서 목표까지 가는 방식으로 바뀌어가고 있습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt; &lt;/p&gt;
&lt;h3&gt;모델이 강해질수록 지시는 가벼워집니다&lt;/h3&gt;
&lt;p&gt;컨텍스트를 어떻게 채울지가 바뀌면, 프롬프트를 쓰는 습관 자체도 함께 낡습니다. 제가 딱 그랬어요. AI를 처음 익히던 무렵의 모델은 추론이 약해서 단계를 하나하나 알려줘야 했고, 그래서 "단계별로 생각해(let’s think step by step)" 강제, 과한 few-shot, 깐깐한 절차 나열을 좋은 습관으로 배웠습니다.&lt;/p&gt;
&lt;p&gt;그런데 지금의 추론 모델들은 복잡한 문제를 해결하도록 발전해왔고 스스로 단계를 나눕니다. 배워둔 습관이 오히려 독이 됐어요. 제약을 스무 개 나열하니 서로 충돌했고, few-shot을 도배하니 그 예시만 사용했고, 어설픈 사고 경로를 강요하면 성능이 떨어지기도 했습니다. 그래서 "어떻게 하는지"를 하나하나 알려주던 걸 접고, "무엇이 성공인지"를 주고 맡기는 쪽으로 제 습관을 바꿨습니다. 제약과 금지를 잔뜩 나열하기보다 핵심만 긍정형으로 남기고, 로직을 가르치려던 few-shot은 형식과 톤을 고정하는 용도로만 줄였습니다.&lt;/p&gt;
&lt;p&gt;가장 크게 바꾼 습관은 목표를 검증 가능하게 말하는 것이었습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;”버그 고쳐줘” → “이 실패하는 테스트를 통과시켜줘”&lt;/li&gt;
&lt;li&gt;“정리해줘” → “이 표에서 누락된 행을 찾아 표시해줘”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이렇게 성공 조건과 지켜야 할 규칙을 못 박아 모델의 행동을 붙들어 두는 층이 하네스입니다. 과도한 절차를 대신 "무엇이 성공인가"를 규정해 두는 것이죠. 그럼 이 방향을 실제 MCP 가이드에는 어떻게 담았을까요.&lt;/p&gt;
&lt;p&gt; &lt;/p&gt;
&lt;h3&gt;내가 검토한 두 갈래: 가이드라인과 하네스(프롬프트)&lt;/h3&gt;
&lt;p&gt;개선 방법은 두 갈래였습니다. 하나는 문맥이 부족하다고 보고 가이드라인을 더 주는 것, 다른 하나는 하네스 엔지니어링이 필요하다고 보고 동작을 규정하는 프롬프트를 더하는 것이었습니다. 둘은 푸는 문제가 다릅니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;구분&lt;/th&gt;
&lt;th&gt;가이드라인 추가&lt;/th&gt;
&lt;th&gt;하네스(프롬프트) 추가&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;푸는 문제&lt;/td&gt;
&lt;td&gt;모델이 무엇을 알아야 하는가 (지식과 맥락)&lt;/td&gt;
&lt;td&gt;모델이 어떻게 행동해야 하는가 (절차, 검증, 중단 조건)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;들어가는 내용&lt;/td&gt;
&lt;td&gt;디자인 시스템 규칙, 컴포넌트와 토큰 정의, 예시&lt;/td&gt;
&lt;td&gt;출력 형식 계약, 검증 루프, 유저 입력에 휘둘리지 않게 하는 규정&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;강할 때&lt;/td&gt;
&lt;td&gt;모델이 우리 도메인을 모를 때&lt;/td&gt;
&lt;td&gt;유저 입력이 들쭉날쭉해도 일관된 동작이 필요할 때&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;과하면&lt;/td&gt;
&lt;td&gt;컨텍스트가 길어져 핵심이 묻힘&lt;/td&gt;
&lt;td&gt;절차를 과하게 명시해 최신 추론 모델의 발목을 잡음&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;가이드라인 쪽에서 실제로 어려움을 겪은 적이 있습니다. dp 값은 반드시 spacing 토큰으로 지정하라고 가이드했더니, 모델이 이미지 크기 108dp를 spacing1000(80) + spacing300(24) + spacing50(4)으로 조합해버렸습니다. 간격이 아니라 크기 값은 토큰 없이 로우(raw) dp를 써야 하는데 말이죠. 게다가 few-shot 예시까지 함께 넣었더니 그 패턴을 모든 사이즈 계산에 확대 적용했습니다.&lt;/p&gt;
&lt;p&gt;규칙이 틀린 게 아니라 너무 문자 그대로였던 게 문제였습니다. 그래서 특정 케이스를 못 박는 대신, "간격과 여백은 spacing 토큰으로, 아이콘이나 이미지 같은 크기 값은 로우 dp로" 처럼 경계를 원리로 이해되게 한 단계 추상화해서 다시 썼습니다.&lt;/p&gt;
&lt;p&gt; &lt;/p&gt;
&lt;h2&gt;그래서 무엇이 달라졌나&lt;/h2&gt;
&lt;p&gt;원리와 경향을 알고 가이드와 프롬프트를 다시 손봤습니다. 크게 세 가지를 바꿨습니다. 관련 있는 것만 남겨 컨텍스트를 큐레이션했고, 흩어져 있던 규칙을 레이어별로 모아 어떤 결정을 어디서 내릴지를 고정했고, 절차를 떠먹이는 대신 성공 기준과 행동 규정(하네스)을 줬습니다.&lt;br&gt;
 &lt;/p&gt;
&lt;h3&gt;하네스를 정리했습니다: 긴 문장을 쪼개고, 결정의 자리를 고정&lt;/h3&gt;
&lt;p&gt;가이드와 프롬프트는 틀리진 않았지만 모호했고, 그 모호함이 흔들림의 1차 원인이었습니다. 60단어를 넘는 한 문장에 규칙 대여섯 개가 뭉쳐 있으면 모델은 그중 일부를 실행마다 다르게 빠뜨렸습니다. 그래서 규칙과 예산은 그대로 둔 채 구조만 바꿔, 한 문장을 원자 단위 지시로 쪼갰습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//Before — 한 문장에 규칙 다섯 개
아이콘마다 search-icon으로 ClayMintDrawable id를 확인하고 ClayMintDrawable import를 추가하며, _fill/_line 쌍은 정적 디자인이 아니라 바인딩된 state/variant에서 접미사를 고르고, 상태가 있는 아이콘은 코드에서 분기한다(state → _fill / !state → _line);  ...&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;//After — 원자 지시로 분해
아이콘마다:
- search-icon으로 ClayMintDrawable id를 확인한다.
- ClayMintDrawable import를 추가한다.
- _fill / _line 쌍은 바인딩된 state/variant에서 접미사를 고른다.
- 코드에서 상태로 분기한다(state → _fill, !state → _line).
- search-icon 결과가 없을 때만 그 드로어블을 추론으로 표시한다.&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;또 하나는 결정의 소유권을 고정한 것입니다. "raw 픽셀을 토큰으로 바꿀지"(토큰 해석), "그 토큰을 padding에 쓸지 width에 쓸지"(간격 방출), "어디까지 추론으로 표시할지"(추론 경계)까지, 이 세 결정을 한 문장에 섞어두니 모델이 매번 다르게 합쳤습니다. 각 결정을 늘 같은 레이어의 같은 규칙에서 내리도록 나눴더니, 같은 값이 같은 자리에서 같게 처리됐습니다.&lt;br&gt;
 &lt;/p&gt;
&lt;h3&gt;절차 대신 성공 기준과 하네스를 줬습니다&lt;/h3&gt;
&lt;p&gt;"이렇게 저렇게 해라"를 잔뜩 나열하는 대신, 무엇이 맞는지를 정의하고 그걸 스스로 지키게 했습니다. 두 가지가 핵심이었습니다.&lt;/p&gt;
&lt;p&gt;첫째, 사후 검증 규칙셋입니다. 생성이 끝난 코드를 컴포넌트 manifest·토큰·아이콘 카탈로그가 확인 사항을 기준으로 삼아 점검하는 규칙 열여섯 개를 뒀습니다. 예를 들어 "컴포넌트 검색 결과가 비면 그 역할에는 컴포넌트가 없다는 뜻이니, 그럴듯한 이름을 지어 부르지 말고 빌딩블록이나 토큰 기반 custom으로 만들라" 같은 식입니다. 도구가 확정할 수 없는 것(픽셀 단위 시각 일치)은 애초에 검증 범위 밖으로 명시해, 규칙이 못 지킬 약속을 하지 않게 했습니다.&lt;/p&gt;
&lt;p&gt;둘째, 컴포넌트 선택 사다리입니다. 전에는 "완성형 컴포넌트에 있나 없나" 이분법뿐이라, 없으면 곧장 custom UI로 직행했고 그 구조가 실행마다 달랐습니다. 완성형과 custom 사이에 표준 빌딩블록 단계를 끼워, 완성형 → 빌딩블록 → 토큰 기반 custom → raw 순으로 한 단계씩만 내려가게 했습니다. "없으면 즉흥 custom"이 "표준 재료 우선"으로 수렴했습니다.&lt;/p&gt;
&lt;p&gt; &lt;/p&gt;
&lt;h3&gt;도입부에서 소개한 그 코드가 규칙을 지키기 시작했습니다&lt;/h3&gt;
&lt;p&gt;도입부의 어긋났던 네 지점이 개선 뒤 어떻게 바뀌었는지 나란히 놓으면 이렇습니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;지점&lt;/th&gt;
&lt;th&gt;before (1장 코드)&lt;/th&gt;
&lt;th&gt;after&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;스페이싱&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Arrangement.spacedBy(10.dp)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Arrangement.spacedBy(ClayMintTheme.spacings.spacing125)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;기본값&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;pressedEffect = PressedEffect.None&lt;/code&gt; 명시&lt;/td&gt;
&lt;td&gt;명시 안 함&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;텍스트&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Text(...)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ClayText(...)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;스타일&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;style = ItemTitleTextStyle&lt;/code&gt; (커스텀 선언)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;style = ClayMintTheme.typography.descriptionMedium&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;결과는 분명했습니다. 결과가 상이한 경우가 0이 되진 않았지만, 가이드가 지켜지지 않는 빈도가 5번에 1~2번에서 10번에 1번 미만으로 줄었습니다. 도구가 확정하지 못해 [추론]으로 남기던 영역 자체가 줄어든 덕분입니다. 그러면서 마땅한 완성형 컴포넌트가 없을 때 무엇으로 대신할지 고르는 판단처럼, 사람이 아니면 안 된다고 보고 남겨뒀던 부분도 이제 충분히 맡길 만해졌습니다.&lt;/p&gt;
&lt;p&gt;여기서 "어겼다"와 "나아졌다"의 기준은 이렇습니다. 같은 Figma 입력을 여러 번(기본 4회) 실행해 산출물 코드에서 뽑은 구조 신호(어떤 컴포넌트·토큰·아이콘을 썼는지, [추론] 주석을 어디에 달았는지)가 실행마다 갈리면 흔들림 1건으로, 생성 코드가 manifest·토큰·가이드 규칙에 어긋나면 규칙 위반 1건으로 셉니다. 판정은 눈대중이나 LLM 채점이 아니라(채점기 자체가 흔들리니까) 이 신호 집합을 기계적으로 비교해 내렸고, 위 "5번에 1~2번"은 그 반복 실행에서 관찰한 값입니다.&lt;/p&gt;
&lt;p&gt; &lt;/p&gt;
&lt;h2&gt;정리&lt;/h2&gt;
&lt;p&gt;원하는 결과를 얻지 못한 진짜 원인은 도구가 아니라, 모델을 모르고 쓰던 저 자신이었습니다. LLM모델이 프롬프트를 관련성을 따져 읽고, 작업 공간이 유한하며, 나에게 동의하려 한다는 것. 이 세 가지만 알아도 프롬프트를 보는 눈이 달라집니다.&lt;/p&gt;
&lt;p&gt;다만 이게 모든 모델에 통하는 만능은 아닙니다. 위 습관들은 추론이 좋은 최신 모델을 전제로 합니다. 오래됐거나 추론이 약한 모델에서는 few-shot과 단계별 가이드가 여전히 잘 먹히고, 작은 경량 모델을 하위 작업에 쓸 때는 오히려 절차와 검증 규칙을 더 촘촘하게 주는 편이 안정적입니다.&lt;/p&gt;
&lt;p&gt;결국 남은 몫은 모델에게 잘 부탁하는 법이 아닌, 모델이 어떻게 읽는지를 알고 무엇을 컨텍스트로 제공할지 설계하는 일입니다. 혹시 분명 다 적었는데 왜 안 되지 싶다면, 프롬프트를 한 줄 더 다듬기 전에 책상(Context Window)에 뭐가 올라가 있는지부터 보시길 권합니다.&lt;/p&gt;
&lt;p&gt;생성만 놓고 보면 이전보다 훨씬 쉬워졌고, 진짜 실력은 검증과 판단 그리고 방향 설정에 있습니다.&lt;/p&gt;
&lt;p&gt; &lt;/p&gt;
&lt;h3&gt;한눈에 보는 결론: Do / Don’t&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Don’t (예전 습관)&lt;/th&gt;
&lt;th&gt;Do (지금)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;관련 있어 보이면 일단 다 넣는다&lt;/td&gt;
&lt;td&gt;필요한 것만 골라 넣는다 (컨텍스트 큐레이션)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;지시의 중요도와 상관없이 프롬프트를 늘어놓는다&lt;/td&gt;
&lt;td&gt;핵심 지시는 앞이나 끝에 둔다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;프롬프트가 길수록 더 정확해진다고 믿는다&lt;/td&gt;
&lt;td&gt;길이는 비용이자 노이즈다. 충분한 만큼만 넣는다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;절차를 하나하나 떠먹인다&lt;/td&gt;
&lt;td&gt;목표와 성공 기준을 주고 맡긴다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;금지 사항을 잔뜩 나열한다&lt;/td&gt;
&lt;td&gt;원하는 행동을 긍정형으로 지시한다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;로직까지 few-shot으로 가르친다&lt;/td&gt;
&lt;td&gt;예시는 형식과 톤 고정용으로만 쓴다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;규칙을 특정 케이스로 못 박는다&lt;/td&gt;
&lt;td&gt;경계를 원리로 적는다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;큰 작업을 프롬프트 하나에 다 몰아넣는다&lt;/td&gt;
&lt;td&gt;하위 작업은 정제된 목표만 줘서 분리한다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"이거 괜찮지?"로 확인받는다&lt;/td&gt;
&lt;td&gt;"약점과 반례부터 찾아줘"로 검증시킨다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;한 번 잘 나왔으니 늘 그렇게 나온다고 믿는다&lt;/td&gt;
&lt;td&gt;출력은 확률이라 매번 흔들린다고 보고, 중요한 건 다시 확인한다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;최신 모델이니 무조건 짧게 쓴다&lt;/td&gt;
&lt;td&gt;대상 모델의 성격에 맞춰 조절한다&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;The post &lt;a href="https://techblog.woowahan.com/26459/"&gt;AI가 내 프롬프트를 흘려듣는 이유: 원리부터 다시 본 컨텍스트 엔지니어링&lt;/a&gt; first appeared on &lt;a href="https://techblog.woowahan.com"&gt;우아한형제들 기술블로그&lt;/a&gt;.&lt;/body&gt;&lt;/html&gt;
</content>
    <id>https://techblog.woowahan.com/26459/</id>
    <link href="https://techblog.woowahan.com/26459/"/>
    <summary type="html">&lt;p&gt;분명 가이드에 다 적었는데, 왜 안 될까요? 분명 가이드에 다 적어놨습니다. 그런데 AI는 또 엉뚱한 코드를 내놓았습니다. 저는 디자인 시스템 안에서 MCP(Model Context Protocol) 서버 개발을 주도하고 있었습니다. AI가 우리 디자인 시스템 규칙에 맞는 코드를 생성하도록 가이드하는 게 목적이었고, 그러려면 우리가 원하는 코드가 무엇인지를 모델에게 정확히 전달해야 했습니다. 가이드라인을 짜고 실제 데이터를 넣어가며 테스트했지만 원하는 [&amp;#8230;]&lt;/p&gt;
The post &lt;a href="https://techblog.woowahan.com/26459/"&gt;AI가 내 프롬프트를 흘려듣는 이유: 원리부터 다시 본 컨텍스트 엔지니어링&lt;/a&gt; first appeared on &lt;a href="https://techblog.woowahan.com"&gt;우아한형제들 기술블로그&lt;/a&gt;.</summary>
    <title>AI가 내 프롬프트를 흘려듣는 이유: 원리부터 다시 본 컨텍스트 엔지니어링</title>
    <updated>2026-07-07T15:00:39+09:00</updated>
    <dc:date>2026-07-07T15:00:39+09:00</dc:date>
  </entry>
  <entry>
    <author>
      <name>owen@infograb.net (Owen)</name>
    </author>
    <content type="html">&lt;!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd"&gt;
&lt;html&gt;&lt;body&gt;&lt;p&gt;같은 코드를 리뷰시켜도 판정이 매번 달라지는 이유는 LLM의 비결정성 때문입니다. temperature를 0으로 내려도 흔들림은 남습니다. 코드 위임·폭 좁히기·다수결·하네스로 LLM 판정의 일관성을 높이는 네 가지 방법을, 총 55회 호출 실험 결과와 함께 정리했습니다.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;
</content>
    <id>https://insight.infograb.net/blog/2026/07/15/ai-review/</id>
    <link href="https://insight.infograb.net/blog/2026/07/15/ai-review/"/>
    <summary type="html">같은 코드를 리뷰시켜도 판정이 매번 달라지는 이유는 LLM의 비결정성 때문입니다. temperature를 0으로 내려도 흔들림은 남습니다. 코드 위임·폭 좁히기·다수결·하네스로 LLM 판정의 일관성을 높이는 네 가지 방법을, 총 55회 호출 실험 결과와 함께 정리했습니다.</summary>
    <title>AI 코드 리뷰 자동화, 판정은 왜 흔들릴까 - 원인과 해결법 4가지</title>
    <updated>2026-07-15T09:00:00+09:00</updated>
    <dc:date>2026-07-15T09:00:00+09:00</dc:date>
  </entry>
  <entry>
    <author>
      <name>grace@infograb.net (Grace)</name>
    </author>
    <content type="html">&lt;!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd"&gt;
&lt;html&gt;&lt;body&gt;&lt;p&gt;n8n 워크플로가 100개를 넘으면 누가 만들었고 무엇에 물려 있는지부터 흐려집니다. n8n 기본 기능으로 소유권·구조를 어디까지 볼 수 있는지, 그 빈자리를 인포그랩 Nelper는 어떻게 채우는지 정리했습니다. &lt;/p&gt;&lt;/body&gt;&lt;/html&gt;
</content>
    <id>https://insight.infograb.net/blog/2026/07/15/n8n-visibility/</id>
    <link href="https://insight.infograb.net/blog/2026/07/15/n8n-visibility/"/>
    <summary type="html">n8n 워크플로가 100개를 넘으면 누가 만들었고 무엇에 물려 있는지부터 흐려집니다. n8n 기본 기능으로 소유권·구조를 어디까지 볼 수 있는지, 그 빈자리를 인포그랩 Nelper는 어떻게 채우는지 정리했습니다. </summary>
    <title>n8n 워크플로 관리 - 워크플로는 남는데 소유권은 왜 사라질까</title>
    <updated>2026-07-15T09:00:00+09:00</updated>
    <dc:date>2026-07-15T09:00:00+09:00</dc:date>
  </entry>
  <entry>
    <author>
      <name>NHN Cloud</name>
    </author>
    <content type="html">&lt;!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd"&gt;
&lt;html&gt;&lt;body&gt;&lt;p&gt;[![NHN Cloud_meetup banner_FactoryX WP_202606_900.png](https://image.toast.com/aaaadh/real/2026/techblog/NHN%20Cloudmeetup%20bannerFactoryX%20WP202606900.png)](https://www.nhncloud.com/kr)

## 어제의 경험으로 내일의 변화를 만들어 내는 공장, NHN FactoryX
필요한 만큼의 GPU를 사서 랙에 꽂으면 AI 인프라가 완성될까요? 

실제로 대규모 GPU 클러스터를 운영해 본 팀이라면, 장비 구매는 끝이 아니라 시작이라는 것을 알 수 있습니다. 수백 장의 GPU가 동시에 통신을 시도하는 순간 네트워크에 병목이 발생하고, 학습 데이터를 읽어오는 속도가 GPU 연산을 따라가지 못해 고가의 장비가 데이터를 기다립니다. 최신 AI GPU 랙의 전력 밀도는 60~90kW에 달하지만 공랭식 냉각의 실질적 한계는 30kW 부근이라 그 이상에서는 GPU 온도를 억제할 수 없고, 결국 시스템이 스스로 성능을 깎아내리기 시작합니다.

GPU를 '사는 것'과 GPU를 'AI 인프라로 만드는 것'은 완전히 다른 문제입니다. 그리고 이 간극을 메우는 데 필요한 기술적 의사결정은 생각보다 많고, 예상보다 깊습니다.

NHN FactoryX는 NHN Cloud가 7년 이상의 GPU 인프라 운영 경험을 바탕으로 만든 AI 워크로드를 위한 풀스택 환경입니다. NHN FactoryX 서울 데이터 센터에 4,080장의 GPU를 단일 클러스터로 묶어 FP8 기준 27.4 EFLOPS 규모의 연산 능력을 확보했습니다. 구조는 세 개의 계층으로 나뉩니다. 맨 아래에서 수랭식 데이터 센터와 GPU 클러스터(Infrastructure)가 물리적 기반을 제공하고, 그 위에서 GPU Live와 AI EasyMaker(Platform)가 GPU 자원의 동적 할당과 ML 파이프라인을 담당하며, 최상위에는 ProjectX(Service)가 위치해 기업 전용 AI 에이전트 개발·연동 환경을 제공합니다. 현재 산업계·학계·공공 기관이 이 플랫폼 위에서 LLM 학습, 멀티모달 AI 연구, 추론 서비스를 운영하고 있습니다.

![01_3layers_900.png](https://image.toast.com/aaaadh/real/2026/techblog/013layers900.png)

이번에 발간한 〈NHN FactoryX 기술 백서〉는 이 중 인프라와 플랫폼 영역의 설계 근거를 약 60여 페이지에 걸쳐 풀어 설명한 기술 백서입니다.

&lt;br&gt;
## NHN FactoryX의 모든 것, 〈NHN FactoryX 기술 백서〉
AI 인프라의 도입은 더 이상 장비 구매가 아니라 기술 검증을 동반하는 의사결정 프로세스입니다. GPU 모델 사양만 비교해서는 안 되며, 인터커넥트 토폴로지, 스토리지 I/O, 네트워크 병목 대응, 냉각 설계, 선형 확장성까지, 풀스택 관점의 설계를 동시에 고민해야 합니다. 이 전체를 한 곳에서 체계적으로 다룬 자료가 부재했기 때문에 NHN Cloud는 AI 인프라 요구 사항에 NHN FactoryX가 어떻게 답하고 있는지를 하나의 문서로 정리한 기술 백서를 발간했습니다.

이 백서는 위에서 다룬 NHN FactoryX의 세 레이어 가운데 인프라와 플랫폼, 즉 AI 워크로드를 물리적으로 지탱하고 자원을 지능적으로 배분하는 두 레이어의 설계 철학과 기술 근거를 담고 있습니다. 백서는 먼저 기존의 범용 클라우드 인프라가 AI 워크로드에 대응하기에 어떤 점에서 한계가 있는지 짚는 데서 출발합니다. 기존 데이터 센터는 CPU 중심 연산, 공랭식 냉각, 이더넷 기반 네트워크를 전제로 설계되어 범용 워크로드에는 효율적이지만, 대규모 AI 학습에서는 구조적 병목에 직면합니다. 이 한계를 넘기 위해 NHN FactoryX가 어떤 설계 선택을 했는지를, 다음 네 개의 핵심 영역으로 정리합니다.

![02_agenda_900.png](https://image.toast.com/aaaadh/real/2026/techblog/02agenda900.png)

각 영역은 독립적으로 참조할 수 있지만, 풀스택 관점에서 서로 연결되어 있습니다. 여기에서는 이 백서가 다루는 질문 중 핵심이 되는 내용을 간략하게 훑어보고자 합니다.

&lt;br&gt;
## 〈NHN FactoryX 기술 백서〉 훑어보기
### "왜 공기로는 안 되나요?" - 공랭식 냉각의 구조적 한계
AI GPU의 전력 밀도는 세대마다 가파르게 올라가고 있습니다. A100 시절에는 랙당 13\~39kW 수준이었지만, 최신 B200/B300 세대에서는 랙당 90kW에 육박합니다. 문제는 여기서 발생합니다. 공기의 체적 열용량은 물의 약 1/3,500에 불과합니다. 같은 열을 제거하려면 공기는 물보다 3,500배 많은 양이 필요하다는 뜻입니다. 랙당 전력 밀도가 30kW를 넘는 순간부터 공랭식은 열역학적 한계에 부딪힙니다.

〈NHN FactoryX 기술 백서〉는 이 한계를 ASHRAE 등급 기준과 CFD 시뮬레이션 연구를 인용해 구조적으로 짚어보고, D2C 수랭식 도입 시 반드시 검증해야 할 다섯 가지 요건(벤더 호환성, 인터페이스 표준화, 운전 파라미터, 확장성, 수질 관리)을 하나하나 설명합니다. NHN FactoryX는 이 과정을 거쳐 프리쿨링 연계 운전으로 PUE 1.15 이하를 달성할 수 있는 구조를 만들었습니다. 공랭식 대비 연간 냉각 에너지를 약 30~40% 절감할 수 있는 수치입니다.

자세한 내용은  &lt;strong&gt;〈NHN FactoryX 기술 백서〉 2.3절 전력 및 냉각 설계&lt;/strong&gt;에서 확인하실 수 있습니다.

&lt;br&gt;

### "GPU 수천 장을 어떻게 나눠 쓰나요?" - GPU Live의 설계 철학
GPU 클러스터를 팀별로 고정 배정하면 운영은 단순하지만, 대규모 클러스터 트레이스 분석에서 자원 활용률이 한 자릿수에서 30%대에 머무르는 사례가 보고되어 왔습니다.

GPU Live는 이 문제를 동적 할당 기술로 풀되, 자체 스케줄러를 새로 만들지 않고, Kubernetes 표준 생태계의 검증된 컴포넌트 위에 NHN FactoryX 환경에 맞는 운영 정책을 얹는 방식을 택했습니다. GPU 드라이버 업데이트나 분산 학습 프레임워크의 변화를 표준 경로로 흡수할 수 있고, 향후 NPU 같은 이종 가속기가 등장해도 동일한 표준 인터페이스로 통합이 가능하도록 하기 위함입니다.

그 위에 GPU Live가 직접 설계한 영역이 있습니다. 3계층 멀티테넌시(System → Organization → Project)로 조직 간 자원·정책·권한을 격리하고, GPU 기준 Best-fit 빈패킹으로 단편화를 줄이며, 쿼터·오버부킹·자동 회수를 결합해 활용률을 끌어올립니다. 학습 워크로드는 Gang Scheduling으로 일괄 시작·종료하고, 추론 워크로드는 인스턴스 단위로 장애 복구와 재배치를 처리합니다. 같은 GPU 풀에서 성격이 전혀 다른 두 워크로드를 안정적으로 수용하는 구조입니다.

자세한 내용은  &lt;strong&gt;〈NHN FactoryX 기술 백서〉 2.4절 GPU 동적 할당 기술(GPU Live)&lt;/strong&gt;에서 확인하실 수 있습니다.

&lt;br&gt;

### "베어메탈이면 충분하지 않나요?" — GPUaaS를 선택한 이유
베어메탈 방식은 GPU 자원을 물리 서버 단위로 직접 할당하므로 하드웨어 성능을 최대한 활용할 수 있습니다. 다만 다수의 사용자가 동일 인프라를 공유할 경우 테넌트 간 보안 격리가 보장되지 않고, 테넌트별 환경 구성이 상이하여 장애 발생 시 일관된 운영 정책을 적용하기 어렵다는 한계가 있습니다.

NHN FactoryX는 이 한계를 극복하기 위해 GPU 인프라 상위 계층에 GPUaaS 플랫폼을 구축하는 방식을 택했습니다. 테넌트·워크스페이스·프로젝트로 이어지는 계층적 구조를 기반으로 자원과 보안 통제를 일관되게 적용하고, 한 기관에서 대규모 학습으로 자원 사용량이 급증하더라도 다른 기관의 서비스에는 영향이 없는 격리 구조를 실현했습니다. 실제로 957개 GPU 노드 위에서 산업계·학계·연구계의 다수 기관이 독립된 워크스페이스를 운영하고 있습니다.

서비스 계층이 추가되면 네트워크 병목으로 인한 성능 저하가 우려될 수 있지만, NHN FactoryX는 GPUaaS 플랫폼 환경에서 255노드 규모의 HPL(High Performance Linpack) 벤치마크를 수행해 베어메탈과 동일한 수준의 성능을 확인했습니다. 컨테이너가 호스트 수준의 네트워크 성능을 유지하도록 설계한 결과입니다.

자세한 내용은  &lt;strong&gt;〈NHN FactoryX 기술 백서〉 06장 활용 사례&lt;/strong&gt;에서 확인하실 수 있습니다.

&lt;br&gt;
## 마치며

이 글에서는 〈NHN FactoryX 기술 백서〉의 핵심 내용 중 일부를 살펴보았습니다. 백서 전체를 관통하는 메시지를 한 문장으로 요약해 보면 이렇습니다. AI 인프라는 개별 하드웨어의 성능 향상을 넘어, 물리적 공간 설계부터 전력 및 냉각 효율, 초고속 네트워크 튜닝, 소프트웨어 정의 기반의 관리 플랫폼까지 모든 요소가 유기적으로 통합되어야 합니다.

NHN FactoryX는 이 통합을 하나의 공간에서 실현합니다. 수랭식 데이터 센터 위에 GPU 클러스터를 구성하고, GPUaaS 플랫폼으로 자원을 관리하며, AI 개발 플랫폼까지 3개 레이어를 풀스택으로 제공합니다. 동일한 GPUaaS 아키텍처 위에서 멀티테넌트 공유 환경과 기업 프라이빗 전용 환경이 모두 구현되며, 957개 GPU 노드 규모에서 베어메탈 동등 수준의 성능과 조직 간 보안 격리를 동시에 달성하고 있습니다.

AI 인프라는 현재진행형입니다. GPU 세대 전환에 따른 전력 밀도 증가, Kubernetes DRA를 비롯한 자원 관리 표준의 진화, NPU 등 이종 가속기의 등장은 인프라 설계의 전제 조건을 지속적으로 변화시킵니다. NHN FactoryX는 차세대 GPU 플랫폼까지의 상위 호환성을 전제로 수랭식 인프라를 설계하였으며, GPU Live의 동적 할당 모델을 이종 가속기로 확장하기 위한 기술 검토를 진행하고 있습니다.

백서에는 이 글에서 미처 다 다루지 못한 GPU 세대별 사양 비교, 5가지 성능 저하 패턴과 설계 단계 체크리스트, Scalable Unit 기반 증설 전략, 기업 프라이빗 AI 인프라 구축 사례 등이 구체적인 수치와 다이어그램과 함께 담겨 있습니다. AI 인프라 도입을 검토하는 기술 의사결정자, 대규모 AI 시스템을 설계하거나 운영하는 아키텍트 및 엔지니어 분들께 실질적인 기술 레퍼런스로 활용되길 기대합니다.

긴 글을 읽어 주셔서 감사합니다. ‍♀️


&amp;gt;  [NHN FactoryX 공식 포털 바로 가기](https://www.nhnfactoryx.com/?utm_source=meetup&amp;amp;utm_medium=post&amp;amp;utm_campaign=meetup&amp;amp;utm_content=0713_xwhitepaper)
&amp;gt; 
&amp;gt;  [NHN FactoryX 기술 백서 다운로드](https://info.nhncloud.com/factoryx-whitepaper?utm_source=meetup&amp;amp;utm_medium=post&amp;amp;utm_campaign=meetup&amp;amp;utm_content=0713_xwhitepaper)
&amp;gt; 
&amp;gt;  [NHN FactoryX 소개서 다운로드](https://info.nhncloud.com/factoryxbrochure?utm_source=meetup&amp;amp;utm_medium=post&amp;amp;utm_campaign=meetup&amp;amp;utm_content=0713_xwhitepaper)

[![NHN Cloud_meetup banner_footer_grayd9d9d9_202606_900.png](https://image.toast.com/aaaadh/real/2026/techblog/NHN%20Cloudmeetup%20bannerfootergrayd9d9d9202606900.png)](https://www.nhncloud.com/kr)&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;
</content>
    <id>https://meetup.nhncloud.com/posts/417</id>
    <link href="https://meetup.nhncloud.com/posts/417"/>
    <summary type="html">[![NHN Cloud_meetup banner_FactoryX WP_202606_900.png](https://image.toast.com/aaaadh/real/2026/techblog/NHN%20Cloudmeetup%20bannerFactoryX%20WP202606900.png)](https://www.nhncloud.com/kr)

## 어제의 경험으로 내일의 변화를 만들어 내는 공장, NHN FactoryX
필요한 만큼의 GPU를 사서 랙에 꽂으면 AI 인프라가 완성될까요? 

실제로 대규모 GPU 클러스터를 운영해 본 팀이라면, 장비 구매는 끝이 아니라 시작이라는 것을 알 수 있습니다. 수백 장의 GPU가 동시에 통신을 시도하는 순간 네트워크에 병목이 발생하고, 학습 데이터를 읽어오는 속도가 GPU 연산을 따라가지 못해 고가의 장비가 데이터를 기다립니다. 최신 AI GPU 랙의 전력 밀도는 60~90kW에 달하지만 공랭식 냉각의 실질적 한계는 30kW 부근이라 그 이상에서는 GPU 온도를 억제할 수 없고, 결국 시스템이 스스로 성능을 깎아내리기 시작합니다.

GPU를 '사는 것'과 GPU를 'AI 인프라로 만드는 것'은 완전히 다른 문제입니다. 그리고 이 간극을 메우는 데 필요한 기술적 의사결정은 생각보다 많고, 예상보다 깊습니다.

NHN FactoryX는 NHN Cloud가 7년 이상의 GPU 인프라 운영 경험을 바탕으로 만든 AI 워크로드를 위한 풀스택 환경입니다. NHN FactoryX 서울 데이터 센터에 4,080장의 GPU를 단일 클러스터로 묶어 FP8 기준 27.4 EFLOPS 규모의 연산 능력을 확보했습니다. 구조는 세 개의 계층으로 나뉩니다. 맨 아래에서 수랭식 데이터 센터와 GPU 클러스터(Infrastructure)가 물리적 기반을 제공하고, 그 위에서 GPU Live와 AI EasyMaker(Platform)가 GPU 자원의 동적 할당과 ML 파이프라인을 담당하며, 최상위에는 ProjectX(Service)가 위치해 기업 전용 AI 에이전트 개발·연동 환경을 제공합니다. 현재 산업계·학계·공공 기관이 이 플랫폼 위에서 LLM 학습, 멀티모달 AI 연구, 추론 서비스를 운영하고 있습니다.

![01_3layers_900.png](https://image.toast.com/aaaadh/real/2026/techblog/013layers900.png)

이번에 발간한 〈NHN FactoryX 기술 백서〉는 이 중 인프라와 플랫폼 영역의 설계 근거를 약 60여 페이지에 걸쳐 풀어 설명한 기술 백서입니다.

&lt;br&gt;
## NHN FactoryX의 모든 것, 〈NHN FactoryX 기술 백서〉
AI 인프라의 도입은 더 이상 장비 구매가 아니라 기술 검증을 동반하는 의사결정 프로세스입니다. GPU 모델 사양만 비교해서는 안 되며, 인터커넥트 토폴로지, 스토리지 I/O, 네트워크 병목 대응, 냉각 설계, 선형 확장성까지, 풀스택 관점의 설계를 동시에 고민해야 합니다. 이 전체를 한 곳에서 체계적으로 다룬 자료가 부재했기 때문에 NHN Cloud는 AI 인프라 요구 사항에 NHN FactoryX가 어떻게 답하고 있는지를 하나의 문서로 정리한 기술 백서를 발간했습니다.

이 백서는 위에서 다룬 NHN FactoryX의 세 레이어 가운데 인프라와 플랫폼, 즉 AI 워크로드를 물리적으로 지탱하고 자원을 지능적으로 배분하는 두 레이어의 설계 철학과 기술 근거를 담고 있습니다. 백서는 먼저 기존의 범용 클라우드 인프라가 AI 워크로드에 대응하기에 어떤 점에서 한계가 있는지 짚는 데서 출발합니다. 기존 데이터 센터는 CPU 중심 연산, 공랭식 냉각, 이더넷 기반 네트워크를 전제로 설계되어 범용 워크로드에는 효율적이지만, 대규모 AI 학습에서는 구조적 병목에 직면합니다. 이 한계를 넘기 위해 NHN FactoryX가 어떤 설계 선택을 했는지를, 다음 네 개의 핵심 영역으로 정리합니다.

![02_agenda_900.png](https://image.toast.com/aaaadh/real/2026/techblog/02agenda900.png)

각 영역은 독립적으로 참조할 수 있지만, 풀스택 관점에서 서로 연결되어 있습니다. 여기에서는 이 백서가 다루는 질문 중 핵심이 되는 내용을 간략하게 훑어보고자 합니다.

&lt;br&gt;
## 〈NHN FactoryX 기술 백서〉 훑어보기
### "왜 공기로는 안 되나요?" - 공랭식 냉각의 구조적 한계
AI GPU의 전력 밀도는 세대마다 가파르게 올라가고 있습니다. A100 시절에는 랙당 13\~39kW 수준이었지만, 최신 B200/B300 세대에서는 랙당 90kW에 육박합니다. 문제는 여기서 발생합니다. 공기의 체적 열용량은 물의 약 1/3,500에 불과합니다. 같은 열을 제거하려면 공기는 물보다 3,500배 많은 양이 필요하다는 뜻입니다. 랙당 전력 밀도가 30kW를 넘는 순간부터 공랭식은 열역학적 한계에 부딪힙니다.

〈NHN FactoryX 기술 백서〉는 이 한계를 ASHRAE 등급 기준과 CFD 시뮬레이션 연구를 인용해 구조적으로 짚어보고, D2C 수랭식 도입 시 반드시 검증해야 할 다섯 가지 요건(벤더 호환성, 인터페이스 표준화, 운전 파라미터, 확장성, 수질 관리)을 하나하나 설명합니다. NHN FactoryX는 이 과정을 거쳐 프리쿨링 연계 운전으로 PUE 1.15 이하를 달성할 수 있는 구조를 만들었습니다. 공랭식 대비 연간 냉각 에너지를 약 30~40% 절감할 수 있는 수치입니다.

자세한 내용은  &lt;strong&gt;〈NHN FactoryX 기술 백서〉 2.3절 전력 및 냉각 설계&lt;/strong&gt;에서 확인하실 수 있습니다.

&lt;br&gt;

### "GPU 수천 장을 어떻게 나눠 쓰나요?" - GPU Live의 설계 철학
GPU 클러스터를 팀별로 고정 배정하면 운영은 단순하지만, 대규모 클러스터 트레이스 분석에서 자원 활용률이 한 자릿수에서 30%대에 머무르는 사례가 보고되어 왔습니다.

GPU Live는 이 문제를 동적 할당 기술로 풀되, 자체 스케줄러를 새로 만들지 않고, Kubernetes 표준 생태계의 검증된 컴포넌트 위에 NHN FactoryX 환경에 맞는 운영 정책을 얹는 방식을 택했습니다. GPU 드라이버 업데이트나 분산 학습 프레임워크의 변화를 표준 경로로 흡수할 수 있고, 향후 NPU 같은 이종 가속기가 등장해도 동일한 표준 인터페이스로 통합이 가능하도록 하기 위함입니다.

그 위에 GPU Live가 직접 설계한 영역이 있습니다. 3계층 멀티테넌시(System → Organization → Project)로 조직 간 자원·정책·권한을 격리하고, GPU 기준 Best-fit 빈패킹으로 단편화를 줄이며, 쿼터·오버부킹·자동 회수를 결합해 활용률을 끌어올립니다. 학습 워크로드는 Gang Scheduling으로 일괄 시작·종료하고, 추론 워크로드는 인스턴스 단위로 장애 복구와 재배치를 처리합니다. 같은 GPU 풀에서 성격이 전혀 다른 두 워크로드를 안정적으로 수용하는 구조입니다.

자세한 내용은  &lt;strong&gt;〈NHN FactoryX 기술 백서〉 2.4절 GPU 동적 할당 기술(GPU Live)&lt;/strong&gt;에서 확인하실 수 있습니다.

&lt;br&gt;

### "베어메탈이면 충분하지 않나요?" — GPUaaS를 선택한 이유
베어메탈 방식은 GPU 자원을 물리 서버 단위로 직접 할당하므로 하드웨어 성능을 최대한 활용할 수 있습니다. 다만 다수의 사용자가 동일 인프라를 공유할 경우 테넌트 간 보안 격리가 보장되지 않고, 테넌트별 환경 구성이 상이하여 장애 발생 시 일관된 운영 정책을 적용하기 어렵다는 한계가 있습니다.

NHN FactoryX는 이 한계를 극복하기 위해 GPU 인프라 상위 계층에 GPUaaS 플랫폼을 구축하는 방식을 택했습니다. 테넌트·워크스페이스·프로젝트로 이어지는 계층적 구조를 기반으로 자원과 보안 통제를 일관되게 적용하고, 한 기관에서 대규모 학습으로 자원 사용량이 급증하더라도 다른 기관의 서비스에는 영향이 없는 격리 구조를 실현했습니다. 실제로 957개 GPU 노드 위에서 산업계·학계·연구계의 다수 기관이 독립된 워크스페이스를 운영하고 있습니다.

서비스 계층이 추가되면 네트워크 병목으로 인한 성능 저하가 우려될 수 있지만, NHN FactoryX는 GPUaaS 플랫폼 환경에서 255노드 규모의 HPL(High Performance Linpack) 벤치마크를 수행해 베어메탈과 동일한 수준의 성능을 확인했습니다. 컨테이너가 호스트 수준의 네트워크 성능을 유지하도록 설계한 결과입니다.

자세한 내용은  &lt;strong&gt;〈NHN FactoryX 기술 백서〉 06장 활용 사례&lt;/strong&gt;에서 확인하실 수 있습니다.

&lt;br&gt;
## 마치며

이 글에서는 〈NHN FactoryX 기술 백서〉의 핵심 내용 중 일부를 살펴보았습니다. 백서 전체를 관통하는 메시지를 한 문장으로 요약해 보면 이렇습니다. AI 인프라는 개별 하드웨어의 성능 향상을 넘어, 물리적 공간 설계부터 전력 및 냉각 효율, 초고속 네트워크 튜닝, 소프트웨어 정의 기반의 관리 플랫폼까지 모든 요소가 유기적으로 통합되어야 합니다.

NHN FactoryX는 이 통합을 하나의 공간에서 실현합니다. 수랭식 데이터 센터 위에 GPU 클러스터를 구성하고, GPUaaS 플랫폼으로 자원을 관리하며, AI 개발 플랫폼까지 3개 레이어를 풀스택으로 제공합니다. 동일한 GPUaaS 아키텍처 위에서 멀티테넌트 공유 환경과 기업 프라이빗 전용 환경이 모두 구현되며, 957개 GPU 노드 규모에서 베어메탈 동등 수준의 성능과 조직 간 보안 격리를 동시에 달성하고 있습니다.

AI 인프라는 현재진행형입니다. GPU 세대 전환에 따른 전력 밀도 증가, Kubernetes DRA를 비롯한 자원 관리 표준의 진화, NPU 등 이종 가속기의 등장은 인프라 설계의 전제 조건을 지속적으로 변화시킵니다. NHN FactoryX는 차세대 GPU 플랫폼까지의 상위 호환성을 전제로 수랭식 인프라를 설계하였으며, GPU Live의 동적 할당 모델을 이종 가속기로 확장하기 위한 기술 검토를 진행하고 있습니다.

백서에는 이 글에서 미처 다 다루지 못한 GPU 세대별 사양 비교, 5가지 성능 저하 패턴과 설계 단계 체크리스트, Scalable Unit 기반 증설 전략, 기업 프라이빗 AI 인프라 구축 사례 등이 구체적인 수치와 다이어그램과 함께 담겨 있습니다. AI 인프라 도입을 검토하는 기술 의사결정자, 대규모 AI 시스템을 설계하거나 운영하는 아키텍트 및 엔지니어 분들께 실질적인 기술 레퍼런스로 활용되길 기대합니다.

긴 글을 읽어 주셔서 감사합니다. ‍♀️


&gt;  [NHN FactoryX 공식 포털 바로 가기](https://www.nhnfactoryx.com/?utm_source=meetup&amp;utm_medium=post&amp;utm_campaign=meetup&amp;utm_content=0713_xwhitepaper)
&gt; 
&gt;  [NHN FactoryX 기술 백서 다운로드](https://info.nhncloud.com/factoryx-whitepaper?utm_source=meetup&amp;utm_medium=post&amp;utm_campaign=meetup&amp;utm_content=0713_xwhitepaper)
&gt; 
&gt;  [NHN FactoryX 소개서 다운로드](https://info.nhncloud.com/factoryxbrochure?utm_source=meetup&amp;utm_medium=post&amp;utm_campaign=meetup&amp;utm_content=0713_xwhitepaper)

[![NHN Cloud_meetup banner_footer_grayd9d9d9_202606_900.png](https://image.toast.com/aaaadh/real/2026/techblog/NHN%20Cloudmeetup%20bannerfootergrayd9d9d9202606900.png)](https://www.nhncloud.com/kr)</summary>
    <title>AI 인프라 설계의 기술적 레퍼런스, &lt;NHN FactoryX 기술 백서&gt;를 소개합니다</title>
    <updated>2026-07-13T11:57:40+09:00</updated>
    <dc:date>2026-07-13T11:57:40+09:00</dc:date>
  </entry>
  <entry>
    <author>
      <name>NHN Cloud</name>
    </author>
    <content type="html">&lt;!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd"&gt;
&lt;html&gt;&lt;body&gt;&lt;p&gt;[![NHN Cloud_meetup banner_cve_202607_900.png](https://image.toast.com/aaaadh/real/2026/techblog/NHN%20Cloudmeetup%20bannercve202607900.png)](https://www.nhncloud.com/kr)

## 들어가며
안녕하세요. NHN Cloud NoSQL개발파트 이준영입니다. 

여러분은 CVE(Common Vulnerabilities and Exposures)에 대해 들어보셨나요?
CVE란 공개적으로 알려진 소프트웨어 및 하드웨어의 보안 취약점을 식별하고 명명하기 위한 표준화된 체계입니다. 각 취약점은 CVE ID라는 고유 식별자를 부여받으며, 이는 전 세계 보안 커뮤니티가 동일한 취약점을 정확하고 일관되게 추적하고 논의할 수 있도록 돕습니다.

저는 작년에 개인 프로젝트로 Valkey와 Redis의 취약점을 연구하였는데요. 운이 좋게 CVE ID까지 발급받게 되었습니다.
이번 글에서는 해당 취약점의 분석 내용을 공유하고자 합니다.

### CVE 정보
| 구분 | 세부 내용|
| --- | --- |
| CVE | [https://nvd.nist.gov/vuln/detail/CVE-2025-67733](https://nvd.nist.gov/vuln/detail/CVE-2025-67733) |
| PoC | [https://github.com/JYlab/CVE-2025-67733](https://github.com/JYlab/CVE-2025-67733) |
| GitHub Security Advisory | [https://github.com/valkey-io/valkey/security/advisories/GHSA-p876-p7q5-hv2m](https://github.com/valkey-io/valkey/security/advisories/GHSA-p876-p7q5-hv2m) |
| Valkey 패치 | - 취약 버전: 9.0.1 이하&lt;br&gt;- 패치 버전: 9.0.2, 8.1.6, 8.0.7, 7.2.12 |
| Redis 패치 | - 취약 버전: 8.4.1 이하&lt;br&gt;- 패치 버전: 8.4.2, 8.2.5, 8.0.6, 7.4.8, 7.2.13 |
| CVSS | CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:N/I:L/A:H (8.5/10) |

&lt;br&gt;
## 개요

먼저 RESP 인젝션 공격에 대해 설명하고자 합니다. 분석 내용은 Valkey 8.1.4 버전의 코드를 기준으로 작성하였습니다.

해당 취약점을 통해 Valkey 서버의 소켓 포이즈닝을 유발하여 데이터를 변조할 수 있으며, DoS 공격 또한 가능합니다.
공격 방법은 Valkey의 `EVAL` 명령에 다음과 같은 Lua 스크립트를 실행하는 것입니다.

![NHN Cloud_meetup_data-forgery_1.png](https://image.toast.com/aaaadh/real/2026/techblog/NHN%20Cloudmeetupdataforgery1.png)

이것이 어떻게 취약점을 유발하는지 이해하려면 `RESP`, `Lua 스크립트 실행 흐름`, `redis.error_reply() 서버 API`, `error() 내장 함수` 등에 대한 이해가 필요합니다. 따라서 배경 지식을 먼저 설명한 뒤 취약점 분석 내용을 공유하겠습니다.

&lt;br&gt;
## RESP

RESP(Redis Serialization Protocol)는 Valkey, Redis 계열에서 클라이언트↔서버가 명령/응답을 주고받을 때 사용하는 직렬화 프로토콜입니다. 타입 접두어(prefix) + CRLF(`\r\n`)로 구분하고, Bulk는 길이를 명시하여 사용합니다.

대표 타입(Simple String, Error, Integer, Bulk String, Array)과 사용 예시는 다음과 같습니다.

### Simple String(성공 메시지)

* 프로토콜: `+`
* 예시: `+OK\r\n`, `+PONG\r\n`

### Error(실패 사유)

* 프로토콜: `-`
* 예시: `-ERR unknown command 'HELLO'\r\n`

### Integer(정수 반환 - 카운트/증가 결과 등)

* 프로토콜: `:`
* 예시: `:0\r\n`, `:123\r\n`

### Bulk String(길이+데이터 - 값/바이너리)

* 프로토콜: `$`
* 예시: `$5\r\nhello\r\n`, `$-1\r\n` (null), `$0\r\n\r\n` (빈 값)

### Array(여러 RESP 값의 리스트 - 명령도 보통 이 형태)

* 프로토콜: `*`
* 예시: `*0\r\n`, `*2\r\n$3\r\nfoo\r\n$3\r\nbar\r\n`, `*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$5\r\nvalue\r\n`

&lt;br&gt;
## Lua 스크립트 실행 흐름

Valkey에서 `EVAL` 명령으로 Lua 스크립트를 실행하면 다음과 같은 흐름으로 처리됩니다.

```text
EVAL 명령 → luaCallFunction() → Lua 스크립트 실행 → (오류 발생 시) → 오류 처리 → 클라이언트 응답
```

### luaCallFunction()

`luaCallFunction()`은 Lua 스크립트를 실행하고, 실행 결과나 오류를 처리하는 핵심 함수입니다.

![NHN Cloud_meetup_luaCallFunction_1.png](https://image.toast.com/aaaadh/real/2026/techblog/NHN%20CloudmeetupluaCallFunction1.png)

이 함수에서 중요한 점은 다음과 같습니다.

1. `lua_pcall()`로 Lua 스크립트를 실행
2. 오류 발생 시 `luaExtractErrorInformation()`으로 오류 정보 추출
3. 오류 타입에 따라 `luaReplyToRedisReply()` 또는 직접 오류 응답 전송

오류가 발생했을 때, 오류 메시지가 어떻게 만들어지고 클라이언트에게 전송되는지가 이 취약점의 핵심입니다.


## redis.error\_reply() 서버 API 동작 방식

Valkey의 Lua 스크립트 엔진에서는 오류 발생 시 오류를 테이블 형태로 반환할 수 있는 기능을 제공합니다. `redis.error_reply()`는 이와 같은 오류 테이블을 커스텀하게 정의할 때 사용하는 API입니다.

![NHN Cloud_meetup_luaRedisErrorReplyCommand_1.png](https://image.toast.com/aaaadh/real/2026/techblog/NHN%20CloudmeetupluaRedisErrorReplyCommand1.png)

위 Server API를 호출하면 C 함수 중 `luaRedisErrorReplyCommand()`가 호출됩니다. 이 함수는 `redis.error_reply` 파라미터의 개수와 String 여부를 판단하고, 만약 문자열에 `-`가 없으면 `-`를 추가로 붙인 뒤 `luaPushErrorBuff()` 함수에 `err_buff` 인자로 넘깁니다. RESP의 Error 프로토콜 양식이 `-`로 시작하기 때문입니다.

&lt;br&gt;
## error() 내장 함수

`error()`는 Lua의 내장 함수로, 스크립트 실행을 즉시 중단하고 오류를 발생시킵니다. Valkey에서 Lua 스크립트가 `error()`를 호출하면, 인자로 전달된 값이 클라이언트에게 오류 응답으로 전송됩니다.

![NHN Cloud_meetup_ex_error_1.png](https://image.toast.com/aaaadh/real/2026/techblog/NHN%20Cloudmeetupexerror1.png)

여기서 핵심은 `error()`의 인자로 `redis.error_reply()`가 반환한 오류 테이블을 전달할 수 있다는 점입니다. 이 경우 오류 테이블의 `err` 필드 값이 그대로 RESP Error 응답으로 클라이언트에게 전송됩니다.

공격 관점에서의 흐름은 다음과 같습니다.

1. `redis.error_reply(...)` → 악성 페이로드가 포함된 오류 테이블 생성
2. `error(오류 테이블)` → 스크립트 중단 및 오류 응답 전송 트리거
3. 서버 → 클라이언트로 오류 메시지 전송(이 과정에서 취약점 발생)

즉, `error()`는 `redis.error_reply()`로 만든 악성 페이로드를 실제로 클라이언트에게 전송하는 트리거 역할을 합니다.

다음 그림은 `luaCallFunction()` 함수의 일부이며, `error()`에 의해 발생한 오류 값이 `lua_pcall()`에 의해 캡처되는 모습입니다.

![NHN Cloud_meetup_error_1.png](https://image.toast.com/aaaadh/real/2026/techblog/NHN%20Cloudmeetuperror1.png)


## luaPushErrorBuff() 함수

다음 그림은 `luaPushErrorBuff()` 함수의 일부입니다. 입력으로 들어온 `err_buff`로부터 error code를 분리한 뒤, `\r\n`를 `sdstrim()`한 후 `final_msg`로 만들어 오류 테이블의 `err` 필드를 채웁니다.

![NHN Cloud_meetup_luaPushErrorBuff_1.png](https://image.toast.com/aaaadh/real/2026/techblog/NHN%20CloudmeetupluaPushErrorBuff1.png)

여기서 중요한 부분은 `sdstrim()` 함수에 의해 취약점이 발생한다는 점입니다. `sdstrim(msg, "\r\n")` 함수는 `msg` 문자열의 양 끝에서 `\r`, `\n`에 해당하는 문자가 있으면 이를 제거하는 함수입니다.

해당 함수의 문제점은 문자열 전체에서 `\r\n`를 제거하는 것이 아니라, 문자열 양 끝에서만 `\r\n`을 제거한다는 점입니다.

&lt;br&gt;
## 취약점: 소켓 포이즈닝을 통한 데이터 변조

취약한 오류 메시지가 실제로 클라이언트 버퍼에 어떻게 쓰여지는지 살펴보겠습니다. (여기서 말하는 '클라이언트 버퍼'는 서버가 각 클라이언트 연결별로 유지하는 서버 사이드 출력 버퍼, 즉 `c-&amp;gt;buf`를 의미합니다.)

### 전체 흐름

```text
luaCallFunction()
    ↓
luaExtractErrorInformation() → err_info.msg = "X\r\n+FAKE"
    ↓
sdscatfmt(final_msg, "-%s", err_info.msg) → final_msg = "-X\r\n+FAKE"
    ↓
addReplyErrorSdsEx(c, final_msg, flags)
    ↓
addReplyErrorLength(c, err, sdslen(err))
    ↓
addReplyProto(c, s, len)  ← s = "-X\r\n+FAKE"
addReplyProto(c, "\r\n", 2)
    ↓
_addReplyToBufferOrList(c, s, len)
    ↓
_addReplyToBuffer(c, s, len)
    ↓
memcpy(c-&amp;gt;buf + c-&amp;gt;bufpos, s, reply_len)  ← 클라이언트 버퍼에 직접 쓰기
```

### 상세 분석

**&lt;span style="color: rgb(3, 3, 3);"&gt;1\. addReplyErrorSdsEx() - networking.c:706&lt;/span&gt;**

해당 코드 내에서 `addReplyErrorLength` 함수를 호출하면서 오류 메시지를 버퍼에 추가합니다.
![NHN Cloud_meetup_addReplyErrorSdsEx_1.png](https://image.toast.com/aaaadh/real/2026/techblog/NHN%20CloudmeetupaddReplyErrorSdsEx1.png)

이 함수는 `sdsmapchars()` 같은 sanitization 없이 오류 메시지를 그대로 전달합니다.

**&lt;span style="color: rgb(3, 3, 3);"&gt;2\. addReplyErrorLength() - networking.c:565&lt;/span&gt;**
![NHN Cloud_meetup_addReplyErrorLength_1.png](https://image.toast.com/aaaadh/real/2026/techblog/NHN%20CloudmeetupaddReplyErrorLength1.png)

오류 메시지가 이미 `-`로 시작하면 그대로 전송합니다. `addReplyProto()` 함수를 호출하며 `s` 값을 버퍼에 넣기 위한 함수를 호출합니다.

**&lt;span style="color: rgb(3, 3, 3);"&gt;3\. \_addReplyToBuffer() - networking.c:403&lt;/span&gt;**
![NHN Cloud_meetup_addReplyToBuffer_1.png](https://image.toast.com/aaaadh/real/2026/techblog/NHN%20CloudmeetupaddReplyToBuffer1.png)

최종적으로 `memcpy()`를 통해 클라이언트의 출력 버퍼(`c-&amp;gt;buf`)에 데이터가 직접 기록됩니다.

따라서 공격자가 `error(redis.error_reply("X\r\n+FAKE"))`를 실행하면 버퍼 상태가 다음과 같이 채워집니다.
![NHN Cloud_meetup_client_buffer_1.png](https://image.toast.com/aaaadh/real/2026/techblog/NHN%20Cloudmeetupclientbuffer1.png)

클라이언트는 `\r\n`을 RESP 메시지 경계로 인식하므로, 이를 두 개의 독립된 응답으로 파싱합니다.

1. `-X\r\n` → 첫 번째 응답(Error)
2. `+FAKE\r\n` → 두 번째 응답(Simple String) - 다음 명령의 응답으로 처리됨

따라서 이후 `PING` 명령을 보내면, 서버의 실제 응답(`+PONG\r\n`) 대신 버퍼에 남아있던 `+FAKE`가 반환됩니다. 이렇게 소켓 포이즈닝이 가능합니다.

&lt;br&gt;
## 취약점: DoS

RESP 인젝션을 활용하면 클라이언트를 대기 상태로 만들어 서비스 거부 공격을 수행할 수 있습니다.
RESP의 Bulk String 타입은 `$길이\r\n데이터\r\n` 형식으로, 클라이언트는 명시된 길이만큼 데이터를 수신할 때까지 대기합니다.

공격자가 거대한 길이를 주입하면
![NHN Cloud_meetup_dos_1.png](https://image.toast.com/aaaadh/real/2026/techblog/NHN%20Cloudmeetupdos1.png)

클라이언트 버퍼가 다음과 같이 채워집니다.
![NHN Cloud_meetup_client_buffer_2.png](https://image.toast.com/aaaadh/real/2026/techblog/NHN%20Cloudmeetupclientbuffer2.png)

공격 시나리오는 대략 다음과 같습니다.

1. 첫 번째 응답 `-X\r\n`을 정상 처리(Error)
2. 두 번째 응답 `$999999999\r\n`을 파싱
3. 999,999,999 바이트의 데이터를 기다림
4. 데이터가 오지 않으므로 클라이언트는 비정상 동작

여러 커넥션에서 동시에 이 공격을 수행하면, 다음과 같은 메커니즘으로 인해 서버 레벨에서 DoS가 발생할 수 있습니다.

* 모든 클라이언트 커넥션이 멈춤(hang) 상태
* 서버의 커넥션 슬롯 소진
* 새로운 클라이언트 접속 불가 → 서비스 전체 마비

## 마치며

이 글에서는 RESP 인젝션 취약점의 원리와 이를 활용한 소켓 포이즈닝 및 DoS 공격 방법에 대해 알아보았습니다. 오류 메시지 경로에서 CRLF를 충분히 검증하지 않는다는 작은 허점이 데이터 변조와 서비스 마비로 이어질 수 있다는 점에서, 아무리 단순한 문자열 처리 로직이라도 보안 관점에서의 검증이 중요하다는 것을 다시 한번 느꼈습니다.

이 글이 Valkey나 Redis의 내부 동작에 관심이 있는 분들, 그리고 오픈소스 보안 취약점 연구에 도전해 보고 싶은 분들에게 도움이 되길 바랍니다. 긴 글 읽어 주셔서 감사합니다. 

[![NHN Cloud_meetup banner_footer_202507-01.png](https://image.toast.com/aaaadh/real/2026/techblog/NHN%20Cloudmeetup%20bannerfooter20250701%281%29.png)](https://www.nhncloud.com/kr)&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;
</content>
    <id>https://meetup.nhncloud.com/posts/416</id>
    <link href="https://meetup.nhncloud.com/posts/416"/>
    <summary type="html">[![NHN Cloud_meetup banner_cve_202607_900.png](https://image.toast.com/aaaadh/real/2026/techblog/NHN%20Cloudmeetup%20bannercve202607900.png)](https://www.nhncloud.com/kr)

## 들어가며
안녕하세요. NHN Cloud NoSQL개발파트 이준영입니다. 

여러분은 CVE(Common Vulnerabilities and Exposures)에 대해 들어보셨나요?
CVE란 공개적으로 알려진 소프트웨어 및 하드웨어의 보안 취약점을 식별하고 명명하기 위한 표준화된 체계입니다. 각 취약점은 CVE ID라는 고유 식별자를 부여받으며, 이는 전 세계 보안 커뮤니티가 동일한 취약점을 정확하고 일관되게 추적하고 논의할 수 있도록 돕습니다.

저는 작년에 개인 프로젝트로 Valkey와 Redis의 취약점을 연구하였는데요. 운이 좋게 CVE ID까지 발급받게 되었습니다.
이번 글에서는 해당 취약점의 분석 내용을 공유하고자 합니다.

### CVE 정보
| 구분 | 세부 내용|
| --- | --- |
| CVE | [https://nvd.nist.gov/vuln/detail/CVE-2025-67733](https://nvd.nist.gov/vuln/detail/CVE-2025-67733) |
| PoC | [https://github.com/JYlab/CVE-2025-67733](https://github.com/JYlab/CVE-2025-67733) |
| GitHub Security Advisory | [https://github.com/valkey-io/valkey/security/advisories/GHSA-p876-p7q5-hv2m](https://github.com/valkey-io/valkey/security/advisories/GHSA-p876-p7q5-hv2m) |
| Valkey 패치 | - 취약 버전: 9.0.1 이하&lt;br&gt;- 패치 버전: 9.0.2, 8.1.6, 8.0.7, 7.2.12 |
| Redis 패치 | - 취약 버전: 8.4.1 이하&lt;br&gt;- 패치 버전: 8.4.2, 8.2.5, 8.0.6, 7.4.8, 7.2.13 |
| CVSS | CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:N/I:L/A:H (8.5/10) |

&lt;br&gt;
## 개요

먼저 RESP 인젝션 공격에 대해 설명하고자 합니다. 분석 내용은 Valkey 8.1.4 버전의 코드를 기준으로 작성하였습니다.

해당 취약점을 통해 Valkey 서버의 소켓 포이즈닝을 유발하여 데이터를 변조할 수 있으며, DoS 공격 또한 가능합니다.
공격 방법은 Valkey의 `EVAL` 명령에 다음과 같은 Lua 스크립트를 실행하는 것입니다.

![NHN Cloud_meetup_data-forgery_1.png](https://image.toast.com/aaaadh/real/2026/techblog/NHN%20Cloudmeetupdataforgery1.png)

이것이 어떻게 취약점을 유발하는지 이해하려면 `RESP`, `Lua 스크립트 실행 흐름`, `redis.error_reply() 서버 API`, `error() 내장 함수` 등에 대한 이해가 필요합니다. 따라서 배경 지식을 먼저 설명한 뒤 취약점 분석 내용을 공유하겠습니다.

&lt;br&gt;
## RESP

RESP(Redis Serialization Protocol)는 Valkey, Redis 계열에서 클라이언트↔서버가 명령/응답을 주고받을 때 사용하는 직렬화 프로토콜입니다. 타입 접두어(prefix) + CRLF(`\r\n`)로 구분하고, Bulk는 길이를 명시하여 사용합니다.

대표 타입(Simple String, Error, Integer, Bulk String, Array)과 사용 예시는 다음과 같습니다.

### Simple String(성공 메시지)

* 프로토콜: `+`
* 예시: `+OK\r\n`, `+PONG\r\n`

### Error(실패 사유)

* 프로토콜: `-`
* 예시: `-ERR unknown command 'HELLO'\r\n`

### Integer(정수 반환 - 카운트/증가 결과 등)

* 프로토콜: `:`
* 예시: `:0\r\n`, `:123\r\n`

### Bulk String(길이+데이터 - 값/바이너리)

* 프로토콜: `$`
* 예시: `$5\r\nhello\r\n`, `$-1\r\n` (null), `$0\r\n\r\n` (빈 값)

### Array(여러 RESP 값의 리스트 - 명령도 보통 이 형태)

* 프로토콜: `*`
* 예시: `*0\r\n`, `*2\r\n$3\r\nfoo\r\n$3\r\nbar\r\n`, `*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$5\r\nvalue\r\n`

&lt;br&gt;
## Lua 스크립트 실행 흐름

Valkey에서 `EVAL` 명령으로 Lua 스크립트를 실행하면 다음과 같은 흐름으로 처리됩니다.

```text
EVAL 명령 → luaCallFunction() → Lua 스크립트 실행 → (오류 발생 시) → 오류 처리 → 클라이언트 응답
```

### luaCallFunction()

`luaCallFunction()`은 Lua 스크립트를 실행하고, 실행 결과나 오류를 처리하는 핵심 함수입니다.

![NHN Cloud_meetup_luaCallFunction_1.png](https://image.toast.com/aaaadh/real/2026/techblog/NHN%20CloudmeetupluaCallFunction1.png)

이 함수에서 중요한 점은 다음과 같습니다.

1. `lua_pcall()`로 Lua 스크립트를 실행
2. 오류 발생 시 `luaExtractErrorInformation()`으로 오류 정보 추출
3. 오류 타입에 따라 `luaReplyToRedisReply()` 또는 직접 오류 응답 전송

오류가 발생했을 때, 오류 메시지가 어떻게 만들어지고 클라이언트에게 전송되는지가 이 취약점의 핵심입니다.


## redis.error\_reply() 서버 API 동작 방식

Valkey의 Lua 스크립트 엔진에서는 오류 발생 시 오류를 테이블 형태로 반환할 수 있는 기능을 제공합니다. `redis.error_reply()`는 이와 같은 오류 테이블을 커스텀하게 정의할 때 사용하는 API입니다.

![NHN Cloud_meetup_luaRedisErrorReplyCommand_1.png](https://image.toast.com/aaaadh/real/2026/techblog/NHN%20CloudmeetupluaRedisErrorReplyCommand1.png)

위 Server API를 호출하면 C 함수 중 `luaRedisErrorReplyCommand()`가 호출됩니다. 이 함수는 `redis.error_reply` 파라미터의 개수와 String 여부를 판단하고, 만약 문자열에 `-`가 없으면 `-`를 추가로 붙인 뒤 `luaPushErrorBuff()` 함수에 `err_buff` 인자로 넘깁니다. RESP의 Error 프로토콜 양식이 `-`로 시작하기 때문입니다.

&lt;br&gt;
## error() 내장 함수

`error()`는 Lua의 내장 함수로, 스크립트 실행을 즉시 중단하고 오류를 발생시킵니다. Valkey에서 Lua 스크립트가 `error()`를 호출하면, 인자로 전달된 값이 클라이언트에게 오류 응답으로 전송됩니다.

![NHN Cloud_meetup_ex_error_1.png](https://image.toast.com/aaaadh/real/2026/techblog/NHN%20Cloudmeetupexerror1.png)

여기서 핵심은 `error()`의 인자로 `redis.error_reply()`가 반환한 오류 테이블을 전달할 수 있다는 점입니다. 이 경우 오류 테이블의 `err` 필드 값이 그대로 RESP Error 응답으로 클라이언트에게 전송됩니다.

공격 관점에서의 흐름은 다음과 같습니다.

1. `redis.error_reply(...)` → 악성 페이로드가 포함된 오류 테이블 생성
2. `error(오류 테이블)` → 스크립트 중단 및 오류 응답 전송 트리거
3. 서버 → 클라이언트로 오류 메시지 전송(이 과정에서 취약점 발생)

즉, `error()`는 `redis.error_reply()`로 만든 악성 페이로드를 실제로 클라이언트에게 전송하는 트리거 역할을 합니다.

다음 그림은 `luaCallFunction()` 함수의 일부이며, `error()`에 의해 발생한 오류 값이 `lua_pcall()`에 의해 캡처되는 모습입니다.

![NHN Cloud_meetup_error_1.png](https://image.toast.com/aaaadh/real/2026/techblog/NHN%20Cloudmeetuperror1.png)


## luaPushErrorBuff() 함수

다음 그림은 `luaPushErrorBuff()` 함수의 일부입니다. 입력으로 들어온 `err_buff`로부터 error code를 분리한 뒤, `\r\n`를 `sdstrim()`한 후 `final_msg`로 만들어 오류 테이블의 `err` 필드를 채웁니다.

![NHN Cloud_meetup_luaPushErrorBuff_1.png](https://image.toast.com/aaaadh/real/2026/techblog/NHN%20CloudmeetupluaPushErrorBuff1.png)

여기서 중요한 부분은 `sdstrim()` 함수에 의해 취약점이 발생한다는 점입니다. `sdstrim(msg, "\r\n")` 함수는 `msg` 문자열의 양 끝에서 `\r`, `\n`에 해당하는 문자가 있으면 이를 제거하는 함수입니다.

해당 함수의 문제점은 문자열 전체에서 `\r\n`를 제거하는 것이 아니라, 문자열 양 끝에서만 `\r\n`을 제거한다는 점입니다.

&lt;br&gt;
## 취약점: 소켓 포이즈닝을 통한 데이터 변조

취약한 오류 메시지가 실제로 클라이언트 버퍼에 어떻게 쓰여지는지 살펴보겠습니다. (여기서 말하는 '클라이언트 버퍼'는 서버가 각 클라이언트 연결별로 유지하는 서버 사이드 출력 버퍼, 즉 `c-&gt;buf`를 의미합니다.)

### 전체 흐름

```text
luaCallFunction()
    ↓
luaExtractErrorInformation() → err_info.msg = "X\r\n+FAKE"
    ↓
sdscatfmt(final_msg, "-%s", err_info.msg) → final_msg = "-X\r\n+FAKE"
    ↓
addReplyErrorSdsEx(c, final_msg, flags)
    ↓
addReplyErrorLength(c, err, sdslen(err))
    ↓
addReplyProto(c, s, len)  ← s = "-X\r\n+FAKE"
addReplyProto(c, "\r\n", 2)
    ↓
_addReplyToBufferOrList(c, s, len)
    ↓
_addReplyToBuffer(c, s, len)
    ↓
memcpy(c-&gt;buf + c-&gt;bufpos, s, reply_len)  ← 클라이언트 버퍼에 직접 쓰기
```

### 상세 분석

**&lt;span style="color: rgb(3, 3, 3);"&gt;1\. addReplyErrorSdsEx() - networking.c:706&lt;/span&gt;**

해당 코드 내에서 `addReplyErrorLength` 함수를 호출하면서 오류 메시지를 버퍼에 추가합니다.
![NHN Cloud_meetup_addReplyErrorSdsEx_1.png](https://image.toast.com/aaaadh/real/2026/techblog/NHN%20CloudmeetupaddReplyErrorSdsEx1.png)

이 함수는 `sdsmapchars()` 같은 sanitization 없이 오류 메시지를 그대로 전달합니다.

**&lt;span style="color: rgb(3, 3, 3);"&gt;2\. addReplyErrorLength() - networking.c:565&lt;/span&gt;**
![NHN Cloud_meetup_addReplyErrorLength_1.png](https://image.toast.com/aaaadh/real/2026/techblog/NHN%20CloudmeetupaddReplyErrorLength1.png)

오류 메시지가 이미 `-`로 시작하면 그대로 전송합니다. `addReplyProto()` 함수를 호출하며 `s` 값을 버퍼에 넣기 위한 함수를 호출합니다.

**&lt;span style="color: rgb(3, 3, 3);"&gt;3\. \_addReplyToBuffer() - networking.c:403&lt;/span&gt;**
![NHN Cloud_meetup_addReplyToBuffer_1.png](https://image.toast.com/aaaadh/real/2026/techblog/NHN%20CloudmeetupaddReplyToBuffer1.png)

최종적으로 `memcpy()`를 통해 클라이언트의 출력 버퍼(`c-&gt;buf`)에 데이터가 직접 기록됩니다.

따라서 공격자가 `error(redis.error_reply("X\r\n+FAKE"))`를 실행하면 버퍼 상태가 다음과 같이 채워집니다.
![NHN Cloud_meetup_client_buffer_1.png](https://image.toast.com/aaaadh/real/2026/techblog/NHN%20Cloudmeetupclientbuffer1.png)

클라이언트는 `\r\n`을 RESP 메시지 경계로 인식하므로, 이를 두 개의 독립된 응답으로 파싱합니다.

1. `-X\r\n` → 첫 번째 응답(Error)
2. `+FAKE\r\n` → 두 번째 응답(Simple String) - 다음 명령의 응답으로 처리됨

따라서 이후 `PING` 명령을 보내면, 서버의 실제 응답(`+PONG\r\n`) 대신 버퍼에 남아있던 `+FAKE`가 반환됩니다. 이렇게 소켓 포이즈닝이 가능합니다.

&lt;br&gt;
## 취약점: DoS

RESP 인젝션을 활용하면 클라이언트를 대기 상태로 만들어 서비스 거부 공격을 수행할 수 있습니다.
RESP의 Bulk String 타입은 `$길이\r\n데이터\r\n` 형식으로, 클라이언트는 명시된 길이만큼 데이터를 수신할 때까지 대기합니다.

공격자가 거대한 길이를 주입하면
![NHN Cloud_meetup_dos_1.png](https://image.toast.com/aaaadh/real/2026/techblog/NHN%20Cloudmeetupdos1.png)

클라이언트 버퍼가 다음과 같이 채워집니다.
![NHN Cloud_meetup_client_buffer_2.png](https://image.toast.com/aaaadh/real/2026/techblog/NHN%20Cloudmeetupclientbuffer2.png)

공격 시나리오는 대략 다음과 같습니다.

1. 첫 번째 응답 `-X\r\n`을 정상 처리(Error)
2. 두 번째 응답 `$999999999\r\n`을 파싱
3. 999,999,999 바이트의 데이터를 기다림
4. 데이터가 오지 않으므로 클라이언트는 비정상 동작

여러 커넥션에서 동시에 이 공격을 수행하면, 다음과 같은 메커니즘으로 인해 서버 레벨에서 DoS가 발생할 수 있습니다.

* 모든 클라이언트 커넥션이 멈춤(hang) 상태
* 서버의 커넥션 슬롯 소진
* 새로운 클라이언트 접속 불가 → 서비스 전체 마비

## 마치며

이 글에서는 RESP 인젝션 취약점의 원리와 이를 활용한 소켓 포이즈닝 및 DoS 공격 방법에 대해 알아보았습니다. 오류 메시지 경로에서 CRLF를 충분히 검증하지 않는다는 작은 허점이 데이터 변조와 서비스 마비로 이어질 수 있다는 점에서, 아무리 단순한 문자열 처리 로직이라도 보안 관점에서의 검증이 중요하다는 것을 다시 한번 느꼈습니다.

이 글이 Valkey나 Redis의 내부 동작에 관심이 있는 분들, 그리고 오픈소스 보안 취약점 연구에 도전해 보고 싶은 분들에게 도움이 되길 바랍니다. 긴 글 읽어 주셔서 감사합니다. 

[![NHN Cloud_meetup banner_footer_202507-01.png](https://image.toast.com/aaaadh/real/2026/techblog/NHN%20Cloudmeetup%20bannerfooter20250701%281%29.png)](https://www.nhncloud.com/kr)</summary>
    <title>Redis와 Valkey를 해킹해 보자(Feat. CVE ID)</title>
    <updated>2026-07-06T14:15:25+09:00</updated>
    <dc:date>2026-07-06T14:15:25+09:00</dc:date>
  </entry>
  <entry>
    <author>
      <name>Philip Park</name>
    </author>
    <content type="html">&lt;!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd"&gt;
&lt;html&gt;&lt;body&gt;
&lt;p&gt;글. 박성진(Philip) / 전시개발팀&lt;/p&gt;
&lt;h3&gt;&lt;em&gt;이 글은 3부작입니다.&lt;/em&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;(1/3) 더 빠르게, 그리고 무너지지 않게&lt;/strong&gt; — 문제 정의와 아키텍처 의사결정, 그리고 내고장성 설계 &lt;em&gt;(현재 글)&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;(2/3) 가격 계산 모듈 분리 + skills 로 마이그레이션 — 트랜잭션 스크립트에서 숙소 메타 + 가격 계산 모듈로 &lt;em&gt;(by &lt;/em&gt;@엉Ung(진영준) / 전시개발팀 &lt;em&gt;)&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;(3/3) &lt;strong&gt;데이터 통합 &lt;/strong&gt;— MongoDB 원칙으로 document를 통합하고 동기화를 재설계 &lt;em&gt;(by &lt;/em&gt;@조이Joy(우재호) / 전시개발팀 &lt;em&gt;)&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;안녕하세요, 여기어때 전시 개발팀 백엔드 개발자 필립입니다.&lt;/p&gt;
&lt;p&gt;전시 영역에는 숙소 목록(PLP), 상세(PDP), 객실 상세(RDP)처럼 사용자가 가장 많이 마주하는 화면들이 있습니다. 이 화면에 상품을 내려주는 상품 API를 한동안 운영하면서, 저희는 트래픽이 몰릴 때마다 같은 곳에서 발목을 잡혔습니다 — 바로 캐시였습니다.&lt;/p&gt;
&lt;p&gt;이번 글에서는 그 캐시를 V2에서 V3로 다시 설계하며 풀어간 이야기를 해보려 합니다. 빠르게 만드는 것도 목표였지만, 그보다 “캐시가 무너져도 서비스는 멈추지 않게” 만드는 데 더 많은 고민을 쏟았는데요. 어떤 문제를 만났고, 여러 갈래의 선택지를 어떻게 저울질했으며, 결국 어떤 구조에 도달했는지를 차례대로 풀어보겠습니다. 비슷한 고민을 하고 계신 분들께 작은 참고가 되면 좋겠습니다.&lt;/p&gt;
&lt;h3&gt;결과부터&lt;/h3&gt;
&lt;p&gt;전시 상품을 내려주는 표준 상품 API(Builder-API)를 V2에서 V3로 다시 설계했습니다. 핵심 지표부터 말씀드리면,&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;처리량(TPS) 3배 향상&lt;/strong&gt; — 211 → 666 &lt;em&gt;(채택 구조 기준, 스테이지 vuser 6000)&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;P95 응답 시간 대폭 단축&lt;/strong&gt; — 응답 캐시에 의존하던 4.13초 구간을 캐시 없이도 1초대로&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MongoDB CPU 부하 20~30% → 10% 이하&lt;/strong&gt; — 컬렉션 통합으로 조회 비용 자체를 줄임&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;하지만 이번 작업에서 더 공들인 건 숫자가 아니었습니다. &lt;strong&gt;원인이 무엇이든 캐시 계층이 무너져도 서비스는 멈추지 않는 구조&lt;/strong&gt;, 그걸 만드는 데 가장 많은 고민을 했습니다. 왜 그랬는지부터 이야기하겠습니다.&lt;/p&gt;
&lt;h3&gt;어쩌다 캐시가 발목을 잡았나&lt;/h3&gt;
&lt;figure&gt;&lt;img alt="" src="https://cdn-images-1.medium.com/max/1024/1*j0s4x0VRsxI7_UK-9m29DA.png"&gt;&lt;/figure&gt;&lt;p&gt;기존 구조에서 우리는 캐시에 깊게 의존하고 있었습니다. 제휴점 × 체크인/체크아웃 날짜 조합마다 &lt;strong&gt;최종 응답 전체를 통째로 Redis에 캐싱&lt;/strong&gt;했습니다. 빠르긴 했지만, 같은 메타 정보가 날짜 조합마다 중복 저장되면서 캐시는 점점 무거워졌습니다.&lt;/p&gt;
&lt;p&gt;문제는 트래픽이 몰릴 때 드러났습니다. 대용량 응답(한 요청에 메타·상품을 합쳐 수십 MB에 달하는 경우도 있었습니다)이 높은 TPS로 동시에 나가면, Redis 노드의 &lt;strong&gt;아웃바운드 네트워크가 인스턴스 baseline을 넘어섭니다.&lt;/strong&gt; baseline을 넘는 순간 패킷이 큐잉·드랍되고, 응답 지연이 누적되다 결국 클라이언트의 command timeout으로 터집니다. 한 노드에서 시작된 timeout은 곧 전체 pod로 번졌습니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;em&gt;Slow query 로그는 깨끗했습니다. Redis가 명령을 느리게 처리한 게 아니라, &lt;/em&gt;&lt;strong&gt;&lt;em&gt;네트워크 대역폭 한계에 걸려 응답을 내보내지 못한&lt;/em&gt;&lt;/strong&gt;&lt;em&gt; 것이기 때문입니다. 원인이 애플리케이션 로직이 아니라 인프라 한계선이었다는 점이 중요합니다.&lt;/em&gt;
&lt;/blockquote&gt;
&lt;p&gt;더 뼈아팠던 건 &lt;strong&gt;복구 수단마저 캐시 구조에 묶여 있었다&lt;/strong&gt;는 점입니다. 장애 중에 pod를 늘려보려 했지만, 새로 뜬 pod 역시 같은 Redis를 바라보니 부하만 더 얹는 꼴이었습니다. 스케일아웃이 해법이 되지 못했습니다.&lt;/p&gt;
&lt;p&gt;여기서 두 가지 구조적 한계가 분명해졌습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;캐시가 단일 장애점이다.&lt;/strong&gt; Redis 하나가 흔들리면 전부 멈춘다.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Redis가 병목이라 스케일아웃이 막혀 있다.&lt;/strong&gt; 애플리케이션 pod를 늘려도, 병목이 Redis에 있으니 처리량이 따라 늘지 않는다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;우리가 마주한 구조적 문제는 사실 세 갈래였습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;① &lt;strong&gt;RDB를 그대로 옮긴 파편화된 Document 구조&lt;/strong&gt; — PLP 한 번 조회에 15개 이상 컬렉션을 건드리고, 다중 조인성 쿼리가 slow query를 유발했습니다. → &lt;em&gt;컬렉션 통합으로 해결, &lt;/em&gt;&lt;strong&gt;&lt;em&gt;2편&lt;/em&gt;&lt;/strong&gt;&lt;em&gt;에서 다룹니다.&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;② &lt;strong&gt;서비스 코드에 트랜잭션 스크립트 스타일로 엉겨 있던 가격 계산&lt;/strong&gt; — 숙소 메타 조회와 가격 계산이 한 덩어리라 모듈화가 어려웠습니다. → &lt;em&gt;숙소 메타 + 가격 계산 모듈로 분리, &lt;/em&gt;&lt;strong&gt;&lt;em&gt;3편&lt;/em&gt;&lt;/strong&gt;&lt;em&gt;에서 다룹니다.&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;③ &lt;strong&gt;캐시 단일 의존&lt;/strong&gt; — 이 글에서 끝까지 파고들 주제입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;어떤 메모리 아키텍처로 서빙할 것인가&lt;/h3&gt;
&lt;p&gt;개선은 두 단계로 접근했습니다.&lt;/p&gt;
&lt;h3&gt;Phase 1 — 연산과 구조부터 풀다&lt;/h3&gt;
&lt;p&gt;가장 먼저 손본 건 조회 비용 그 자체였습니다. 기존 구조에는 두 가지 병목이 있었습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;첫째, 파편화된 Document를 향한 다중 조인.&lt;/strong&gt; RDB 테이블을 그대로 MongoDB 컬렉션으로 복사해둔 탓에, PLP 한 번 조회에 &lt;strong&gt;15개 이상의 컬렉션&lt;/strong&gt;을 건드려야 했습니다. 관련 데이터가 여기저기 흩어져 있으니 10여 개 Document를 조인하는 무거운 쿼리가 만들어졌고, 이게 slow query와 불필요한 네트워크 대역폭 낭비로 이어졌습니다. 그래서 Document DB의 특성에 맞게 — 정규화보다 조회 효율을 우선해 — 관련 데이터를 하나의 Document로 통합했습니다. &lt;strong&gt;10여 개를 조인하던 쿼리가 3~4개의 단순 쿼리로&lt;/strong&gt; 줄었습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;둘째, 제휴점별 순차 처리.&lt;/strong&gt; 여러 제휴점을 한 번에 조회할 때, 전체 응답 시간이 &lt;em&gt;각 제휴점 처리 시간의 합&lt;/em&gt;이 되는 구조였습니다. 제휴점이 늘수록 응답이 선형으로 느려졌고, 1.2초를 훌쩍 넘기는 경우가 잦았습니다.&lt;/p&gt;
&lt;pre&gt;// 기존: 순차 처리로 인한 느림&lt;br&gt;for (Place place : places) {&lt;br&gt;    PdpResponse pdp = pdpService.getPdp(place.getId()); // IO 대기&lt;br&gt;    Price lowestPrice = calculateLowestPrice(pdp);      // CPU 작업&lt;br&gt;}&lt;/pre&gt;
&lt;p&gt;이걸 풀기 위해 &lt;strong&gt;Virtual Thread&lt;/strong&gt;를 도입했습니다. Properties·RoomRatePlans·Products를 먼저 배치로 적재한 뒤, 제휴점별 모델 생성을 Virtual Thread로 병렬 처리해 한 요청에서 &lt;strong&gt;수십 건의 IO를 동시에&lt;/strong&gt; 흘렸습니다. 플랫폼 스레드 풀을 쓸 때 겪던 스레드 고갈과 컨텍스트 스위칭 오버헤드 없이, 경량 스레드로 대규모 병렬 처리가 가능해졌습니다. 개별 제휴점 실패가 전체 응답을 무너뜨리지 않도록 에러도 격리했습니다.&lt;/p&gt;
&lt;p&gt;이 두 가지를 풀고 나니, MongoDB 직접 조회의 비용 자체가 크게 떨어졌습니다. &lt;strong&gt;나중에 “캐시 없이도 버틴다”는 전제를 가능하게 한 토대&lt;/strong&gt;가 바로 이 단계입니다.&lt;/p&gt;
&lt;h3&gt;그래서, 어떤 메모리 아키텍처로 서빙할 것인가&lt;/h3&gt;
&lt;p&gt;연산·구조를 풀고 나니 진짜 설계 질문이 남았습니다. &lt;strong&gt;이 데이터를 어떤 메모리 아키텍처로 들고 서빙할 것인가.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;그전에, 장애 직후의 응급조치(스펙업, Replica 증설·payload 줄이기)는 egress를 줄여주긴 했지만 본질적으로 &lt;strong&gt;시간 벌기&lt;/strong&gt;였습니다. 트래픽이 더 늘면 baseline은 다시 넘습니다. 그래서 튜닝이 아니라 구조를 다시 골랐습니다. 후보는 셋이었습니다.&lt;/p&gt;
&lt;figure&gt;&lt;img alt="" src="https://cdn-images-1.medium.com/max/1024/1*dOgWDpIqETnbhjVQ-AB_kA.png"&gt;&lt;/figure&gt;&lt;h3&gt;채택 — CDC로 항상 최신인 메타 캐시 + 실시간 가격&lt;/h3&gt;
&lt;p&gt;우리가 택한 건 &lt;em&gt;TTL로 만료시키는 캐시&lt;/em&gt;가 아니라, &lt;strong&gt;변경이 생길 때만 이벤트로 갱신하는 캐시&lt;/strong&gt;였습니다. 메타(제휴점 정보 properties, 객실 상품 정보 room_rate_plans)는 자주 바뀌지 않으니, MongoDB에 변경이 생기는 순간 그 변경을 캐시에 반영하면 됩니다. 이를 Kafka 기반 CDC(Change Data Capture)로 구현했습니다.&lt;/p&gt;
&lt;p&gt;흐름은 이렇습니다. 숙소 정보, 가격 데이터 변경을 감지하면 전시 워커가 CDC 메시지를 만들고, 변경된 id들을 chunk 단위로 묶어 Kafka로 발행합니다. 전시 가격 어플리케이션의 Consumer는 파티션별로 메시지를 받아 &lt;strong&gt;Virtual Thread로 병렬 처리&lt;/strong&gt;하며, MongoDB에서 최신 데이터를 읽어 Redis에 반영합니다.&lt;/p&gt;
&lt;p&gt;이 구조가 우리가 가진 문제를 한꺼번에 풀었습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;캐시는 항상 최신이다.&lt;/strong&gt; TTL 만료를 기다리지 않고 변경 즉시 반영되므로, “캐시가 오래돼 틀린 값”이라는 부류의 문제가 사라집니다.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;캐시에는 가볍고 변동 적은 메타만&lt;/strong&gt; 남으니 중복·payload 비대 문제가 사라지고, Redis egress에 여유가 생깁니다.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;가격은 실시간 조회&lt;/strong&gt;라 캐시·실제 가격 불일치가 원천 차단됩니다(사용자 신뢰도와 직결).&lt;/li&gt;
&lt;li&gt;Phase 1에서 Mongo 조회 비용을 낮춰뒀기에, &lt;strong&gt;가격 경로는 평시에도 이미 Redis를 거치지 않습니다.&lt;/strong&gt; 즉 Redis가 흔들려도 가격 경로는 멀쩡합니다.&lt;/li&gt;
&lt;li&gt;운영 충돌을 막기 위해 &lt;strong&gt;v2/v3 Redis를 완전히 분리&lt;/strong&gt;(별도 RedisTemplate)했고, 실패한 id만 Slack으로 알리되 업서트는 멱등이라 메시지는 ACK 처리해 중복 캐싱을 막았습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;그 결과 Redis 부하가 내려가 master CPU를 10% 아래로 유지할 수 있게 됐고, MongoDB CPU 부하도 20~30%에서 10% 이하로 떨어졌습니다. 무엇보다 &lt;strong&gt;Redis가 더는 병목이 아니게 되면서, 이제 애플리케이션 pod를 늘리면 그대로 스케일아웃이 됩니다.&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;&lt;em&gt;(필요하면 상품 캐시나 pod별 로컬 캐시로 처리량을 더 끌어올릴 카드도 확인했지만, 로컬 캐시는 각 pod가 CDC를 받아야 해서 이번 phase에선 의식적으로 미뤘습니다.)&lt;/em&gt;&lt;/blockquote&gt;
&lt;h3&gt;캐시가 무너져도 멈추지 않게&lt;/h3&gt;
&lt;p&gt;여기까지가 “더 빠르게”였습니다. 이제 “무너지지 않게”입니다.&lt;/p&gt;
&lt;p&gt;우리가 한 번 겪은 일은 분명했습니다 — &lt;strong&gt;원인이 무엇이든(심지어 우리가 잘못 쓴 탓이든) 캐시 계층은 무너질 수 있다.&lt;/strong&gt; 그렇다면 막으려 애쓰는 것만으로는 부족하고, redis가 &lt;strong&gt;죽어도 버티는&lt;/strong&gt; 구조가 필요했습니다. 그래서 조회 경로를 3 tier로 설계했습니다.&lt;/p&gt;
&lt;figure&gt;&lt;img alt="" src="https://cdn-images-1.medium.com/max/1024/1*upoSrU8qZP2FGYtSVqJclQ.png"&gt;&lt;/figure&gt;&lt;p&gt;이 방어선은 가격 경로만이 아니라 &lt;strong&gt;메타 조회·가격 조회 모두를 커버&lt;/strong&gt;합니다. 어느 한 계층이 무너져도 다음 계층이 받아내고, 최후에는 MongoDB 직접 조회로 떨어지더라도 서비스는 살아 있습니다.&lt;/p&gt;
&lt;p&gt;Phase 1에서 Mongo 조회 비용을 낮춰둔 게 여기서 빛을 발합니다 — Level 3가 “겨우 죽지만 않는” 수준이 아니라 실제로 버틸 만한 성능이거든요.&lt;/p&gt;
&lt;p&gt;조회 코드는 “Redis → Local → Mongo, 안 되면 다음”이라는 의도만 드러나게 했습니다.&lt;/p&gt;
&lt;p&gt;방어선의 복잡함은 설정(서킷 브레이커)으로 빼두었기 때문에, 메타든 가격이든 같은 패턴으로 감싸 일관되게 보호할 수 있습니다.&lt;/p&gt;
&lt;pre&gt;// 조회는 항상 위에서부터 시도하고, 서킷이 열려 있으면 다음 단계로 떨어진다. &lt;br&gt;// 메타 조회·가격 조회 모두 같은 패턴으로 감싼다. &lt;br&gt;public PropertyData getProperty(String partnerId) {&lt;br&gt;    // Level 1 — Redis (Primary): 평시 경로&lt;br&gt;    return Try.ofSupplier(&lt;br&gt;            redisCircuitBreaker.decorateSupplier(&lt;br&gt;                () -&amp;gt; redisCache.getProperty(partnerId)))&lt;br&gt;        // Level 2 — Local Cache (Secondary): Redis 서킷 OPEN 시&lt;br&gt;        .recover(throwable -&amp;gt;&lt;br&gt;            localCircuitBreaker.decorateSupplier(&lt;br&gt;                () -&amp;gt; localCache.getProperty(partnerId)).get())&lt;br&gt;        // Level 3 — MongoDB Direct (Fallback): 최후의 보루&lt;br&gt;        .recover(throwable -&amp;gt;&lt;br&gt;            mongoRepository.findProperty(partnerId))&lt;br&gt;        .get();&lt;br&gt;}&lt;/pre&gt;
&lt;p&gt;전환은 &lt;strong&gt;서킷 브레이커&lt;/strong&gt;(resilience4j)로 자동화하되, 티어마다 성격이 다르므로 &lt;strong&gt;민감도를 다르게&lt;/strong&gt; 뒀습니다.&lt;/p&gt;
&lt;pre&gt;# Redis 티어: 빨리 끊고, 오래 쉬게 한다&lt;br&gt;redis-tier:&lt;br&gt;  failure-rate-threshold: 50&lt;br&gt;  slow-call-duration-threshold: 5s&lt;br&gt;  wait-duration-in-open-state: 10m&lt;br&gt;  record-exceptions:&lt;br&gt;    - RedisConnectionFailureException&lt;br&gt;    - QueryTimeoutException&lt;br&gt;# Local 티어: 더 관대하게 — 마지막 보루에 가까우므로&lt;br&gt;local-tier:&lt;br&gt;  wait-duration-in-open-state: 5m&lt;br&gt;  record-exceptions:&lt;br&gt;    - OutOfMemoryError&lt;br&gt;    - IllegalStateException&lt;/pre&gt;
&lt;p&gt;설계 의도는 이렇습니다. &lt;strong&gt;Redis는 한번 흔들리면 빠르게 차단하고 충분히 쉬게&lt;/strong&gt; 합니다(다시 붙였다가 또 무너지는 걸 막기 위해, OPEN을 길게). 반대로 &lt;strong&gt;Local 캐시는 마지막에 가까운 방어선이라 쉽게 끊지 않습니다.&lt;/strong&gt; 그리고 각 티어가 “무엇을 장애로 볼지”도 분리했습니다 — Redis는 연결 실패·타임아웃을, Local은 메모리 고갈류를 기준으로.&lt;/p&gt;
&lt;h3&gt;그리고 실전에서 검증됐습니다&lt;/h3&gt;
&lt;p&gt;설계를 마친 뒤, 서킷을 강제로 열어 로컬로 매끄럽게 넘어가는지 사전 확인했습니다.&lt;/p&gt;
&lt;p&gt;그런데 얼마 지나지 않아 &lt;strong&gt;예기치 않게 Redis 연결이 끊기는 상황&lt;/strong&gt;이 실제로 발생했습니다.&lt;/p&gt;
&lt;p&gt;결과는 설계한 그대로였습니다. 서킷 브레이커가 열리며 트래픽이 곧바로 로컬 캐시로 흘렀고, &lt;strong&gt;사용자는 아무것도 느끼지 못했습니다.&lt;/strong&gt; 과거 같은 상황이라면 전 pod가 timeout으로 무너졌을 텐데, 이번엔 모니터링 그래프 위의 작은 점 하나로 끝났습니다.&lt;/p&gt;
&lt;h3&gt;정리하며&lt;/h3&gt;
&lt;p&gt;성능 향상이 1차 목표였던 건 맞습니다. 하지만 정말 공들인 건 &lt;strong&gt;“이 캐시가 또 무너지면 어쩌지”라는 불안을 구조로 없애는 일&lt;/strong&gt;이었습니다. 여러 메모리 아키텍처를 저울질하고, 빠른 길 대신 버티는 길을 고르고, 티어마다 차단 정책을 다르게 다듬은 이유가 거기에 있습니다.&lt;/p&gt;
&lt;p&gt;다음 편에서는 이 모든 걸 가능하게 한 토대 — &lt;strong&gt;파편화된 MongoDB Document를 어떻게 도메인 단위로 다시 설계했는지&lt;/strong&gt;(조이), 그리고 &lt;strong&gt;가격 계산을 어떻게 독립 모듈로 떼어냈고 skill을 활용해 페이지 타입별 마이그레이션을 어떻게 처리했는지&lt;/strong&gt; 더 깊이 다룹니다.&lt;/p&gt;
&lt;p&gt;그리고 한 가지 더. 이 글에는 &lt;em&gt;기록&lt;/em&gt;의 의미도 있습니다. 지금 전시팀이 수행 하는 가격제공이 변경 예정 입니다.&lt;/p&gt;
&lt;p&gt;즉 이 시리즈에서 이야기하는 코드의 일부는 머지않아 우리 손을 떠납니다. 그 전에, 우리가 어떤 문제를 만났고 어떤 고민 끝에 지금의 구조에 도달했는지를 남겨두고 싶었습니다. 떠나보내는 코드에 대한 작은 회고이기도 합니다.&lt;/p&gt;
&lt;p&gt;긴 글 읽어주셔서 감사합니다.&lt;/p&gt;
&lt;img src="https://medium.com/_/stat?event=post.clientViewed&amp;amp;referrerSource=full_rss&amp;amp;postId=e14159d375b4" width="1" height="1" alt=""&gt;&lt;hr&gt;
&lt;p&gt;&lt;a href="https://techblog.gccompany.co.kr/%EB%8D%94-%EB%B9%A0%EB%A5%B4%EA%B2%8C-%EA%B7%B8%EB%A6%AC%EA%B3%A0-%EB%AC%B4%EB%84%88%EC%A7%80%EC%A7%80-%EC%95%8A%EA%B2%8C-%EC%A0%84%EC%8B%9C-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98-%EA%B0%9C%EC%84%A0%EA%B8%B0-1-3-e14159d375b4"&gt;더 빠르게, 그리고 무너지지 않게 — 전시 아키텍처 개선기 (1/3)&lt;/a&gt; was originally published in &lt;a href="https://techblog.gccompany.co.kr"&gt;여기어때 기술블로그&lt;/a&gt; on Medium, where people are continuing the conversation by highlighting and responding to this story.&lt;/p&gt;
&lt;/body&gt;&lt;/html&gt;
</content>
    <id>https://techblog.gccompany.co.kr/%EB%8D%94-%EB%B9%A0%EB%A5%B4%EA%B2%8C-%EA%B7%B8%EB%A6%AC%EA%B3%A0-%EB%AC%B4%EB%84%88%EC%A7%80%EC%A7%80-%EC%95%8A%EA%B2%8C-%EC%A0%84%EC%8B%9C-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98-%EA%B0%9C%EC%84%A0%EA%B8%B0-1-3-e14159d375b4?source=rss----18356045d353---4</id>
    <link href="https://techblog.gccompany.co.kr/%EB%8D%94-%EB%B9%A0%EB%A5%B4%EA%B2%8C-%EA%B7%B8%EB%A6%AC%EA%B3%A0-%EB%AC%B4%EB%84%88%EC%A7%80%EC%A7%80-%EC%95%8A%EA%B2%8C-%EC%A0%84%EC%8B%9C-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98-%EA%B0%9C%EC%84%A0%EA%B8%B0-1-3-e14159d375b4?source=rss----18356045d353---4"/>
    <title>더 빠르게, 그리고 무너지지 않게 — 전시 아키텍처 개선기 (1/3)</title>
    <updated>2026-07-13T15:53:34+09:00</updated>
    <dc:date>2026-07-13T15:53:34+09:00</dc:date>
  </entry>
  <entry>
    <author>
      <name>박지형Paori(파오리) / 주문결제개발팀</name>
    </author>
    <content type="html">&lt;!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd"&gt;
&lt;html&gt;&lt;body&gt;
&lt;p&gt;글. 박지형(Paori) / 주문결제개발팀&lt;/p&gt;
&lt;p&gt;안녕하세요, 주문결제개발팀 파오리 입니다.&lt;/p&gt;
&lt;p&gt;제가 2026년 상반기 우리 팀의 FT (&lt;em&gt;퍼실리테이터; &lt;/em&gt;&lt;a href="https://medium.com/gccompany/%ED%8D%BC%EC%8B%A4%EB%A6%AC%ED%85%8C%EC%9D%B4%ED%84%B0-facilitator-%EC%97%90-%EB%8C%80%ED%95%B4%EC%84%9C-%EB%93%A4%EC%96%B4%EB%B3%B4%EC%85%A8%EB%82%98%EC%9A%94-842dccf8cb87"&gt;&lt;em&gt;참고&lt;/em&gt;&lt;/a&gt;) 로서 참여했던 &lt;br&gt;&amp;lt;AI 수석회의&amp;gt; 활동에 대해 글 써보려고 합니다.&lt;/p&gt;
&lt;h4&gt;&lt;strong&gt;바야흐로 AI 전성시대!&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;빠르게 변화하고 진화하며 AI 기술이 쏟아지고 있지만&lt;br&gt;그만큼 개인의 관심과 의욕에 따라 개인별 편차도 커지고 있습니다.&lt;br&gt;한 조직 내에서 이 편차가 커지면 모두가 같은 높이의 시선으로 AI 를 받아들이고 활용하기에는 아무래도 어려운 면이 있죠.&lt;/p&gt;
&lt;p&gt;하지만 같은 서비스를 운영하고 개발해나가야 하는 조직이니&lt;br&gt;시선의 높이를 맞추고 새로운 정보를 공유해나가는 과정은 반드시 필요합니다.&lt;/p&gt;
&lt;p&gt;그래서 제가 속한 코어플랫폼실의 개발팀에서는&lt;br&gt;각 팀마다 유난히 AI 에 관심이 많고, 팀의 AI 관련 작업을 주도하는 분들을 ‘AI 수석’ 으로 명명하고 모임을 가졌습니다.&lt;/p&gt;
&lt;blockquote&gt;&amp;lt;AI 수석회의&amp;gt;&lt;br&gt;회의 날짜 : 2026.05.14 부터 매주 목요일 오후 4시&lt;br&gt;참석 인원 : 각 팀의 FT, AI 수석, 이 회의에 관심 있는 팀원 누구나&lt;/blockquote&gt;
&lt;p&gt;팀별로 돌아가면서 AI 수석이 발표를 준비했으며,&lt;br&gt;발표의 내용은 팀 내에서 사용하고 있는 AI 기술 및 공유하고 싶은 내용들로 자유롭게 구성하였습니다. &lt;br&gt;발표가 끝나면 서로 고민했던 지점들에 대한 의견도 나누고 &lt;br&gt;AI 를 사용하며 알게 된 소소한 꿀팁들을 가볍게 나누는 시간도 가졌답니다.&lt;/p&gt;
&lt;p&gt;이렇게 진행한 회의는 Gemini 가 작성한 회의록으로 남았습니다.&lt;/p&gt;
&lt;figure&gt;&lt;img alt="" src="https://cdn-images-1.medium.com/max/558/1*aCNpLuvhcJbeIjXhGIgK5Q.png"&gt;&lt;figcaption&gt;궁금하신 분은 주문결제개발팀 컨플루언스를 방문해 보세요!&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;모든 내용을 자세히 공유 드리기는 어려운 관계로, 각 회의들의 발표 주제들을 간략히 소개드리려고 합니다.&lt;/p&gt;
&lt;h4&gt;&lt;strong&gt;2026.05.14 회의&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;발표자 :&lt;/strong&gt; 주문결제개발팀 애쉬&lt;br&gt;&lt;strong&gt;주제 : AI 패러다임이 전환됨에 따라 주문결제개발팀에서 추진 중인 ‘Agent-Common’ 프레임워크의 도입 목적 및 방향성 공유&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;현재 개발자마다 사용하는 프롬프트와 규칙이 파편화되어 있어,&lt;br&gt;여러 도메인(국내숙소, 해외숙소 등 카테고리)을 관리하고 있는 주문결제개발팀에서는 일관성을 유지하기 어렵다는 문제가 있습니다. 이를 개선하고자 애쉬는 공통의 규칙을 정의하고, 깃 액션 파이프라인을 통한 소스 코드 규칙 관리, 런타임 최신화 과정, 그리고 사후 검증 피드백 시스템을 구축해서 개발 품질을 상향 평준화하는 &lt;strong&gt;“중앙통제관리소”&lt;/strong&gt; 와 같은 방향성을 제시했습니다.&lt;/p&gt;
&lt;p&gt;팀 전체 룰이 있고 각 도메인별 룰을 필요할 때 lazy loading 해오는 방식으로 비용절감을 하도록 한 점이 인상 깊었습니다. 한편으로는 새 프로젝트들도 많이 생겨나고 있는 상황에서 개발자들의 자율성과 강제 규칙 적용 사이의 경계선에 대한 고민도 이야기를 나누었습니다.&lt;/p&gt;
&lt;figure&gt;&lt;img alt="" src="https://cdn-images-1.medium.com/max/1024/1*TxwnxDcEnyWgBa9iew2QxA.png"&gt;&lt;/figure&gt;&lt;h4&gt;&lt;strong&gt;2026.05.21 회의&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;발표자 : &lt;/strong&gt;정산개발팀 페퍼&lt;strong&gt;&lt;br&gt;주제 : LLM 컨텍스트 관리 경험기 공유&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;정산개발팀에서는 claude team plan 사용 당시 토큰 절감에 대해 고민을 많이 하셨다고 합니다. 그 결과 LLM Wiki 를 도입하여 초기 토큰 소모 과다와 품질저하 문제를 해결했습니다.&lt;/p&gt;
&lt;p&gt;처음에는 기획서나 코드를 통째로 넘겨 분석시켰지만 불필요한 내용까지 포함되어 토큰 소모가 컸고, 생성되는 개발 계획서의 분량이 지나치게 많아 리뷰 피로도가 높았습니다. 또한 claude code 가 자잘한 의사결정을 임의로 수행하는 등 품질 저하와 반복적인 수정 요청이 발생하는 문제가 있어, 효율적인 대화형 개발환경을 모색하게 되었습니다.&lt;/p&gt;
&lt;p&gt;이 문제는 claude 를 처음 도입하고 사용하는 초기에 많이들 고민하셨을 것 같은데요, 저도 초기에는 사내에서만 공유되는 기획이나 통용되는 용어들에 대해 반복해서 학습시키고 이야기 하는 것에 토큰을 소모하는 게 여러모로 낭비라는 생각을 했습니다.&lt;/p&gt;
&lt;p&gt;정산개발팀의 LLM Wiki 는 데이터베이스 (SQL, DDL), 도메인 지식, 소스 코드로 구성된 Raw 폴더와 이를 구조화한 Wiki 폴더를 가집니다. 특히 각 Wiki 하위의 index.md 파일은 클로드가 필요한 파일을 식별하고 로드하는 핵심 경로 역할을 하며, 옵시디언 툴을 활용하여 데이터 관리의 효율성을 높였습니다.&lt;/p&gt;
&lt;figure&gt;&lt;img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ewqPTiAkAR4uo_GD-vAQWw.png"&gt;&lt;/figure&gt;&lt;h4&gt;&lt;strong&gt;2026.05.28 회의&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;발표자 :&lt;/strong&gt; 유저혜택개발팀 윌비&lt;br&gt;&lt;strong&gt;주제 : AI 작업 표준화 전략과 협업 도구 활용을 통한 효율적인 개발 운영 체계 구축 논의&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;윌비는 AI 워크스페이스를 만들어 AI 관련 작업을 단일 저장소에서 통합 관리하며 공통 가이드와 컨텍스트 주입을 통한 운영 일관성 확보, 모든 협업 이력을 파일로 기록하고 피드백 루프를 운영하며 작업 효율성을 높이는 것에 대해 이야기했습니다.&lt;/p&gt;
&lt;p&gt;개발 작업, 검증/품질 체크, git /commit, 문서화, 운영작업, 모니터링/장애 대응 과 같이 카테고리별로 팀에서 반복적으로 작업하는 것들에 대해 스킬을 만들어서 공통으로 사용하도록 했는데요, 공유해주신 AI 워크스페이스의 README 만 보아도 유용하게 쓰일 수 있는 스킬들이 많았습니다.&lt;/p&gt;
&lt;figure&gt;&lt;img alt="" src="https://cdn-images-1.medium.com/max/1024/1*UftR3tQHoSf8Rk6fGygm4w.png"&gt;&lt;/figure&gt;&lt;figure&gt;&lt;img alt="" src="https://cdn-images-1.medium.com/max/1024/1*dG60ceuE96YYqj_Bs7cjfw.png"&gt;&lt;/figure&gt;&lt;p&gt;이러한 AI 와의 협업과정에서 모든 입력값, 프롬프트, AI 의 답변, 그리고 AI가 스스로 판단한 결정 사항들을 파일에 기록하여 맥락을 공유하고 있었습니다. 작업 커밋 시에는 사용스킬과 피드백을 링크하여 사람이 검토하도록 하였는데요, AI 가 작업을 하되, 주도권은 사람에게 남겨두도록 고민한 포인트가 인상적이었습니다.&lt;/p&gt;
&lt;figure&gt;&lt;img alt="" src="https://cdn-images-1.medium.com/max/1024/1*QuCtjPAcDn6OTJ1OMx1p7Q.png"&gt;&lt;/figure&gt;&lt;h4&gt;&lt;strong&gt;2026.06.04 회의&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;발표자 :&lt;/strong&gt; 광고플랫폼개발팀 지하&lt;br&gt;&lt;strong&gt;주제 :&lt;/strong&gt; &lt;strong&gt;광고플랫폼개발팀의 하네스 구조를 통한 개발 일관성 확보와 함께 가드레일 구현 및 시스템 운영 효율화 방안 논의&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;광고플랫폼개발팀에서는 대규모 언어 모델을 활용할 때 발생하는 비결정적인 결과물 문제를 해결하기 위해 &lt;strong&gt;하네스(Harness) 구조&lt;/strong&gt;의 ‘AD’ 라는 프로젝트를 개발하였습니다. 어떤 팀원이 어떤 기능을 개발하더라도 일관된 결과물을 도출할 수 있도록 컨텍스트를 주입하고, 파괴적 명령어 차단 등 가드레일 설정을 하고 있습니다.&lt;/p&gt;
&lt;p&gt;위에서 보았던 애쉬, 윌비의 발표 내용과도 일맥상통하는 주제라고 생각하는데요, 우리 실의 많은 팀들이 AI 를 도입해 볼 만한 포인트로 규칙과 가드레일의 공통화를 보고 있네요.&lt;/p&gt;
&lt;p&gt;초기에는 세션 대화를 정규식으로 긁어서 신호를 만들던 hook 이 있었지만,&lt;br&gt;노이즈가 89%에 달했다고 합니다. 그래서 컨벤션 위반, 리뷰 결과, 차단 로그, 결정 기록 과 같은 신호를 정하고 고정된 출처에서만 받도록 했다고 합니다.&lt;/p&gt;
&lt;figure&gt;&lt;img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Fh10zy-i9wESNdJSqvaLNg.png"&gt;&lt;/figure&gt;&lt;figure&gt;&lt;img alt="" src="https://cdn-images-1.medium.com/max/1024/1*fCn2uYz-PrFJ4eL4lrMNRg.png"&gt;&lt;/figure&gt;&lt;h4&gt;&lt;strong&gt;2026.06.11 회의&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;발표자 :&lt;/strong&gt; 주문결제개발팀 월터&lt;br&gt;&lt;strong&gt;주제 : 급변하는 AI 도구 환경 속에서 개발자의 핵심 역량 변화와 전략적 도구 활용 방안 논의&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;이 전까지의 회의에서는 AI 를 활용하여 우리에게 맞는 도구를 만들어 내는 것에 대한 이야기 들을 많이 했는데요, 이런 이야기 들을 반복하다 보니 월터는 조금 더 큰 영역에서의 고민을 제안했습니다.&lt;/p&gt;
&lt;p&gt;개발자들이 각자 스킬, 기능, 도구를 만들지만 빠르게 발전하고 있는 AI 생태계에서 항상 더 좋은 기능들을 포함한 도구들이 빠르게 업데이트 되고 있습니다.&lt;/p&gt;
&lt;p&gt;그 과정에서 우리(개발자들)가 키워야 하는 능력은 무엇인가 에 대한 이야기를 나눠 주셨는데요, 결론은 결국 &lt;strong&gt;나에게 필요한 좋은 도구를 골라낼 수 있는 좋은 눈과 AI 가 일을 더 잘하게 명령할 수 있는 힘&lt;/strong&gt;인 것 같네요.&lt;br&gt;앞선 회의와는 다른 주제였지만 모두가 공감하며 한참 대화를 나눈 것이 좋았습니다.&lt;/p&gt;
&lt;figure&gt;&lt;img alt="" src="https://cdn-images-1.medium.com/max/1024/1*KiKhT5siehSfPKpwm_sKrQ.png"&gt;&lt;/figure&gt;&lt;h4&gt;&lt;strong&gt;2026.06.18 회의&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;발표자 :&lt;/strong&gt; 주문결제개발팀 애쉬&lt;br&gt;&lt;strong&gt;주제 :&lt;/strong&gt; &lt;strong&gt;MCP 기반의 자동화된 이슈 근본 원인 분석 도입기 공유 및 아키텍처 구축 방안 논의&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;애쉬는 주문결제개발팀 해외숙소 모니터링 과정에서 장애 발생 시 즉각적으로 오류 지점과 로그를 파악하여 분석시간을 단축시키는 RCA Agent 를 도입했습니다.&lt;/p&gt;
&lt;p&gt;기존의 장애 대응은 사람의 손을 많이 타는데요,&lt;br&gt;LGTM 스택(Loki · Grafana · Tempo · Mimir)과 슬랙 알림 채널로 장애가 수집되면, 담당자가 Grafana 를 열어 LogQL · TraceQL · PromQL 을 직접 작성하고, 스택 트레이스를 읽고, 코드베이스를 grep 하고, 최근 커밋을 확인하면서 원인을 추정합니다.&lt;/p&gt;
&lt;p&gt;하지만 운영을 하다 보면 대부분 비슷한 원인으로 에러 메시지가 발생하고 &lt;br&gt;위의 과정을 5–15분 걸려 파악하고 나면 이미 어느 정도 예측했던 원인과 맞아 떨어지죠. 그래서 이 5–15분을 자동화한 agent 를 만들었습니다.&lt;/p&gt;
&lt;p&gt;정상적인 예외와 시스템 에러에 따라 분석 과정을 달리하여 제법 정확도 높게 슬랙으로 회신해주는 것이 흥미로웠어요.&lt;/p&gt;
&lt;figure&gt;&lt;img alt="" src="https://cdn-images-1.medium.com/max/1024/1*DN5PuRq4XNuUTG3N1xjvqA.png"&gt;&lt;/figure&gt;&lt;h4&gt;&lt;strong&gt;2026.06.25 회의&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;발표자 : &lt;/strong&gt;정산개발팀 이스트&lt;br&gt;&lt;strong&gt;주제 : 클로드 코드 환경에서 사용되는 인기 있는 오픈소스 플러그인 조사 및 적용기 공유&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;상반기를 마무리하는 마지막 회의로, 요즘 뜨고 있는 Claude Code 오픈소스 플러그인 9종에 대해 소개하고 적용기를 공유하는 시간을 가졌습니다.&lt;/p&gt;
&lt;p&gt;크게 &lt;strong&gt;A. 하네스 / B. 메모리 / C. 컨텍스트&lt;/strong&gt; 이 세 가지의 분류로 플러그인을 소개해주셨는데요,&lt;/p&gt;
&lt;p&gt;[A. 하네스] 에서는 superpowers , ECC (Everything Claude Code), gstack, oh-my-claudecode (OMC)&lt;/p&gt;
&lt;figure&gt;&lt;img alt="" src="https://cdn-images-1.medium.com/max/1024/1*POk_VizNLHiZkYa75mz8Vw.png"&gt;&lt;/figure&gt;&lt;p&gt;[B. 메모리] 에서는 claude-mem, agentmemory, mem0&lt;/p&gt;
&lt;figure&gt;&lt;img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Rk7NEuFq-OjbMB0A4KQdGw.png"&gt;&lt;/figure&gt;&lt;p&gt;[C. 컨텍스트] 에서는 graphify, Understanding-Anything&lt;br&gt;을 소개해주셨습니다.&lt;/p&gt;
&lt;figure&gt;&lt;img alt="" src="https://cdn-images-1.medium.com/max/1024/1*G5Dc3e9ROBAK9FuiUcPNyg.png"&gt;&lt;/figure&gt;&lt;p&gt;겉으로는 다 장점이 명확하고 유용하게 쓰일 수 있을 것 같았는데요,&lt;br&gt;실제 여러 사용자들의 후기 종합 결과, 호평보다는 혹평이 더 많았습니다. &lt;br&gt;그리고 새로운 모델들이 빠르게 해당 pain point 를 개선해서 배포되고 있기도 합니다.&lt;/p&gt;
&lt;p&gt;실제로 이스트의 사용기 후기도 &lt;strong&gt;“아직 ‘이거다' 싶은 쓸 만한 오픈소스 프로젝트는 없다.” &lt;/strong&gt;라는 후기를 남겨주셨습니다. &lt;br&gt;결국은 위에서 월터가 나눠주신 이야기 처럼 “지식베이스 구축" 이 가장 중요한 것이라는 결론에 도달하였네요.&lt;/p&gt;
&lt;p&gt;회의를 시작하기 이전에는 다른 팀에서 어느 정도의 범위로 어떻게 AI 를 &lt;br&gt;활용하고 있는지 알지 못했는데, 이 회의에 참석하면서 다른 팀에서 &lt;br&gt;어떻게 AI 를 사용하는지 알게 되어 매우 유익한 시간이었습니다.&lt;/p&gt;
&lt;p&gt;또한, 제가 사용해 보지 않은 도구들의 사용기와 구축기를 들으면서 &lt;br&gt;‘이런 것도 있구나’ , ‘이 부분은 나도 활용해 보면 좋을 것 같다' 라는 생각도 &lt;br&gt;들었습니다.&lt;/p&gt;
&lt;p&gt;팀별 담당 도메인이 다르다 보니 각 팀마다 운영 프로세스도 다르고 &lt;br&gt;프로젝트 구조도 다른데, 각자 적합한 도구와 컨텍스트 를 적용시킨 경험기를 들으며 다른 팀의 업무와 역할에 대해서도 이해도를 높일 수 있었고,&lt;/p&gt;
&lt;p&gt;한편으로는 수없이 많이 쏟아지는 AI 도구들과 일주일, 한 달 사이에도 빠르게 발전하고 변화하는 AI 에 대해 두려움 혹은 앞으로 개발자로서 나아가야 할 방향에 대해서도 가볍게 공유하고 토론하는 시간도 있어서 한 번씩 마음다짐도 할 수 있었네요. (ㅎㅎ)&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;거창한 발표가 아니더라도 다른 팀과 소소한 꿀팁을 공유하는 모임,&lt;br&gt;한 번씩 해보면 어떨까요?&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;AI 수석회의를 함께 한 분들&lt;br&gt;유저혜택개발팀 제이알, 광고플랫폼개발팀 제시카, 주문결제개발팀 월터,&lt;br&gt;정산개발팀 이스트, 광고플랫폼개발팀 지하, 유저혜택개발팀 윌비,&lt;br&gt;정산기획팀 와이제이, 정산개발팀 페퍼, 주문결제개발팀 애쉬&lt;/blockquote&gt;
&lt;img src="https://medium.com/_/stat?event=post.clientViewed&amp;amp;referrerSource=full_rss&amp;amp;postId=6df49a441855" width="1" height="1" alt=""&gt;&lt;hr&gt;
&lt;p&gt;&lt;a href="https://techblog.gccompany.co.kr/ai-%EC%88%98%EC%84%9D%ED%9A%8C%EC%9D%98-6df49a441855"&gt;AI 수석회의&lt;/a&gt; was originally published in &lt;a href="https://techblog.gccompany.co.kr"&gt;여기어때 기술블로그&lt;/a&gt; on Medium, where people are continuing the conversation by highlighting and responding to this story.&lt;/p&gt;
&lt;/body&gt;&lt;/html&gt;
</content>
    <id>https://techblog.gccompany.co.kr/ai-%EC%88%98%EC%84%9D%ED%9A%8C%EC%9D%98-6df49a441855?source=rss----18356045d353---4</id>
    <link href="https://techblog.gccompany.co.kr/ai-%EC%88%98%EC%84%9D%ED%9A%8C%EC%9D%98-6df49a441855?source=rss----18356045d353---4"/>
    <title>AI 수석회의</title>
    <updated>2026-07-13T13:27:49+09:00</updated>
    <dc:date>2026-07-13T13:27:49+09:00</dc:date>
  </entry>
  <entry>
    <author>
      <name>Google for Developers</name>
    </author>
    <content type="html">&lt;!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd"&gt;
&lt;html&gt;
&lt;head&gt;
&lt;meta http-equiv="Content-Type" content="text/html; charset=UTF-8"&gt;
&lt;style&gt;
.post-body span {
white-space: normal !important; }
&lt;/style&gt;

&lt;/head&gt;
&lt;body&gt;
&lt;p&gt;            &lt;a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiZ5jGOkZf1r1k0dc4YmXDxP0M8JioqNqlhJb8NuVCVBw556o5wlXNpOZimVOOSyBvRRNFZPktWwYIM4BcKOJLa7X_lttEqMoHHaiBDTL5mjxlvNzRkxwjltznLwcx5pd4XxYC2vEzMgRvvlYsSTop9hJgPg_f24dVSUZoIVCuS8UdQX4vh66yQrhxPLw/s3334/Google%20Dev_Header_final.png" style="font-family: inherit; margin-left: 1em; margin-right: 1em; text-align: center;"&gt;&lt;img border="0" data-original-height="835" data-original-width="3334" height="160" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiZ5jGOkZf1r1k0dc4YmXDxP0M8JioqNqlhJb8NuVCVBw556o5wlXNpOZimVOOSyBvRRNFZPktWwYIM4BcKOJLa7X_lttEqMoHHaiBDTL5mjxlvNzRkxwjltznLwcx5pd4XxYC2vEzMgRvvlYsSTop9hJgPg_f24dVSUZoIVCuS8UdQX4vh66yQrhxPLw/w640-h160/Google%20Dev_Header_final.png" width="640"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p dir="ltr" style="line-height: 1.68; margin-bottom: 0pt; margin-top: 0pt;"&gt;&lt;span style="color: black; font-family: inherit; font-variant-alternates: normal; font-variant-east-asian: normal; font-variant-emoji: normal; font-variant-numeric: normal; font-variant-position: normal; vertical-align: baseline; white-space-collapse: preserve;"&gt;개발자 여러분, 안녕하세요!&lt;/span&gt;&lt;/p&gt;
&lt;p dir="ltr" style="line-height: 1.68; margin-bottom: 0pt; margin-top: 0pt;"&gt;&lt;span style="color: black; font-variant-alternates: normal; font-variant-east-asian: normal; font-variant-emoji: normal; font-variant-numeric: normal; font-variant-position: normal; vertical-align: baseline; white-space-collapse: preserve;"&gt;7월 둘째 주 블로그에 발표된 Google의 주요 개발자 제품별 최신 소식을 살펴보세요.&lt;/span&gt;&lt;/p&gt;
&lt;p dir="ltr" style="line-height: 1.68; margin-bottom: 0pt; margin-top: 0pt;"&gt;&lt;span id="docs-internal-guid-02d963c5-7fff-b97c-f79a-9177957eea84"&gt;&lt;br&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p dir="ltr" style="line-height: 1.68; margin-bottom: 0pt; margin-top: 0pt;"&gt;&lt;span style="color: black; font-family: inherit; font-variant-alternates: normal; font-variant-east-asian: normal; font-variant-emoji: normal; font-variant-numeric: normal; font-variant-position: normal; font-weight: 700; vertical-align: baseline; white-space-collapse: preserve;"&gt;[주요 개발자 블로그 업데이트]&lt;/span&gt;&lt;/p&gt;
&lt;p dir="ltr" style="line-height: 1.68; margin-bottom: 0pt; margin-top: 0pt;"&gt;&lt;span style="background-color: #ffe599; color: black; font-style: normal; font-variant: normal; font-weight: 700; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;"&gt;&lt;span style="font-family: inherit;"&gt;AI &amp;amp; Machine Learning&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style="text-align: left;"&gt;
&lt;li&gt;&lt;span style="font-family: inherit;"&gt;&lt;span style="font-variant: normal; vertical-align: baseline; white-space: pre-wrap;"&gt;Gemini API Managed Agents 기능 확장: 백그라운드 작업, 원격 MCP 등 지원 (&lt;/span&gt;&lt;a href="https://blog.google/innovation-and-ai/technology/developers-tools/expanding-managed-agents-gemini-api/" style="text-decoration: none; white-space: pre;" target="_blank"&gt;&lt;span style="color: #1155cc; font-variant: normal; text-decoration-skip-ink: none; text-decoration: underline; vertical-align: baseline; white-space: pre-wrap;"&gt;Expanding Managed Agents in Gemini API: background tasks, remote MCP and more&lt;/span&gt;&lt;/a&gt;&lt;span style="font-variant: normal; vertical-align: baseline; white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style="font-family: inherit;"&gt;&lt;span style="font-variant: normal; vertical-align: baseline; white-space: pre-wrap;"&gt;Google의 고성능 웹 AI 추론 엔진, LiteRT.js 소개 (&lt;/span&gt;&lt;a href="https://developers.googleblog.com/litertjs-googles-high-performance-web-ai-inference/" style="text-decoration: none; white-space: pre;" target="_blank"&gt;&lt;span style="color: #1155cc; font-variant: normal; text-decoration-skip-ink: none; text-decoration: underline; vertical-align: baseline; white-space: pre-wrap;"&gt;LiteRT.js, Google's high performance Web AI Inference&lt;/span&gt;&lt;/a&gt;&lt;span style="font-variant: normal; vertical-align: baseline; white-space: pre-wrap;"&gt;) &lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style="font-family: inherit;"&gt;&lt;span style="font-variant: normal; vertical-align: baseline; white-space: pre-wrap;"&gt;Gemini Spark 업데이트: macOS 버전 출시, 앱 연동 기능 추가 등 (&lt;/span&gt;&lt;a href="https://blog.google/innovation-and-ai/products/gemini-app/gemini-spark-updates-june-2026/" style="text-decoration: none; white-space: pre;" target="_blank"&gt;&lt;span style="color: #1155cc; font-variant: normal; text-decoration-skip-ink: none; text-decoration: underline; vertical-align: baseline; white-space: pre-wrap;"&gt;Gemini Spark updates: macOS launch, connected apps and more&lt;/span&gt;&lt;/a&gt;&lt;span style="font-variant: normal; vertical-align: baseline; white-space: pre-wrap;"&gt;) &lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style="font-family: inherit;"&gt;&lt;span style="font-variant: normal; vertical-align: baseline; white-space: pre-wrap;"&gt;Gemini 앱, 더 많은 사용자에게 개인화된 이미지 생성 기능 제공 (&lt;/span&gt;&lt;a href="https://blog.google/innovation-and-ai/products/gemini-app/personal-intelligence-nano-banana-us-expansion/" style="text-decoration: none; white-space: pre;" target="_blank"&gt;&lt;span style="color: #1155cc; font-variant: normal; text-decoration-skip-ink: none; text-decoration: underline; vertical-align: baseline; white-space: pre-wrap;"&gt;The Gemini app is bringing personalized image creation to more users.&lt;/span&gt;&lt;/a&gt;&lt;span style="font-variant: normal; vertical-align: baseline; white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style="font-family: inherit;"&gt;&lt;span style="font-variant: normal; vertical-align: baseline; white-space: pre-wrap;"&gt;학습 도중 TPU 가동을 중단해도 몇 초 만에 복구: MaxText를 활용한 탄력적 학습 소개 (&lt;/span&gt;&lt;a href="https://developers.googleblog.com/we-terminated-a-tpu-mid-training-and-it-recovered-in-seconds-introduction-to-elastic-training-with-maxtext/" style="text-decoration: none; white-space: pre;" target="_blank"&gt;&lt;span style="color: #1155cc; font-variant: normal; text-decoration-skip-ink: none; text-decoration: underline; vertical-align: baseline; white-space: pre-wrap;"&gt;We terminated a TPU mid-training and it recovered in seconds: Introduction to elastic training with MaxText&lt;/span&gt;&lt;/a&gt;&lt;span style="font-variant: normal; vertical-align: baseline; white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style="font-family: inherit;"&gt;&lt;span style="font-variant: normal; vertical-align: baseline; white-space: pre-wrap;"&gt;도메인 간 격차 줄이기: Antigravity와 Gemini로 구축한 AI 레이스 코치 &lt;/span&gt;&lt;a href="https://developers.googleblog.com/bridging-the-domain-gap-ai-race-coach-built-with-antigravity-and-gemini" style="text-decoration: none; white-space: pre;" target="_blank"&gt;&lt;span style="color: #1155cc; font-variant: normal; text-decoration-skip-ink: none; text-decoration: underline; vertical-align: baseline; white-space: pre-wrap;"&gt;(Bridging the Domain Gap: AI Race Coach built with Antigravity and Gemini&lt;/span&gt;&lt;/a&gt;&lt;span style="font-variant: normal; vertical-align: baseline; white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style="font-family: inherit;"&gt;&lt;span style="font-variant: normal; vertical-align: baseline; white-space: pre-wrap;"&gt;ADK Go 2.0으로 신뢰할 수 있는 멀티 에이전트 앱 구축하기: 새로운 그래프 기반 워크플로 엔진, 내장형 Human-in-the-Loop, 동적 오케스트레이션 알아보기 (&lt;/span&gt;&lt;a href="https://developers.googleblog.com/announcing-adk-go-20/" style="text-decoration: none; white-space: pre;" target="_blank"&gt;&lt;span style="color: #1155cc; font-variant: normal; text-decoration-skip-ink: none; text-decoration: underline; vertical-align: baseline; white-space: pre-wrap;"&gt;Build reliable multi-agent applications with ADK Go 2.0. Discover our new graph-based workflow engine, built-in human-in-the-loop, and dynamic orchestration&lt;/span&gt;&lt;/a&gt;&lt;span style="font-variant: normal; vertical-align: baseline; white-space: pre-wrap;"&gt;) &lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style="font-family: inherit;"&gt;&lt;span style="font-variant: normal; vertical-align: baseline; white-space: pre-wrap;"&gt;공공기관이 위기 대응력을 높이기 위해 Google의 혁신적인 AI 기술을 활용하는 방법 (&lt;/span&gt;&lt;a href="https://blog.google/innovation-and-ai/technology/research/technology-global-crisis-resilience/" style="text-decoration: none; white-space: pre;" target="_blank"&gt;&lt;span style="color: #1155cc; font-variant: normal; text-decoration-skip-ink: none; text-decoration: underline; vertical-align: baseline; white-space: pre-wrap;"&gt;How governments and organizations are leveraging Google’s AI breakthroughs for crisis resilience&lt;/span&gt;&lt;/a&gt;&lt;span style="font-variant: normal; vertical-align: baseline; white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style="font-family: inherit;"&gt;&lt;span style="font-variant: normal; vertical-align: baseline; white-space: pre-wrap;"&gt;모두가 함께 배우고 성장하는 시대: Google ‘AI올림’과 함께 AI 역량 한 단계 업그레이드하기 &lt;/span&gt;&lt;a href="https://blog.google/intl/ko-kr/company-news/technology/google-ai-professional-certificate-kr/" style="text-decoration: none;" target="_blank"&gt;&lt;span style="color: #1155cc; font-variant: normal; text-decoration-skip-ink: none; text-decoration: underline; vertical-align: baseline; white-space: pre-wrap;"&gt;(국문&lt;/span&gt;&lt;/a&gt;&lt;span style="font-variant: normal; vertical-align: baseline; white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p role="presentation" style="line-height: 1.68; margin-bottom: 0pt; margin-top: 0pt; text-align: left; white-space: pre;"&gt;&lt;/p&gt;
&lt;p dir="ltr" style="line-height: 1.68; margin-bottom: 0pt; margin-top: 0pt;"&gt;&lt;b id="docs-internal-guid-fa153214-7fff-7a39-be4e-6afc6dbb4d74" style="font-weight: normal;"&gt;&lt;span style="font-family: inherit;"&gt;&lt;br&gt;&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p dir="ltr" style="line-height: 1.68; margin-bottom: 0pt; margin-top: 0pt;"&gt;&lt;span style="background-color: #ffe599; color: black; font-style: normal; font-variant: normal; font-weight: 700; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;"&gt;&lt;span style="font-family: inherit;"&gt;Android&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p dir="ltr" style="line-height: 1.68; margin-bottom: 0pt; margin-top: 0pt;"&gt;&lt;/p&gt;
&lt;p role="presentation" style="line-height: 1.68; margin-bottom: 0pt; margin-top: 0pt; text-align: left; white-space-collapse: preserve;"&gt;&lt;/p&gt;
&lt;ul style="text-align: left; text-wrap-mode: nowrap;"&gt;&lt;li&gt;&lt;span style="font-family: inherit;"&gt;&lt;span style="font-variant: normal; vertical-align: baseline; white-space: pre-wrap;"&gt;Android용 LLM 평가 방식의 진화: Android 벤치마크의 새로운 시대 (&lt;/span&gt;&lt;a href="https://android-developers.googleblog.com/2026/07/android-bench-llm-measurement.html" style="text-decoration: none;" target="_blank"&gt;&lt;span style="color: #1155cc; font-variant: normal; text-decoration-skip-ink: none; text-decoration: underline; vertical-align: baseline; white-space: pre-wrap;"&gt;Evolving how LLMs are measured for Android: the next era of Android Bench&lt;/span&gt;&lt;/a&gt;&lt;span style="font-variant: normal; vertical-align: baseline; white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;&lt;/ul&gt;
&lt;div&gt;&lt;br&gt;&lt;/div&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;div style="line-height: 1.68; margin-bottom: 0pt; margin-top: 0pt;"&gt;&lt;span id="docs-internal-guid-f353c7e6-7fff-4ce8-9cb9-434260c17814" style="font-family: inherit;"&gt;&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span id="docs-internal-guid-d373cb86-7fff-d08b-193a-8a9cd353ad34"&gt;&lt;p style="line-height: 1.68; margin-bottom: 12pt; margin-top: 12pt;"&gt;&lt;span style="font-family: inherit; font-variant-alternates: normal; font-variant-east-asian: normal; font-variant-emoji: normal; font-variant-numeric: normal; font-variant-position: normal; vertical-align: baseline; white-space-collapse: preserve;"&gt;&lt;span style="font-family: inherit;"&gt;* &lt;/span&gt;국문 번역본으로 보시려면 Chrome 창에서 각 영문 링크로 이동한 후 마우스 우측 버튼을 눌러 ‘한국어로 번역'을 선택하시면 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p dir="ltr" style="line-height: 1.68; margin-bottom: 0pt; margin-top: 0pt;"&gt;&lt;span style="font-family: inherit;"&gt;&lt;span face='"Google Sans", sans-serif' style="font-variant-alternates: normal; font-variant-east-asian: normal; font-variant-emoji: normal; font-variant-numeric: normal; font-variant-position: normal; vertical-align: baseline; white-space-collapse: preserve;"&gt;* 최신 개발자 문서에 대한 알림을 받아보려면, &lt;/span&gt;&lt;a href="http://developer.google.com/profile/u/me?hl=ko" style="text-decoration-line: none;" target="_blank"&gt;&lt;span face='"Google Sans", sans-serif' style="color: #1155cc; font-variant-alternates: normal; font-variant-east-asian: normal; font-variant-emoji: normal; font-variant-numeric: normal; font-variant-position: normal; text-decoration-line: underline; text-decoration-skip-ink: none; vertical-align: baseline; white-space-collapse: preserve;"&gt;여기&lt;/span&gt;&lt;/a&gt;&lt;span face='"Google Sans", sans-serif' style="font-variant-alternates: normal; font-variant-east-asian: normal; font-variant-emoji: normal; font-variant-numeric: normal; font-variant-position: normal; vertical-align: baseline; white-space-collapse: preserve;"&gt;에서 Google 개발자 프로필을 생성하여 손쉽게 살펴보세요. 다양한 개발자 학습 과정과 커뮤니티 이벤트에 참여하면 여러분의 프로필에 표시할 수 있는 온라인 인증 배지도 함께 드립니다. &lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;span style="font-family: inherit;"&gt;&lt;br&gt;&lt;br&gt;&lt;/span&gt;&lt;p dir="ltr" style="line-height: 1.68; margin-bottom: 0pt; margin-top: 0pt;"&gt;&lt;span style="font-family: inherit; font-variant-alternates: normal; font-variant-east-asian: normal; font-variant-emoji: normal; font-variant-numeric: normal; font-variant-position: normal; font-weight: 700; vertical-align: baseline; white-space-collapse: preserve;"&gt;Google for Developers&lt;/span&gt;&lt;/p&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/body&gt;
&lt;/html&gt;
</content>
    <id>http://developers-kr.googleblog.com/2026/07/weeklyupdate-week2.html</id>
    <link href="http://developers-kr.googleblog.com/2026/07/weeklyupdate-week2.html"/>
    <title>Gemini API Managed Agents 기능 확장 등 7월 둘째 주 Google for Developers 위클리 업데이트를 지금 확인하세요! </title>
    <updated>2026-07-10T10:59:36.759+09:00</updated>
    <dc:date>2026-07-10T10:59:36.759+09:00</dc:date>
  </entry>
  <entry>
    <author>
      <name>Dreamhack</name>
    </author>
    <content type="html">&lt;!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd"&gt;
&lt;html&gt;&lt;body&gt;&lt;p&gt;AI가 취약점 스캔부터 보고서 초안까지 작성하는 지금, 보안 커리어를 지킬 수 있는 핵심 역량은 기술 실력이 아니라 커뮤니케이션 역량일 수 있어요.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;
</content>
    <id>https://theori.io/ko/blog/ai-era-security-communication-skills</id>
    <link href="https://theori.io/ko/blog/ai-era-security-communication-skills"/>
    <summary type="html">AI가 취약점 스캔부터 보고서 초안까지 작성하는 지금, 보안 커리어를 지킬 수 있는 핵심 역량은 기술 실력이 아니라 커뮤니케이션 역량일 수 있어요.</summary>
    <title>AI가 해킹하는 시대, 보안 직군에서 진짜 필요한 역량은 무엇일까</title>
    <updated>2026-07-10T11:19:10+09:00</updated>
    <dc:date>2026-07-10T11:19:10+09:00</dc:date>
  </entry>
  <entry>
    <author>
      <name>DarionKim</name>
    </author>
    <content type="html">&lt;!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd"&gt;
&lt;html&gt;&lt;body&gt;
&lt;p&gt;&lt;figure class="imageblock alignCenter" data-ke-mobilestyle="widthOrigin" data-origin-width="1998" data-origin-height="882"&gt;&lt;span data-url="https://blog.kakaocdn.net/dn/tPH10/dJMcabSn8uU/4cDoq9AyMaWhWALMT6cVXK/img.png" data-phocus="https://blog.kakaocdn.net/dn/tPH10/dJMcabSn8uU/4cDoq9AyMaWhWALMT6cVXK/img.png"&gt;&lt;img src="https://blog.kakaocdn.net/dn/tPH10/dJMcabSn8uU/4cDoq9AyMaWhWALMT6cVXK/img.png" srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtPH10%2FdJMcabSn8uU%2F4cDoq9AyMaWhWALMT6cVXK%2Fimg.png" onerror="this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';" loading="lazy" width="1998" height="882" data-origin-width="1998" data-origin-height="882"&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2&gt;GIGO, 그런데 우리 건 차원이 다르다&lt;/h2&gt;
&lt;p&gt;RAG에는 오래된 격언이 있다. &lt;strong&gt;"쓰레기를 넣으면 쓰레기가 나온다(Garbage In, Garbage Out)."&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;그런데 사내 지식은 — 쓰레기인지 아닌지조차 애매했다. 작년에 폐기된 규정인데 멀쩡해 보이는 문서, 부서마다 다르게 부르는 용어, 같은 내용이 세 군데에 흩어진 중복, 그리고 &lt;em&gt;절대 새어 나가면 안 되는 개인정보&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;1탄~4탄에서 검색·권한·대화·채널을 아무리 잘 만들어도, 들어온 지식이 엉망이면 답도 엉망이다. &lt;strong&gt;좋은 검색의 대부분은 검색 알고리즘이 아니라 _수집 단계_에서 결정된다.&lt;/strong&gt; 그리고 개인정보 한 건이 새어나가면 그건 "틀린 답변"이 아니라 &lt;strong&gt;보안 사고&lt;/strong&gt;다. 사내 지식은 "얼마나 정확한가"와 "얼마나 위험한가"를 동시에 조심해야 하는, 일반 GIGO보다 한 단계 무거운 문제였다.&lt;/p&gt;
&lt;h2&gt;Notion 하나 쓰는 회사 vs 20년 Excel·PPT 쓴 회사&lt;/h2&gt;
&lt;p&gt;다른 회사 이야기를 들으면 부러울 때가 있다. 처음부터 지식을 &lt;strong&gt;Notion 하나&lt;/strong&gt; 같은 단일 도구에 모아 쓰는 회사는, 도서관을 신축 설계도 한 장으로 짓는 것과 같다. 소스가 하나고, 게다가 &lt;strong&gt;Notion은 태생이 마크다운&lt;/strong&gt; — 텍스트 구조가 이미 깔끔해서 파싱이 거의 공짜다. 서가 규칙이 처음부터 하나다.&lt;/p&gt;
&lt;p&gt;우리는 달랐다. &lt;strong&gt;20년 넘게 Excel·PowerPoint·사내 게시판·메일로 지식을 쌓아온&lt;/strong&gt; 회사였다. 여기서 진짜 난이도는 "소스가 많다"가 아니라 &lt;strong&gt;"20년치 이질적 포맷"&lt;/strong&gt; 자체다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Excel: 표, 병합 셀, 여러 시트, 수식 결과값, 셀 안에 줄바꿈으로 욱여넣은 문단.&lt;/li&gt;
&lt;li&gt;PowerPoint: 텍스트가 도형·표·이미지 안에 흩어져 있고, 발표 흐름은 사람 머릿속에만 있다.&lt;/li&gt;
&lt;li&gt;게다가 &lt;strong&gt;표를 이미지로 캡처해 붙인&lt;/strong&gt; 문서도 흔하다 — 그 안의 글자는 텍스트가 아니라 그림이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Notion 회사가 "책을 그대로 서가에 꽂는" 일이라면, 우리는 &lt;strong&gt;책을 펼쳐 표·도형·이미지에서 글자를 발라내는&lt;/strong&gt; 일부터 해야 했다. 통째로 다시 지을 순 없었다 — 이미 그 안에 20년치 지식이 살고 있었으니까. 그래서 만든 건 "하나의 표준 서가"가 아니라, &lt;strong&gt;소스마다 맞춘 전용 손수레(connector)&lt;/strong&gt; 였다. Confluence, Jira, SharePoint, 게시판, Teams… 소스마다 다른 문·다른 포맷에 맞춰 각각의 connector를 만들고, 그 손수레들이 정해진 시간에 각 소스를 도는 &lt;strong&gt;순회 일정(scheduler)&lt;/strong&gt; 을 짰다.&lt;/p&gt;
&lt;p&gt;&lt;figure class="imageblock alignCenter" data-ke-mobilestyle="widthOrigin" data-origin-width="1950" data-origin-height="814"&gt;&lt;span data-url="https://blog.kakaocdn.net/dn/vwbZe/dJMcagGgKrS/OF6UrxYRbFInSbDrygTBHK/img.png" data-phocus="https://blog.kakaocdn.net/dn/vwbZe/dJMcagGgKrS/OF6UrxYRbFInSbDrygTBHK/img.png"&gt;&lt;img src="https://blog.kakaocdn.net/dn/vwbZe/dJMcagGgKrS/OF6UrxYRbFInSbDrygTBHK/img.png" srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvwbZe%2FdJMcagGgKrS%2FOF6UrxYRbFInSbDrygTBHK%2Fimg.png" onerror="this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';" loading="lazy" width="1950" height="814" data-origin-width="1950" data-origin-height="814"&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2&gt;검수대(Ingestion Gate)&lt;/h2&gt;
&lt;p&gt;포맷을 발라 텍스트를 얻었다고 끝이 아니다. 도서관에 책을 들이기 전, 검수대를 뒀다. 모든 문서는 색인되기 전에 이 게이트를 통과한다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[소스] → 파싱 → 한국어 전처리 → 청킹 → (검수대: ingestion gate)
                                          ├─ 품질: 너무 짧거나 깨진 문서?
                                          ├─ 보안: PII/기밀? → 격리(quarantine)
                                          └─ 메타: 권한 태깅(tenant/kb/tier/acl)
        → 중복 제거 → 임베딩 → 저장(Qdrant)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class="imageblock alignCenter" data-ke-mobilestyle="widthOrigin" data-origin-width="1902" data-origin-height="824"&gt;&lt;span data-url="https://blog.kakaocdn.net/dn/MhVNC/dJMcadCzZPo/DLlkXGeCpUl4facDQMlkXk/img.png" data-phocus="https://blog.kakaocdn.net/dn/MhVNC/dJMcadCzZPo/DLlkXGeCpUl4facDQMlkXk/img.png"&gt;&lt;img src="https://blog.kakaocdn.net/dn/MhVNC/dJMcadCzZPo/DLlkXGeCpUl4facDQMlkXk/img.png" srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMhVNC%2FdJMcadCzZPo%2FDLlkXGeCpUl4facDQMlkXk%2Fimg.png" onerror="this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';" loading="lazy" width="1902" height="824" data-origin-width="1902" data-origin-height="824"&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2&gt;100G를 통째로 나를 수는 없다 — 분할과 델타&lt;/h2&gt;
&lt;p&gt;소스 하나가 워낙 커서(대용량), 손수레로 한 번에 다 나르려면 트럭 몇 대로도 부족했다. 두 가지를 했다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;분할(partition)&lt;/strong&gt;: 큰 자료 뭉치를 통째로 나르지 않고, 감당할 수 있는 단위로 쪼개서 나른다.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;델타(delta)&lt;/strong&gt;: 매번 소스 전체를 다시 뒤지지 않는다. fingerprint로 &lt;strong&gt;지난번과 달라진 부분만&lt;/strong&gt; 골라 옮긴다 — 안 그러면 순회할 때마다 도서관 하나를 통째로 다시 옮기는 셈이 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class="imageblock alignCenter" data-ke-mobilestyle="widthOrigin" data-origin-width="2000" data-origin-height="946"&gt;&lt;span data-url="https://blog.kakaocdn.net/dn/2g2ax/dJMcahyl6cy/Winvcd2hEJexgLJ6lzoObk/img.png" data-phocus="https://blog.kakaocdn.net/dn/2g2ax/dJMcahyl6cy/Winvcd2hEJexgLJ6lzoObk/img.png"&gt;&lt;img src="https://blog.kakaocdn.net/dn/2g2ax/dJMcahyl6cy/Winvcd2hEJexgLJ6lzoObk/img.png" srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F2g2ax%2FdJMcahyl6cy%2FWinvcd2hEJexgLJ6lzoObk%2Fimg.png" onerror="this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';" loading="lazy" width="2000" height="946" data-origin-width="2000" data-origin-height="946"&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;여러 소스를 돌다 보면 같은 문서가 이름만 바꾼 채 여러 곳에 꽂혀 있는 것도 발견한다 — 그대로 다 꽂으면 사용자는 같은 답을 세 번 받거나, 서로 다른 버전의 답을 받는다. 그래서 서가에 꽂기 전, 마지막으로 한 번 더 골라내는 절차(중복 제거)를 거친다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;수집 스택:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;소스 connector : Confluence / Jira / SharePoint / 사내 게시판 / Teams — 소스별 어댑터
포맷 파싱      : Excel / PPT / PDF / 문서 — 표·도형·이미지에서 텍스트 추출
증분 크롤      : fingerprint 기반 변경분만 재수집
워크플로       : Temporal(스케줄·재시도·크롤 상태 추적)
PII 스크럽     : 정규 포맷 + 우회 케이스(하이픈 없는 번호 등) 규칙 엔진
중복 제거      : 해시 → 근사중복 → 의미중복 다단계 파이프라인
임베딩/저장    : 청킹 → 임베딩(OpenAI) → Qdrant(1탄의 통합 컬렉션, 권한 태그 포함)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class="imageblock alignCenter" data-ke-mobilestyle="widthOrigin" data-origin-width="1900" data-origin-height="836"&gt;&lt;span data-url="https://blog.kakaocdn.net/dn/bOhW32/dJMcag0rp1w/xIzHPKYI7OfSQCcH1BUhvk/img.png" data-phocus="https://blog.kakaocdn.net/dn/bOhW32/dJMcag0rp1w/xIzHPKYI7OfSQCcH1BUhvk/img.png"&gt;&lt;img src="https://blog.kakaocdn.net/dn/bOhW32/dJMcag0rp1w/xIzHPKYI7OfSQCcH1BUhvk/img.png" srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbOhW32%2FdJMcag0rp1w%2FxIzHPKYI7OfSQCcH1BUhvk%2Fimg.png" onerror="this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';" loading="lazy" width="1900" height="836" data-origin-width="1900" data-origin-height="836"&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2&gt;심화 — 검수대는 무엇을 막고, 우리는 어디서 데였나&lt;/h2&gt;
&lt;blockquote data-ke-style="style1"&gt;&lt;p data-ke-size="size16"&gt;&lt;span style="font-family: 'Noto Serif KR';"&gt;&lt;p&gt;이 절은 깊다. 검수대가 실제로 어떻게 판정하고, 대용량 수집이 어디서 터졌는지 궁금한 분을 위한 detail이다. 바쁘면 체크리스트로 건너뛰어도 좋다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;① 검수대 판정 — 무엇을 막고, 무엇을 통과시키나&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;게이트는 결과를 네 갈래로 나눈다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if verdict in {보안 위반: PII·기밀}:     action = QUARANTINE   # 색인 안 함, 격리
elif core_fail &amp;gt;= 2:                     action = REJECT       # 핵심 품질 다중 실패
elif core_fail == 1:                     action = HOLD         # 재작업 여지
elif core_warn:                          action = PROCEED      # 로그만, 색인은 진행 (아래 ②)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;핵심은 &lt;strong&gt;보안 위반(PII·기밀)만 색인을 막는 하드 블록(격리)&lt;/strong&gt; 이고, 나머지 품질 문제는 정도에 따라 REJECT/HOLD로 나뉜다는 것. "정확도"와 "위험도"를 같은 잣대로 다루지 않는다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;② 영리한 선택 — 경미한 경고(WARN)는 색인을 막지 않기로&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;처음엔 품질 경고(WARN)도 색인을 막았다. 그랬더니 문제가 생겼다 — 사소한 경고 하나 때문에 &lt;strong&gt;멀쩡한 문서가 0청크로 저장돼 검색에 안 잡히는&lt;/strong&gt; 일이 벌어졌다. 그래서 정책을 뒤집었다: &lt;strong&gt;Core WARN은 PROCEED(로그만 남기고 색인은 진행)&lt;/strong&gt;. 막아서 얻는 안전보다, 막아서 잃는 "있는데 안 나오는 지식"이 더 컸기 때문이다. 게이트의 목적은 "완벽한 것만 통과"가 아니라 "위험한 것만 차단"이었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class="imageblock alignCenter" data-ke-mobilestyle="widthOrigin" data-origin-width="1922" data-origin-height="476"&gt;&lt;span data-url="https://blog.kakaocdn.net/dn/bi1DCn/dJMcah6fcpN/1zSpgYuXkxA54w9Woshmk1/img.png" data-phocus="https://blog.kakaocdn.net/dn/bi1DCn/dJMcah6fcpN/1zSpgYuXkxA54w9Woshmk1/img.png"&gt;&lt;img src="https://blog.kakaocdn.net/dn/bi1DCn/dJMcah6fcpN/1zSpgYuXkxA54w9Woshmk1/img.png" srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbi1DCn%2FdJMcah6fcpN%2F1zSpgYuXkxA54w9Woshmk1%2Fimg.png" onerror="this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';" loading="lazy" width="1922" height="476" data-origin-width="1922" data-origin-height="476"&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;③ 숫자, 정직하게&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;여기도 자랑할 실측 %는 없다. 아래 ④의 사고를 고친 뒤 "0청크 → 정상 청크 + 한글 근거 복원"을 확인한 정성적 결과가 전부다. 없는 숫자는 안 만든다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;④ 우리가 처음엔 틀렸다 — chunks=0의 진범은 파일명 길이였다&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;대용량 재적재 중, 어떤 소스의 문서들이 &lt;strong&gt;인제스트했는데 청크가 0개&lt;/strong&gt;로 나왔다. 원인을 다섯 번 오진했다: PostgreSQL 갭 → S3 키 불일치 → 버킷 IAM 권한 → boto3 버전 → 인코딩(ASCII rekey). 다 아니었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class="imageblock alignCenter" data-ke-mobilestyle="widthOrigin" data-origin-width="1340" data-origin-height="848"&gt;&lt;span data-url="https://blog.kakaocdn.net/dn/cbIQZN/dJMcado6MIT/TjZvmVLlqX6mNmIuo9DkBK/img.png" data-phocus="https://blog.kakaocdn.net/dn/cbIQZN/dJMcado6MIT/TjZvmVLlqX6mNmIuo9DkBK/img.png"&gt;&lt;img src="https://blog.kakaocdn.net/dn/cbIQZN/dJMcado6MIT/TjZvmVLlqX6mNmIuo9DkBK/img.png" srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcbIQZN%2FdJMcado6MIT%2FTjZvmVLlqX6mNmIuo9DkBK%2Fimg.png" onerror="this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';" loading="lazy" width="604" height="382" data-origin-width="1340" data-origin-height="848"&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;진범은 어이없게도 &lt;strong&gt;파일명 길이&lt;/strong&gt;였다. 리눅스 파일시스템의 파일명 한계는 &lt;strong&gt;255바이트&lt;/strong&gt;인데, 그 소스의 한글 파일명은 인코딩하면 283바이트로 그 한계를 넘었다. 그래서 다운로드가 로컬에 저장되는 순간 &lt;code&gt;ENAMETOOLONG&lt;/code&gt;으로 실패했는데 — 이걸 넓은 &lt;code&gt;except: continue&lt;/code&gt;가 삼키면서 겉으로는 &lt;strong&gt;"S3 object not found"&lt;/strong&gt; 로 위장됐다. 있는 파일을, 없다고 보고하고 있었던 것이다.&lt;/p&gt;
&lt;p&gt;고친 방식: 다운로드 로컬 파일명을 원본 키 대신 &lt;strong&gt;짧은 해시(sha256 앞 16자)&lt;/strong&gt; 로 쓰고, 사용자에게 보일 제목(한글 citation)은 원본 키에서 따로 유도했다. 그리고 그 broad except의 마스킹을 걷어내 진짜 에러가 드러나게 했다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# S3 키 basename이 로컬 255바이트 한계를 넘으면 ENAMETOOLONG →
# 이전엔 "S3 object not found"로 위장됐다. 짧은 해시명으로 우회.
safe_name = hashlib.sha256(key.encode("utf-8")).hexdigest()[:16] + Path(key).suffix&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;교훈 두 개. &lt;strong&gt;하나의 증상(chunks=0)이 다섯 개의 서로 다른 원인을 가릴 수 있다.&lt;/strong&gt; 그리고 &lt;strong&gt;넓은 &lt;code&gt;except&lt;/code&gt;는 사고를 고치는 게 아니라 숨긴다&lt;/strong&gt; — 3탄에서 본 "같은 증상, 다른 원인"이 수집 파이프라인에서 다시 나타난 셈이다.&lt;/p&gt;
&lt;h2&gt;들이기 전에 고민한 것들 (체크리스트)&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;PII redaction&lt;/strong&gt; — 주민번호·카드·전화·이메일을 &lt;em&gt;들어오기 전에&lt;/em&gt; 스크럽한다. 표준 포맷뿐 아니라 &lt;em&gt;우회 케이스&lt;/em&gt;(하이픈 없는 번호, 공백 구분, 국제 포맷)까지. 한 번 새면 영속 저장소에 남는다.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;중복(dedup)&lt;/strong&gt; — 같은 문서가 여러 소스에 → 다단계 파이프라인으로 거른다(해시 → 근사 중복 → 의미 중복 → 충돌 탐지).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;신선도(freshness)&lt;/strong&gt; — 오래된 규정이 최신처럼 답하면 그게 사고다. 소스별 신선도 SLA를 두고, 늦으면 경보·재수집.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;권한 메타&lt;/strong&gt; — &lt;em&gt;수집 시점에&lt;/em&gt; 출입증(2탄의 tenant/kb/tier)을 박아야 한다. 검색 시점에 뒤늦게 권한을 붙일 수 없다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class="imageblock alignCenter" data-ke-mobilestyle="widthOrigin" data-origin-width="970" data-origin-height="1042"&gt;&lt;span data-url="https://blog.kakaocdn.net/dn/uAcGV/dJMcaa0gUtf/yKcamKZZdRVZNAzt5OJy51/img.png" data-phocus="https://blog.kakaocdn.net/dn/uAcGV/dJMcaa0gUtf/yKcamKZZdRVZNAzt5OJy51/img.png"&gt;&lt;img src="https://blog.kakaocdn.net/dn/uAcGV/dJMcaa0gUtf/yKcamKZZdRVZNAzt5OJy51/img.png" srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FuAcGV%2FdJMcaa0gUtf%2FyKcamKZZdRVZNAzt5OJy51%2Fimg.png" onerror="this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';" loading="lazy" width="460" height="494" data-origin-width="970" data-origin-height="1042"&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2&gt;한 줄로&lt;/h2&gt;
&lt;blockquote data-ke-style="style1"&gt;&lt;p data-ke-size="size16"&gt;&lt;span style="font-family: 'Noto Serif KR';"&gt;&lt;p&gt;좋은 답변은 좋은 검색에서, 좋은 검색은 좋은 저장에서, 좋은 저장은 — &lt;strong&gt;좋은 수집&lt;/strong&gt;에서 시작한다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;
&lt;h2&gt;시리즈를 마치며&lt;/h2&gt;
&lt;p&gt;저장(1) → 권한(2) → 대화(3) → 채널(4) → 그리고 그 모든 것의 출발점, 수집(번외). 한 바퀴를 돌았다.&lt;/p&gt;
&lt;p&gt;우리가 내내 붙잡은 원칙 하나로 닫는다: &lt;strong&gt;구조를 만드는 것은 시작이지 끝이 아니다.&lt;/strong&gt; "저장이 잘 됐나?"가 아니라 &lt;strong&gt;"각 사용자의 질문에 근거 있게 답했나?"&lt;/strong&gt; 를 — 실제 사용자가, 측정된 품질로, 운영 경로에서 증거와 함께 — 확인할 때, 비로소 완료다.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;시리즈&lt;/strong&gt;: &lt;a href="https://gsretail.tistory.com/89"&gt;1탄 저장&lt;/a&gt; · &lt;a href="https://gsretail.tistory.com/90"&gt;2탄 권한&lt;/a&gt; · &lt;a href="https://gsretail.tistory.com/92"&gt;3탄-1부 대화&lt;/a&gt; · &lt;a href="https://gsretail.tistory.com/93"&gt;3탄-2부 대화&lt;/a&gt; · &lt;a href="https://gsretail.tistory.com/94"&gt;4탄 채널&lt;/a&gt; · &lt;strong&gt;번외 수집 (이 글)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class="imageblock undefined" data-ke-mobilestyle="widthOrigin" data-filename="22787.jpg" data-origin-width="960" data-origin-height="960"&gt;&lt;span data-url="https://blog.kakaocdn.net/dn/02zUJ/btsMAMg3Wtb/7LuKJYntKKZdzNzAcaQAs0/tfile.dat" data-phocus="https://blog.kakaocdn.net/dn/02zUJ/btsMAMg3Wtb/7LuKJYntKKZdzNzAcaQAs0/tfile.dat"&gt;&lt;img src="https://blog.kakaocdn.net/dn/02zUJ/btsMAMg3Wtb/7LuKJYntKKZdzNzAcaQAs0/tfile.dat" srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F02zUJ%2FbtsMAMg3Wtb%2F7LuKJYntKKZdzNzAcaQAs0%2Ftfile.dat" onerror="this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';" loading="lazy" width="80" height="960" data-filename="22787.jpg" data-origin-width="960" data-origin-height="960"&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;김헌기 Darion&lt;/strong&gt; · AX본부 &amp;gt; AI데이터부문 &amp;gt; AI혁신지원팀&lt;/p&gt;
&lt;p&gt;AI 및 공통 Tech 기반의 기술 표준화 업무를 수행하고 있습니다. 동료들과 함께 기술 문화 만들기와 낯선 기술자와의 인연의 시작에 관심이 많습니다.&lt;/p&gt;
&lt;/body&gt;&lt;/html&gt;
</content>
    <id>https://gsretail.tistory.com/95</id>
    <link href="https://gsretail.tistory.com/95"/>
    <summary type="html">&lt;p&gt;&lt;figure class="imageblock alignCenter" data-ke-mobileStyle="widthOrigin" data-origin-width="1998" data-origin-height="882"&gt;&lt;span data-url="https://blog.kakaocdn.net/dn/tPH10/dJMcabSn8uU/4cDoq9AyMaWhWALMT6cVXK/img.png" data-phocus="https://blog.kakaocdn.net/dn/tPH10/dJMcabSn8uU/4cDoq9AyMaWhWALMT6cVXK/img.png"&gt;&lt;img src="https://blog.kakaocdn.net/dn/tPH10/dJMcabSn8uU/4cDoq9AyMaWhWALMT6cVXK/img.png" srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtPH10%2FdJMcabSn8uU%2F4cDoq9AyMaWhWALMT6cVXK%2Fimg.png" onerror="this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';" loading="lazy" width="1998" height="882" data-origin-width="1998" data-origin-height="882"/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2&gt;GIGO, 그런데 우리 건 차원이 다르다&lt;/h2&gt;
&lt;p&gt;RAG에는 오래된 격언이 있다. &lt;strong&gt;&amp;quot;쓰레기를 넣으면 쓰레기가 나온다(Garbage In, Garbage Out).&amp;quot;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;그런데 사내 지식은 — 쓰레기인지 아닌지조차 애매했다. 작년에 폐기된 규정인데 멀쩡해 보이는 문서, 부서마다 다르게 부르는 용어, 같은 내용이 세 군데에 흩어진 중복, 그리고 &lt;em&gt;절대 새어 나가면 안 되는 개인정보&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;1탄~4탄에서 검색·권한·대화·채널을 아무리 잘 만들어도, 들어온 지식이 엉망이면 답도 엉망이다. &lt;strong&gt;좋은 검색의 대부분은 검색 알고리즘이 아니라 _수집 단계_에서 결정된다.&lt;/strong&gt; 그리고 개인정보 한 건이 새어나가면 그건 &amp;quot;틀린 답변&amp;quot;이 아니라 &lt;strong&gt;보안 사고&lt;/strong&gt;다. 사내 지식은 &amp;quot;얼마나 정확한가&amp;quot;와 &amp;quot;얼마나 위험한가&amp;quot;를 동시에 조심해야 하는, 일반 GIGO보다 한 단계 무거운 문제였다.&lt;/p&gt;
&lt;h2&gt;Notion 하나 쓰는 회사 vs 20년 Excel·PPT 쓴 회사&lt;/h2&gt;
&lt;p&gt;다른 회사 이야기를 들으면 부러울 때가 있다. 처음부터 지식을 &lt;strong&gt;Notion 하나&lt;/strong&gt; 같은 단일 도구에 모아 쓰는 회사는, 도서관을 신축 설계도 한 장으로 짓는 것과 같다. 소스가 하나고, 게다가 &lt;strong&gt;Notion은 태생이 마크다운&lt;/strong&gt; — 텍스트 구조가 이미 깔끔해서 파싱이 거의 공짜다. 서가 규칙이 처음부터 하나다.&lt;/p&gt;
&lt;p&gt;우리는 달랐다. &lt;strong&gt;20년 넘게 Excel·PowerPoint·사내 게시판·메일로 지식을 쌓아온&lt;/strong&gt; 회사였다. 여기서 진짜 난이도는 &amp;quot;소스가 많다&amp;quot;가 아니라 &lt;strong&gt;&amp;quot;20년치 이질적 포맷&amp;quot;&lt;/strong&gt; 자체다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Excel: 표, 병합 셀, 여러 시트, 수식 결과값, 셀 안에 줄바꿈으로 욱여넣은 문단.&lt;/li&gt;
&lt;li&gt;PowerPoint: 텍스트가 도형·표·이미지 안에 흩어져 있고, 발표 흐름은 사람 머릿속에만 있다.&lt;/li&gt;
&lt;li&gt;게다가 &lt;strong&gt;표를 이미지로 캡처해 붙인&lt;/strong&gt; 문서도 흔하다 — 그 안의 글자는 텍스트가 아니라 그림이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Notion 회사가 &amp;quot;책을 그대로 서가에 꽂는&amp;quot; 일이라면, 우리는 &lt;strong&gt;책을 펼쳐 표·도형·이미지에서 글자를 발라내는&lt;/strong&gt; 일부터 해야 했다. 통째로 다시 지을 순 없었다 — 이미 그 안에 20년치 지식이 살고 있었으니까. 그래서 만든 건 &amp;quot;하나의 표준 서가&amp;quot;가 아니라, &lt;strong&gt;소스마다 맞춘 전용 손수레(connector)&lt;/strong&gt; 였다. Confluence, Jira, SharePoint, 게시판, Teams… 소스마다 다른 문·다른 포맷에 맞춰 각각의 connector를 만들고, 그 손수레들이 정해진 시간에 각 소스를 도는 &lt;strong&gt;순회 일정(scheduler)&lt;/strong&gt; 을 짰다.&lt;/p&gt;
&lt;p&gt;&lt;figure class="imageblock alignCenter" data-ke-mobileStyle="widthOrigin" data-origin-width="1950" data-origin-height="814"&gt;&lt;span data-url="https://blog.kakaocdn.net/dn/vwbZe/dJMcagGgKrS/OF6UrxYRbFInSbDrygTBHK/img.png" data-phocus="https://blog.kakaocdn.net/dn/vwbZe/dJMcagGgKrS/OF6UrxYRbFInSbDrygTBHK/img.png"&gt;&lt;img src="https://blog.kakaocdn.net/dn/vwbZe/dJMcagGgKrS/OF6UrxYRbFInSbDrygTBHK/img.png" srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvwbZe%2FdJMcagGgKrS%2FOF6UrxYRbFInSbDrygTBHK%2Fimg.png" onerror="this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';" loading="lazy" width="1950" height="814" data-origin-width="1950" data-origin-height="814"/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2&gt;검수대(Ingestion Gate)&lt;/h2&gt;
&lt;p&gt;포맷을 발라 텍스트를 얻었다고 끝이 아니다. 도서관에 책을 들이기 전, 검수대를 뒀다. 모든 문서는 색인되기 전에 이 게이트를 통과한다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[소스] → 파싱 → 한국어 전처리 → 청킹 → (검수대: ingestion gate)
                                          ├─ 품질: 너무 짧거나 깨진 문서?
                                          ├─ 보안: PII/기밀? → 격리(quarantine)
                                          └─ 메타: 권한 태깅(tenant/kb/tier/acl)
        → 중복 제거 → 임베딩 → 저장(Qdrant)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;figure class="imageblock alignCenter" data-ke-mobileStyle="widthOrigin" data-origin-width="1902" data-origin-height="824"&gt;&lt;span data-url="https://blog.kakaocdn.net/dn/MhVNC/dJMcadCzZPo/DLlkXGeCpUl4facDQMlkXk/img.png" data-phocus="https://blog.kakaocdn.net/dn/MhVNC/dJMcadCzZPo/DLlkXGeCpUl4facDQMlkXk/img.png"&gt;&lt;img src="https://blog.kakaocdn.net/dn/MhVNC/dJMcadCzZPo/DLlkXGeCpUl4facDQMlkXk/img.png" srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMhVNC%2FdJMcadCzZPo%2FDLlkXGeCpUl4facDQMlkXk%2Fimg.png" onerror="this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';" loading="lazy" width="1902" height="824" data-origin-width="1902" data-origin-height="824"/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2&gt;100G를 통째로 나를 수는 없다 — 분할과 델타&lt;/h2&gt;
&lt;p&gt;소스 하나가 워낙 커서(대용량), 손수레로 한 번에 다 나르려면 트럭 몇 대로도 부족했다. 두 가지를 했다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;분할(partition)&lt;/strong&gt;: 큰 자료 뭉치를 통째로 나르지 않고, 감당할 수 있는 단위로 쪼개서 나른다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;델타(delta)&lt;/strong&gt;: 매번 소스 전체를 다시 뒤지지 않는다. fingerprint로 &lt;strong&gt;지난번과 달라진 부분만&lt;/strong&gt; 골라 옮긴다 — 안 그러면 순회할 때마다 도서관 하나를 통째로 다시 옮기는 셈이 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class="imageblock alignCenter" data-ke-mobileStyle="widthOrigin" data-origin-width="2000" data-origin-height="946"&gt;&lt;span data-url="https://blog.kakaocdn.net/dn/2g2ax/dJMcahyl6cy/Winvcd2hEJexgLJ6lzoObk/img.png" data-phocus="https://blog.kakaocdn.net/dn/2g2ax/dJMcahyl6cy/Winvcd2hEJexgLJ6lzoObk/img.png"&gt;&lt;img src="https://blog.kakaocdn.net/dn/2g2ax/dJMcahyl6cy/Winvcd2hEJexgLJ6lzoObk/img.png" srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F2g2ax%2FdJMcahyl6cy%2FWinvcd2hEJexgLJ6lzoObk%2Fimg.png" onerror="this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';" loading="lazy" width="2000" height="946" data-origin-width="2000" data-origin-height="946"/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;여러 소스를 돌다 보면 같은 문서가 이름만 바꾼 채 여러 곳에 꽂혀 있는 것도 발견한다 — 그대로 다 꽂으면 사용자는 같은 답을 세 번 받거나, 서로 다른 버전의 답을 받는다. 그래서 서가에 꽂기 전, 마지막으로 한 번 더 골라내는 절차(중복 제거)를 거친다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;수집 스택:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;소스 connector : Confluence / Jira / SharePoint / 사내 게시판 / Teams — 소스별 어댑터
포맷 파싱      : Excel / PPT / PDF / 문서 — 표·도형·이미지에서 텍스트 추출
증분 크롤      : fingerprint 기반 변경분만 재수집
워크플로       : Temporal(스케줄·재시도·크롤 상태 추적)
PII 스크럽     : 정규 포맷 + 우회 케이스(하이픈 없는 번호 등) 규칙 엔진
중복 제거      : 해시 → 근사중복 → 의미중복 다단계 파이프라인
임베딩/저장    : 청킹 → 임베딩(OpenAI) → Qdrant(1탄의 통합 컬렉션, 권한 태그 포함)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;figure class="imageblock alignCenter" data-ke-mobileStyle="widthOrigin" data-origin-width="1900" data-origin-height="836"&gt;&lt;span data-url="https://blog.kakaocdn.net/dn/bOhW32/dJMcag0rp1w/xIzHPKYI7OfSQCcH1BUhvk/img.png" data-phocus="https://blog.kakaocdn.net/dn/bOhW32/dJMcag0rp1w/xIzHPKYI7OfSQCcH1BUhvk/img.png"&gt;&lt;img src="https://blog.kakaocdn.net/dn/bOhW32/dJMcag0rp1w/xIzHPKYI7OfSQCcH1BUhvk/img.png" srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbOhW32%2FdJMcag0rp1w%2FxIzHPKYI7OfSQCcH1BUhvk%2Fimg.png" onerror="this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';" loading="lazy" width="1900" height="836" data-origin-width="1900" data-origin-height="836"/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2&gt;심화 — 검수대는 무엇을 막고, 우리는 어디서 데였나&lt;/h2&gt;
&lt;blockquote data-ke-style="style1"&gt;&lt;p data-ke-size="size16"&gt;&lt;span style="font-family: 'Noto Serif KR';"&gt;&lt;p&gt;이 절은 깊다. 검수대가 실제로 어떻게 판정하고, 대용량 수집이 어디서 터졌는지 궁금한 분을 위한 detail이다. 바쁘면 체크리스트로 건너뛰어도 좋다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;&lt;strong&gt;① 검수대 판정 — 무엇을 막고, 무엇을 통과시키나&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;게이트는 결과를 네 갈래로 나눈다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if verdict in {보안 위반: PII·기밀}:     action = QUARANTINE   # 색인 안 함, 격리
elif core_fail &amp;gt;= 2:                     action = REJECT       # 핵심 품질 다중 실패
elif core_fail == 1:                     action = HOLD         # 재작업 여지
elif core_warn:                          action = PROCEED      # 로그만, 색인은 진행 (아래 ②)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;핵심은 &lt;strong&gt;보안 위반(PII·기밀)만 색인을 막는 하드 블록(격리)&lt;/strong&gt; 이고, 나머지 품질 문제는 정도에 따라 REJECT/HOLD로 나뉜다는 것. &amp;quot;정확도&amp;quot;와 &amp;quot;위험도&amp;quot;를 같은 잣대로 다루지 않는다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;② 영리한 선택 — 경미한 경고(WARN)는 색인을 막지 않기로&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;처음엔 품질 경고(WARN)도 색인을 막았다. 그랬더니 문제가 생겼다 — 사소한 경고 하나 때문에 &lt;strong&gt;멀쩡한 문서가 0청크로 저장돼 검색에 안 잡히는&lt;/strong&gt; 일이 벌어졌다. 그래서 정책을 뒤집었다: &lt;strong&gt;Core WARN은 PROCEED(로그만 남기고 색인은 진행)&lt;/strong&gt;. 막아서 얻는 안전보다, 막아서 잃는 &amp;quot;있는데 안 나오는 지식&amp;quot;이 더 컸기 때문이다. 게이트의 목적은 &amp;quot;완벽한 것만 통과&amp;quot;가 아니라 &amp;quot;위험한 것만 차단&amp;quot;이었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class="imageblock alignCenter" data-ke-mobileStyle="widthOrigin" data-origin-width="1922" data-origin-height="476"&gt;&lt;span data-url="https://blog.kakaocdn.net/dn/bi1DCn/dJMcah6fcpN/1zSpgYuXkxA54w9Woshmk1/img.png" data-phocus="https://blog.kakaocdn.net/dn/bi1DCn/dJMcah6fcpN/1zSpgYuXkxA54w9Woshmk1/img.png"&gt;&lt;img src="https://blog.kakaocdn.net/dn/bi1DCn/dJMcah6fcpN/1zSpgYuXkxA54w9Woshmk1/img.png" srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbi1DCn%2FdJMcah6fcpN%2F1zSpgYuXkxA54w9Woshmk1%2Fimg.png" onerror="this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';" loading="lazy" width="1922" height="476" data-origin-width="1922" data-origin-height="476"/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;③ 숫자, 정직하게&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;여기도 자랑할 실측 %는 없다. 아래 ④의 사고를 고친 뒤 &amp;quot;0청크 → 정상 청크 + 한글 근거 복원&amp;quot;을 확인한 정성적 결과가 전부다. 없는 숫자는 안 만든다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;④ 우리가 처음엔 틀렸다 — chunks=0의 진범은 파일명 길이였다&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;대용량 재적재 중, 어떤 소스의 문서들이 &lt;strong&gt;인제스트했는데 청크가 0개&lt;/strong&gt;로 나왔다. 원인을 다섯 번 오진했다: PostgreSQL 갭 → S3 키 불일치 → 버킷 IAM 권한 → boto3 버전 → 인코딩(ASCII rekey). 다 아니었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class="imageblock alignCenter" data-ke-mobileStyle="widthOrigin" data-origin-width="1340" data-origin-height="848"&gt;&lt;span data-url="https://blog.kakaocdn.net/dn/cbIQZN/dJMcado6MIT/TjZvmVLlqX6mNmIuo9DkBK/img.png" data-phocus="https://blog.kakaocdn.net/dn/cbIQZN/dJMcado6MIT/TjZvmVLlqX6mNmIuo9DkBK/img.png"&gt;&lt;img src="https://blog.kakaocdn.net/dn/cbIQZN/dJMcado6MIT/TjZvmVLlqX6mNmIuo9DkBK/img.png" srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcbIQZN%2FdJMcado6MIT%2FTjZvmVLlqX6mNmIuo9DkBK%2Fimg.png" onerror="this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';" loading="lazy" width="604" height="382" data-origin-width="1340" data-origin-height="848"/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;진범은 어이없게도 &lt;strong&gt;파일명 길이&lt;/strong&gt;였다. 리눅스 파일시스템의 파일명 한계는 &lt;strong&gt;255바이트&lt;/strong&gt;인데, 그 소스의 한글 파일명은 인코딩하면 283바이트로 그 한계를 넘었다. 그래서 다운로드가 로컬에 저장되는 순간 &lt;code&gt;ENAMETOOLONG&lt;/code&gt;으로 실패했는데 — 이걸 넓은 &lt;code&gt;except: continue&lt;/code&gt;가 삼키면서 겉으로는 &lt;strong&gt;&amp;quot;S3 object not found&amp;quot;&lt;/strong&gt; 로 위장됐다. 있는 파일을, 없다고 보고하고 있었던 것이다.&lt;/p&gt;
&lt;p&gt;고친 방식: 다운로드 로컬 파일명을 원본 키 대신 &lt;strong&gt;짧은 해시(sha256 앞 16자)&lt;/strong&gt; 로 쓰고, 사용자에게 보일 제목(한글 citation)은 원본 키에서 따로 유도했다. 그리고 그 broad except의 마스킹을 걷어내 진짜 에러가 드러나게 했다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# S3 키 basename이 로컬 255바이트 한계를 넘으면 ENAMETOOLONG →
# 이전엔 &amp;quot;S3 object not found&amp;quot;로 위장됐다. 짧은 해시명으로 우회.
safe_name = hashlib.sha256(key.encode(&amp;quot;utf-8&amp;quot;)).hexdigest()[:16] + Path(key).suffix&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;교훈 두 개. &lt;strong&gt;하나의 증상(chunks=0)이 다섯 개의 서로 다른 원인을 가릴 수 있다.&lt;/strong&gt; 그리고 &lt;strong&gt;넓은 &lt;code&gt;except&lt;/code&gt;는 사고를 고치는 게 아니라 숨긴다&lt;/strong&gt; — 3탄에서 본 &amp;quot;같은 증상, 다른 원인&amp;quot;이 수집 파이프라인에서 다시 나타난 셈이다.&lt;/p&gt;
&lt;h2&gt;들이기 전에 고민한 것들 (체크리스트)&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;PII redaction&lt;/strong&gt; — 주민번호·카드·전화·이메일을 &lt;em&gt;들어오기 전에&lt;/em&gt; 스크럽한다. 표준 포맷뿐 아니라 &lt;em&gt;우회 케이스&lt;/em&gt;(하이픈 없는 번호, 공백 구분, 국제 포맷)까지. 한 번 새면 영속 저장소에 남는다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;중복(dedup)&lt;/strong&gt; — 같은 문서가 여러 소스에 → 다단계 파이프라인으로 거른다(해시 → 근사 중복 → 의미 중복 → 충돌 탐지).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;신선도(freshness)&lt;/strong&gt; — 오래된 규정이 최신처럼 답하면 그게 사고다. 소스별 신선도 SLA를 두고, 늦으면 경보·재수집.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;권한 메타&lt;/strong&gt; — &lt;em&gt;수집 시점에&lt;/em&gt; 출입증(2탄의 tenant/kb/tier)을 박아야 한다. 검색 시점에 뒤늦게 권한을 붙일 수 없다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class="imageblock alignCenter" data-ke-mobileStyle="widthOrigin" data-origin-width="970" data-origin-height="1042"&gt;&lt;span data-url="https://blog.kakaocdn.net/dn/uAcGV/dJMcaa0gUtf/yKcamKZZdRVZNAzt5OJy51/img.png" data-phocus="https://blog.kakaocdn.net/dn/uAcGV/dJMcaa0gUtf/yKcamKZZdRVZNAzt5OJy51/img.png"&gt;&lt;img src="https://blog.kakaocdn.net/dn/uAcGV/dJMcaa0gUtf/yKcamKZZdRVZNAzt5OJy51/img.png" srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FuAcGV%2FdJMcaa0gUtf%2FyKcamKZZdRVZNAzt5OJy51%2Fimg.png" onerror="this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';" loading="lazy" width="460" height="494" data-origin-width="970" data-origin-height="1042"/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2&gt;한 줄로&lt;/h2&gt;
&lt;blockquote data-ke-style="style1"&gt;&lt;p data-ke-size="size16"&gt;&lt;span style="font-family: 'Noto Serif KR';"&gt;&lt;p&gt;좋은 답변은 좋은 검색에서, 좋은 검색은 좋은 저장에서, 좋은 저장은 — &lt;strong&gt;좋은 수집&lt;/strong&gt;에서 시작한다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h2&gt;시리즈를 마치며&lt;/h2&gt;
&lt;p&gt;저장(1) → 권한(2) → 대화(3) → 채널(4) → 그리고 그 모든 것의 출발점, 수집(번외). 한 바퀴를 돌았다.&lt;/p&gt;
&lt;p&gt;우리가 내내 붙잡은 원칙 하나로 닫는다: &lt;strong&gt;구조를 만드는 것은 시작이지 끝이 아니다.&lt;/strong&gt; &amp;quot;저장이 잘 됐나?&amp;quot;가 아니라 &lt;strong&gt;&amp;quot;각 사용자의 질문에 근거 있게 답했나?&amp;quot;&lt;/strong&gt; 를 — 실제 사용자가, 측정된 품질로, 운영 경로에서 증거와 함께 — 확인할 때, 비로소 완료다.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;시리즈&lt;/strong&gt;: &lt;a href="https://gsretail.tistory.com/89"&gt;1탄 저장&lt;/a&gt; · &lt;a href="https://gsretail.tistory.com/90"&gt;2탄 권한&lt;/a&gt; · &lt;a href="https://gsretail.tistory.com/92"&gt;3탄-1부 대화&lt;/a&gt; · &lt;a href="https://gsretail.tistory.com/93"&gt;3탄-2부 대화&lt;/a&gt; · &lt;a href="https://gsretail.tistory.com/94"&gt;4탄 채널&lt;/a&gt; · &lt;strong&gt;번외 수집 (이 글)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class="imageblock undefined" data-ke-mobileStyle="widthOrigin" data-filename="22787.jpg" data-origin-width="960" data-origin-height="960"&gt;&lt;span data-url="https://blog.kakaocdn.net/dn/02zUJ/btsMAMg3Wtb/7LuKJYntKKZdzNzAcaQAs0/tfile.dat" data-phocus="https://blog.kakaocdn.net/dn/02zUJ/btsMAMg3Wtb/7LuKJYntKKZdzNzAcaQAs0/tfile.dat"&gt;&lt;img src="https://blog.kakaocdn.net/dn/02zUJ/btsMAMg3Wtb/7LuKJYntKKZdzNzAcaQAs0/tfile.dat" srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F02zUJ%2FbtsMAMg3Wtb%2F7LuKJYntKKZdzNzAcaQAs0%2Ftfile.dat" onerror="this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';" loading="lazy" width="80" height="960" data-filename="22787.jpg" data-origin-width="960" data-origin-height="960"/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;김헌기 Darion&lt;/strong&gt; · AX본부 &amp;gt; AI데이터부문 &amp;gt; AI혁신지원팀&lt;/p&gt;
&lt;p&gt;AI 및 공통 Tech 기반의 기술 표준화 업무를 수행하고 있습니다. 동료들과 함께 기술 문화 만들기와 낯선 기술자와의 인연의 시작에 관심이 많습니다.&lt;/p&gt;</summary>
    <title>[사내 지식 AI 만들기 ⑤&amp;middot;수집] 좋은 답변의 90%는 검색 전에 결정된다 &amp;mdash; Notion 회사와는 차원이 다른 20년치 수집</title>
    <updated>2026-07-12T09:59:58+09:00</updated>
    <dc:date>2026-07-12T09:59:58+09:00</dc:date>
  </entry>
  <entry>
    <author>
      <name>DarionKim</name>
    </author>
    <content type="html">&lt;!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd"&gt;
&lt;html&gt;&lt;body&gt;
&lt;h2&gt;우리가 만든 앱, 그리고 폐기한 앱&lt;/h2&gt;
&lt;p&gt;솔직히 고백하면 — 우리는 모바일 앱을 만들었다. 포털도 만들었다. 그리고 &lt;strong&gt;폐기했다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;이유는 단순하고 아팠다.&lt;/p&gt;
&lt;blockquote data-ke-style="style1"&gt;&lt;p data-ke-size="size16"&gt;&lt;span style="font-family: 'Noto Serif KR';"&gt;&lt;p&gt;사용자는 우리 앱에 안 들어왔다. 이미 &lt;strong&gt;Teams에 살고 있었다.&lt;/strong&gt;&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;좋은 지식 AI를 만드는 것과, 사용자가 그걸 쓰게 만드는 것은 완전히 다른 일이었다. 멋진 새 집(앱)을 지었는데, 사용자는 이사 올 생각이 없었다. 하루 종일 일하는 곳은 워크스페이스(Teams, Google Chat)였으니까.&lt;/p&gt;
&lt;h2&gt;채널은 글로벌 업체를 못 이긴다 — 그래서 Thin 플랫폼&lt;/h2&gt;
&lt;p&gt;방향을 틀면서 우리가 인정한 게 하나 있다. &lt;strong&gt;채널 그 자체(메신저 UX, 알림, 첨부, 카드 렌더링…)는 글로벌 업체가 수십 년 투자한 영역이고, 우리가 그 수준으로 만들 수 없다.&lt;/strong&gt; Teams를, Google Chat을 우리가 다시 만들 수는 없다.&lt;/p&gt;
&lt;p&gt;그리고 하나에 &lt;strong&gt;락인(lock-in)&lt;/strong&gt; 되는 것도 위험했다. 한 플랫폼에 올인하면 그 플랫폼 정책 하나에 서비스가 흔들린다.&lt;/p&gt;
&lt;p&gt;그래서 택한 게 &lt;strong&gt;Thin 플랫폼&lt;/strong&gt;이다 — 채널을 _소유_하지 않는다. 대신 각 채널에 얇게 연결만 하고, 무거운 지능(검색·권한·근거·rerank)은 우리 쪽 공통 substrate에 둔다. 채널은 갈아 끼울 수 있는 얇은 어댑터가 되고, 우리는 벤더와 경쟁하는 대신 그 위에 올라탄다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;        [디지털 동료]  (1~3탄: 저장 · 권한 · 대화)
             │  공통 substrate (검색/권한/rerank/근거)   ← 무겁게, 우리가 소유
   ┌─────────┼──────────┬───────────┐
 Teams봇   Google Chat   Outlook    A2A(에이전트 연동)    ← 얇게, 갈아끼움&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class="imageblock alignCenter" data-ke-mobilestyle="widthOrigin" data-origin-width="912" data-origin-height="882"&gt;&lt;span data-url="https://blog.kakaocdn.net/dn/d3QtVn/dJMcahZnk81/Gpt3Jl0uBFj7tFr1L1L5w0/img.png" data-phocus="https://blog.kakaocdn.net/dn/d3QtVn/dJMcahZnk81/Gpt3Jl0uBFj7tFr1L1L5w0/img.png"&gt;&lt;img src="https://blog.kakaocdn.net/dn/d3QtVn/dJMcahZnk81/Gpt3Jl0uBFj7tFr1L1L5w0/img.png" srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd3QtVn%2FdJMcahZnk81%2FGpt3Jl0uBFj7tFr1L1L5w0%2Fimg.png" onerror="this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';" loading="lazy" width="450" height="435" data-origin-width="912" data-origin-height="882"&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2&gt;디지털 동료 하나 → 채널 N개&lt;/h2&gt;
&lt;p&gt;Thin 플랫폼의 보상은 여기서 나온다. 신원 확인과 UX 포장을 채널별로 한 번씩만 풀어두면, 그 다음부터는 —&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;디지털 동료 하나를 추가하면&lt;/strong&gt; 이미 연결된 모든 채널에 동시에 나타난다.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;채널 하나를 연결하면&lt;/strong&gt; 이미 있는 모든 동료가 그 채널에서 한 번에 쓸 수 있게 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;동료 M명 × 채널 N개를 M×N번 따로 만드는 게 아니라, &lt;strong&gt;M + N번만 만들면 되는 구조&lt;/strong&gt;다. 이게 "직접 UI를 안 만든다"가 단순 비용 절감을 넘어 _구조적 레버리지_가 되는 지점이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class="imageblock alignCenter" data-ke-mobilestyle="widthOrigin" data-origin-width="1908" data-origin-height="906"&gt;&lt;span data-url="https://blog.kakaocdn.net/dn/Sfo7F/dJMcaf8iwfg/73yhF3Vyd9lPDQuZFyTZrk/img.png" data-phocus="https://blog.kakaocdn.net/dn/Sfo7F/dJMcaf8iwfg/73yhF3Vyd9lPDQuZFyTZrk/img.png"&gt;&lt;img src="https://blog.kakaocdn.net/dn/Sfo7F/dJMcaf8iwfg/73yhF3Vyd9lPDQuZFyTZrk/img.png" srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FSfo7F%2FdJMcaf8iwfg%2F73yhF3Vyd9lPDQuZFyTZrk%2Fimg.png" onerror="this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';" loading="lazy" width="1908" height="906" data-origin-width="1908" data-origin-height="906"&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2&gt;같은 답, 다른 얼굴&lt;/h2&gt;
&lt;p&gt;인증만 채널마다 다른 게 아니었다. 같은 질문에 같은 근거로 답해도, 그걸 _보여주는 방식_은 채널마다 달라야 했다. Teams에선 풍부한 카드(Adaptive Card)로, Google Chat에선 그 채널의 카드 포맷으로, 사람이 보지 않는 에이전트 연동 채널에선 애초에 "보여줄" 필요 없이 구조화된 데이터로 — 같은 답을 그 채널이 이해하는 언어로 다시 포장한다.&lt;/p&gt;
&lt;p&gt;여기서 데인 교훈 하나: &lt;strong&gt;pod에서 함수로 찍어 본 게 사용자 화면이 아니다.&lt;/strong&gt; 이건 그냥 넘어갈 얘기가 아니라 심화에서 따로 다룬다.&lt;/p&gt;
&lt;h2&gt;직접 UI를 안 만든다는 원칙&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;모바일·포털은 폐기. &lt;strong&gt;워크스페이스 플랫폼 + 채널 + 디지털 동료 연결&lt;/strong&gt;로 통합.&lt;/li&gt;
&lt;li&gt;새 기능 = 새 화면을 또 만드는 게 아니라, 기존 채널에 동료의 능력을 더하는 것.&lt;/li&gt;
&lt;li&gt;운영 작업(관리자)은 별도 UI 대신 CLI·슬래시 명령으로.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;우리는 _프론트엔드를 다시 만드는 일_을 줄이고, _동료의 능력_에 집중할 수 있게 됐다.&lt;/p&gt;
&lt;p&gt;&lt;figure class="imageblock alignCenter" data-ke-mobilestyle="widthOrigin" data-origin-width="2050" data-origin-height="688"&gt;&lt;span data-url="https://blog.kakaocdn.net/dn/bnF7Ld/dJMcagGgKdR/Z8l8aK1kMenpqpukNFMKT0/img.png" data-phocus="https://blog.kakaocdn.net/dn/bnF7Ld/dJMcagGgKdR/Z8l8aK1kMenpqpukNFMKT0/img.png"&gt;&lt;img src="https://blog.kakaocdn.net/dn/bnF7Ld/dJMcagGgKdR/Z8l8aK1kMenpqpukNFMKT0/img.png" srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbnF7Ld%2FdJMcagGgKdR%2FZ8l8aK1kMenpqpukNFMKT0%2Fimg.png" onerror="this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';" loading="lazy" width="2050" height="688" data-origin-width="2050" data-origin-height="688"&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2&gt;심화 — 채널마다 다른 인증, 그리고 렌더링은 화면에서 검증한다&lt;/h2&gt;
&lt;blockquote data-ke-style="style1"&gt;&lt;p data-ke-size="size16"&gt;&lt;span style="font-family: 'Noto Serif KR';"&gt;&lt;p&gt;이 절은 깊다. 채널을 얇게 연결한다는 게 코드에서 뭘 뜻하는지 궁금한 분을 위한 detail이다. 바쁘면 맺음으로 건너뛰어도 좋다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;① 같은 "누구인지" 확인인데, grant 타입이 다르다&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;채널을 하나 늘릴 때마다 가장 먼저 부딪힌 건 신원 확인 방식이 제각각이라는 점이었다. 두 개만 나란히 놓아 보자.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Google Chat — 서비스 신원(OIDC) 검증.&lt;/strong&gt; Google이 발급한 OIDC id_token을 우리 쪽 verifier가 검증한다(정해진 발신자 allowlist인 &lt;code&gt;service_account&lt;/code&gt; 모드, 또는 도메인 정책상 허용된 사용자 이메일 토큰). 핵심은 &lt;strong&gt;"이 요청이 신뢰된 발신자(서비스)에서 왔는가"&lt;/strong&gt; 를 토큰으로 확인하는, machine-to-machine에 가까운 모델이다.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;에이전트 연동(A2A) — 위임 토큰 교환(OBO).&lt;/strong&gt; 여기선 사람이 채널에 로그인하는 게 아니라, &lt;strong&gt;에이전트가 사용자를 대신해(on-behalf-of)&lt;/strong&gt; 통신한다. 사용자 동의로 얻은 토큰을 downstream 토큰으로 &lt;strong&gt;교환(On-Behalf-Of)&lt;/strong&gt; 해서, 그 위임 신원으로 다음 시스템에 접근한다. authorization-code에서 파생된 &lt;strong&gt;위임(delegated)&lt;/strong&gt; 모델이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;같은 "이 사용자가 누구인지 확인한다"인데, 한쪽은 _서비스 신원을 검증_하고 다른 쪽은 _사용자 위임을 교환_한다 — grant 타입 자체가 다르다. 그래서 채널 하나 = 인증 어댑터 하나였다. (이 위임 교환은 실제로 한 번 크게 데였다 — &lt;code&gt;invalid_grant&lt;/code&gt;가 무더기로 나 별도 원인분석을 했다.) 각 채널의 인증을 2탄의 출입증(payload 권한)에 다시 맞물리는 게 Thin 플랫폼의 진짜 일이었다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;② 영리한 선택 — 채널을 소유하지 않는다&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;기각한 대안은 명확했다: 자체 채널·앱을 만들어 그 안에서 모든 걸 통제하기(모바일·포털이 그거였고, 폐기했다). Thin 플랫폼은 그 반대다 — 채널 UX는 벤더에게 맡기고, 우리는 그 위에 얇은 어댑터로만 붙는다. 잃는 것: 채널 화면을 100% 우리 맘대로 못 함. 얻는 것: 벤더와 경쟁 안 함 + 락인 회피 + M+N 레버리지.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;③ 숫자, 정직하게&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;여기엔 자랑할 하드 넘버가 없다. M+N의 이득은 "채널·동료를 늘려도 연결 작업이 곱이 아니라 합으로 는다"는 구조적 사실이지 측정된 %가 아니다. 없는 숫자는 안 만든다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;④ 우리가 처음엔 틀렸다 — pod에서 찍은 건 사용자 화면이 아니다&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;가장 아팠던 채널 사고는 렌더링이었다. Teams 카드가 &lt;strong&gt;단일 줄바꿈을 공백으로 뭉개&lt;/strong&gt; 잘 정리한 답이 &lt;strong&gt;한 덩어리 벽처럼&lt;/strong&gt; 나온 적이 있다. 또 한 번은 카드를 감싸던 내부 호환 마커(&lt;code&gt;__ADAPTIVE_CARD__&lt;/code&gt;)가 안 벗겨져 &lt;strong&gt;raw JSON이 그대로 사용자에게 노출&lt;/strong&gt;된 적도 있다. 둘 다 pod 안에서 함수를 호출해 찍어 보면 멀쩡했다 — &lt;strong&gt;"함수 출력이 맞다"와 "그 채널에서 실제로 그렇게 보인다"는 다른 일&lt;/strong&gt;이었으니까.&lt;/p&gt;
&lt;p&gt;그래서 렌더링을 화면 기준으로 강제하는 4겹을 깔았다: commit 훅(원시 마커 누출 차단) · CI(카드 골든 렌더 테스트) · 런타임(발신 직전 outbound guard) · Datadog 모니터(마커 누출·렌더 실패율). 채널을 얇게 연결한다는 건, 역설적으로 &lt;strong&gt;각 채널의 화면을 더 깐깐하게 검증한다&lt;/strong&gt;는 뜻이었다. (사실 문서를 한 도구에서 다른 도구로 옮길 때마다 겪는 일이다 — 원본에선 멀쩡한데 옮긴 화면에선 깨지는. 규모만 다를 뿐 같은 교훈이다.)&lt;/p&gt;
&lt;p&gt;&lt;figure class="imageblock alignCenter" data-ke-mobilestyle="widthOrigin" data-origin-width="2002" data-origin-height="836"&gt;&lt;span data-url="https://blog.kakaocdn.net/dn/bHfJzw/dJMcab5XV22/ZWTEslmnkVEnr5dq75hZG0/img.png" data-phocus="https://blog.kakaocdn.net/dn/bHfJzw/dJMcab5XV22/ZWTEslmnkVEnr5dq75hZG0/img.png"&gt;&lt;img src="https://blog.kakaocdn.net/dn/bHfJzw/dJMcab5XV22/ZWTEslmnkVEnr5dq75hZG0/img.png" srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbHfJzw%2FdJMcab5XV22%2FZWTEslmnkVEnr5dq75hZG0%2Fimg.png" onerror="this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';" loading="lazy" width="2002" height="836" data-origin-width="2002" data-origin-height="836"&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2&gt;맺음 — 그런데 좋은 지식이 없으면 다 소용없다&lt;/h2&gt;
&lt;p&gt;저장(1) → 권한(2) → 대화(3) → 채널(4). 이제 사용자는 자기 자리에서, 권한 안에서, 근거 있는 답을 받는다.&lt;/p&gt;
&lt;p&gt;그런데 이 모든 건 _좋은 지식이 들어와 있을 때_만 의미가 있다. 쓰레기가 들어가면 쓰레기가 나온다. 그 지식을 _어떻게 모으고, 들이기 전에 무엇을 검수했는지_는 — &lt;strong&gt;&lt;a href="https://gsretail.tistory.com/95"&gt;번외편 — 좋은 답변의 90%는 검색 전에 결정된다&lt;/a&gt;&lt;/strong&gt;에서.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;시리즈&lt;/strong&gt;: &lt;a href="https://gsretail.tistory.com/89"&gt;1탄 저장&lt;/a&gt; · &lt;a href="https://gsretail.tistory.com/90"&gt;2탄 권한&lt;/a&gt; · &lt;a href="https://gsretail.tistory.com/92"&gt;3탄-1부 대화&lt;/a&gt; · &lt;a href="https://gsretail.tistory.com/93"&gt;3탄-2부 대화&lt;/a&gt; · &lt;strong&gt;4탄 채널 (이 글)&lt;/strong&gt; · &lt;a href="https://gsretail.tistory.com/95"&gt;번외 수집&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class="imageblock undefined" data-ke-mobilestyle="widthOrigin" data-filename="22787.jpg" data-origin-width="960" data-origin-height="960"&gt;&lt;span data-url="https://blog.kakaocdn.net/dn/02zUJ/btsMAMg3Wtb/7LuKJYntKKZdzNzAcaQAs0/tfile.dat" data-phocus="https://blog.kakaocdn.net/dn/02zUJ/btsMAMg3Wtb/7LuKJYntKKZdzNzAcaQAs0/tfile.dat"&gt;&lt;img src="https://blog.kakaocdn.net/dn/02zUJ/btsMAMg3Wtb/7LuKJYntKKZdzNzAcaQAs0/tfile.dat" srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F02zUJ%2FbtsMAMg3Wtb%2F7LuKJYntKKZdzNzAcaQAs0%2Ftfile.dat" onerror="this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';" loading="lazy" width="80" height="960" data-filename="22787.jpg" data-origin-width="960" data-origin-height="960"&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;김헌기 Darion&lt;/strong&gt; · AX본부 &amp;gt; AI데이터부문 &amp;gt; AI혁신지원팀&lt;/p&gt;
&lt;p&gt;AI 및 공통 Tech 기반의 기술 표준화 업무를 수행하고 있습니다. 동료들과 함께 기술 문화 만들기와 낯선 기술자와의 인연의 시작에 관심이 많습니다.&lt;/p&gt;
&lt;/body&gt;&lt;/html&gt;
</content>
    <id>https://gsretail.tistory.com/94</id>
    <link href="https://gsretail.tistory.com/94"/>
    <summary type="html">&lt;h2&gt;우리가 만든 앱, 그리고 폐기한 앱&lt;/h2&gt;
&lt;p&gt;솔직히 고백하면 — 우리는 모바일 앱을 만들었다. 포털도 만들었다. 그리고 &lt;strong&gt;폐기했다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;이유는 단순하고 아팠다.&lt;/p&gt;
&lt;blockquote data-ke-style="style1"&gt;&lt;p data-ke-size="size16"&gt;&lt;span style="font-family: 'Noto Serif KR';"&gt;&lt;p&gt;사용자는 우리 앱에 안 들어왔다. 이미 &lt;strong&gt;Teams에 살고 있었다.&lt;/strong&gt;&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;좋은 지식 AI를 만드는 것과, 사용자가 그걸 쓰게 만드는 것은 완전히 다른 일이었다. 멋진 새 집(앱)을 지었는데, 사용자는 이사 올 생각이 없었다. 하루 종일 일하는 곳은 워크스페이스(Teams, Google Chat)였으니까.&lt;/p&gt;
&lt;h2&gt;채널은 글로벌 업체를 못 이긴다 — 그래서 Thin 플랫폼&lt;/h2&gt;
&lt;p&gt;방향을 틀면서 우리가 인정한 게 하나 있다. &lt;strong&gt;채널 그 자체(메신저 UX, 알림, 첨부, 카드 렌더링…)는 글로벌 업체가 수십 년 투자한 영역이고, 우리가 그 수준으로 만들 수 없다.&lt;/strong&gt; Teams를, Google Chat을 우리가 다시 만들 수는 없다.&lt;/p&gt;
&lt;p&gt;그리고 하나에 &lt;strong&gt;락인(lock-in)&lt;/strong&gt; 되는 것도 위험했다. 한 플랫폼에 올인하면 그 플랫폼 정책 하나에 서비스가 흔들린다.&lt;/p&gt;
&lt;p&gt;그래서 택한 게 &lt;strong&gt;Thin 플랫폼&lt;/strong&gt;이다 — 채널을 _소유_하지 않는다. 대신 각 채널에 얇게 연결만 하고, 무거운 지능(검색·권한·근거·rerank)은 우리 쪽 공통 substrate에 둔다. 채널은 갈아 끼울 수 있는 얇은 어댑터가 되고, 우리는 벤더와 경쟁하는 대신 그 위에 올라탄다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;        [디지털 동료]  (1~3탄: 저장 · 권한 · 대화)
             │  공통 substrate (검색/권한/rerank/근거)   ← 무겁게, 우리가 소유
   ┌─────────┼──────────┬───────────┐
 Teams봇   Google Chat   Outlook    A2A(에이전트 연동)    ← 얇게, 갈아끼움&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;figure class="imageblock alignCenter" data-ke-mobileStyle="widthOrigin" data-origin-width="912" data-origin-height="882"&gt;&lt;span data-url="https://blog.kakaocdn.net/dn/d3QtVn/dJMcahZnk81/Gpt3Jl0uBFj7tFr1L1L5w0/img.png" data-phocus="https://blog.kakaocdn.net/dn/d3QtVn/dJMcahZnk81/Gpt3Jl0uBFj7tFr1L1L5w0/img.png"&gt;&lt;img src="https://blog.kakaocdn.net/dn/d3QtVn/dJMcahZnk81/Gpt3Jl0uBFj7tFr1L1L5w0/img.png" srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd3QtVn%2FdJMcahZnk81%2FGpt3Jl0uBFj7tFr1L1L5w0%2Fimg.png" onerror="this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';" loading="lazy" width="450" height="435" data-origin-width="912" data-origin-height="882"/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2&gt;디지털 동료 하나 → 채널 N개&lt;/h2&gt;
&lt;p&gt;Thin 플랫폼의 보상은 여기서 나온다. 신원 확인과 UX 포장을 채널별로 한 번씩만 풀어두면, 그 다음부터는 —&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;디지털 동료 하나를 추가하면&lt;/strong&gt; 이미 연결된 모든 채널에 동시에 나타난다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;채널 하나를 연결하면&lt;/strong&gt; 이미 있는 모든 동료가 그 채널에서 한 번에 쓸 수 있게 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;동료 M명 × 채널 N개를 M×N번 따로 만드는 게 아니라, &lt;strong&gt;M + N번만 만들면 되는 구조&lt;/strong&gt;다. 이게 &amp;quot;직접 UI를 안 만든다&amp;quot;가 단순 비용 절감을 넘어 _구조적 레버리지_가 되는 지점이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class="imageblock alignCenter" data-ke-mobileStyle="widthOrigin" data-origin-width="1908" data-origin-height="906"&gt;&lt;span data-url="https://blog.kakaocdn.net/dn/Sfo7F/dJMcaf8iwfg/73yhF3Vyd9lPDQuZFyTZrk/img.png" data-phocus="https://blog.kakaocdn.net/dn/Sfo7F/dJMcaf8iwfg/73yhF3Vyd9lPDQuZFyTZrk/img.png"&gt;&lt;img src="https://blog.kakaocdn.net/dn/Sfo7F/dJMcaf8iwfg/73yhF3Vyd9lPDQuZFyTZrk/img.png" srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FSfo7F%2FdJMcaf8iwfg%2F73yhF3Vyd9lPDQuZFyTZrk%2Fimg.png" onerror="this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';" loading="lazy" width="1908" height="906" data-origin-width="1908" data-origin-height="906"/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2&gt;같은 답, 다른 얼굴&lt;/h2&gt;
&lt;p&gt;인증만 채널마다 다른 게 아니었다. 같은 질문에 같은 근거로 답해도, 그걸 _보여주는 방식_은 채널마다 달라야 했다. Teams에선 풍부한 카드(Adaptive Card)로, Google Chat에선 그 채널의 카드 포맷으로, 사람이 보지 않는 에이전트 연동 채널에선 애초에 &amp;quot;보여줄&amp;quot; 필요 없이 구조화된 데이터로 — 같은 답을 그 채널이 이해하는 언어로 다시 포장한다.&lt;/p&gt;
&lt;p&gt;여기서 데인 교훈 하나: &lt;strong&gt;pod에서 함수로 찍어 본 게 사용자 화면이 아니다.&lt;/strong&gt; 이건 그냥 넘어갈 얘기가 아니라 심화에서 따로 다룬다.&lt;/p&gt;
&lt;h2&gt;직접 UI를 안 만든다는 원칙&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;모바일·포털은 폐기. &lt;strong&gt;워크스페이스 플랫폼 + 채널 + 디지털 동료 연결&lt;/strong&gt;로 통합.&lt;/li&gt;
&lt;li&gt;새 기능 = 새 화면을 또 만드는 게 아니라, 기존 채널에 동료의 능력을 더하는 것.&lt;/li&gt;
&lt;li&gt;운영 작업(관리자)은 별도 UI 대신 CLI·슬래시 명령으로.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;우리는 _프론트엔드를 다시 만드는 일_을 줄이고, _동료의 능력_에 집중할 수 있게 됐다.&lt;/p&gt;
&lt;p&gt;&lt;figure class="imageblock alignCenter" data-ke-mobileStyle="widthOrigin" data-origin-width="2050" data-origin-height="688"&gt;&lt;span data-url="https://blog.kakaocdn.net/dn/bnF7Ld/dJMcagGgKdR/Z8l8aK1kMenpqpukNFMKT0/img.png" data-phocus="https://blog.kakaocdn.net/dn/bnF7Ld/dJMcagGgKdR/Z8l8aK1kMenpqpukNFMKT0/img.png"&gt;&lt;img src="https://blog.kakaocdn.net/dn/bnF7Ld/dJMcagGgKdR/Z8l8aK1kMenpqpukNFMKT0/img.png" srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbnF7Ld%2FdJMcagGgKdR%2FZ8l8aK1kMenpqpukNFMKT0%2Fimg.png" onerror="this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';" loading="lazy" width="2050" height="688" data-origin-width="2050" data-origin-height="688"/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2&gt;심화 — 채널마다 다른 인증, 그리고 렌더링은 화면에서 검증한다&lt;/h2&gt;
&lt;blockquote data-ke-style="style1"&gt;&lt;p data-ke-size="size16"&gt;&lt;span style="font-family: 'Noto Serif KR';"&gt;&lt;p&gt;이 절은 깊다. 채널을 얇게 연결한다는 게 코드에서 뭘 뜻하는지 궁금한 분을 위한 detail이다. 바쁘면 맺음으로 건너뛰어도 좋다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;&lt;strong&gt;① 같은 &amp;quot;누구인지&amp;quot; 확인인데, grant 타입이 다르다&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;채널을 하나 늘릴 때마다 가장 먼저 부딪힌 건 신원 확인 방식이 제각각이라는 점이었다. 두 개만 나란히 놓아 보자.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Google Chat — 서비스 신원(OIDC) 검증.&lt;/strong&gt; Google이 발급한 OIDC id_token을 우리 쪽 verifier가 검증한다(정해진 발신자 allowlist인 &lt;code&gt;service_account&lt;/code&gt; 모드, 또는 도메인 정책상 허용된 사용자 이메일 토큰). 핵심은 &lt;strong&gt;&amp;quot;이 요청이 신뢰된 발신자(서비스)에서 왔는가&amp;quot;&lt;/strong&gt; 를 토큰으로 확인하는, machine-to-machine에 가까운 모델이다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;에이전트 연동(A2A) — 위임 토큰 교환(OBO).&lt;/strong&gt; 여기선 사람이 채널에 로그인하는 게 아니라, &lt;strong&gt;에이전트가 사용자를 대신해(on-behalf-of)&lt;/strong&gt; 통신한다. 사용자 동의로 얻은 토큰을 downstream 토큰으로 &lt;strong&gt;교환(On-Behalf-Of)&lt;/strong&gt; 해서, 그 위임 신원으로 다음 시스템에 접근한다. authorization-code에서 파생된 &lt;strong&gt;위임(delegated)&lt;/strong&gt; 모델이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;같은 &amp;quot;이 사용자가 누구인지 확인한다&amp;quot;인데, 한쪽은 _서비스 신원을 검증_하고 다른 쪽은 _사용자 위임을 교환_한다 — grant 타입 자체가 다르다. 그래서 채널 하나 = 인증 어댑터 하나였다. (이 위임 교환은 실제로 한 번 크게 데였다 — &lt;code&gt;invalid_grant&lt;/code&gt;가 무더기로 나 별도 원인분석을 했다.) 각 채널의 인증을 2탄의 출입증(payload 권한)에 다시 맞물리는 게 Thin 플랫폼의 진짜 일이었다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;② 영리한 선택 — 채널을 소유하지 않는다&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;기각한 대안은 명확했다: 자체 채널·앱을 만들어 그 안에서 모든 걸 통제하기(모바일·포털이 그거였고, 폐기했다). Thin 플랫폼은 그 반대다 — 채널 UX는 벤더에게 맡기고, 우리는 그 위에 얇은 어댑터로만 붙는다. 잃는 것: 채널 화면을 100% 우리 맘대로 못 함. 얻는 것: 벤더와 경쟁 안 함 + 락인 회피 + M+N 레버리지.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;③ 숫자, 정직하게&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;여기엔 자랑할 하드 넘버가 없다. M+N의 이득은 &amp;quot;채널·동료를 늘려도 연결 작업이 곱이 아니라 합으로 는다&amp;quot;는 구조적 사실이지 측정된 %가 아니다. 없는 숫자는 안 만든다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;④ 우리가 처음엔 틀렸다 — pod에서 찍은 건 사용자 화면이 아니다&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;가장 아팠던 채널 사고는 렌더링이었다. Teams 카드가 &lt;strong&gt;단일 줄바꿈을 공백으로 뭉개&lt;/strong&gt; 잘 정리한 답이 &lt;strong&gt;한 덩어리 벽처럼&lt;/strong&gt; 나온 적이 있다. 또 한 번은 카드를 감싸던 내부 호환 마커(&lt;code&gt;__ADAPTIVE_CARD__&lt;/code&gt;)가 안 벗겨져 &lt;strong&gt;raw JSON이 그대로 사용자에게 노출&lt;/strong&gt;된 적도 있다. 둘 다 pod 안에서 함수를 호출해 찍어 보면 멀쩡했다 — &lt;strong&gt;&amp;quot;함수 출력이 맞다&amp;quot;와 &amp;quot;그 채널에서 실제로 그렇게 보인다&amp;quot;는 다른 일&lt;/strong&gt;이었으니까.&lt;/p&gt;
&lt;p&gt;그래서 렌더링을 화면 기준으로 강제하는 4겹을 깔았다: commit 훅(원시 마커 누출 차단) · CI(카드 골든 렌더 테스트) · 런타임(발신 직전 outbound guard) · Datadog 모니터(마커 누출·렌더 실패율). 채널을 얇게 연결한다는 건, 역설적으로 &lt;strong&gt;각 채널의 화면을 더 깐깐하게 검증한다&lt;/strong&gt;는 뜻이었다. (사실 문서를 한 도구에서 다른 도구로 옮길 때마다 겪는 일이다 — 원본에선 멀쩡한데 옮긴 화면에선 깨지는. 규모만 다를 뿐 같은 교훈이다.)&lt;/p&gt;
&lt;p&gt;&lt;figure class="imageblock alignCenter" data-ke-mobileStyle="widthOrigin" data-origin-width="2002" data-origin-height="836"&gt;&lt;span data-url="https://blog.kakaocdn.net/dn/bHfJzw/dJMcab5XV22/ZWTEslmnkVEnr5dq75hZG0/img.png" data-phocus="https://blog.kakaocdn.net/dn/bHfJzw/dJMcab5XV22/ZWTEslmnkVEnr5dq75hZG0/img.png"&gt;&lt;img src="https://blog.kakaocdn.net/dn/bHfJzw/dJMcab5XV22/ZWTEslmnkVEnr5dq75hZG0/img.png" srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbHfJzw%2FdJMcab5XV22%2FZWTEslmnkVEnr5dq75hZG0%2Fimg.png" onerror="this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';" loading="lazy" width="2002" height="836" data-origin-width="2002" data-origin-height="836"/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2&gt;맺음 — 그런데 좋은 지식이 없으면 다 소용없다&lt;/h2&gt;
&lt;p&gt;저장(1) → 권한(2) → 대화(3) → 채널(4). 이제 사용자는 자기 자리에서, 권한 안에서, 근거 있는 답을 받는다.&lt;/p&gt;
&lt;p&gt;그런데 이 모든 건 _좋은 지식이 들어와 있을 때_만 의미가 있다. 쓰레기가 들어가면 쓰레기가 나온다. 그 지식을 _어떻게 모으고, 들이기 전에 무엇을 검수했는지_는 — &lt;strong&gt;&lt;a href="https://gsretail.tistory.com/95"&gt;번외편 — 좋은 답변의 90%는 검색 전에 결정된다&lt;/a&gt;&lt;/strong&gt;에서.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;시리즈&lt;/strong&gt;: &lt;a href="https://gsretail.tistory.com/89"&gt;1탄 저장&lt;/a&gt; · &lt;a href="https://gsretail.tistory.com/90"&gt;2탄 권한&lt;/a&gt; · &lt;a href="https://gsretail.tistory.com/92"&gt;3탄-1부 대화&lt;/a&gt; · &lt;a href="https://gsretail.tistory.com/93"&gt;3탄-2부 대화&lt;/a&gt; · &lt;strong&gt;4탄 채널 (이 글)&lt;/strong&gt; · &lt;a href="https://gsretail.tistory.com/95"&gt;번외 수집&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class="imageblock undefined" data-ke-mobileStyle="widthOrigin" data-filename="22787.jpg" data-origin-width="960" data-origin-height="960"&gt;&lt;span data-url="https://blog.kakaocdn.net/dn/02zUJ/btsMAMg3Wtb/7LuKJYntKKZdzNzAcaQAs0/tfile.dat" data-phocus="https://blog.kakaocdn.net/dn/02zUJ/btsMAMg3Wtb/7LuKJYntKKZdzNzAcaQAs0/tfile.dat"&gt;&lt;img src="https://blog.kakaocdn.net/dn/02zUJ/btsMAMg3Wtb/7LuKJYntKKZdzNzAcaQAs0/tfile.dat" srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F02zUJ%2FbtsMAMg3Wtb%2F7LuKJYntKKZdzNzAcaQAs0%2Ftfile.dat" onerror="this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';" loading="lazy" width="80" height="960" data-filename="22787.jpg" data-origin-width="960" data-origin-height="960"/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;김헌기 Darion&lt;/strong&gt; · AX본부 &amp;gt; AI데이터부문 &amp;gt; AI혁신지원팀&lt;/p&gt;
&lt;p&gt;AI 및 공통 Tech 기반의 기술 표준화 업무를 수행하고 있습니다. 동료들과 함께 기술 문화 만들기와 낯선 기술자와의 인연의 시작에 관심이 많습니다.&lt;/p&gt;</summary>
    <title>사내 지식 AI 만들기 ④&amp;middot;채널] 모바일 앱을 또 만들 뻔했다 &amp;mdash; 채널은 글로벌 업체를 못 이긴다, 그래서 Thin 플랫폼</title>
    <updated>2026-07-12T09:58:51+09:00</updated>
    <dc:date>2026-07-12T09:58:51+09:00</dc:date>
  </entry>
  <entry>
    <author>
      <name>DarionKim</name>
    </author>
    <content type="html">&lt;!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd"&gt;
&lt;html&gt;&lt;body&gt;
&lt;p&gt;&lt;figure class="imageblock alignCenter" data-ke-mobilestyle="widthOrigin" data-origin-width="1598" data-origin-height="764"&gt;&lt;span data-url="https://blog.kakaocdn.net/dn/chBddT/dJMcadvO78E/eoK6MfxyNNh0X4UpgzHKz0/img.png" data-phocus="https://blog.kakaocdn.net/dn/chBddT/dJMcadvO78E/eoK6MfxyNNh0X4UpgzHKz0/img.png"&gt;&lt;img src="https://blog.kakaocdn.net/dn/chBddT/dJMcadvO78E/eoK6MfxyNNh0X4UpgzHKz0/img.png" srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FchBddT%2FdJMcadvO78E%2FeoK6MfxyNNh0X4UpgzHKz0%2Fimg.png" onerror="this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';" loading="lazy" width="575" height="275" data-origin-width="1598" data-origin-height="764"&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2&gt;정확도 1.0, 그런데 남은 질문&lt;/h2&gt;
&lt;p&gt;1부에서 정확도 1.0을 냈다. 에이전트가 KB를 정확히 고르고, 권한 안에서, 근거를 재정렬해서 답했다. 그런데도 사용자는 여전히 나쁜 경험을 겪었다. 이유는 하나였다 — &lt;strong&gt;"검색이 맞았는가"와 "이 사람에게 답이 됐는가"는 다른 질문&lt;/strong&gt;이기 때문이다.&lt;/p&gt;
&lt;p&gt;이 구분을 실감 나게 보여주는 비교 대상이 하나 있다.&lt;/p&gt;
&lt;h2&gt;좋은 비교 대상, 그러나 다른 질문 — NotebookLM&lt;/h2&gt;
&lt;p&gt;여기서 자주 듣는 말이 있다. "이건 NotebookLM에서는 잘 나오는데요?" 틀린 말은 아니다. NotebookLM은 문서 몇 개를 넣고 요약·질문·근거 정리를 시키는 데 실제로 뛰어나다. 범위가 좁고, 문맥이 유지되고, 목적이 분명해서 잘 나온다.&lt;/p&gt;
&lt;p&gt;문제는 비교 대상이 다르다는 데 있다. NotebookLM은 &lt;strong&gt;문서를 잘 읽는 도구&lt;/strong&gt;다. 기업의 지식 AI는 그 위에 몇 가지를 더 얹어야 하는 &lt;strong&gt;운영체계&lt;/strong&gt;다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;같은 질문도 &lt;strong&gt;누가 물었는지&lt;/strong&gt;에 따라 답이 달라진다 — 그 사람이 그 문서를 볼 권한이 있는지부터 확인해야 한다(2탄).&lt;/li&gt;
&lt;li&gt;최신 공지와 오래된 규정이 충돌하면, &lt;strong&gt;어느 쪽을 우선할지&lt;/strong&gt;도 판단해야 한다.&lt;/li&gt;
&lt;li&gt;답이 틀렸을 때, &lt;strong&gt;누가·어떤 근거로·어떤 경로로&lt;/strong&gt; 답했는지 나중에 재현할 수 있어야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이건 문서 QA가 아니라 운영이다. 그래서 NotebookLM은 &lt;strong&gt;경쟁 대상이 아니라 기준점&lt;/strong&gt;으로 쓰는 게 맞았다. 같은 문서를 넣었을 때 NotebookLM은 답하는데 우리 검색은 못한다면 — 그건 검색·청킹·리랭킹(1부)·근거검증·프롬프트 중 어딘가가 부족하다는 신호다. 반대로 NotebookLM이 잘한다고 그 경험을 그대로 기업 시스템에 옮길 수 있는 것도 아니다. 권한이 있어야 하고, 감사가 있어야 하고, 실패가 드러나야 하고, 업무로 이어져야 한다 — 문서를 잘 읽는 것과 회사 안에서 그 답을 운영 가능하게 만드는 것은 다른 일이다.&lt;/p&gt;
&lt;h2&gt;RAG Score vs Answer Readiness&lt;/h2&gt;
&lt;p&gt;이제 이름을 붙여보자.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;RAG Score        : 문서를 잘 읽었는가?         (검색·태깅·재정렬의 정확도)
Answer Readiness : 이 사람에게 답이 됐는가?     (맥락·권한·시점까지 반영한 실사용 준비도)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;RAG Score는 도구의 품질이고, Answer Readiness는 운영체계의 자격이다.&lt;/strong&gt; 1부는 전자의 이야기였다. 여기서부터는 후자다.&lt;/p&gt;
&lt;p&gt;&lt;figure class="imageblock alignCenter" data-ke-mobilestyle="widthOrigin" data-origin-width="1924" data-origin-height="826"&gt;&lt;span data-url="https://blog.kakaocdn.net/dn/bkpg4N/dJMcacw1Nnp/sUbqIfWjdxUP9VFwzDLVG0/img.png" data-phocus="https://blog.kakaocdn.net/dn/bkpg4N/dJMcacw1Nnp/sUbqIfWjdxUP9VFwzDLVG0/img.png"&gt;&lt;img src="https://blog.kakaocdn.net/dn/bkpg4N/dJMcacw1Nnp/sUbqIfWjdxUP9VFwzDLVG0/img.png" srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbkpg4N%2FdJMcacw1Nnp%2FsUbqIfWjdxUP9VFwzDLVG0%2Fimg.png" onerror="this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';" loading="lazy" width="668" height="287" data-origin-width="1924" data-origin-height="826"&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2&gt;Query Expansion — 사람은 완전한 문장으로 안 묻는다&lt;/h2&gt;
&lt;p&gt;"그거 다시 설명해줘." "방금 그건 휴가에도 적용돼?" — 이런 말은 그 자체로는 검색할 수 없다. 이전에 뭘 물었는지, 뭘 가리키는지가 없으면 "그거"는 그냥 대명사다.&lt;/p&gt;
&lt;p&gt;그래서 검색으로 넘기기 전에 질문을 한 번 다듬는다. 세 갈래로:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;문맥 보존 명확화&lt;/strong&gt; — "그거"가 뭘 가리키는지 앞선 대화에서 찾아 완전한 문장으로 바꾼다.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;핵심 키워드 추출&lt;/strong&gt; — 검색엔진이 더 잘 무는 형태로 다시 뽑는다.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;의미 보존 변형&lt;/strong&gt; — 같은 뜻을 다르게 표현한 질문 몇 개를 더 만들어서, 하나로 안 걸리는 것도 다른 표현으론 걸리게 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;여기서도 익숙한 구조가 나온다. 사전에 등록된 동의어·줄임말은 &lt;strong&gt;규칙 기반으로 항상&lt;/strong&gt; 확장하고, 그걸로 부족하면 &lt;strong&gt;LLM 기반으로 의미를 더 풀어서&lt;/strong&gt; 확장한다. 값싼 걸 먼저, 비싼 건 필요할 때만 — 1부 rerank에서 봤던 것과 같은 이중 구조다.&lt;/p&gt;
&lt;p&gt;&lt;figure class="imageblock alignCenter" data-ke-mobilestyle="widthOrigin" data-origin-width="2020" data-origin-height="500"&gt;&lt;span data-url="https://blog.kakaocdn.net/dn/bH7esA/dJMcadCy2RU/U8ucHphMd47LDVGuxGXxc0/img.png" data-phocus="https://blog.kakaocdn.net/dn/bH7esA/dJMcadCy2RU/U8ucHphMd47LDVGuxGXxc0/img.png"&gt;&lt;img src="https://blog.kakaocdn.net/dn/bH7esA/dJMcadCy2RU/U8ucHphMd47LDVGuxGXxc0/img.png" srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbH7esA%2FdJMcadCy2RU%2FU8ucHphMd47LDVGuxGXxc0%2Fimg.png" onerror="this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';" loading="lazy" width="917" height="227" data-origin-width="2020" data-origin-height="500"&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3&gt;세 번 넘어졌던 이야기&lt;/h3&gt;
&lt;p&gt;이거 켜는 데 세 번 넘어졌다.&lt;/p&gt;
&lt;p&gt;첫 번째는 이름이었다. 코드는 한 이름의 환경변수만 봤는데, 운영자는 다른(그러나 더 흔히 쓰이던) 이름으로 설정을 넣었다. 둘 다 "쿼리 확장을 켜라"는 같은 뜻이었는데, 코드가 한쪽만 알아들어서 — dev에는 켜졌고 prod에는 조용히 안 켜져 있었다. 에러 한 번 없이, 그냥 확장이 안 됐을 뿐이었다.&lt;/p&gt;
&lt;p&gt;두 번째는 기본값이었다. 이름을 통일한 다음에도 기본값이 꺼짐으로 남아 있었다. 로그를 뒤져서 잡았다 — 같은 기간 dev 로그엔 "쿼리 확장됨" 흔적이 여러 건 있었는데, prod 로그엔 한 건도 없었다. 품질 지표(주제 적합도)도 유독 낮게 나오던 원인 중 하나가 이거였다. 기본값을 켬으로 뒤집었다.&lt;/p&gt;
&lt;p&gt;세 번째는 인터페이스였다. LLM 기반 확장을 붙이자 "지원하지 않는 클라이언트 타입"이라는 에러가 매 요청마다 조용히 찍히기 시작했다 — 확장을 시도했다가 실패하고 원래 쿼리로 되돌아가는 걸 반복하고 있었다. 새로 붙인 LLM 클라이언트가 기대하던 메서드 이름과 실제 메서드 이름이 달랐던 것 — 어댑터로 감싸서 맞췄다.&lt;/p&gt;
&lt;p&gt;세 번 다 겉으로는 "왜 확장이 안 켜지지"라는 같은 증상이었다. 원인은 매번 달랐다 — 이름 불일치, 기본값 반전, 인터페이스 불일치. 셋이 서로 아무 관계가 없다는 게 오히려 배운 점이었다. 같은 증상이 여러 다른 원인을 가릴 수 있다는 것.&lt;/p&gt;
&lt;h2&gt;개인화 스레드 회수 — 출입증이 이제 기억한다&lt;/h2&gt;
&lt;p&gt;2탄 끝에서 열어둔 질문이 있었다. "출입증은 못 들어가는 문만 막아줄 뿐, 이 사람이 평소 자주 드나드는 문이 어딘지는 알려주지 않는다."&lt;/p&gt;
&lt;p&gt;이제 그 답을 할 수 있다. 매 턴마다, 이 사람이 &lt;strong&gt;누구이고&lt;/strong&gt;(조직·부서·직급) &lt;strong&gt;무엇을 기억하고 있는지&lt;/strong&gt;(장기 기억)를 한 번에 모아 하나의 맥락으로 만든다. 이 맥락이 두 곳에 쓰인다.&lt;/p&gt;
&lt;p&gt;하나는 &lt;strong&gt;대화 이어가기&lt;/strong&gt;다. "그거 다시 설명해줘" 같은 말은 그 자체로는 아무것도 아니다. 기억이 있어야 "그거"가 뭔지 안다. 장기 기억에서 방금 무슨 대화가 있었는지를 찾아, 그 말을 완전한 질문으로 다시 써서 검색에 넘긴다. (앞서 본 Query Expansion의 문맥 보존 명확화와 같은 문제를, 대화가 남긴 흔적 쪽에서 한 번 더 푸는 셈이다.)&lt;/p&gt;
&lt;p&gt;다른 하나는 &lt;strong&gt;조직에 맞춘 우선순위&lt;/strong&gt;다. 같은 질문도 이 사람이 어느 조직·부서에 있는지에 따라 무엇을 먼저 보여줄지가 달라진다. 접근 가능한 범위(2탄)는 그대로지만, 그 안에서 _이 사람에게 더 맞는 것_을 앞에 놓는다.&lt;/p&gt;
&lt;p&gt;두 가지 다 최근에 실제로 켜졌다. "이 사람이 평소 자주 드나드는 문"에 대한 답은 결국, 문을 열어주는 것과는 별개로 — &lt;strong&gt;그 사람을 기억하고, 그 기억을 대화와 라우팅 양쪽에 실제로 쓰는 것&lt;/strong&gt;이었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class="imageblock alignCenter" data-ke-mobilestyle="widthOrigin" data-origin-width="1732" data-origin-height="794"&gt;&lt;span data-url="https://blog.kakaocdn.net/dn/wd6Pl/dJMcagGfHG1/Kb7ogcSMtRuxvd1OZmLOD1/img.png" data-phocus="https://blog.kakaocdn.net/dn/wd6Pl/dJMcagGfHG1/Kb7ogcSMtRuxvd1OZmLOD1/img.png"&gt;&lt;img src="https://blog.kakaocdn.net/dn/wd6Pl/dJMcagGfHG1/Kb7ogcSMtRuxvd1OZmLOD1/img.png" srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fwd6Pl%2FdJMcagGfHG1%2FKb7ogcSMtRuxvd1OZmLOD1%2Fimg.png" onerror="this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';" loading="lazy" width="1732" height="794" data-origin-width="1732" data-origin-height="794"&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2&gt;Before/After — 뭐가 달라졌나&lt;/h2&gt;
&lt;p&gt;숫자로 자랑할 단계는 아직 아니다. 그런데 체감은 있다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;같은 질문을 두 번 다른 말로 다시 물어야 했던 게, 한 번으로 줄었다 — 문맥 재작성과 쿼리 확장이 같이 자리를 잡은 덕분이다.&lt;/li&gt;
&lt;li&gt;"그거 다시" 같은 되묻기가 끊기지 않고 이어진다 — 장기 기억이 무엇을 가리키는지 찾아준다.&lt;/li&gt;
&lt;li&gt;같은 권한 범위 안에서도, 이 사람 조직에 더 맞는 답이 앞으로 온다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;여전히 남은 것도 있다. 그리고 그건 다음 이야기 몫이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class="imageblock alignCenter" data-ke-mobilestyle="widthOrigin" data-origin-width="1918" data-origin-height="854"&gt;&lt;span data-url="https://blog.kakaocdn.net/dn/bwvSmD/dJMcagTJOkt/G8XCNkBKUdckxqbklA5Fd1/img.png" data-phocus="https://blog.kakaocdn.net/dn/bwvSmD/dJMcagTJOkt/G8XCNkBKUdckxqbklA5Fd1/img.png"&gt;&lt;img src="https://blog.kakaocdn.net/dn/bwvSmD/dJMcagTJOkt/G8XCNkBKUdckxqbklA5Fd1/img.png" srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbwvSmD%2FdJMcagTJOkt%2FG8XCNkBKUdckxqbklA5Fd1%2Fimg.png" onerror="this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';" loading="lazy" width="671" height="299" data-origin-width="1918" data-origin-height="854"&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2&gt;심화 — "그거"를 자립형 질문으로 바꾸는 프롬프트, 그리고 "Pod" 사고&lt;/h2&gt;
&lt;blockquote data-ke-style="style1"&gt;&lt;p data-ke-size="size16"&gt;&lt;span style="font-family: 'Noto Serif KR';"&gt;&lt;p&gt;이 절은 깊다. 대화 재작성과 쿼리 확장이 실제 코드에서 어떻게 도는지 궁금한 분을 위한 detail이다. 바쁘면 맺음으로 건너뛰어도 좋다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;① 코드 — "그거"를 푸는 실제 프롬프트 + 방어막&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;본문의 "문맥 보존 명확화"는 실제로 이런 프롬프트로 돈다.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-python"&gt;def _build_prompt(query, history):
    return (
        "사용자의 모호한 후속 질문을, 대화 기록에서 대명사·지시어(그거, 저분, 위, 아까)를 "
        "풀어 자립형 한국어 검색 질의로 다시 써라. 하나의 간결한 질의로. 답하지 말 것. "
        "이미 자립형이면 그대로 반환.\n\n"
        f"[대화 기록]\n{history}\n\n[현재 질문] {query}\n\n[자립형 질문]"
    )&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;중요한 건 프롬프트 자체가 아니라 그 주위의 &lt;strong&gt;방어막&lt;/strong&gt;이다. 재작성 결과는 (a) 비어있지 않고 (b) 원본 대비 터무니없이 길지 않고 (c) 프롬프트의 섹션 마커를 되돌려 뱉지 않을 때만 믿는다(&lt;code&gt;_is_plausible_rewrite&lt;/code&gt;). 하나라도 실패하면(타임아웃·예외·이상한 출력) &lt;strong&gt;조용히 옛 방식(직전 대화를 그냥 뒤에 이어 붙이기)으로 폴백&lt;/strong&gt;하고, Datadog에 &lt;code&gt;applied|timeout|error|rejected&lt;/code&gt; 태그를 남긴다. "LLM 한 스텝 + 하드 폴백 + 관측 트립와이어" — 이 기능 하나를 넘어 재사용되는 패턴이다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;② 우리가 처음엔 틀렸다 — "Pod"가 "Print On Demand"가 되다&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;쿼리 확장이 켜지자 새로운 실패가 나왔다. 쿠버네티스 "Pod"를 물은 사용자의 질의가, 같은 용어 사전에 있던 &lt;strong&gt;전혀 다른 도메인의 약어&lt;/strong&gt;로 조용히 확장됐다(리테일 쪽 "Pod = Print On Demand"). 사전에 IT 약어와 리테일 약어가 같이 살았고, 확장 로직은 사용자가 어느 도메인을 뜻했는지 몰랐던 것.&lt;/p&gt;
&lt;p&gt;여기서 배운 게 이 시리즈에서 드문 결이다 — &lt;strong&gt;확장은 "못 찾던 걸 찾게" 할 뿐 아니라, "맞던 걸 틀리게" 만들 수도 있다.&lt;/strong&gt; 본문의 세 번 넘어진 이야기(안 켜지는 문제)와는 정반대 방향의 실패다. 고친 방식: 질의에 이미 다른 IT·인프라 용어가 섞여 있으면, 알려진 교차-도메인 동음이의(pod, node, service, image, port, token, storage, disk)에 대해선 사전 확장을 &lt;strong&gt;건너뛴다&lt;/strong&gt; — 사전을 무조건 믿지 않고 문맥으로 가드.&lt;/p&gt;
&lt;p&gt;그리고 하나 더 — 이 확장이 답변 품질을 실제로 올리는지 A/B로 재려 했는데, 정작 골든 질문 세트에 "짧은 도메인 약어 질의"가 너무 적어 통계적으로 판정할 수 없었다. 그때 결과를 우겨서 내는 대신 &lt;strong&gt;INSUFFICIENT_DATA로 정직하게 기록&lt;/strong&gt;하고, 검정력이 낮다는 단서를 붙여 A/B 결론을 냈다. 기능의 버그가 아니라, &lt;strong&gt;기능을 재는 방식의 결함을 먼저 공개한&lt;/strong&gt; 사례다.&lt;/p&gt;
&lt;h2&gt;맺음 — 그리고 다음 질문&lt;/h2&gt;
&lt;p&gt;1부에서 라우팅을 풀었고, 2부에서 대화의 결을 채웠다. 태깅 정확도와 대화 능력, 둘 다 갖췄으니 이제 완성일까?&lt;/p&gt;
&lt;p&gt;아직 하나가 남는다. 이 좋은 답을 &lt;em&gt;어디서&lt;/em&gt; 보여줄 것인가. 우리는 하마터면 앱을 또 만들 뻔했다. 그 이야기는 &lt;strong&gt;4탄 — 모바일 앱을 또 만들 뻔했다&lt;/strong&gt;에서.&lt;/p&gt;
&lt;p&gt;(그리고 — 이 모든 걸 사람이 아니라 에이전트가 굴린다는 이야기, Claude나 Codex 같은 코딩 에이전트도 결국 같은 틀 안의 "에이전트"라는 이야기는 언젠가 따로.)&lt;/p&gt;
&lt;p&gt;&lt;figure class="imageblock alignCenter" data-ke-mobilestyle="widthOrigin" data-origin-width="1974" data-origin-height="932"&gt;&lt;span data-url="https://blog.kakaocdn.net/dn/YJtxq/dJMcacX5j8O/v7NsEoOkXQbnG3TRXqNaBk/img.png" data-phocus="https://blog.kakaocdn.net/dn/YJtxq/dJMcacX5j8O/v7NsEoOkXQbnG3TRXqNaBk/img.png"&gt;&lt;img src="https://blog.kakaocdn.net/dn/YJtxq/dJMcacX5j8O/v7NsEoOkXQbnG3TRXqNaBk/img.png" srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FYJtxq%2FdJMcacX5j8O%2Fv7NsEoOkXQbnG3TRXqNaBk%2Fimg.png" onerror="this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';" loading="lazy" width="578" height="273" data-origin-width="1974" data-origin-height="932"&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;시리즈&lt;/strong&gt;: &lt;a href="https://gsretail.tistory.com/89"&gt;1탄 저장&lt;/a&gt; · &lt;a href="https://gsretail.tistory.com/90"&gt;2탄 권한&lt;/a&gt; · &lt;a href="https://gsretail.tistory.com/92"&gt;3탄-1부 대화&lt;/a&gt; · &lt;strong&gt;3탄-2부 대화 (이 글)&lt;/strong&gt; · 4탄 채널 · 번외 수집&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;figure class="imageblock undefined" data-ke-mobilestyle="widthOrigin" data-filename="22787.jpg" data-origin-width="960" data-origin-height="960"&gt;&lt;span data-url="https://blog.kakaocdn.net/dn/02zUJ/btsMAMg3Wtb/7LuKJYntKKZdzNzAcaQAs0/tfile.dat" data-phocus="https://blog.kakaocdn.net/dn/02zUJ/btsMAMg3Wtb/7LuKJYntKKZdzNzAcaQAs0/tfile.dat"&gt;&lt;img src="https://blog.kakaocdn.net/dn/02zUJ/btsMAMg3Wtb/7LuKJYntKKZdzNzAcaQAs0/tfile.dat" srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F02zUJ%2FbtsMAMg3Wtb%2F7LuKJYntKKZdzNzAcaQAs0%2Ftfile.dat" onerror="this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';" loading="lazy" width="80" height="960" data-filename="22787.jpg" data-origin-width="960" data-origin-height="960"&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;김헌기 Darion&lt;/strong&gt; · AX본부 &amp;gt; AI데이터부문 &amp;gt; AI혁신지원팀&lt;/p&gt;
&lt;p&gt;AI 및 공통 Tech 기반의 기술 표준화 업무를 수행하고 있습니다. 동료들과 함께 기술 문화 만들기와 낯선 기술자와의 인연의 시작에 관심이 많습니다.&lt;/p&gt;
&lt;/body&gt;&lt;/html&gt;
</content>
    <id>https://gsretail.tistory.com/93</id>
    <link href="https://gsretail.tistory.com/93"/>
    <summary type="html">&lt;p&gt;&lt;figure class="imageblock alignCenter" data-ke-mobileStyle="widthOrigin" data-origin-width="1598" data-origin-height="764"&gt;&lt;span data-url="https://blog.kakaocdn.net/dn/chBddT/dJMcadvO78E/eoK6MfxyNNh0X4UpgzHKz0/img.png" data-phocus="https://blog.kakaocdn.net/dn/chBddT/dJMcadvO78E/eoK6MfxyNNh0X4UpgzHKz0/img.png"&gt;&lt;img src="https://blog.kakaocdn.net/dn/chBddT/dJMcadvO78E/eoK6MfxyNNh0X4UpgzHKz0/img.png" srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FchBddT%2FdJMcadvO78E%2FeoK6MfxyNNh0X4UpgzHKz0%2Fimg.png" onerror="this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';" loading="lazy" width="575" height="275" data-origin-width="1598" data-origin-height="764"/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2&gt;정확도 1.0, 그런데 남은 질문&lt;/h2&gt;
&lt;p&gt;1부에서 정확도 1.0을 냈다. 에이전트가 KB를 정확히 고르고, 권한 안에서, 근거를 재정렬해서 답했다. 그런데도 사용자는 여전히 나쁜 경험을 겪었다. 이유는 하나였다 — &lt;strong&gt;&amp;quot;검색이 맞았는가&amp;quot;와 &amp;quot;이 사람에게 답이 됐는가&amp;quot;는 다른 질문&lt;/strong&gt;이기 때문이다.&lt;/p&gt;
&lt;p&gt;이 구분을 실감 나게 보여주는 비교 대상이 하나 있다.&lt;/p&gt;
&lt;h2&gt;좋은 비교 대상, 그러나 다른 질문 — NotebookLM&lt;/h2&gt;
&lt;p&gt;여기서 자주 듣는 말이 있다. &amp;quot;이건 NotebookLM에서는 잘 나오는데요?&amp;quot; 틀린 말은 아니다. NotebookLM은 문서 몇 개를 넣고 요약·질문·근거 정리를 시키는 데 실제로 뛰어나다. 범위가 좁고, 문맥이 유지되고, 목적이 분명해서 잘 나온다.&lt;/p&gt;
&lt;p&gt;문제는 비교 대상이 다르다는 데 있다. NotebookLM은 &lt;strong&gt;문서를 잘 읽는 도구&lt;/strong&gt;다. 기업의 지식 AI는 그 위에 몇 가지를 더 얹어야 하는 &lt;strong&gt;운영체계&lt;/strong&gt;다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;같은 질문도 &lt;strong&gt;누가 물었는지&lt;/strong&gt;에 따라 답이 달라진다 — 그 사람이 그 문서를 볼 권한이 있는지부터 확인해야 한다(2탄).&lt;/li&gt;
&lt;li&gt;최신 공지와 오래된 규정이 충돌하면, &lt;strong&gt;어느 쪽을 우선할지&lt;/strong&gt;도 판단해야 한다.&lt;/li&gt;
&lt;li&gt;답이 틀렸을 때, &lt;strong&gt;누가·어떤 근거로·어떤 경로로&lt;/strong&gt; 답했는지 나중에 재현할 수 있어야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이건 문서 QA가 아니라 운영이다. 그래서 NotebookLM은 &lt;strong&gt;경쟁 대상이 아니라 기준점&lt;/strong&gt;으로 쓰는 게 맞았다. 같은 문서를 넣었을 때 NotebookLM은 답하는데 우리 검색은 못한다면 — 그건 검색·청킹·리랭킹(1부)·근거검증·프롬프트 중 어딘가가 부족하다는 신호다. 반대로 NotebookLM이 잘한다고 그 경험을 그대로 기업 시스템에 옮길 수 있는 것도 아니다. 권한이 있어야 하고, 감사가 있어야 하고, 실패가 드러나야 하고, 업무로 이어져야 한다 — 문서를 잘 읽는 것과 회사 안에서 그 답을 운영 가능하게 만드는 것은 다른 일이다.&lt;/p&gt;
&lt;h2&gt;RAG Score vs Answer Readiness&lt;/h2&gt;
&lt;p&gt;이제 이름을 붙여보자.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;RAG Score        : 문서를 잘 읽었는가?         (검색·태깅·재정렬의 정확도)
Answer Readiness : 이 사람에게 답이 됐는가?     (맥락·권한·시점까지 반영한 실사용 준비도)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;RAG Score는 도구의 품질이고, Answer Readiness는 운영체계의 자격이다.&lt;/strong&gt; 1부는 전자의 이야기였다. 여기서부터는 후자다.&lt;/p&gt;
&lt;p&gt;&lt;figure class="imageblock alignCenter" data-ke-mobileStyle="widthOrigin" data-origin-width="1924" data-origin-height="826"&gt;&lt;span data-url="https://blog.kakaocdn.net/dn/bkpg4N/dJMcacw1Nnp/sUbqIfWjdxUP9VFwzDLVG0/img.png" data-phocus="https://blog.kakaocdn.net/dn/bkpg4N/dJMcacw1Nnp/sUbqIfWjdxUP9VFwzDLVG0/img.png"&gt;&lt;img src="https://blog.kakaocdn.net/dn/bkpg4N/dJMcacw1Nnp/sUbqIfWjdxUP9VFwzDLVG0/img.png" srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbkpg4N%2FdJMcacw1Nnp%2FsUbqIfWjdxUP9VFwzDLVG0%2Fimg.png" onerror="this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';" loading="lazy" width="668" height="287" data-origin-width="1924" data-origin-height="826"/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2&gt;Query Expansion — 사람은 완전한 문장으로 안 묻는다&lt;/h2&gt;
&lt;p&gt;&amp;quot;그거 다시 설명해줘.&amp;quot; &amp;quot;방금 그건 휴가에도 적용돼?&amp;quot; — 이런 말은 그 자체로는 검색할 수 없다. 이전에 뭘 물었는지, 뭘 가리키는지가 없으면 &amp;quot;그거&amp;quot;는 그냥 대명사다.&lt;/p&gt;
&lt;p&gt;그래서 검색으로 넘기기 전에 질문을 한 번 다듬는다. 세 갈래로:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;문맥 보존 명확화&lt;/strong&gt; — &amp;quot;그거&amp;quot;가 뭘 가리키는지 앞선 대화에서 찾아 완전한 문장으로 바꾼다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;핵심 키워드 추출&lt;/strong&gt; — 검색엔진이 더 잘 무는 형태로 다시 뽑는다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;의미 보존 변형&lt;/strong&gt; — 같은 뜻을 다르게 표현한 질문 몇 개를 더 만들어서, 하나로 안 걸리는 것도 다른 표현으론 걸리게 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;여기서도 익숙한 구조가 나온다. 사전에 등록된 동의어·줄임말은 &lt;strong&gt;규칙 기반으로 항상&lt;/strong&gt; 확장하고, 그걸로 부족하면 &lt;strong&gt;LLM 기반으로 의미를 더 풀어서&lt;/strong&gt; 확장한다. 값싼 걸 먼저, 비싼 건 필요할 때만 — 1부 rerank에서 봤던 것과 같은 이중 구조다.&lt;/p&gt;
&lt;p&gt;&lt;figure class="imageblock alignCenter" data-ke-mobileStyle="widthOrigin" data-origin-width="2020" data-origin-height="500"&gt;&lt;span data-url="https://blog.kakaocdn.net/dn/bH7esA/dJMcadCy2RU/U8ucHphMd47LDVGuxGXxc0/img.png" data-phocus="https://blog.kakaocdn.net/dn/bH7esA/dJMcadCy2RU/U8ucHphMd47LDVGuxGXxc0/img.png"&gt;&lt;img src="https://blog.kakaocdn.net/dn/bH7esA/dJMcadCy2RU/U8ucHphMd47LDVGuxGXxc0/img.png" srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbH7esA%2FdJMcadCy2RU%2FU8ucHphMd47LDVGuxGXxc0%2Fimg.png" onerror="this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';" loading="lazy" width="917" height="227" data-origin-width="2020" data-origin-height="500"/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3&gt;세 번 넘어졌던 이야기&lt;/h3&gt;
&lt;p&gt;이거 켜는 데 세 번 넘어졌다.&lt;/p&gt;
&lt;p&gt;첫 번째는 이름이었다. 코드는 한 이름의 환경변수만 봤는데, 운영자는 다른(그러나 더 흔히 쓰이던) 이름으로 설정을 넣었다. 둘 다 &amp;quot;쿼리 확장을 켜라&amp;quot;는 같은 뜻이었는데, 코드가 한쪽만 알아들어서 — dev에는 켜졌고 prod에는 조용히 안 켜져 있었다. 에러 한 번 없이, 그냥 확장이 안 됐을 뿐이었다.&lt;/p&gt;
&lt;p&gt;두 번째는 기본값이었다. 이름을 통일한 다음에도 기본값이 꺼짐으로 남아 있었다. 로그를 뒤져서 잡았다 — 같은 기간 dev 로그엔 &amp;quot;쿼리 확장됨&amp;quot; 흔적이 여러 건 있었는데, prod 로그엔 한 건도 없었다. 품질 지표(주제 적합도)도 유독 낮게 나오던 원인 중 하나가 이거였다. 기본값을 켬으로 뒤집었다.&lt;/p&gt;
&lt;p&gt;세 번째는 인터페이스였다. LLM 기반 확장을 붙이자 &amp;quot;지원하지 않는 클라이언트 타입&amp;quot;이라는 에러가 매 요청마다 조용히 찍히기 시작했다 — 확장을 시도했다가 실패하고 원래 쿼리로 되돌아가는 걸 반복하고 있었다. 새로 붙인 LLM 클라이언트가 기대하던 메서드 이름과 실제 메서드 이름이 달랐던 것 — 어댑터로 감싸서 맞췄다.&lt;/p&gt;
&lt;p&gt;세 번 다 겉으로는 &amp;quot;왜 확장이 안 켜지지&amp;quot;라는 같은 증상이었다. 원인은 매번 달랐다 — 이름 불일치, 기본값 반전, 인터페이스 불일치. 셋이 서로 아무 관계가 없다는 게 오히려 배운 점이었다. 같은 증상이 여러 다른 원인을 가릴 수 있다는 것.&lt;/p&gt;
&lt;h2&gt;개인화 스레드 회수 — 출입증이 이제 기억한다&lt;/h2&gt;
&lt;p&gt;2탄 끝에서 열어둔 질문이 있었다. &amp;quot;출입증은 못 들어가는 문만 막아줄 뿐, 이 사람이 평소 자주 드나드는 문이 어딘지는 알려주지 않는다.&amp;quot;&lt;/p&gt;
&lt;p&gt;이제 그 답을 할 수 있다. 매 턴마다, 이 사람이 &lt;strong&gt;누구이고&lt;/strong&gt;(조직·부서·직급) &lt;strong&gt;무엇을 기억하고 있는지&lt;/strong&gt;(장기 기억)를 한 번에 모아 하나의 맥락으로 만든다. 이 맥락이 두 곳에 쓰인다.&lt;/p&gt;
&lt;p&gt;하나는 &lt;strong&gt;대화 이어가기&lt;/strong&gt;다. &amp;quot;그거 다시 설명해줘&amp;quot; 같은 말은 그 자체로는 아무것도 아니다. 기억이 있어야 &amp;quot;그거&amp;quot;가 뭔지 안다. 장기 기억에서 방금 무슨 대화가 있었는지를 찾아, 그 말을 완전한 질문으로 다시 써서 검색에 넘긴다. (앞서 본 Query Expansion의 문맥 보존 명확화와 같은 문제를, 대화가 남긴 흔적 쪽에서 한 번 더 푸는 셈이다.)&lt;/p&gt;
&lt;p&gt;다른 하나는 &lt;strong&gt;조직에 맞춘 우선순위&lt;/strong&gt;다. 같은 질문도 이 사람이 어느 조직·부서에 있는지에 따라 무엇을 먼저 보여줄지가 달라진다. 접근 가능한 범위(2탄)는 그대로지만, 그 안에서 _이 사람에게 더 맞는 것_을 앞에 놓는다.&lt;/p&gt;
&lt;p&gt;두 가지 다 최근에 실제로 켜졌다. &amp;quot;이 사람이 평소 자주 드나드는 문&amp;quot;에 대한 답은 결국, 문을 열어주는 것과는 별개로 — &lt;strong&gt;그 사람을 기억하고, 그 기억을 대화와 라우팅 양쪽에 실제로 쓰는 것&lt;/strong&gt;이었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class="imageblock alignCenter" data-ke-mobileStyle="widthOrigin" data-origin-width="1732" data-origin-height="794"&gt;&lt;span data-url="https://blog.kakaocdn.net/dn/wd6Pl/dJMcagGfHG1/Kb7ogcSMtRuxvd1OZmLOD1/img.png" data-phocus="https://blog.kakaocdn.net/dn/wd6Pl/dJMcagGfHG1/Kb7ogcSMtRuxvd1OZmLOD1/img.png"&gt;&lt;img src="https://blog.kakaocdn.net/dn/wd6Pl/dJMcagGfHG1/Kb7ogcSMtRuxvd1OZmLOD1/img.png" srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fwd6Pl%2FdJMcagGfHG1%2FKb7ogcSMtRuxvd1OZmLOD1%2Fimg.png" onerror="this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';" loading="lazy" width="1732" height="794" data-origin-width="1732" data-origin-height="794"/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2&gt;Before/After — 뭐가 달라졌나&lt;/h2&gt;
&lt;p&gt;숫자로 자랑할 단계는 아직 아니다. 그런데 체감은 있다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;같은 질문을 두 번 다른 말로 다시 물어야 했던 게, 한 번으로 줄었다 — 문맥 재작성과 쿼리 확장이 같이 자리를 잡은 덕분이다.&lt;/li&gt;
&lt;li&gt;&amp;quot;그거 다시&amp;quot; 같은 되묻기가 끊기지 않고 이어진다 — 장기 기억이 무엇을 가리키는지 찾아준다.&lt;/li&gt;
&lt;li&gt;같은 권한 범위 안에서도, 이 사람 조직에 더 맞는 답이 앞으로 온다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;여전히 남은 것도 있다. 그리고 그건 다음 이야기 몫이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class="imageblock alignCenter" data-ke-mobileStyle="widthOrigin" data-origin-width="1918" data-origin-height="854"&gt;&lt;span data-url="https://blog.kakaocdn.net/dn/bwvSmD/dJMcagTJOkt/G8XCNkBKUdckxqbklA5Fd1/img.png" data-phocus="https://blog.kakaocdn.net/dn/bwvSmD/dJMcagTJOkt/G8XCNkBKUdckxqbklA5Fd1/img.png"&gt;&lt;img src="https://blog.kakaocdn.net/dn/bwvSmD/dJMcagTJOkt/G8XCNkBKUdckxqbklA5Fd1/img.png" srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbwvSmD%2FdJMcagTJOkt%2FG8XCNkBKUdckxqbklA5Fd1%2Fimg.png" onerror="this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';" loading="lazy" width="671" height="299" data-origin-width="1918" data-origin-height="854"/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2&gt;심화 — &amp;quot;그거&amp;quot;를 자립형 질문으로 바꾸는 프롬프트, 그리고 &amp;quot;Pod&amp;quot; 사고&lt;/h2&gt;
&lt;blockquote data-ke-style="style1"&gt;&lt;p data-ke-size="size16"&gt;&lt;span style="font-family: 'Noto Serif KR';"&gt;&lt;p&gt;이 절은 깊다. 대화 재작성과 쿼리 확장이 실제 코드에서 어떻게 도는지 궁금한 분을 위한 detail이다. 바쁘면 맺음으로 건너뛰어도 좋다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;&lt;strong&gt;① 코드 — &amp;quot;그거&amp;quot;를 푸는 실제 프롬프트 + 방어막&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;본문의 &amp;quot;문맥 보존 명확화&amp;quot;는 실제로 이런 프롬프트로 돈다.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-python"&gt;def _build_prompt(query, history):
    return (
        &amp;quot;사용자의 모호한 후속 질문을, 대화 기록에서 대명사·지시어(그거, 저분, 위, 아까)를 &amp;quot;
        &amp;quot;풀어 자립형 한국어 검색 질의로 다시 써라. 하나의 간결한 질의로. 답하지 말 것. &amp;quot;
        &amp;quot;이미 자립형이면 그대로 반환.\n\n&amp;quot;
        f&amp;quot;[대화 기록]\n{history}\n\n[현재 질문] {query}\n\n[자립형 질문]&amp;quot;
    )&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;중요한 건 프롬프트 자체가 아니라 그 주위의 &lt;strong&gt;방어막&lt;/strong&gt;이다. 재작성 결과는 (a) 비어있지 않고 (b) 원본 대비 터무니없이 길지 않고 (c) 프롬프트의 섹션 마커를 되돌려 뱉지 않을 때만 믿는다(&lt;code&gt;_is_plausible_rewrite&lt;/code&gt;). 하나라도 실패하면(타임아웃·예외·이상한 출력) &lt;strong&gt;조용히 옛 방식(직전 대화를 그냥 뒤에 이어 붙이기)으로 폴백&lt;/strong&gt;하고, Datadog에 &lt;code&gt;applied|timeout|error|rejected&lt;/code&gt; 태그를 남긴다. &amp;quot;LLM 한 스텝 + 하드 폴백 + 관측 트립와이어&amp;quot; — 이 기능 하나를 넘어 재사용되는 패턴이다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;② 우리가 처음엔 틀렸다 — &amp;quot;Pod&amp;quot;가 &amp;quot;Print On Demand&amp;quot;가 되다&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;쿼리 확장이 켜지자 새로운 실패가 나왔다. 쿠버네티스 &amp;quot;Pod&amp;quot;를 물은 사용자의 질의가, 같은 용어 사전에 있던 &lt;strong&gt;전혀 다른 도메인의 약어&lt;/strong&gt;로 조용히 확장됐다(리테일 쪽 &amp;quot;Pod = Print On Demand&amp;quot;). 사전에 IT 약어와 리테일 약어가 같이 살았고, 확장 로직은 사용자가 어느 도메인을 뜻했는지 몰랐던 것.&lt;/p&gt;
&lt;p&gt;여기서 배운 게 이 시리즈에서 드문 결이다 — &lt;strong&gt;확장은 &amp;quot;못 찾던 걸 찾게&amp;quot; 할 뿐 아니라, &amp;quot;맞던 걸 틀리게&amp;quot; 만들 수도 있다.&lt;/strong&gt; 본문의 세 번 넘어진 이야기(안 켜지는 문제)와는 정반대 방향의 실패다. 고친 방식: 질의에 이미 다른 IT·인프라 용어가 섞여 있으면, 알려진 교차-도메인 동음이의(pod, node, service, image, port, token, storage, disk)에 대해선 사전 확장을 &lt;strong&gt;건너뛴다&lt;/strong&gt; — 사전을 무조건 믿지 않고 문맥으로 가드.&lt;/p&gt;
&lt;p&gt;그리고 하나 더 — 이 확장이 답변 품질을 실제로 올리는지 A/B로 재려 했는데, 정작 골든 질문 세트에 &amp;quot;짧은 도메인 약어 질의&amp;quot;가 너무 적어 통계적으로 판정할 수 없었다. 그때 결과를 우겨서 내는 대신 &lt;strong&gt;INSUFFICIENT_DATA로 정직하게 기록&lt;/strong&gt;하고, 검정력이 낮다는 단서를 붙여 A/B 결론을 냈다. 기능의 버그가 아니라, &lt;strong&gt;기능을 재는 방식의 결함을 먼저 공개한&lt;/strong&gt; 사례다.&lt;/p&gt;
&lt;h2&gt;맺음 — 그리고 다음 질문&lt;/h2&gt;
&lt;p&gt;1부에서 라우팅을 풀었고, 2부에서 대화의 결을 채웠다. 태깅 정확도와 대화 능력, 둘 다 갖췄으니 이제 완성일까?&lt;/p&gt;
&lt;p&gt;아직 하나가 남는다. 이 좋은 답을 &lt;em&gt;어디서&lt;/em&gt; 보여줄 것인가. 우리는 하마터면 앱을 또 만들 뻔했다. 그 이야기는 &lt;strong&gt;4탄 — 모바일 앱을 또 만들 뻔했다&lt;/strong&gt;에서.&lt;/p&gt;
&lt;p&gt;(그리고 — 이 모든 걸 사람이 아니라 에이전트가 굴린다는 이야기, Claude나 Codex 같은 코딩 에이전트도 결국 같은 틀 안의 &amp;quot;에이전트&amp;quot;라는 이야기는 언젠가 따로.)&lt;/p&gt;
&lt;p&gt;&lt;figure class="imageblock alignCenter" data-ke-mobileStyle="widthOrigin" data-origin-width="1974" data-origin-height="932"&gt;&lt;span data-url="https://blog.kakaocdn.net/dn/YJtxq/dJMcacX5j8O/v7NsEoOkXQbnG3TRXqNaBk/img.png" data-phocus="https://blog.kakaocdn.net/dn/YJtxq/dJMcacX5j8O/v7NsEoOkXQbnG3TRXqNaBk/img.png"&gt;&lt;img src="https://blog.kakaocdn.net/dn/YJtxq/dJMcacX5j8O/v7NsEoOkXQbnG3TRXqNaBk/img.png" srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FYJtxq%2FdJMcacX5j8O%2Fv7NsEoOkXQbnG3TRXqNaBk%2Fimg.png" onerror="this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';" loading="lazy" width="578" height="273" data-origin-width="1974" data-origin-height="932"/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;시리즈&lt;/strong&gt;: &lt;a href="https://gsretail.tistory.com/89"&gt;1탄 저장&lt;/a&gt; · &lt;a href="https://gsretail.tistory.com/90"&gt;2탄 권한&lt;/a&gt; · &lt;a href="https://gsretail.tistory.com/92"&gt;3탄-1부 대화&lt;/a&gt; · &lt;strong&gt;3탄-2부 대화 (이 글)&lt;/strong&gt; · 4탄 채널 · 번외 수집&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;figure class="imageblock undefined" data-ke-mobileStyle="widthOrigin" data-filename="22787.jpg" data-origin-width="960" data-origin-height="960"&gt;&lt;span data-url="https://blog.kakaocdn.net/dn/02zUJ/btsMAMg3Wtb/7LuKJYntKKZdzNzAcaQAs0/tfile.dat" data-phocus="https://blog.kakaocdn.net/dn/02zUJ/btsMAMg3Wtb/7LuKJYntKKZdzNzAcaQAs0/tfile.dat"&gt;&lt;img src="https://blog.kakaocdn.net/dn/02zUJ/btsMAMg3Wtb/7LuKJYntKKZdzNzAcaQAs0/tfile.dat" srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F02zUJ%2FbtsMAMg3Wtb%2F7LuKJYntKKZdzNzAcaQAs0%2Ftfile.dat" onerror="this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';" loading="lazy" width="80" height="960" data-filename="22787.jpg" data-origin-width="960" data-origin-height="960"/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;김헌기 Darion&lt;/strong&gt; · AX본부 &amp;gt; AI데이터부문 &amp;gt; AI혁신지원팀&lt;/p&gt;
&lt;p&gt;AI 및 공통 Tech 기반의 기술 표준화 업무를 수행하고 있습니다. 동료들과 함께 기술 문화 만들기와 낯선 기술자와의 인연의 시작에 관심이 많습니다.&lt;/p&gt;</summary>
    <title>[사내 지식 AI 만들기 ③-2부&amp;middot;대화] RAG Score와 Answer Readiness는 다른 질문이다</title>
    <updated>2026-07-10T14:12:11+09:00</updated>
    <dc:date>2026-07-10T14:12:11+09:00</dc:date>
  </entry>
  <entry>
    <author>
      <name>DarionKim</name>
    </author>
    <content type="html">&lt;!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd"&gt;
&lt;html&gt;&lt;body&gt;
&lt;h2&gt;사서가 필요했다&lt;/h2&gt;
&lt;p&gt;2탄까지 — 지식은 통합 컬렉션에 잘 저장되고, 권한대로 안전하게 조회된다. 그런데 한 가지가 끈질기게 남았다.&lt;/p&gt;
&lt;blockquote data-ke-style="style1"&gt;&lt;p data-ke-size="size16"&gt;&lt;span style="font-family: 'Noto Serif KR';"&gt;&lt;p&gt;"이 질문은 어느 KB를 봐야 하지?"&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;per-KB 시절엔 사람이 골라줬다. "그건 인사 KB에 물어보세요." 새 KB가 끝없이 추가되는 환경에서, 이 수동 선택은 곧 한계에 부딪혔다. 게다가 사용자는 한 줄로 묻지 않는다.&lt;/p&gt;
&lt;blockquote data-ke-style="style1"&gt;&lt;p data-ke-size="size16"&gt;&lt;span style="font-family: 'Noto Serif KR';"&gt;&lt;p&gt;"지난번 그 정산 건, 점포 기준이랑 본사 기준 다르지? 그리고 그거 휴가에도 영향 있어?"&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;한 마디에 &lt;strong&gt;정산·점포운영·인사&lt;/strong&gt; 세 지식이 얽힌다. &lt;strong&gt;멀티턴, 멀티인텐트.&lt;/strong&gt; 사람이 KB를 골라주는 고전 RAG로는 이 대화를 감당할 수 없다. 잘못 고르면 — 문서는 있는데 답을 못 한다.&lt;/p&gt;
&lt;p&gt;이건 가정이 아니라 반복해서 겪은 패턴이었다. 같은 사용자가 부서 경계를 넘나드는 질문을 매번 &lt;strong&gt;다른 봇에 따로&lt;/strong&gt; 물어야 했고, "이걸 왜 매번 나눠서 물어야 하나"라는 불만이 쌓였다. 지식은 잘 저장돼 있는데(1탄), 그걸 하나의 대화로 잇지 못하면 사용자에게는 여전히 흩어진 지식일 뿐이었다.&lt;/p&gt;
&lt;h2&gt;RAG → AgenticRAG&lt;/h2&gt;
&lt;p&gt;전환점은 이거였다. &lt;strong&gt;"어느 지식을 볼지"를 사람이 하드코딩하는 대신, 에이전트가 대화 맥락 + 사용자 권한 + 의도로 동적으로 고르게&lt;/strong&gt; 했다.&lt;/p&gt;
&lt;blockquote data-ke-style="style1"&gt;&lt;p data-ke-size="size16"&gt;&lt;span style="font-family: 'Noto Serif KR';"&gt;&lt;p&gt;  &lt;em&gt;여기서 "Agentic"의 범위&lt;/em&gt;: 에이전트가 &lt;strong&gt;무엇을 검색할지 스스로 정한다&lt;/strong&gt;는 뜻이다 — 의도를 분해하고, 접근 가능한 KB scope를 고르고, 멀티인텐트를 라우팅한다. (검색 결과를 스스로 평가·재시도하는 더 무거운 _적응형 루프_는 지연·비용 트레이드오프가 있어 별개의 프론티어로, 신중히 단계적으로 켠다. 과장하지 않는다.)&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;멀티턴/멀티인텐트 질문
   → 의도 분해 (멀티 인텐트 감지)
   → 사용자 권한(2탄) 해석 → 접근 가능한 KB scope 자동 선택   ← 사람이 KB 안 고름
   → scope 내 통합 검색(1탄)
   → 필요하면 clarify ("어느 점포 기준이요?")
   → rerank → 근거 기반 답변&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class="imageblock alignCenter" data-ke-mobilestyle="widthOrigin" data-origin-width="1950" data-origin-height="850"&gt;&lt;span data-url="https://blog.kakaocdn.net/dn/st7pi/dJMcaa0fR82/sw6bljKr5A56040MaPyYg0/img.png" data-phocus="https://blog.kakaocdn.net/dn/st7pi/dJMcaa0fR82/sw6bljKr5A56040MaPyYg0/img.png"&gt;&lt;img src="https://blog.kakaocdn.net/dn/st7pi/dJMcaa0fR82/sw6bljKr5A56040MaPyYg0/img.png" srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fst7pi%2FdJMcaa0fR82%2Fsw6bljKr5A56040MaPyYg0%2Fimg.png" onerror="this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';" loading="lazy" width="608" height="265" data-origin-width="1950" data-origin-height="850"&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;대화 스택:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;LLM 라우팅   : LiteLLM Proxy (작업별 모델 분리 — 분류는 가볍게, 답변은 무겁게)
의도 분해    : LLM 기반 intent classifier (멀티인텐트 감지)
에이전트 조율: Supervisor 패턴 (역할별 에이전트에게 위임)
비동기 실행  : Temporal 워크플로 (에이전트 작업 상태 추적)
권한 신호    : 사용자 조직 기준 KB 접근범위 결정&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;한 명의 사서가 아니라 &lt;strong&gt;역할이 나뉜 여러 에이전트&lt;/strong&gt;가 이 대화를 나눠 맡는다. 지식을 아는 에이전트, 그걸 조율하는 층, 실제로 화면을 조작하거나 알림을 보내는 에이전트 — 저마다 자기 몫을 하고, 서로에게 넘긴다. 그리고 &lt;em&gt;누가 물었는지&lt;/em&gt;(조직)가 그 라우팅에 함께 반영된다 — 다만 "이 사람이 평소 자주 찾는 지식까지" 반영하는 개인화는 아직 다른 이야기다. (2부에서 왜 그런지 다룬다.)&lt;/p&gt;
&lt;h2&gt;사서 비유 — 사서 한 명에서, 사서 팀으로&lt;/h2&gt;
&lt;p&gt;예전 도서관에선 사서가 "303호 서가로 가세요"라고 위치를 알려줬다 — 사용자가 서가 번호를 알아야 했다(수동 KB 선택). 처음엔 사서 한 명(에이전트 하나)이 질문을 듣고 알아서 서가를 도는 정도로 충분해 보였다.&lt;/p&gt;
&lt;p&gt;그런데 질문이 여러 전문 분야에 걸치자, 사서 한 명으로는 부족했다. 그래서 도서관을 &lt;strong&gt;사서 팀&lt;/strong&gt;으로 다시 그렸다 — 각 분야를 잘 아는 사서들이 있고(&lt;strong&gt;아는 역할&lt;/strong&gt;), 그 사서들 사이에서 "이 질문은 누구한테 먼저 물어야 하나"를 정리해주는 총괄 사서가 있고(&lt;strong&gt;조율하는 역할&lt;/strong&gt;), 그리고 자료를 찾아주는 데서 그치지 않고 실제로 뭔가를 갖다주거나 대신 처리해야 할 때 움직이는 사람도 있다(&lt;strong&gt;실행하는 역할&lt;/strong&gt;). &lt;strong&gt;사용자는 여전히 한 명의 사서에게 물을 뿐이지만, 그 뒤에서는 팀이 협업한다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class="imageblock alignCenter" data-ke-mobilestyle="widthOrigin" data-origin-width="1956" data-origin-height="940"&gt;&lt;span data-url="https://blog.kakaocdn.net/dn/8cIRu/dJMcaa65Z1M/Ka3AMpAJf117IZIhu5SUh0/img.png" data-phocus="https://blog.kakaocdn.net/dn/8cIRu/dJMcaa65Z1M/Ka3AMpAJf117IZIhu5SUh0/img.png"&gt;&lt;img src="https://blog.kakaocdn.net/dn/8cIRu/dJMcaa65Z1M/Ka3AMpAJf117IZIhu5SUh0/img.png" srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F8cIRu%2FdJMcaa65Z1M%2FKa3AMpAJf117IZIhu5SUh0%2Fimg.png" onerror="this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';" loading="lazy" width="627" height="301" data-origin-width="1956" data-origin-height="940"&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;핵심은 "검색을 똑똑하게"이기 &lt;em&gt;이전에&lt;/em&gt;, &lt;strong&gt;"무엇을 검색할지를 에이전트가 정한다"&lt;/strong&gt;였다. 끝없이 자라는 KB를 사람이 일일이 관리하던 구조에서, 에이전트가 권한과 의도로 길을 찾는 구조로.&lt;/p&gt;
&lt;h2&gt;rerank — 후보를 찾은 다음, 순서를 다시 세운다&lt;/h2&gt;
&lt;p&gt;"무엇을 검색할지"는 에이전트가 정했다. 그런데 검색(1탄, dense+sparse hybrid)은 &lt;em&gt;후보를 넓게&lt;/em&gt; 찾는 데 강할 뿐, 그 후보를 질문에 가장 잘 맞는 순서로 세우는 데는 약하다. 임베딩 검색(bi-encoder)은 질문과 문서를 각각 따로 벡터로 만들어 거리로 비교하기 때문에 빠르지만, 질문과 문서를 나란히 놓고 세밀하게 견주지는 못한다.&lt;/p&gt;
&lt;p&gt;그래서 상위 후보에만 한 번 더 손을 댄다. 질문과 문서를 함께 입력해 정밀하게 관련도를 매기는 &lt;strong&gt;cross-encoder 기반 reranker&lt;/strong&gt;를 상위 후보에만 돌린다 — 전체 후보에 돌리면 느리니까, 값싼 1차 검색으로 후보를 좁힌 다음 비싼 2차 정렬을 그 위에만 얹는 구조다. 상용 rerank API를 1순위로 쓰고, 막히면 자체 호스팅 cross-encoder로 넘어가는 이중화도 해뒀다.&lt;/p&gt;
&lt;p&gt;순서를 정할 때도 모델 점수 하나만 보지 않는다. &lt;strong&gt;모델 관련도 + 원래 검색 순위 + 출처 종류(공식 문서·웹·그래프·FAQ 별 가중치) + 필요하면 결과 다양성&lt;/strong&gt;을 가중합해서 최종 순서를 만든다. FAQ로 등록된 답이 있으면 살짝 밀어주고, 비슷한 문서만 줄줄이 나오지 않도록 다양성 점수도 얹는다. "무엇을 볼지"는 에이전트가 고르고, "어떤 순서로 보여줄지"는 이 재정렬이 고른다 — 둘이 같이 맞아야 사용자가 실제로 원하는 답에 먼저 닿는다.&lt;/p&gt;
&lt;p&gt;(이 "값싸고 넓게 + 비싸고 정밀하게" 이중 구조, 기억해 두면 2부에서 한 번 더 보게 된다 — 검색 말고 다른 layer에서.)&lt;/p&gt;
&lt;p&gt;&lt;figure class="imageblock alignCenter" data-ke-mobilestyle="widthOrigin" data-origin-width="1986" data-origin-height="850"&gt;&lt;span data-url="https://blog.kakaocdn.net/dn/4b8GY/dJMcab5WTkg/76I3Rszl2gnkF3yX1pyIKK/img.png" data-phocus="https://blog.kakaocdn.net/dn/4b8GY/dJMcab5WTkg/76I3Rszl2gnkF3yX1pyIKK/img.png"&gt;&lt;img src="https://blog.kakaocdn.net/dn/4b8GY/dJMcab5WTkg/76I3Rszl2gnkF3yX1pyIKK/img.png" srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F4b8GY%2FdJMcab5WTkg%2F76I3Rszl2gnkF3yX1pyIKK%2Fimg.png" onerror="this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';" loading="lazy" width="582" height="249" data-origin-width="1986" data-origin-height="850"&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2&gt;정확도 1.0인데, 왜 여전히 나쁜 경험이었나&lt;/h2&gt;
&lt;p&gt;여기서 또 한 번 배웠다. 어느 질문이 어느 지식 범위(KB)에 속하는지, API로 &lt;strong&gt;태그를 정확히 매기는 것 자체는 정확도 1.0&lt;/strong&gt;까지도 낼 수 있었다. 그런데 그것만으론 부족했다.&lt;/p&gt;
&lt;p&gt;사람은 한 번에 묻지 않는다. 되묻고, 앞서 한 말을 다시 끌어오고, 한 문장에 여러 의도를 섞는다. &lt;strong&gt;지식 범위를 완벽하게 골라내는 것과, 그 반복·다의도 대화를 자연스럽게 받아내는 것은 다른 능력&lt;/strong&gt;이었다. 태깅 정확도가 아무리 높아도, 대화 기능이 그 대화의 흐름을 못 따라가면 — 사용자에게 돌아오는 건 잘 만든 지식이 아니라, &lt;strong&gt;뚝뚝 끊기는 나쁜 경험&lt;/strong&gt;뿐이었다.&lt;/p&gt;
&lt;p&gt;즉 1탄의 저장, 2탄의 권한이 아무리 정교해도, &lt;strong&gt;이 위에서 사람과 자연스럽게 대화를 이어가는 능력이 없으면 그 지식은 사용자에게 닿지 않는다.&lt;/strong&gt; 지식 범위를 고르는 일(라우팅)과 대화를 이어가는 일(대화 관리)은 같이 완성돼야 하는 한 쌍이었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class="imageblock alignCenter" data-ke-mobilestyle="widthOrigin" data-origin-width="1636" data-origin-height="816"&gt;&lt;span data-url="https://blog.kakaocdn.net/dn/bq7Ksr/dJMcajiB1dj/bCmwMVTpExSE4KAopkglF0/img.png" data-phocus="https://blog.kakaocdn.net/dn/bq7Ksr/dJMcajiB1dj/bCmwMVTpExSE4KAopkglF0/img.png"&gt;&lt;img src="https://blog.kakaocdn.net/dn/bq7Ksr/dJMcajiB1dj/bCmwMVTpExSE4KAopkglF0/img.png" srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbq7Ksr%2FdJMcajiB1dj%2FbCmwMVTpExSE4KAopkglF0%2Fimg.png" onerror="this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';" loading="lazy" width="598" height="298" data-origin-width="1636" data-origin-height="816"&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2&gt;심화 — reranker는 fallback 안에 fallback, 그리고 점수가 1.0에 붙어버린 사고&lt;/h2&gt;
&lt;blockquote data-ke-style="style1"&gt;&lt;p data-ke-size="size16"&gt;&lt;span style="font-family: 'Noto Serif KR';"&gt;&lt;p&gt;이 절은 깊다. 라우팅과 재정렬이 실제 코드에서 어떻게 방어되는지 궁금한 분을 위한 detail이다. 바쁘면 맺음으로 건너뛰어도 좋다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;① 코드 — 리랭커는 fallback 안에 fallback&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;본문에서 "상용 rerank API 1순위, 막히면 자체 호스팅 cross-encoder"라 했다. 실제 코드는 한 겹 더 깊다.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-python"&gt;def _create_reranker():
    """우선순위: cohere(상용, LiteLLM 경유) &amp;gt; cross_encoder(자체) &amp;gt; 없음."""
    if reranker_type == "cohere" and os.getenv("LITELLM_API_BASE"):
        return CohereReranker()                    # 1순위: 상용 API
    if os.getenv("DISABLE_CROSS_ENCODER_RERANKER") != "true":
        adapter = CrossEncoderIRerankerAdapter()   # 2순위: 자체 호스팅
        loop.create_task(adapter.warmup())         # 모델 로드는 논블로킹
        return adapter
    logger.warning("리랭커 없음 — 벡터 유사도 순서만. 관련도 ~20%% 저하 예상.")
    return None                                    # 3순위: 없음(조용히 넘기지 않고 숫자 로깅)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;눈여겨볼 두 가지: (1) 자체 모델 warmup을 &lt;code&gt;loop.create_task&lt;/code&gt;로 &lt;strong&gt;논블로킹&lt;/strong&gt; 발사 — 콜드 모델 로딩이 그 요청을 붙잡지 않는다. (2) "리랭커가 아예 없는" 최후 분기가 조용히 넘어가지 않고 &lt;strong&gt;"~20% 관련도 저하"라는 숫자를 로그에 남긴다.&lt;/strong&gt; 그리고 자체 어댑터(&lt;code&gt;CrossEncoderIRerankerAdapter&lt;/code&gt;) 안에는 또 3단 fallback이 있다 — 원격 자체호스팅 HTTP → 인프로세스 로컬 모델 → 원순위 기반 선형감쇠. &lt;strong&gt;fallback 안의 fallback.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;② 영리한 선택 — 점수 정규화: 절대 clamp vs 배치 상대 stretch&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;싼 1차 점수(벡터 유사도)와 비싼 2차 점수(cross-encoder 관련도)를 가중합할 때, 두 점수의 스케일이 다르다(코사인은 대략 0&lt;del&gt;1, rerank logit은 -10&lt;/del&gt;10 같은 무한대역). 우리가 고른 방식은 &lt;strong&gt;각 점수를 절대 기준 0~1로 clamp&lt;/strong&gt;하는 것. 기각한 대안:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;정규화 없이 그냥 합산&lt;/strong&gt; — 스케일이 달라 무의미.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;배치 안에서 min-max로 늘리기(stretch)&lt;/strong&gt; — 이게 함정이었다(④ 참조).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;순위-위치 가중&lt;/strong&gt;(1등=1.0, 2등=0.95…) — 점수 크기를 버려서, 톱1이 "진짜 좋아서"인지 "그 배치에서 제일 덜 나빠서"인지 구분 못 함.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;③ 숫자, 정직하게 — 둘은 실측, 하나는 아직&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;점수 포화 fix&lt;/strong&gt;: golden-question 평가에서 리랭커 톱1 점수가 천장에 붙어 있었다 — 실제 관련도와 무관하게 &lt;strong&gt;톱1이 ≥0.94인 비율이 89.6%.&lt;/strong&gt; 배치 min-max 정규화가 그 배치의 최고점을 항상 1.0으로 만든 탓. 목표를 "톱1 ≥0.94 비율 &amp;lt;50%"로 잡고 고쳤다.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;KB scope 확장 A/B&lt;/strong&gt;(프로덕션 데이터, 골든 26문항): 좁은 기본 scope vs 권한 내 넓힌 scope → 도메인 질문 근거율(&lt;a href="mailto:grounded@0.5"&gt;grounded@0.5&lt;/a&gt;)이 &lt;strong&gt;3/22(13%)에서 10/22(45%)로&lt;/strong&gt;, 대조군 회귀 0.&lt;/li&gt;
&lt;li&gt;intent classifier 정확도: 하드 넘버 아직 없음(목표 ≥80% top-1, 8문항 PoC만) — 정직하게 "측정 인프라는 설계했으나 아직 대규모 미가동".&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;④ 우리가 처음엔 틀렸다 — flag 켜니 "이 시스템 뭐야?"가 깨졌다&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;KB scope를 넓히는 건 명백히 좋아 보였다(③의 A/B가 증명). 그런데 프로덕션에서 flag를 켜자, 그동안 항상 맞히던 기본 질문 — "이 시스템 뭐야?" — 이 &lt;strong&gt;즉시 회귀&lt;/strong&gt;했다. 격리된 A/B 경로에선 멀쩡했는데, 라이브 end-to-end 경로에서만 깨졌다.&lt;/p&gt;
&lt;p&gt;원인은 ②의 점수 포화와 &lt;strong&gt;같은 버그 계열&lt;/strong&gt;이었다: 명시적 KB 리스트가 없을 때만 도는 내부 "KB 선택" 스텝이, min/max 기반 정규화로 &lt;strong&gt;아무 상관없는 대형 KB에 인공적 천장 점수&lt;/strong&gt;를 줘서 톱으로 올려버린 것. 좁혀진 후보 집합에서 "제일 top인 하나"가 실제 관련도와 무관하게 1.0을 받는, 바로 그 실패.&lt;/p&gt;
&lt;p&gt;고친 순서가 교훈이다: (1) 같은 날 flag를 한 줄로 되돌려 롤백 → (2) 다음날 격리로 원인 규명(명시적 KB 리스트로 넘기면 정상, "필터 없이 파이프라인이 알아서 고르게" 하면 회귀) → (3) 넓힌 scope를 &lt;strong&gt;항상 명시적 KB 리스트로&lt;/strong&gt; 넘겨 그 버그 경로를 고치는 대신 &lt;strong&gt;우회&lt;/strong&gt;해 재출시. 이 사고는 지금 라이브 코드의 docstring(&lt;code&gt;_resolve_accessible_default_kb_filter&lt;/code&gt;)에 그대로 적혀 있다 — &lt;strong&gt;코드가 자기 과거 회귀를 기억한다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;교훈: 격리된 실험에서 좋았다고 라이브에서도 좋은 게 아니다. "이 경로에서만 도는 스텝"이 실험 경로엔 없고 라이브 경로엔 있으면, 그 차이가 정확히 회귀가 숨는 자리다.&lt;/p&gt;
&lt;h2&gt;맺음 — 그리고 이 반전이 남긴 질문&lt;/h2&gt;
&lt;p&gt;이제 에이전트가 알아서 지식을 찾는다. 멀티턴으로 얽힌 질문도, 권한 안에서, 근거를 모아 답한다. 그런데 정확도 1.0짜리 라우팅으로도 사용자는 여전히 나쁜 경험을 겪었다 — 그 이유를 따라가면 질문이 하나 남는다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;"검색이 맞았는가"와 "이 사람에게 답이 됐는가"는 다른 질문이었다.&lt;/strong&gt; 그리고 2탄이 심어놓은 또 다른 숙제 — 이 사람이 _평소 자주 찾는 지식_은 여전히 못 알아본다는 것 — 도 그대로 남아있었다.&lt;/p&gt;
&lt;p&gt;이 두 질문에 어떻게 답을 하려 했는지는 &lt;strong&gt;&lt;a href="https://gsretail.tistory.com/93"&gt;3탄-2부 — RAG Score와 Answer Readiness는 다른 질문이다&lt;/a&gt;&lt;/strong&gt;에서.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;시리즈&lt;/strong&gt;: &lt;a href="https://gsretail.tistory.com/89"&gt;1탄 저장&lt;/a&gt; · &lt;a href="https://gsretail.tistory.com/90"&gt;2탄 권한&lt;/a&gt; · &lt;strong&gt;3탄-1부 대화 (이 글)&lt;/strong&gt; · &lt;a href="https://gsretail.tistory.com/93"&gt;3탄-2부 대화&lt;/a&gt; · 4탄 채널 · 번외 수집&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;figure class="imageblock undefined" data-ke-mobilestyle="widthOrigin" data-filename="22787.jpg" data-origin-width="960" data-origin-height="960"&gt;&lt;span data-url="https://blog.kakaocdn.net/dn/02zUJ/btsMAMg3Wtb/7LuKJYntKKZdzNzAcaQAs0/tfile.dat" data-phocus="https://blog.kakaocdn.net/dn/02zUJ/btsMAMg3Wtb/7LuKJYntKKZdzNzAcaQAs0/tfile.dat"&gt;&lt;img src="https://blog.kakaocdn.net/dn/02zUJ/btsMAMg3Wtb/7LuKJYntKKZdzNzAcaQAs0/tfile.dat" srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F02zUJ%2FbtsMAMg3Wtb%2F7LuKJYntKKZdzNzAcaQAs0%2Ftfile.dat" onerror="this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';" loading="lazy" width="80" height="960" data-filename="22787.jpg" data-origin-width="960" data-origin-height="960"&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;김헌기 Darion&lt;/strong&gt; · AX본부 &amp;gt; AI데이터부문 &amp;gt; AI혁신지원팀&lt;/p&gt;
&lt;p&gt;AI 및 공통 Tech 기반의 기술 표준화 업무를 수행하고 있습니다. 동료들과 함께 기술 문화 만들기와 낯선 기술자와의 인연의 시작에 관심이 많습니다.&lt;/p&gt;
&lt;/body&gt;&lt;/html&gt;
</content>
    <id>https://gsretail.tistory.com/92</id>
    <link href="https://gsretail.tistory.com/92"/>
    <summary type="html">&lt;h2&gt;사서가 필요했다&lt;/h2&gt;
&lt;p&gt;2탄까지 — 지식은 통합 컬렉션에 잘 저장되고, 권한대로 안전하게 조회된다. 그런데 한 가지가 끈질기게 남았다.&lt;/p&gt;
&lt;blockquote data-ke-style="style1"&gt;&lt;p data-ke-size="size16"&gt;&lt;span style="font-family: 'Noto Serif KR';"&gt;&lt;p&gt;&amp;quot;이 질문은 어느 KB를 봐야 하지?&amp;quot;&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;per-KB 시절엔 사람이 골라줬다. &amp;quot;그건 인사 KB에 물어보세요.&amp;quot; 새 KB가 끝없이 추가되는 환경에서, 이 수동 선택은 곧 한계에 부딪혔다. 게다가 사용자는 한 줄로 묻지 않는다.&lt;/p&gt;
&lt;blockquote data-ke-style="style1"&gt;&lt;p data-ke-size="size16"&gt;&lt;span style="font-family: 'Noto Serif KR';"&gt;&lt;p&gt;&amp;quot;지난번 그 정산 건, 점포 기준이랑 본사 기준 다르지? 그리고 그거 휴가에도 영향 있어?&amp;quot;&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;한 마디에 &lt;strong&gt;정산·점포운영·인사&lt;/strong&gt; 세 지식이 얽힌다. &lt;strong&gt;멀티턴, 멀티인텐트.&lt;/strong&gt; 사람이 KB를 골라주는 고전 RAG로는 이 대화를 감당할 수 없다. 잘못 고르면 — 문서는 있는데 답을 못 한다.&lt;/p&gt;
&lt;p&gt;이건 가정이 아니라 반복해서 겪은 패턴이었다. 같은 사용자가 부서 경계를 넘나드는 질문을 매번 &lt;strong&gt;다른 봇에 따로&lt;/strong&gt; 물어야 했고, &amp;quot;이걸 왜 매번 나눠서 물어야 하나&amp;quot;라는 불만이 쌓였다. 지식은 잘 저장돼 있는데(1탄), 그걸 하나의 대화로 잇지 못하면 사용자에게는 여전히 흩어진 지식일 뿐이었다.&lt;/p&gt;
&lt;h2&gt;RAG → AgenticRAG&lt;/h2&gt;
&lt;p&gt;전환점은 이거였다. &lt;strong&gt;&amp;quot;어느 지식을 볼지&amp;quot;를 사람이 하드코딩하는 대신, 에이전트가 대화 맥락 + 사용자 권한 + 의도로 동적으로 고르게&lt;/strong&gt; 했다.&lt;/p&gt;
&lt;blockquote data-ke-style="style1"&gt;&lt;p data-ke-size="size16"&gt;&lt;span style="font-family: 'Noto Serif KR';"&gt;&lt;p&gt;  &lt;em&gt;여기서 &amp;quot;Agentic&amp;quot;의 범위&lt;/em&gt;: 에이전트가 &lt;strong&gt;무엇을 검색할지 스스로 정한다&lt;/strong&gt;는 뜻이다 — 의도를 분해하고, 접근 가능한 KB scope를 고르고, 멀티인텐트를 라우팅한다. (검색 결과를 스스로 평가·재시도하는 더 무거운 _적응형 루프_는 지연·비용 트레이드오프가 있어 별개의 프론티어로, 신중히 단계적으로 켠다. 과장하지 않는다.)&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;pre&gt;&lt;code&gt;멀티턴/멀티인텐트 질문
   → 의도 분해 (멀티 인텐트 감지)
   → 사용자 권한(2탄) 해석 → 접근 가능한 KB scope 자동 선택   ← 사람이 KB 안 고름
   → scope 내 통합 검색(1탄)
   → 필요하면 clarify (&amp;quot;어느 점포 기준이요?&amp;quot;)
   → rerank → 근거 기반 답변&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;figure class="imageblock alignCenter" data-ke-mobileStyle="widthOrigin" data-origin-width="1950" data-origin-height="850"&gt;&lt;span data-url="https://blog.kakaocdn.net/dn/st7pi/dJMcaa0fR82/sw6bljKr5A56040MaPyYg0/img.png" data-phocus="https://blog.kakaocdn.net/dn/st7pi/dJMcaa0fR82/sw6bljKr5A56040MaPyYg0/img.png"&gt;&lt;img src="https://blog.kakaocdn.net/dn/st7pi/dJMcaa0fR82/sw6bljKr5A56040MaPyYg0/img.png" srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fst7pi%2FdJMcaa0fR82%2Fsw6bljKr5A56040MaPyYg0%2Fimg.png" onerror="this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';" loading="lazy" width="608" height="265" data-origin-width="1950" data-origin-height="850"/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;대화 스택:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;LLM 라우팅   : LiteLLM Proxy (작업별 모델 분리 — 분류는 가볍게, 답변은 무겁게)
의도 분해    : LLM 기반 intent classifier (멀티인텐트 감지)
에이전트 조율: Supervisor 패턴 (역할별 에이전트에게 위임)
비동기 실행  : Temporal 워크플로 (에이전트 작업 상태 추적)
권한 신호    : 사용자 조직 기준 KB 접근범위 결정&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;한 명의 사서가 아니라 &lt;strong&gt;역할이 나뉜 여러 에이전트&lt;/strong&gt;가 이 대화를 나눠 맡는다. 지식을 아는 에이전트, 그걸 조율하는 층, 실제로 화면을 조작하거나 알림을 보내는 에이전트 — 저마다 자기 몫을 하고, 서로에게 넘긴다. 그리고 &lt;em&gt;누가 물었는지&lt;/em&gt;(조직)가 그 라우팅에 함께 반영된다 — 다만 &amp;quot;이 사람이 평소 자주 찾는 지식까지&amp;quot; 반영하는 개인화는 아직 다른 이야기다. (2부에서 왜 그런지 다룬다.)&lt;/p&gt;
&lt;h2&gt;사서 비유 — 사서 한 명에서, 사서 팀으로&lt;/h2&gt;
&lt;p&gt;예전 도서관에선 사서가 &amp;quot;303호 서가로 가세요&amp;quot;라고 위치를 알려줬다 — 사용자가 서가 번호를 알아야 했다(수동 KB 선택). 처음엔 사서 한 명(에이전트 하나)이 질문을 듣고 알아서 서가를 도는 정도로 충분해 보였다.&lt;/p&gt;
&lt;p&gt;그런데 질문이 여러 전문 분야에 걸치자, 사서 한 명으로는 부족했다. 그래서 도서관을 &lt;strong&gt;사서 팀&lt;/strong&gt;으로 다시 그렸다 — 각 분야를 잘 아는 사서들이 있고(&lt;strong&gt;아는 역할&lt;/strong&gt;), 그 사서들 사이에서 &amp;quot;이 질문은 누구한테 먼저 물어야 하나&amp;quot;를 정리해주는 총괄 사서가 있고(&lt;strong&gt;조율하는 역할&lt;/strong&gt;), 그리고 자료를 찾아주는 데서 그치지 않고 실제로 뭔가를 갖다주거나 대신 처리해야 할 때 움직이는 사람도 있다(&lt;strong&gt;실행하는 역할&lt;/strong&gt;). &lt;strong&gt;사용자는 여전히 한 명의 사서에게 물을 뿐이지만, 그 뒤에서는 팀이 협업한다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class="imageblock alignCenter" data-ke-mobileStyle="widthOrigin" data-origin-width="1956" data-origin-height="940"&gt;&lt;span data-url="https://blog.kakaocdn.net/dn/8cIRu/dJMcaa65Z1M/Ka3AMpAJf117IZIhu5SUh0/img.png" data-phocus="https://blog.kakaocdn.net/dn/8cIRu/dJMcaa65Z1M/Ka3AMpAJf117IZIhu5SUh0/img.png"&gt;&lt;img src="https://blog.kakaocdn.net/dn/8cIRu/dJMcaa65Z1M/Ka3AMpAJf117IZIhu5SUh0/img.png" srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F8cIRu%2FdJMcaa65Z1M%2FKa3AMpAJf117IZIhu5SUh0%2Fimg.png" onerror="this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';" loading="lazy" width="627" height="301" data-origin-width="1956" data-origin-height="940"/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;핵심은 &amp;quot;검색을 똑똑하게&amp;quot;이기 &lt;em&gt;이전에&lt;/em&gt;, &lt;strong&gt;&amp;quot;무엇을 검색할지를 에이전트가 정한다&amp;quot;&lt;/strong&gt;였다. 끝없이 자라는 KB를 사람이 일일이 관리하던 구조에서, 에이전트가 권한과 의도로 길을 찾는 구조로.&lt;/p&gt;
&lt;h2&gt;rerank — 후보를 찾은 다음, 순서를 다시 세운다&lt;/h2&gt;
&lt;p&gt;&amp;quot;무엇을 검색할지&amp;quot;는 에이전트가 정했다. 그런데 검색(1탄, dense+sparse hybrid)은 &lt;em&gt;후보를 넓게&lt;/em&gt; 찾는 데 강할 뿐, 그 후보를 질문에 가장 잘 맞는 순서로 세우는 데는 약하다. 임베딩 검색(bi-encoder)은 질문과 문서를 각각 따로 벡터로 만들어 거리로 비교하기 때문에 빠르지만, 질문과 문서를 나란히 놓고 세밀하게 견주지는 못한다.&lt;/p&gt;
&lt;p&gt;그래서 상위 후보에만 한 번 더 손을 댄다. 질문과 문서를 함께 입력해 정밀하게 관련도를 매기는 &lt;strong&gt;cross-encoder 기반 reranker&lt;/strong&gt;를 상위 후보에만 돌린다 — 전체 후보에 돌리면 느리니까, 값싼 1차 검색으로 후보를 좁힌 다음 비싼 2차 정렬을 그 위에만 얹는 구조다. 상용 rerank API를 1순위로 쓰고, 막히면 자체 호스팅 cross-encoder로 넘어가는 이중화도 해뒀다.&lt;/p&gt;
&lt;p&gt;순서를 정할 때도 모델 점수 하나만 보지 않는다. &lt;strong&gt;모델 관련도 + 원래 검색 순위 + 출처 종류(공식 문서·웹·그래프·FAQ 별 가중치) + 필요하면 결과 다양성&lt;/strong&gt;을 가중합해서 최종 순서를 만든다. FAQ로 등록된 답이 있으면 살짝 밀어주고, 비슷한 문서만 줄줄이 나오지 않도록 다양성 점수도 얹는다. &amp;quot;무엇을 볼지&amp;quot;는 에이전트가 고르고, &amp;quot;어떤 순서로 보여줄지&amp;quot;는 이 재정렬이 고른다 — 둘이 같이 맞아야 사용자가 실제로 원하는 답에 먼저 닿는다.&lt;/p&gt;
&lt;p&gt;(이 &amp;quot;값싸고 넓게 + 비싸고 정밀하게&amp;quot; 이중 구조, 기억해 두면 2부에서 한 번 더 보게 된다 — 검색 말고 다른 layer에서.)&lt;/p&gt;
&lt;p&gt;&lt;figure class="imageblock alignCenter" data-ke-mobileStyle="widthOrigin" data-origin-width="1986" data-origin-height="850"&gt;&lt;span data-url="https://blog.kakaocdn.net/dn/4b8GY/dJMcab5WTkg/76I3Rszl2gnkF3yX1pyIKK/img.png" data-phocus="https://blog.kakaocdn.net/dn/4b8GY/dJMcab5WTkg/76I3Rszl2gnkF3yX1pyIKK/img.png"&gt;&lt;img src="https://blog.kakaocdn.net/dn/4b8GY/dJMcab5WTkg/76I3Rszl2gnkF3yX1pyIKK/img.png" srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F4b8GY%2FdJMcab5WTkg%2F76I3Rszl2gnkF3yX1pyIKK%2Fimg.png" onerror="this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';" loading="lazy" width="582" height="249" data-origin-width="1986" data-origin-height="850"/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2&gt;정확도 1.0인데, 왜 여전히 나쁜 경험이었나&lt;/h2&gt;
&lt;p&gt;여기서 또 한 번 배웠다. 어느 질문이 어느 지식 범위(KB)에 속하는지, API로 &lt;strong&gt;태그를 정확히 매기는 것 자체는 정확도 1.0&lt;/strong&gt;까지도 낼 수 있었다. 그런데 그것만으론 부족했다.&lt;/p&gt;
&lt;p&gt;사람은 한 번에 묻지 않는다. 되묻고, 앞서 한 말을 다시 끌어오고, 한 문장에 여러 의도를 섞는다. &lt;strong&gt;지식 범위를 완벽하게 골라내는 것과, 그 반복·다의도 대화를 자연스럽게 받아내는 것은 다른 능력&lt;/strong&gt;이었다. 태깅 정확도가 아무리 높아도, 대화 기능이 그 대화의 흐름을 못 따라가면 — 사용자에게 돌아오는 건 잘 만든 지식이 아니라, &lt;strong&gt;뚝뚝 끊기는 나쁜 경험&lt;/strong&gt;뿐이었다.&lt;/p&gt;
&lt;p&gt;즉 1탄의 저장, 2탄의 권한이 아무리 정교해도, &lt;strong&gt;이 위에서 사람과 자연스럽게 대화를 이어가는 능력이 없으면 그 지식은 사용자에게 닿지 않는다.&lt;/strong&gt; 지식 범위를 고르는 일(라우팅)과 대화를 이어가는 일(대화 관리)은 같이 완성돼야 하는 한 쌍이었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class="imageblock alignCenter" data-ke-mobileStyle="widthOrigin" data-origin-width="1636" data-origin-height="816"&gt;&lt;span data-url="https://blog.kakaocdn.net/dn/bq7Ksr/dJMcajiB1dj/bCmwMVTpExSE4KAopkglF0/img.png" data-phocus="https://blog.kakaocdn.net/dn/bq7Ksr/dJMcajiB1dj/bCmwMVTpExSE4KAopkglF0/img.png"&gt;&lt;img src="https://blog.kakaocdn.net/dn/bq7Ksr/dJMcajiB1dj/bCmwMVTpExSE4KAopkglF0/img.png" srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbq7Ksr%2FdJMcajiB1dj%2FbCmwMVTpExSE4KAopkglF0%2Fimg.png" onerror="this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';" loading="lazy" width="598" height="298" data-origin-width="1636" data-origin-height="816"/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2&gt;심화 — reranker는 fallback 안에 fallback, 그리고 점수가 1.0에 붙어버린 사고&lt;/h2&gt;
&lt;blockquote data-ke-style="style1"&gt;&lt;p data-ke-size="size16"&gt;&lt;span style="font-family: 'Noto Serif KR';"&gt;&lt;p&gt;이 절은 깊다. 라우팅과 재정렬이 실제 코드에서 어떻게 방어되는지 궁금한 분을 위한 detail이다. 바쁘면 맺음으로 건너뛰어도 좋다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;&lt;strong&gt;① 코드 — 리랭커는 fallback 안에 fallback&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;본문에서 &amp;quot;상용 rerank API 1순위, 막히면 자체 호스팅 cross-encoder&amp;quot;라 했다. 실제 코드는 한 겹 더 깊다.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-python"&gt;def _create_reranker():
    &amp;quot;&amp;quot;&amp;quot;우선순위: cohere(상용, LiteLLM 경유) &amp;gt; cross_encoder(자체) &amp;gt; 없음.&amp;quot;&amp;quot;&amp;quot;
    if reranker_type == &amp;quot;cohere&amp;quot; and os.getenv(&amp;quot;LITELLM_API_BASE&amp;quot;):
        return CohereReranker()                    # 1순위: 상용 API
    if os.getenv(&amp;quot;DISABLE_CROSS_ENCODER_RERANKER&amp;quot;) != &amp;quot;true&amp;quot;:
        adapter = CrossEncoderIRerankerAdapter()   # 2순위: 자체 호스팅
        loop.create_task(adapter.warmup())         # 모델 로드는 논블로킹
        return adapter
    logger.warning(&amp;quot;리랭커 없음 — 벡터 유사도 순서만. 관련도 ~20%% 저하 예상.&amp;quot;)
    return None                                    # 3순위: 없음(조용히 넘기지 않고 숫자 로깅)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;눈여겨볼 두 가지: (1) 자체 모델 warmup을 &lt;code&gt;loop.create_task&lt;/code&gt;로 &lt;strong&gt;논블로킹&lt;/strong&gt; 발사 — 콜드 모델 로딩이 그 요청을 붙잡지 않는다. (2) &amp;quot;리랭커가 아예 없는&amp;quot; 최후 분기가 조용히 넘어가지 않고 &lt;strong&gt;&amp;quot;~20% 관련도 저하&amp;quot;라는 숫자를 로그에 남긴다.&lt;/strong&gt; 그리고 자체 어댑터(&lt;code&gt;CrossEncoderIRerankerAdapter&lt;/code&gt;) 안에는 또 3단 fallback이 있다 — 원격 자체호스팅 HTTP → 인프로세스 로컬 모델 → 원순위 기반 선형감쇠. &lt;strong&gt;fallback 안의 fallback.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;② 영리한 선택 — 점수 정규화: 절대 clamp vs 배치 상대 stretch&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;싼 1차 점수(벡터 유사도)와 비싼 2차 점수(cross-encoder 관련도)를 가중합할 때, 두 점수의 스케일이 다르다(코사인은 대략 0&lt;del&gt;1, rerank logit은 -10&lt;/del&gt;10 같은 무한대역). 우리가 고른 방식은 &lt;strong&gt;각 점수를 절대 기준 0~1로 clamp&lt;/strong&gt;하는 것. 기각한 대안:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;정규화 없이 그냥 합산&lt;/strong&gt; — 스케일이 달라 무의미.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;배치 안에서 min-max로 늘리기(stretch)&lt;/strong&gt; — 이게 함정이었다(④ 참조).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;순위-위치 가중&lt;/strong&gt;(1등=1.0, 2등=0.95…) — 점수 크기를 버려서, 톱1이 &amp;quot;진짜 좋아서&amp;quot;인지 &amp;quot;그 배치에서 제일 덜 나빠서&amp;quot;인지 구분 못 함.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;③ 숫자, 정직하게 — 둘은 실측, 하나는 아직&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;점수 포화 fix&lt;/strong&gt;: golden-question 평가에서 리랭커 톱1 점수가 천장에 붙어 있었다 — 실제 관련도와 무관하게 &lt;strong&gt;톱1이 ≥0.94인 비율이 89.6%.&lt;/strong&gt; 배치 min-max 정규화가 그 배치의 최고점을 항상 1.0으로 만든 탓. 목표를 &amp;quot;톱1 ≥0.94 비율 &amp;lt;50%&amp;quot;로 잡고 고쳤다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;KB scope 확장 A/B&lt;/strong&gt;(프로덕션 데이터, 골든 26문항): 좁은 기본 scope vs 권한 내 넓힌 scope → 도메인 질문 근거율(&lt;a href="mailto:grounded@0.5"&gt;grounded@0.5&lt;/a&gt;)이 &lt;strong&gt;3/22(13%)에서 10/22(45%)로&lt;/strong&gt;, 대조군 회귀 0.&lt;/li&gt;
&lt;li&gt;intent classifier 정확도: 하드 넘버 아직 없음(목표 ≥80% top-1, 8문항 PoC만) — 정직하게 &amp;quot;측정 인프라는 설계했으나 아직 대규모 미가동&amp;quot;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;④ 우리가 처음엔 틀렸다 — flag 켜니 &amp;quot;이 시스템 뭐야?&amp;quot;가 깨졌다&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;KB scope를 넓히는 건 명백히 좋아 보였다(③의 A/B가 증명). 그런데 프로덕션에서 flag를 켜자, 그동안 항상 맞히던 기본 질문 — &amp;quot;이 시스템 뭐야?&amp;quot; — 이 &lt;strong&gt;즉시 회귀&lt;/strong&gt;했다. 격리된 A/B 경로에선 멀쩡했는데, 라이브 end-to-end 경로에서만 깨졌다.&lt;/p&gt;
&lt;p&gt;원인은 ②의 점수 포화와 &lt;strong&gt;같은 버그 계열&lt;/strong&gt;이었다: 명시적 KB 리스트가 없을 때만 도는 내부 &amp;quot;KB 선택&amp;quot; 스텝이, min/max 기반 정규화로 &lt;strong&gt;아무 상관없는 대형 KB에 인공적 천장 점수&lt;/strong&gt;를 줘서 톱으로 올려버린 것. 좁혀진 후보 집합에서 &amp;quot;제일 top인 하나&amp;quot;가 실제 관련도와 무관하게 1.0을 받는, 바로 그 실패.&lt;/p&gt;
&lt;p&gt;고친 순서가 교훈이다: (1) 같은 날 flag를 한 줄로 되돌려 롤백 → (2) 다음날 격리로 원인 규명(명시적 KB 리스트로 넘기면 정상, &amp;quot;필터 없이 파이프라인이 알아서 고르게&amp;quot; 하면 회귀) → (3) 넓힌 scope를 &lt;strong&gt;항상 명시적 KB 리스트로&lt;/strong&gt; 넘겨 그 버그 경로를 고치는 대신 &lt;strong&gt;우회&lt;/strong&gt;해 재출시. 이 사고는 지금 라이브 코드의 docstring(&lt;code&gt;_resolve_accessible_default_kb_filter&lt;/code&gt;)에 그대로 적혀 있다 — &lt;strong&gt;코드가 자기 과거 회귀를 기억한다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;교훈: 격리된 실험에서 좋았다고 라이브에서도 좋은 게 아니다. &amp;quot;이 경로에서만 도는 스텝&amp;quot;이 실험 경로엔 없고 라이브 경로엔 있으면, 그 차이가 정확히 회귀가 숨는 자리다.&lt;/p&gt;
&lt;h2&gt;맺음 — 그리고 이 반전이 남긴 질문&lt;/h2&gt;
&lt;p&gt;이제 에이전트가 알아서 지식을 찾는다. 멀티턴으로 얽힌 질문도, 권한 안에서, 근거를 모아 답한다. 그런데 정확도 1.0짜리 라우팅으로도 사용자는 여전히 나쁜 경험을 겪었다 — 그 이유를 따라가면 질문이 하나 남는다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&amp;quot;검색이 맞았는가&amp;quot;와 &amp;quot;이 사람에게 답이 됐는가&amp;quot;는 다른 질문이었다.&lt;/strong&gt; 그리고 2탄이 심어놓은 또 다른 숙제 — 이 사람이 _평소 자주 찾는 지식_은 여전히 못 알아본다는 것 — 도 그대로 남아있었다.&lt;/p&gt;
&lt;p&gt;이 두 질문에 어떻게 답을 하려 했는지는 &lt;strong&gt;&lt;a href="https://gsretail.tistory.com/93"&gt;3탄-2부 — RAG Score와 Answer Readiness는 다른 질문이다&lt;/a&gt;&lt;/strong&gt;에서.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;시리즈&lt;/strong&gt;: &lt;a href="https://gsretail.tistory.com/89"&gt;1탄 저장&lt;/a&gt; · &lt;a href="https://gsretail.tistory.com/90"&gt;2탄 권한&lt;/a&gt; · &lt;strong&gt;3탄-1부 대화 (이 글)&lt;/strong&gt; · &lt;a href="https://gsretail.tistory.com/93"&gt;3탄-2부 대화&lt;/a&gt; · 4탄 채널 · 번외 수집&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;figure class="imageblock undefined" data-ke-mobileStyle="widthOrigin" data-filename="22787.jpg" data-origin-width="960" data-origin-height="960"&gt;&lt;span data-url="https://blog.kakaocdn.net/dn/02zUJ/btsMAMg3Wtb/7LuKJYntKKZdzNzAcaQAs0/tfile.dat" data-phocus="https://blog.kakaocdn.net/dn/02zUJ/btsMAMg3Wtb/7LuKJYntKKZdzNzAcaQAs0/tfile.dat"&gt;&lt;img src="https://blog.kakaocdn.net/dn/02zUJ/btsMAMg3Wtb/7LuKJYntKKZdzNzAcaQAs0/tfile.dat" srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F02zUJ%2FbtsMAMg3Wtb%2F7LuKJYntKKZdzNzAcaQAs0%2Ftfile.dat" onerror="this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';" loading="lazy" width="80" height="960" data-filename="22787.jpg" data-origin-width="960" data-origin-height="960"/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;김헌기 Darion&lt;/strong&gt; · AX본부 &amp;gt; AI데이터부문 &amp;gt; AI혁신지원팀&lt;/p&gt;
&lt;p&gt;AI 및 공통 Tech 기반의 기술 표준화 업무를 수행하고 있습니다. 동료들과 함께 기술 문화 만들기와 낯선 기술자와의 인연의 시작에 관심이 많습니다.&lt;/p&gt;</summary>
    <title>[사내 지식 AI 만들기 ③-1부&amp;middot;대화] 사용자는 한 줄로 묻지 않았다 &amp;mdash; 그런데 정확도 1.0으로도 부족했다 (RAG &amp;rarr; AgenticRAG)</title>
    <updated>2026-07-10T13:56:17+09:00</updated>
    <dc:date>2026-07-10T13:56:17+09:00</dc:date>
  </entry>
  <dc:date>2026-07-15T09:00:00+09:00</dc:date>
</feed>
