Engineering · Agent Engineering

DB 도구가 느린 이유는 DB가 아니었다: LLM 왕복 99.8%와 이름 충돌, 방언 등록 구조로 통합

에이전트에 DB 도구를 여러 개 연결하면 일부가 조용히 사라지던 문제를 도구 이름 충돌로 진단해 고치고, 느리다는 제보는 실행 기록 27건에서 시간의 99.8%가 LLM 왕복이라는 사실로 좁혔습니다. 결과 행 상한, 테이블·컬럼 사전 적재, DB 방언 등록 구조로 바꿔 질문당 LLM 호출을 4회에서 2회로 줄였습니다.

기간
2026.10 – 2026.10
소속
고객사 구축 · 은행
역할
원인 분석·백엔드 리팩터링

Key Results

4.00 → 2.00회
질문당 LLM 호출Qwen 27B 계열, 판매 DB 질문 4종 × 3회, 정답 12/12 유지
2.6초 → 1.4초
에이전트 응답 시간같은 조건, 입력 토큰 2,930 → 1,435
41.7초 → 12ms
100만 행 표를 LIMIT 없이 조회로컬 PostgreSQL 15, 메모리 598MB → 0.1MB

은행에 구축한 XGEN 캔버스에는 DB를 쓰는 노드가 두 가지 있습니다. DB 조회 노드는 노드에 적은 SQL을 흐름이 도착하면 바로 실행하고, DB 연결 노드는 에이전트에게 테이블 목록(list_tables), 테이블 구조(get_schema), 쿼리(query) 세 도구를 줍니다. 현장에서는 에이전트가 스스로 쿼리를 쓰는 DB 연결 노드를 더 많이 쓰고 있었고, 이 노드에 대해 열흘 남짓한 간격으로 두 제보가 들어왔습니다. 하나는 노드를 여러 개 연결하면 일부만 인식된다는 것, 다른 하나는 느리고 쿼리 결과를 다 읽어서 넘긴다는 것이었습니다.

이 글은 첫 제보를 도구 이름 충돌로 진단해 고친 과정, 두 번째 제보를 실행 기록으로 좁혀 "느린 것은 DB가 아니라 LLM 왕복"이라는 결론에 이른 과정, 그리고 DB 종류별로 흩어진 도구 구현을 도구 계층 하나와 DB 방언 등록으로 합친 과정을 다룹니다. 그 과정에서 스키마가 지정된 PostgreSQL 연결이 항상 실패하던 문제와, 마스킹 규칙 오류 시 원문이 나가던 경로도 함께 찾았습니다.

두 DB 노드는 어떻게 달랐나

항목 DB 조회 노드 DB 연결 노드 (에이전트 도구)
동작 노드의 SQL을 바로 실행, LLM 없음 도구 3개를 에이전트에 제공, 에이전트가 차례로 호출
행 제한 최대 행 수(기본 100)를 LIMIT·FETCH FIRST로 부착 없음. 결과를 전부 읽음. 연결의 최대 행 수는 안내 문장에만 존재
결과 컬럼명이 있는 마크다운 표 + 행 데이터 문자열. PostgreSQL은 컬럼명 없는 튜플 나열, Oracle은 탭 구분, Informix는 튜플 문자열
DB 설명 없음 쿼리 결과마다 앞에 반복. 도구 설명에는 없어 테이블 선택 단계에서 보이지 않음
접속 실행마다 노드 실행마다 엔진 생성과 테이블 목록 조회, Informix는 도구 호출마다 접속

은행 폐쇄망은 로컬 Qwen 모델을 쓰기 때문에 LLM 호출 1회의 시간이 길고, 턴 수가 체감 속도에 미치는 영향이 더 큽니다. 폐쇄망 DB는 Oracle과 Informix가 섞여 있고, 은행망 방화벽 정책도 설계 조건이었습니다.

조용히 사라지는 DB: 도구 이름 충돌

