자연어 SQL 챗봇에서 단일 런타임 기반 웹·슬랙 사내 에이전트로의 진화 과정

“이거 숫자 좀 뽑아줄래?”

사내에서 이 질문이 돌 때마다 누군가는 DB를 열고, 스키마를 찾고, 쿼리를 짜서 결과를 전달했다. 이 귀찮은 반복을 줄여보려고 처음 만든 건 단순한 자연어 SQL 챗봇이었다.

지금 사내에서 돌고 있는 시스템은 단순 챗봇이나 단일 채널이 아니다. Next.js 기반 웹 앱에서는 대시보드·할일·위키·캘린더와 결합된 ‘사내 운영 허브’이자 플로팅 에이전트로 일하고, 슬랙에서는 비동기 대화와 승인 카드로 실시간 업무를 처리한다. 환불이나 배송지 변경, 알림톡 발송 같은 위험한 쓰기 작업은 승인 카드를 띄워 진행하며, 무인 스케줄러가 먼저 CRM 플레이북을 점검하고 슬랙과 웹으로 승인 요청 딥링크를 전달한다.

그 사이 AI 런타임을 두 번 붙였다가 버렸고, 세 번째로 구축한 런타임이 지금까지 돌고 있다. 돌아보면 이 과정은 단순히 로드맵을 따라 제품을 키운 기록이 아니었다. 더 똑똑한 프레임워크를 찾아 헤매다가, 결국 외부 종속성을 끊어내고 우리 업무에 맞는 주도권과 제어권을 서비스 내부에 갖추기까지의 우여곡절에 가깝다.


2025년, RAG로 SQL을 만들던 때

이 이야기의 맨 앞은 사내 에이전트가 아니라, 2025년에 따로 돌려본 RAG 사이드 프로젝트다. 자연어 질문을 스키마 문맥과 붙여 SQL로 바꾸는 실험이었고, 모노레포(turborepo) 위에 프로토타입을 올리는 데 목적이 있었다.

거기서 확인한 건 단순했다. 스키마를 잘 붙여주면 SQL 생성 자체는 생각보다 빨리 된다. 반대로, 생성만 되는 도구는 현업에서 오래 쓰이지 않는다. “숫자 뽑아줘” 뒤에는 해석과 후속 조치가 필요하고, 그 구간이 비어 있으면 신기함만 남고 사용률은 떨어진다.

사내 쿼리봇은 그 실험의 연장선이다. RAG로 쿼리를 만드는 단계에서 멈추지 않고, 웹과 슬랙에서 실제 업무 액션까지 맡기는 쪽으로 범위를 넓혀 갔다.


프레임워크에 껍질을 맡기면 생기는 일

에이전트 시스템을 다루기 시작할 때 가장 빠지기 쉬운 함정은 “어떤 에이전트 프레임워크를 가져다 쓸까?”를 최우선으로 고민하는 것이다. 화려한 멀티에이전트 오케스트레이션이나 자율 루프 기능을 보면 당장이라도 모든 문제를 풀어줄 것 같다.

하지만 에이전트가 단발성 데모를 넘어 실제 사내 업무 환경에 안착하려면, 외부 프레임워크는 언제든 갈아끼울 수 있는 껍질이어야 하고, 진짜 본체인 도메인 지식, 안전장치, 실행 제어권은 우리의 서비스 내부에 존재해야 한다.

외부 프레임워크 올인 구조가 결국 흔들리는 데는 명확한 이유가 있다.

첫째, 사내 인프라 및 보안 정책과의 필연적인 마찰이다. 프레임워크가 제공하는 독자적인 게이트웨이나 런타임은 개인 프로젝트에서는 매끄럽지만, 사내 망 분리, OAuth 토큰 정책, 시크릿 관리, CI/CD 배포 파이프라인과 만나는 순간 계속 거슬리는 걸림돌이 된다. 프레임워크 수명주기에 우리 서비스가 인질로 잡히는 셈이다.

