시맨틱 레이어는 ‘활성 사용자’ 같은 비즈니스 정의를 코드로 적어 두는 층이다. 이 층이 있으면 LLM은 지표와 차원만 고르고, SQL은 정해진 규칙대로 만들어진다. 그래서 자연어 분석이 틀릴 때 그럴듯한 오답 대신 명시적인 에러가 돌아온다.
티켓과 슬랙 봇

"지난달 활성 사용자가 몇 명이었지?" 이 한 문장이 어떤 회사에서는 티켓이 된다. 데이터팀 큐에 들어가 순번을 기다리고, 분석가가 스키마를 뒤져 ‘활성’의 뜻을 확인하고, 며칠 뒤에 표 하나가 돌아온다. 질문한 사람은 그동안 다른 일을 하고 있다.
같은 문장을 다른 회사에서는 슬랙 봇에 그대로 친다. 몇 초 뒤에 표가 붙는다. 둘 다 지금 벌어지는 일이고, 밑에 깔린 기술도 크게 다르지 않다 — 자연어를 SQL로 옮기는 모델은 두 회사 모두 이미 쓸 수 있다.
그런데 한쪽은 실서비스로 굴러가고, 다른 한쪽은 사내 데모에서 멈췄다. 무엇이 갈랐을까. 모델 성능은 아니었다. 갈린 지점은 모델 밑에 어떤 구조를 깔아 뒀느냐였고, 그 구조는 크게 네 부품으로 되어 있다. 그중 하나가 나머지보다 결정적이다.
번역이 아니라 추론이다
첫 부품인 text-to-SQL, 그러니까 자연어를 SQL로 옮기는 일은 언뜻 번역처럼 보인다. 한국어 문장이 들어가고 SQL 문장이 나오니까. 그런데 실제로 시켜 보면 이건 번역이 아니라 추론에 가깝다.
앞의 질문을 그대로 가져와 보자. 데이터베이스에 users, sessions, events 세 테이블이 있다고 하자. ‘지난달’은 어렵지 않다 — 날짜 컬럼에 범위 조건을 걸면 된다. 문제는 ‘활성’이다. 이 단어에 대응하는 컬럼도, 테이블도, 주석도 스키마 어디에도 없다. 그래서 모델은 고를 수밖에 없고, 다음 세 갈래는 전부 문법적으로 멀쩡하며 전부 다른 숫자를 낸다.
-- ① 지난달에 세션이 하나라도 있었으면 활성
SELECT COUNT(DISTINCT user_id) FROM sessions
WHERE started_at >= '2026-08-01' AND started_at < '2026-09-01';
-- ② 무언가를 만들었어야 활성
SELECT COUNT(DISTINCT user_id) FROM events
WHERE event_name IN ('post_create', 'comment_create')
AND created_at >= '2026-08-01' AND created_at < '2026-09-01';
-- ③ 계정 상태가 active 인 사용자로 한정
SELECT COUNT(DISTINCT s.user_id)
FROM sessions s JOIN users u ON u.id = s.user_id
WHERE u.status = 'active'
AND s.started_at >= '2026-08-01' AND s.started_at < '2026-09-01';
로그인만 했으면 활성인가, 무언가를 만들어야 활성인가, 계정 상태까지 봐야 하는가. 스키마만 봐서는 판정할 수 없다. 이 답은 데이터베이스가 아니라 회사 안에 있다.
실패가 여기서만 나오는 것도 아니다 — 여러 현장 기록에서 반복되는 실패는 크게 세 갈래로 모인다. 첫째는 방금 본 스키마·정의 모호성이다. 둘째는 조인이다 — 테이블을 잘못 이어 붙이면 행이 늘어나고 합계가 조용히 부풀어 오른다. 셋째는 환각이다. Uber가 사내 자연어 쿼리 도구 QueryGPT를 만들며 남긴 기록에는 모델이 있지도 않은 테이블과 컬럼을 지어내 그럴듯한 쿼리를 짜더라는 보고가 있다.
셋 중 환각은 그나마 다루기 쉽다. 없는 테이블을 조회하면 데이터베이스가 실행 단계에서 에러를 던지니, 틀렸다는 사실만큼은 화면에 그대로 뜬다. 문제는 앞의 두 갈래다. 정의를 잘못 골라도, 조인을 잘못 이어 붙여도 쿼리는 그대로 돌아가고 숫자는 나오고 표는 예쁘게 그려진다. 그 숫자를 의심할 단서가 화면 위에 하나도 없다.
그러니 답은 여기서 이미 나온 셈이다. 필요한 것은 더 똑똑한 모델이 아니라, 회사 안에만 있던 정의를 코드로 적어 두고 모델이 그 정의를 벗어나지 못하게 하는 구조다. 그 구조의 이름이 시맨틱 레이어(semantic layer)다.
시맨틱 레이어가 실패의 모양을 바꾼다
시맨틱 레이어는 비즈니스 정의를 코드로 선언해 두는 층이다. ‘활성 사용자’가 무엇인지, 어느 테이블을 어떻게 이어 붙이는지, 그 지표를 어떤 차원으로 쪼갤 수 있는지를 한곳에 적어 둔다. 그리고 그 선언에서 SQL을 만들어 내는 엔진이 붙는다 — dbt의 MetricFlow가 그런 엔진이다.
여기서 LLM이 맡는 일이 바뀐다. 이제 LLM은 SQL 전문을 쓰지 않고, 선언돼 있는 지표와 차원 중에서 고르기만 한다. 고른 결과를 SQL로 옮기는 쪽은 엔진이고, 그 변환은 결정론적이다. 같은 선택이면 언제나 같은 SQL이 나온다.
이 구조가 바꾸는 것은 정확도만이 아니다. 실패의 모양이 바뀐다. 그림 1의 위쪽 경로에서 정의가 어긋나면 결과는 그럴듯한 표다. 아래쪽 경로에서 정의 밖 질문이 들어오면 고를 지표가 아예 없으니 에러가 난다. 사람이 알아챌 수 있는 실패로 바뀌는 것이다. 신뢰가 여기서 생긴다.
정확도 숫자도 같은 방향을 가리킨다. dbt가 공개한 ACME Insurance 벤치가 그런 사례다. 이 벤치가 재는 값은 in-scope 정확도 — 벤치가 답변 가능하다고 정해 둔 범위 안의 문항만 골라 채점한 점수다. 원시 text-to-SQL로 그대로 풀었을 때 Claude Sonnet 4.6이 90.0%, GPT-5.3 Codex가 84.1%를 냈다. 같은 문항을 시맨틱 레이어를 거쳐 풀자 각각 98.2%와 100.0%로 올라갔다.
다만 이 숫자를 액면 그대로 믿으면 안 된다. dbt 자신이 만들어 공개한 벤치이고, 문항은 합성 11개이며, ‘잘 설계된 시맨틱 레이어’가 이미 깔려 있다는 전제가 붙어 있다. 읽어야 할 것은 절대수치가 아니라 그림 2가 보여주는 방향이다. 두 모델의 격차가 5.9%p에서 1.8%p로 줄었다. 어느 모델을 쓰느냐가 덜 중요해졌다는 뜻이다.
네 부품이 만드는 나머지 절반
여기까지가 네 부품 중 둘이다. 첫 부품인 text-to-SQL이 왜 혼자서는 부족한지, 두 번째 부품인 시맨틱 레이어가 그것을 어떻게 받치는지는 앞에서 봤다. 나머지 둘이 없으면 이 층도 혼자 굴러가지 못한다.
세 번째 부품은 컨텍스트다. 모델에게 스키마, 자주 쓰는 예시 쿼리, 사내 용어집을 어떤 형태로든 손에 쥐여 줘야 한다. 프롬프트에 직접 넣기도 하고, 검색으로 필요할 때만 끌어오기도 한다. 데이터베이스 자체를 도구로 붙이는 규약으로는 MCP가 자리를 잡았다.
효과는 작지 않다. Anthropic은 사내 셀프서비스 분석을 만들며 온디맨드 마크다운 컨텍스트(자사 표현으로 ‘skills’)를 붙였다. 붙이기 전 정확도가 21% 이하였고, 붙인 뒤 집계 95% 이상이 됐다고 밝혔다. 2026년 중반 시점의 자가보고 내부 평가라는 점은 감안하고 읽어야 한다. 이 사례에서 볼 것은 숫자 자체가 아니다. 모델은 그대로 두고 컨텍스트만 바꿔서 얻은 결과라는 점이다.
Pinterest도 같은 자리를 손봤다. 이들은 테이블 문서를 임베딩에 함께 넣었다. 임베딩은 문장이나 문서를 검색용 벡터로 바꿔 둔 표현을 말한다. 그렇게 바꾸자 테이블 검색 hit rate — 질문에 맞는 테이블을 후보로 집어낸 비율 — 가 40%에서 90%까지 올라갔다고 사내 text-to-SQL 구축기에 적었다.
같은 Pinterest 글에 나오는 첫 시도 채택률 계열 수치는 이후 반증됐다. 그래서 이 글이 인용하는 것은 테이블 검색 hit rate 하나뿐이다. 그리고 이 hit rate는 질문에 맞는 테이블을 골라냈는지를 재는 값이지, 최종 쿼리가 맞았는지를 재는 값이 아니다.
네 번째 부품은 거버넌스다. 셀프서비스라는 말이 데이터베이스를 전부 열어 준다는 뜻은 아니다. 여기를 비워 두면 어떻게 되는지 보여주는 사례가 이미 하나 나와 있다.
참조 구현으로 쓰이던 Postgres MCP 서버는 들어온 쿼리를 BEGIN TRANSACTION READ ONLY로 감싸는 것만으로 읽기 전용을 보장하려 했다. 문장 자체는 검사하지 않았다. 그래서 COMMIT; DROP SCHEMA public CASCADE;처럼 문장을 이어 붙이면 읽기 전용 트랜잭션이 먼저 닫히고 뒤 문장이 그대로 실행됐다. 이 서버는 이후 폐기됐으니 지금 쓸 물건으로 볼 것은 아니고, 교훈으로만 남는다 — 감싸는 것과 검사하는 것은 다른 일이다.
반대편에 놓을 만한 것이 Snowflake의 관리형 MCP 권한 모델이다. 2026년 중반 기준으로 문서에 적힌 모양은 이렇다. MCP 서버에 대한 USAGE 권한은 연결과 도구 발견까지만 허용한다. 도구는 하나씩 따로 준다 — Cortex Agents에는 USAGE, Semantic Views에는 SELECT. 거기에 문장 유형별로 허용·차단을 켜고 끄고, 그 위에 역할 기반 접근제어가 다시 얹힌다. 최소권한을 층으로 쌓아 둔 구조다.
마지막으로 정리해 둘 오해가 하나 있다. 벡터 데이터베이스와 RAG(Retrieval-Augmented Generation)는 시맨틱 레이어를 대신하지 않는다. 검색으로 끌어온 문서를 모델 답변의 근거로 붙여 주는 것이 RAG이고, 지표의 정의를 코드로 고정해 두는 것이 시맨틱 레이어다. 둘은 서로 다른 자리를 맡는다. 정형 데이터를 지표로 묻는 일은 시맨틱 레이어가, 비정형 문서를 뒤지거나 수백 개 테이블 중 후보를 좁히는 일은 검색이 맡는다. Snowflake가 Cortex Analyst(정형)와 Cortex Search(비정형)를 아예 다른 제품으로 가른 것이 이 역할 분담을 그대로 보여준다.
경계 지도
네 부품을 다 깔고 나면 자연어 분석에 무엇을 맡길 수 있는지가 꽤 또렷해진다. 정의된 지표 안에 있는 질문은 맡길 수 있다. Anthropic, Uber, Pinterest, Snowflake는 서로 다른 회사이고 각자 다른 이름을 붙였지만 도달한 모양은 같았다. 질문이 닿을 범위를 먼저 좁히고, 정의는 권한이 관리되는 층에 모으고, 검색으로 후보 테이블을 줄인다.
경계 바깥도 정직하게 그려 둬야 한다. 시맨틱 레이어에 선언되지 않은 질문, 그러니까 아무도 물어본 적 없는 ad-hoc 질문은 여전히 사람 몫이다. 정의 밖 질문을 시스템이 얼마나 잘 되돌려 보내는지는 공개된 수치가 아직 없다시피 하다. 커버리지는 정의를 얼마나 넓게 깔아 뒀느냐로 정해지고, 정의를 넓히는 일 자체는 자동화되지 않는다. 데이터 조직의 일이 없어지는 게 아니라, 티켓을 처리하는 일에서 정의를 관리하는 일로 옮겨 가는 셈이다.
그래서 셀프서비스 분석 데모를 볼 때 물어볼 것은 정확도 숫자가 아니다. 틀리면 어떤 모양으로 틀리는지, 그리고 ‘활성 사용자’ 같은 정의가 어디에서 관리되는지. 이 두 가지를 물으면 화면 밑에 깔린 구조가 그대로 드러난다. 답이 "모델이 알아서 잘합니다"라면, 그 시스템은 아직 데모다.
더 읽기
- dbt, 시맨틱 레이어와 text-to-SQL 벤치마크 비교: https://docs.getdbt.com/blog/semantic-layer-vs-text-to-sql-2026
- Anthropic, 셀프서비스 데이터 분석 구축 기록: https://claude.com/blog/how-anthropic-enables-self-service-data-analytics-with-claude
- Datadog Security Labs, Postgres MCP 서버의 SQL 인젝션 사례 분석: https://securitylabs.datadoghq.com/articles/mcp-vulnerability-case-study-SQL-injection-in-the-postgresql-mcp-server/