첫 제보는 "쿼리 템플릿을 포함한 DB 연결 노드를 여러 개 연결하면 제대로 인식되지 않는다"였습니다. 쿼리 템플릿은 원인과 무관했습니다. 템플릿이 있는 연결은 query 도구 설명에 템플릿 안내가 추가될 뿐이고, 충돌은 템플릿 유무와 상관없이 났습니다.

접두사를 비워 두면 도구 이름은 list_tables, get_schema, query 세 개로 고정됩니다. 노드가 두 개면 같은 이름이 둘씩 생깁니다. 에이전트 그래프의 ToolNode는 도구를 이름으로 키잉하므로 뒤의 것만 남습니다. 같은 동작은 앞서 워크플로우 도구 이름 충돌을 조사할 때 설치된 LangChain으로 직접 확인해 둔 상태였습니다.

ToolNode.tools_by_name 키 개수: 1
같은 이름 2개 바인딩 -> 살아남은 도구 설명: 'B 워크플로우'
호출 결과: 'B 실행됨'
LLM 에 보내지는 function name 목록: [같은 이름, 같은 이름]

LLM은 같은 이름의 함수 두 개를 받지만 실행 쪽에는 하나만 남습니다. 앞의 DB는 오류 없이 호출할 수 없게 되거나, 그 이름으로 부르면 다른 DB가 실행됩니다. 에러 없이 틀리는 쪽이라 사용자에게는 "인식이 안 된다"로 보였습니다.

조사해 보니 충돌 경로가 더 있었습니다. 접두사를 한글(예: 여신, 수신)로 적으면 에이전트의 이름 정리 단계가 도구 이름 규칙(영문·숫자·_·-)에 맞추며 한글을 전부 지워 접두사가 사라졌습니다. 이름이 달라도 설명이 "List all tables available in this database"로 같아 모델이 구분할 근거가 없었습니다. 등록된 DB 도구를 불러오는 노드에서는 외부망 데모의 등록 도구 7개 중 2개가 한글이 지워지며 같은 이름으로 합쳐졌습니다.

수정은 층마다 했습니다. 에이전트 도구 정리 단계에서는 같은 이름이 남으면 뒤의 것에 _1, _2 접미사를 답니다. 이 처리는 처음에 고객 배포 라인에서 따로 작성했다가, 제품 기본 라인에 이미 있던 코드로 교체해 그 함수는 두 라인 사이에 diff가 없습니다. 노드에서는 접두사를 도구 이름 규칙에 맞게 정리하고, 지우고 남는 것이 없으면 DB 연결 노드는 연결 번호를, PostgreSQL MCP 노드는 DB 이름을 접두사로 씁니다. 도구 설명 끝에는 Database: {연결 이름} ({DB 종류}, {DB 이름}) 한 줄을 넣었고, 등록 도구는 정리하면서 글자를 잃은 이름에만 도구 번호를 달았습니다. 도구를 내는 노드 42개를 훑어 DB 관련 세 노드를 같은 규칙으로 맞췄습니다.

검증은 고객 배포 라인 코드를 사이드 컨테이너에 올리고, 로컬 DB 연결과 등록 도구로 한 에이전트의 도구 목록을 구성해 수정 전후를 쟀습니다.

경우 수정 전 고유 이름 / ToolNode 잔존 수정 후
DB 연결 노드 2개, 접두사 없음 3/6 · 3 6/6 · 6
DB 연결 노드 2개, 한글 접두사 3/6 · 3 6/6 · 6
DB 연결 노드 2개, 영문 접두사 6/6 · 6 6/6 · 6
등록 DB 도구 3개 (한글 섞인 이름 2, 한글만 1) 2개가 같은 이름으로 합쳐짐 3개 모두 고유
PostgreSQL MCP 2개, 한글 접두사 3/6 6/6

느린 이유를 실행 기록에서 찾기

두 번째 제보에는 먼저 사태를 파악하고 보고하라는 조건이 있었습니다. 코드를 고치기 전에 외부망 데모 환경의 실행 기록(노드 성능, 에이전트 트레이스 구간)을 조회했습니다.