둘째, “쿼리만 짜주는 챗봇”에 대한 사용자의 냉정한 외면이다. 사용자가 “숫자 좀 뽑아줘”라고 말할 때 진짜 원한 건 단순한 SQL 문장이나 결과 표가 아니다. “그래서 이번 주 매출이 왜 빠졌는지”, “지연된 배송을 어떻게 조치해야 하는지”에 대한 후속 액션이다. 맥락과 조치 권한이 없는 챗봇은 몇 번 신기해하다가 금방 외면받는다.

셋째, 모델 추론 능력보다 중요한 건 안전한 수렴 루프다. AI가 100% 완벽할 수는 없다. 에이전트가 오판했을 때 시스템이 크게 터지지 않고, 인간의 승인과 피드백을 통해 올바른 결과로 수렴하도록 가두는 안전 레일은 프레임워크가 대신 만들어주지 않는다.


쿼리봇의 한계와 두 번의 멈춤

초기에 만든 첫 번째 버전은 전형적인 쿼리봇이었다. 자연어로 물어보면 SQL을 생성하고 결과를 보여주는 웹 UI였다. 어드민 패널을 만들고, 회원 승인 시스템과 스키마 프롬프트 관리 기능도 정성껏 붙였다.

당시의 가설은 “질문만 SQL로 잘 바꾸면 나머지는 사람이 본다”였지만, 사용률은 금방 떨어졌다. 정작 현장에서는 “이 주문 건 환불 처리해줘”, “슬랙에서 그냥 물어보면 안 돼?”라는 운영상의 요구가 쏟아졌다. 얼마 지나지 않아 한참 동안 개발이 멈췄던 건 다른 일이 바빠서이기도 했지만, 매일 켜고 싶지 않은 제품을 만들어두었기 때문이기도 했다.

이후 프로젝트를 다시 붙잡으면서 AI 실행 백엔드를 OpenClaw Gateway라는 외부 프레임워크에 통째로 맡겼다. 스키마 탐색 루프와 리포트 블록, SSE 스트리밍을 구현하고, 대화 런타임을 agent-core라는 모듈로 분리해 GitHub Packages에 배포까지 마쳤다.

이번엔 “기반을 확실히 잡았다”고 생각했다. 하지만 게이트웨이에 기대는 구조는 매일 배포하고 운영하기에 설정과 인증 마찰이 컸다. 패키지를 올린 뒤 또다시 두 달 동안 조용해졌다. 기반을 잡은 게 아니라, 바꾸기 어려운 의존성을 하나 크게 만든 것이었다.


하루 만에 코드를 전면 폐기하던 날

프로젝트가 진행되던 어느 날의 변경 기록은 다시 봐도 좀 웃프다. 오전에 OpenClaw 의존성을 ZeroClaw라는 다른 게이트웨이로 일괄 치환하는 대공사를 진행했다. 환경변수, 포트, 클라이언트, 문서, 예제까지 전부 바꿨다.

그리고 바로 그날 오후, 그 통합 전체를 전면 폐기 처리했다.

기술이나 코드가 안 돌아가서가 아니었다. Anthropic Setup Token 정책이나 OAuth를 우회하는 설정 경로가 보안 규칙상 사용할 수 없게 되었기 때문이다. 외부 프레임워크에 소유권을 넘겨두었더니, 벤더와 회사의 정책 변화 한 번에 코드 전체가 쓸모없는 고철이 되어버렸다.

갈림길에서 세 가지 선택지를 검토했다.

  1. 외부 게이트웨이를 계속 따라가기: 이미 두 번이나 치환했고, 코드 문제가 아닌 정책 이슈라 같은 길을 반복할 이유가 없었다.
  2. 로컬 Claude와 MCP 조합으로 선회: 개인용 도구로는 훌륭하지만, 사내 여러 팀원이 웹과 슬랙에서 같이 쓰는 서비스의 본체로는 부족했다.
  3. 앱 내부로 런타임 제어권 가져오기: 모델 호출, 도구 실행, 대화 영속화를 서비스 안에서 직접 다루는 방식. 당장 손은 더 가지만, 인증과 배포를 우리 서비스 기준으로 완전히 통제할 수 있었다.