첫 가설은 DB 연결 노드 자체가 무겁다는 것이었습니다. 노드 처리 시간에는 실제로 차이가 있었습니다. DB 조회 노드는 48회 실행에서 p50 17ms, 평균 36ms였고 DB 연결 노드는 39회 실행에서 p50 144ms, 평균 186ms, 최대 1,039ms였습니다. 하지만 이것으로는 초 단위의 체감 지연을 설명할 수 없었습니다. DB 도구 호출 자체도 빨랐습니다. list_tables 평균 3ms, get_schema 16ms, query 14ms였습니다(시험 DB가 작다는 한계는 있습니다).

결정적인 것은 에이전트 실행 단위의 분해였습니다. DB 도구를 쓴 에이전트 실행 27건은 p50 4.2초, 평균 20.2초였고 실행당 LLM 호출 평균 3.1회, 도구 호출 2.3회였습니다. LLM 구간 합이 453,930ms, 도구 결과 구간 합이 695ms로 시간의 99.8%가 LLM 왕복이었습니다. 에이전트가 테이블 목록, 구조, 쿼리를 차례로 호출한 뒤 답변하는 구조라 DB가 빨라도 LLM을 3~4번 오가야 했습니다.

두 번째 문제는 결과 크기였습니다. 도구 결과가 기록 한도(5,030자)에 걸린 경우가 list_tables 16건 중 2건, get_schema 24건 중 1건, query 21건 중 1건 있었습니다. 에이전트가 사용자 테이블을 LIMIT 없이 SELECT ... ORDER BY id로 조회해 표 전체가 LLM으로 간 사례도 있었습니다. 코드에서 query 도구는 결과를 fetchall로 전부 읽었고, 연결의 최대 행 수(외부망 데모 연결은 전부 기본값 10,000)는 안내 문장에만 들어 있었습니다. 큰 표에서는 응답 지연과 토큰 초과뿐 아니라 개인정보 노출면도 커집니다.

코드를 읽는 동안 제보와 무관한 결함 세 가지도 나왔습니다.

  • 스키마가 지정된 PostgreSQL 연결에서는 query가 항상 실패했습니다. LangChain이 보내는 SET search_path TO %s를 psycopg3가 처리하지 못해 syntax error at or near "$1"이 났고, 트레이스에 같은 오류 4건이 남아 있었습니다.
  • 구조화 실행이 실패하면 LangChain 경로로 다시 실행했는데, 그 경로에는 컬럼 마스킹이 없어 마스킹 규칙에 오류가 있으면 원문이 나갔습니다.
  • 같은 규칙이 DB별 헬퍼 파일 3개(합계 1,679줄)에 따로 구현돼 있어, 관리자가 건 허용·금지 테이블과 마스킹이 Informix에서는 적용되지 않았습니다.

고칠 방향과 구조 선택

보고에서는 세 단계를 제안했습니다. 1단계는 행 상한 강제, 결과 형식 통일, DB 설명 위치 이동이고, 2단계는 테이블·컬럼을 미리 읽어 도구 설명에 실어 왕복을 줄이는 것, 3단계는 자주 묻는 질문을 등록 쿼리와 쿼리 템플릿으로 1회 호출에 처리하는 선택 과제였습니다. 1·2단계를 한 번에 근본 수정으로 진행하되, 저장된 노드를 지웠다 다시 놓지 않아도 적용돼야 한다는 조건이 정해졌습니다.

2단계 제안에는 연결 엔진과 카탈로그를 캐시하고 Informix 접속을 재사용하는 안도 있었지만 넣지 않았습니다. 노드 준비 시간이 30~40ms라 얻는 것이 작고, 은행망에서는 연결을 열어 둔 채 두면 방화벽에 끊길 위험이 크기 때문입니다.

구조는 한 번 다시 짰습니다. 첫 MR은 DB별 헬퍼 파일 3개를 유지한 채 공통 규칙 모듈을 추가했는데, 허용 DB가 늘 때마다 파일이 하나씩 늘어나는 구조는 비효율이라고 판단해 MR을 닫고 구조를 합쳐 다시 올렸습니다.

선택지 장점 단점 판단
DB별 헬퍼 유지 + 공통 모듈 변경 범위가 작음 DB가 늘 때마다 약 700줄 파일 추가, 정책 적용이 DB마다 어긋날 여지 첫 MR, 닫음
도구 계층 하나 + DB 방언 등록 정책·상한·형식을 한 곳에서 보장, 새 DB는 등록 하나 기존 헬퍼를 삭제해 변경 폭이 큼 채택
연결 엔진·접속 캐시 노드 준비 시간 단축 이득 30~40ms, 은행망 방화벽의 유휴 연결 끊김 위험 제외

도구 계층 하나와 방언 등록

DB 연결 노드 / PostgreSQL MCP 노드
   │  연결 정보 · 정책 · '최대 행 수'
   ▼
create_db_tools(dialect, executor, ...)          도구 계층
   ├ list_tables   미리 읽은 테이블·컬럼을 도구 설명에
   ├ get_schema    세 DB 같은 형식 + 샘플 행 마스킹
   └ query         정책 검사 → 실행기 run(sql, cap) → 마스킹 → 표 → 감사 원장
          │
          ▼
DbDialect 등록                                  방언 등록
   postgresql · oracle · informix    (새 DB는 등록 하나)

행 상한은 노드의 '최대 행 수'(기본 100, 0이면 노드 제한 없음)와 연결의 최대 행 수 중 작은 값입니다. 드라이버에서 상한보다 1행 더 읽어 잘림을 감지하면, 처음 N행만 보여 준다는 사실과 함께 WHERE, COUNT/SUM과 GROUP BY, ORDER BY ... LIMIT으로 좁히라고 안내합니다. PostgreSQL 조회문은 서버 커서로 읽어 상한 너머의 행을 아예 받지 않습니다. 결과는 DB 조회 노드와 같은 마크다운 표(컬럼명 포함, 셀 300자)로 통일했습니다.

왕복을 줄이는 핵심은 테이블·컬럼 사전 적재입니다. 노드 실행 때 허용 테이블과, 정책 적용 후 100개 이하면 컬럼·타입까지 미리 읽어 list_tables 도구 설명에 싣습니다. 별도 스레드에서 3초까지만 기다리고, 접속 정보·스키마·허용/금지 정책을 키로 5분 캐시하며, 실패해도 도구는 그대로 만듭니다. DB 설명과 연결 정책은 query 도구 설명에 한 번만 싣고, 도구 설명 길이 예산(1,024자 안)을 넘으면 list_tables 결과나 첫 쿼리 결과에 한 번만 추가합니다. 에이전트는 테이블을 고르는 단계에서 이미 컬럼을 알고 있으므로 바로 쿼리를 씁니다.

DB 간 차이는 방언이 흡수합니다.

DB 테이블·컬럼 세션 스키마 비고
PostgreSQL inspector 목록 · pg_catalog 컬럼 · PK/UNIQUE/FK 트랜잭션마다 set_config('search_path', :schema, true) 조회문 서버 커서
Oracle all_tables · all_views · all_tab_columns 커넥션마다 CURRENT_SCHEMA 다른 스키마는 OWNER.TABLE, 쓰기 문장 사전 거부
Informix systables · syscolumns 없음 호출마다 접속, 읽기 전용

PostgreSQL의 search_path 문제는 LangChain SQLDatabase를 쓰지 않고 트랜잭션마다 set_config로 지정해 해결했습니다. 마스킹 실패 시 재실행 경로는 없앴고, 실행·마스킹 실패는 오류로 알리며 감사 원장에 error로 남깁니다. 통합으로 의도한 동작 변화도 있습니다. Informix에도 허용·금지 테이블과 마스킹이 적용되고, Oracle은 읽기 전용이 꺼져 있어도 쓰기 문장을 실행 전에 거부합니다(예전에는 실행 후 롤백). get_schema는 세 DB가 같은 형식이고 샘플 행도 마스킹되며, 요청한 테이블 이름은 접근 가능 목록과 대조한 뒤에만 SQL에 들어갑니다.