결국 세 번째 길을 선택했다. 멋진 프레임워크를 고르기 전에, 우리가 실제로 소유하고 배포할 수 있는 자율적인 런타임 구조가 먼저라는 것을 이 비싼 수업료를 치르고서야 배웠다.


제어권을 회수하고 레거시를 지우다

결국 pi 런타임과 AWS Bedrock 기반 Claude Opus 모델을 조합해 앱 내부에 직접 에이전트 실행 토대를 올렸다.

외부 게이트웨이는 버렸지만, 그동안 쌓아둔 핵심 도메인 자산—자주 밟히던 스키마 함정, 검증된 SQL, 리포트 정의—은 버리지 않고 네이티브 도구로 깔끔하게 옮겼다.

그리고 SQL Assistant라는 옛 제품 브랜딩을 완전히 걷어냈다. 쿼리메이커 관련 레거시 DB 모델 13종을 비가역적으로 drop하고, 단순 쿼리 입력창이었던 웹 화면을 대시보드(/dashboard), 팀 공유 할일(/tasks), WYSIWYG 위키(/wiki), 일정(/calendar), 에이전트 어드민 콘솔(/admin)을 아우르는 통합 웹 오피스 허브로 완전히 피봇했다.

웹 화면 하단의 플로팅 에이전트 위젯과 슬랙 봇이 단일 에이전트 런타임을 공유하도록 설계했기 때문에, 작업자는 웹 인터페이스와 슬랙 어느 쪽에서든 동일한 맥락으로 대화할 수 있게 됐다. 지울 때 손이 좀 떨리긴 했지만, 옛 코드를 남겨두면 자꾸 거기에 기웃거릴 것 같았다.

[ 기존 구조: 외부 게이트웨이에 종속된 형태 ]
사용자 요청 -> 사내 서비스 -> 외부 게이트웨이(OpenClaw/ZeroClaw) -> LLM
                                (인증/배포/정책 변경에 취약)

[ 개선 구조: 웹 오피스 허브 & 슬랙 통합 런타임 ]
사용자 요청 (웹 허브 / 슬랙) -> 사내 서비스 (pi + Bedrock)
                               ├── [웹/슬랙 공통] Confirm 승인 게이트 (쓰기 작업 가두기)
                               ├── [도메인 지식] 스키마 함정 지식 & 검증 SQL
                               └── [개선 루프] 웹 어드민 오판 ledger -> 교훈 노하우 승격

이 구조 위에서 웹 기반 팀 관리 도구와 슬랙 Q&A 봇, 어드민 운영 액션 위임(주문 조회, 취소/환불, 송장 처리 등)을 순차적으로 결합했다. 런타임 제어권을 우리가 쥐고 있으니 웹 서비스 화면이든 슬랙 채널이든 새로운 사내 도구를 도킹하는 속도가 비교할 수 없이 빨라졌다.


현장에서 깨지며 만든 안전장치들