기존 노드를 다시 놓지 않아도 되게 하는 장치는 두 곳에 두었습니다. 캔버스는 저장된 노드를 열 때 새 파라미터를 기본값으로 추가하고(포트는 추가하지 않음), 다시 열지 않은 배포 워크플로우는 백엔드가 저장값이 없으면 기본 100을 적용합니다. 연결 설정(허용 테이블 등)을 바꾸면 다음 실행부터 바로 반영됩니다.

결과

같은 질의를 기존 코드와 새 코드에 실행해 비교했습니다.

지표 이전 이후 측정 조건
PostgreSQL 2만 행 표, LIMIT 없이 1,414ms · 2,094,174자 76ms · 7,527자 로컬 PostgreSQL 15
PostgreSQL 100만 행 표 41.7초 · 메모리 598MB · 88,295,840자 12ms · 0.1MB · 6,595자 같은 DB
Oracle 23ai 2만 행 표 418ms · 1,555,245자 34ms · 8,144자 임시 Oracle 23ai Free 컨테이너
스키마 지정 PostgreSQL 연결 문법 오류 정상 조회 로컬 PostgreSQL 15
마스킹 규칙 오류 전화번호 원문 반환 오류로 알림 같은 DB
질문당 LLM 호출 4.00회 2.00회 Qwen 27B 계열, 판매 DB 질문 4종 × 3회
질문당 도구 호출 3.00회 1.00회 같은 조건
입력 토큰 2,930 1,435 같은 조건
응답 시간 2.6초 1.4초 같은 조건
정답 12/12 12/12 같은 조건
이름 충돌 (DB 연결 노드 2개) 고유 이름 3/6 6/6 사이드 컨테이너, 로컬 DB 연결

목록형 질문 하나("대출 상품 주문 중 완료된 건 목록")는 기존 코드에서 도구 결과 698,309자, 입력 466,099토큰이었고, 측정 중 이 질문 하나로 외부 API 쿼터가 소진돼 나머지는 사내 Qwen 게이트웨이로 측정했습니다. 새 코드에서는 100행과 잘림 안내로 6,631자, 13,085토큰입니다. 측정 설계도 한 번 고쳤습니다. "수량 합계가 가장 많은 상품" 질문은 40개가 동률이라 정답을 채점할 수 없어, 답이 유일한 "금액 합계가 가장 큰 상품" 질문으로 바꿨습니다. 회귀 테스트 16건(SQLite 방언을 등록해 "새 DB는 등록 하나"를 확인, 가짜 Informix 드라이버 사용)과 기존 관련 테스트 12건, 실 DB 검증(PostgreSQL 45건, Oracle 27건, Informix 가짜 드라이버 20건, 노드 종단 17건)을 통과했습니다.

회고와 남은 과제

이번 건에서 가장 크게 얻은 것은 "느리다"를 측정 가능한 단위로 쪼갠 일이었습니다. 노드 처리 시간만 봤다면 DB 연결 노드의 144ms를 줄이는 방향으로 갔을 것입니다. 실행을 LLM 구간과 도구 구간으로 나누자 99.8%라는 숫자가 방향을 정해 줬고, 가장 효과가 큰 수정은 DB 접근이 아니라 도구 설명에 무엇을 미리 실을지였습니다. 이름 충돌 역시 에러가 나지 않는 실패라 "인식이 안 된다"는 모호한 제보로 왔고, ToolNode가 실제로 무엇을 남기는지를 직접 확인한 것이 진단을 빠르게 했습니다.

남은 과제는 다음과 같습니다. Informix는 실제 서버 없이 가짜 드라이버로만 검증했습니다. DB 조회 노드, 등록 DB 도구 노드, 연결 관리 API도 같은 방언 등록을 쓰도록 정리하는 것이 다음 후보이며, DB 종류 분기가 남아 있는 파일이 13개입니다. 배포 후에는 저장된 DB 연결 에이전트로 큰 표 질의, 스키마 지정 연결 조회, 캔버스의 '최대 행 수' 자동 표시를 확인해야 합니다. 한글 접두사를 쓰던 담당자는 도구 이름이 연결 번호 기반으로 바뀌므로 영문 접두사로 바꾸도록 안내가 필요합니다.