런타임을 직접 소유하자, 비로소 실수해도 안전하게 수렴하는 제어 루프를 웹 어드민과 업무 채널에 심을 수 있었다.

  • Confirm 승인 게이트: DB 쓰기나 외부 API 호출 등 위험한 작업은 에이전트가 독단적으로 실행하지 못하도록 가두었다. 에이전트는 제안 카드만 생성하고, 사람이 슬랙 승인 카드나 웹 콘솔의 승인 딥링크 페이지에서 버튼을 눌러야만 실제 액션이 실행된다.
  • 웹 어드민 콘솔과 교훈 승격: 에이전트가 잘못 판단한 사례는 답변 피드백 버튼(👍👎)을 통해 기록 테이블에 적재된다. 운영자는 웹 어드민 콘솔에서 클릭 한 번으로 이를 검토해 승인 교훈으로 승격시키며, 프롬프트 예산 상한 내에서 시스템 프롬프트에 동적으로 주입되어 같은 실수를 반복하지 않는다.
  • 무인 스케줄러와 승인 딥링크: CRM 스케줄러가 무인으로 돌며 매스 푸시나 윈백 제안을 생성할 때, 승인이 필요한 작업은 슬랙 메시지 카드와 웹 어드민 펜딩 큐에 함께 적재된다. 전용 딥링크를 동봉해 슬랙과 웹 어디서나 바로 결제/발송 승인 페이지로 연결되게 만들었다.

웹과 슬랙의 실전 가동 현장에서 마주한 해프닝들

설계 문서상으로는 깔끔해 보이던 선택들도, 실제 팀원들이 웹 오피스 허브와 슬랙 채널을 사용하기 시작하자 예상치 못한 곳에서 깨졌다.

  • Next.js 번들 분리와 어드민 토글 캐시 이슈: 웹 어드민에서 에이전트 위임 기능을 켰는데도 특정 라우트에서 ‘비활성’ 상태로 뜨는 문제가 발생했다. Next.js의 Server Components 라우트 번들이 분리되면서 메모리 캐시가 독립적으로 로드되던 문제로, 턴마다 위임 상태를 재확인하도록 수정해 해결했다.
  • 타임존 이중 변환의 덫 (CONVERT_TZ): CRM 스케줄 및 웹 대시보드 리포트를 집계할 때 DB에는 이미 한국 시각이 저장되어 있었는데, SQL 생성 루프에서 CONVERT_TZ(..., '+00:00', '+09:00')를 이중 적용하는 바람에 타임딜 마감 집계가 다음 날로 밀려서 보고되던 기이한 버그가 있었다.
  • undefined 에코 루프: 슬랙 mrkdwn 변환 가드 버그로 본문의 모든 숫자가 undefined로 바뀌어 출력되거나, 봇이 자신이 출력한 메시지의 채널 맥락을 다시 읽어들이며 “undefined님”을 반복 호출하는 에코 루프가 발생하기도 했다.
  • 세션 로테이션의 좌절과 시각 스탬프 방어: 날짜별 세션 자동 로테이션을 도입했다가 사내 대화 습관과 맞지 않아 이틀 만에 단일 세션으로 되돌렸다. 그 과정에서 무기한 세션이 과거 대화의 맥락을 현재 상황처럼 착각하는 환각이 나타나자, 수신 시각 스탬프를 주입해 현재 시점 맥락을 명확히 고정했다.

뇌를 갈아끼워도 일이 끊기지 않는 판

세 번의 런타임 교체를 거치며 얻은 결론은 하나다.

“좋은 뇌나 프레임워크를 고르는 일보다, 뇌를 갈아끼워도 일이 계속되게 만드는 판을 짜는 게 훨씬 어렵고 중요하다.”

에이전트 기술 생태계는 앞으로도 수없이 바뀔 것이다. 지금 유행하는 SDK나 프레임워크도 1년 뒤엔 구시대의 유물이 될 수 있다. 하지만 사내 DB의 스키마 특성, 비즈니스 검증 규칙, 데이터 변경 시의 인간 승인 절차, 오판에서 배운 교훈은 런타임이 바뀌어도 영원히 남는 도메인 자산이다.

외부 프레임워크의 화려함에 현혹되어 제어권을 넘겨주지 않고, 업무 맥락과 안전장치를 서비스 내부에 단단히 구축할 때 비로소 에이전트는 ‘신기한 챗봇’을 넘어 ‘믿고 일을 맡기는 동료’로 작동하기 시작한다.