이 백엔드가 보여주는 것은 온톨로지(의미 계층)에 근거한 자연어 질의 매핑, 읽기 전용 SQL 가드, 근거 필드·행 추적, 테넌트 격리 검증 기술이며, 특정 산업의 도메인 전문성을 주장하지 않습니다. 다만 이 사슬이 말이 되려면 인핸스 AgentOS 라는 맥락, 즉 기업 데이터를 온톨로지로 구조화하고 그 위에서 자연어로 조회·분석하는 Decision Intelligence 플랫폼이 있어야 의미가 있습니다. ‘정의 안 된 건 지어내지 않고 되묻는다’ 는 각도는 옮길 수 있지만, 엔티티·속성·관계를 명시한 온톨로지 형태가 있어야 질의 매핑과 근거 검증이 구체화됩니다. 실제 대규모 멀티테넌트 격리와 프로덕션 운영은 인핸스가 이미 프로덕션에서 풀고 있는 문제이고, 면접에서 모르는 부분은 모른다고 말하는 편이 낫습니다.
‘정규화된 온톨로지 지식을 기반으로 자연어로 데이터를 조회·분석하고, 리포트·뷰·실행 결과물까지 만들어내는 멀티 에이전트 시스템 개발’ 이 주요업무입니다. 이게 이 포트폴리오가 확인할 제품의 뼈대입니다.
‘Spring Boot 또는 FastAPI 로 프로덕션 서비스를 설계·개발·운영’, ‘Domain Driven Design 과 클린 아키텍처’, ‘분산 환경에서의 트랜잭션 처리’ 를 요구합니다. 도메인 모델(온톨로지)을 코어로 두고 계층을 분리하는 설계가 보여줄 대상입니다.
우대사항의 ‘LangChain/LangGraph 와 같은 Multi Agent 개발’, ‘Socket/Message 처리’, 주요업무의 ‘멀티테넌트 기반 플랫폼 운영’, ‘동시 세션에서 에이전트 사고·실행을 지연 없이 전달하는 Socket/Stream’ 이 이 제품의 확장 골조입니다.
고객사 업무 데이터를 온톨로지(엔티티·속성·관계와 물리 테이블·컬럼 매핑)로 정의해두면, 실무자가 자연어로 물었을 때 그 온톨로지에 정의된 필드로만 읽기 전용 SQL을 만들어 실행하고, 어떤 온톨로지 필드와 어떤 행을 근거로 답했는지 함께 보여주는 백엔드입니다. 자연어를 SQL로 바꾸는 건 LLM 이 맡지만, 답의 옳고 그름은 규칙으로 결정됩니다. 생성된 SQL 은 읽기 전용(SELECT)만 통과하고, 참조한 필드는 그 테넌트 온톨로지에 실재해야 하며, 근거 행은 모두 같은 테넌트여야 합니다. 온톨로지에 없는 개념이나 모호한 질문은 지어내지 않고 되묻습니다. 1주차만으로 완성품이고, 답 검증(2주차)·멀티테넌트 물리 격리와 스트리밍(3주차)로 키웁니다. 근거는 채용공고 정렬입니다.
인핸스 AgentOS 백엔드 PE. 고객사 데이터를 연결해두고도 실무자 질문이 올 때마다 SQL을 손으로 짜주던 일을 끝내고, 온톨로지에 정의된 의미·관계로 자연어 질문을 읽기 전용 SQL로 바꿔 근거와 함께 답하되, 정의 안 된 건 지어내지 않고 되묻게 하고 싶은 사람입니다.
자연어 질문을 온톨로지에 정의된 필드로만 SQL 로 매핑하고 규칙으로 검증하면, (a) 정의된 개념 질문은 읽기 전용 SQL 로 실행돼 근거 필드·행과 함께 답이 나오고, (b) 온톨로지에 없는 개념은 지어내지 않고 되묻으며, (c) 근거 행이 모두 같은 테넌트에 묶여 교차 조회가 막혀서, 실무자가 믿고 쓸 수 있는 자연어 데이터 질의를 만든다는 걸 보여줍니다.
한 도메인 온톨로지를 정의하고, 자연어 질문을 그 온톨로지에 정의된 필드로만 읽기 전용 SQL로 바꿔 실행해 사용 필드·근거 행과 함께 답하며, 온톨로지에 없는 개념·모호한 질문은 지어내지 않고 되묻는 동작하는 백엔드입니다. 멀티테넌트는 tenant_id 컬럼 필터로 논리 격리를 가정합니다(물리 격리는 3주차).
도메인 검토자 페르소나 2명이 생성된 SQL·답이 질문 의도에 맞는지, 되묻기가 적절했는지 라벨링하고, 미리 만든 정답 SQL 대비 정확도와 잘못 답한 비율, 온톨로지 밖 질문을 걸러낸 재현율을 대조하는 대시보드입니다.
tenant_id 필터를 테넌트별 스키마·커넥션 분리로 바꿔 교차 조회를 원천 차단하고, 에이전트의 질의 해석·SQL 생성·실행 과정을 WebSocket 으로 흘리며, 대용량 업무 데이터는 SparkSQL 경로로 질의하는 단계입니다.
자연어 질문 30건을 넣으면, 온톨로지에 정의된 필드로만 읽기 전용 SQL을 만들어 실행하고 사용 필드·근거 행과 함께 답하는 걸 시연합니다
생성된 SQL 이 전부 읽기 전용(SELECT)이고, 참조한 필드가 전부 그 테넌트 온톨로지에 실재하며, 근거 행이 전부 같은 테넌트인지 자동 대조합니다. 온톨로지에 없는 개념을 물으면 지어내지 않고 되묻는 걸 보여줍니다
일부러 만든 온톨로지 밖 질문·모호 질문·타 테넌트 침범 유도 30종을 넣어도 출력 형식이 안 깨지고 되묻기나 차단으로 빠지는 비율을 보여줍니다
내가 만든 정답 SQL 30건 중 결과가 일치한 게 25건(83%). 단, 정답과 데이터를 모두 내가 만든 합성 기준이라 ‘성능 검증’ 이 아니라 ‘데모 조건 동작’ 임을 밝힙니다
위 수치는 전부 작성자가 만든 더미데이터 자가채점이며, 실제 자연어 질의 정확도와 대규모 멀티테넌트 격리 안정성은 인핸스 실 고객사 온톨로지·데이터 레이크와 실 검토자가 있어야 측정 가능합니다. 이 백엔드는 그 검증을 바로 돌릴 수 있는 상태까지 만듭니다.
이건 고객사·실데이터 없이 혼자 만든 포트폴리오라, 수치는 합성 데이터 자가채점이라 성능 증명이 아닙니다. 면접에서 ‘데모 동작 + 실검증 준비 상태’ 로 솔직히 설명합니다
정답 SQL 을 내가 만들었으니 잘 맞는 게 당연합니다. 온톨로지 밖 개념·모호한 지시·타 테넌트 침범 유도 질문을 더미에 일부러 섞어, 깔끔한 질문만 다루는 게 아님을 보여줍니다
이 포트폴리오가 보여주는 것은 온톨로지 근거 질의 매핑·읽기 전용 가드·테넌트 격리 검증 기술이지 특정 산업 도메인 전문성이 아닙니다. 답 품질은 온톨로지 정의 품질에 종속되므로, 정의가 부실하면 되묻기가 늘어난다는 한계를 함께 설명합니다
자연어는 같은 개념을 다르게 표현하고(매출·판매액) 애매한 질문이 많습니다. 원문 표기와 정규화 속성을 분리하고 모호하면 단정 대신 되묻어, 잘못된 SQL 을 실행하지 않게 막습니다
실제 도입 시 달라지는 것 명시합니다. 실 고객사 온톨로지 규모·데이터 분포는 더미와 다르므로 매핑 규칙과 되묻기 임계를 다시 맞춰야 하고, 논리 격리(tenant_id 필터)를 실제 스키마·커넥션 물리 격리로 바꿔야 하며, 검토자도 도메인 실무자로 교체해야 합니다(도입 단계 과제로 분리)
① 인라인 샘플 · ② seed.json 다운로드 · ③ 증강 프롬프트. 세 층 모두 동일한 schema 에서 파생.
테넌트별 온톨로지 정의. 엔티티·속성이 어떤 물리 테이블·컬럼으로 매핑되는지 담아, 자연어 질의가 이 정의된 필드로만 SQL 을 만들 수 있게 하는 근거입니다. 물리 매핑 결측(엔티티만 있고 컬럼 미매핑)·설명 결측·원문 표기 흔들림(매출/판매액)을 일부러 섞어, 정의 밖 개념은 되묻고 표기가 흔들려도 정규화로 잡는지 보여줍니다.
| tenant_id | entity | attribute | attribute_raw | table_name | column_name | dtype | description | pii |
|---|---|---|---|---|---|---|---|---|
| t_A | order | amount | 매출 | order_fact | amount | number | 주문 결제 금액(원) | false |
| t_A | order | amount | 판매액 | order_fact | amount | number | — | false |
| t_A | order | region | 지역 | order_fact | region | string | 주문 지역 | false |
| t_A | order | status | 상태 | order_fact | status | enum | 주문 상태(paid·cancelled·refunded) | false |
| t_A | order | order_date | 주문일 | order_fact | order_date | date | 주문 발생일 | false |
아래 JSON Schema 에 맞는 테넌트별 온톨로지 정의 30행 JSON 배열로 생성합니다. 테넌트 2~3개, 엔티티 3~4종.
{"type":"object","properties":{"tenant_id":{"type":"string"},"entity":{"type":"string"},"attribute":{"type":"string"},"attribute_raw":{"type":["string","null"]},"table_name":{"type":"string"},"column_name":{"type":["string","null"]},"dtype":{"type":"string","enum":["string","number","date","enum","id"]},"description":{"type":["string","null"]},"pii":{"type":"boolean"}},"required":["tenant_id","entity","attribute","table_name","dtype","pii"]}
쉬운 데이터만 만들지 말 것. 하드 케이스 의도적 혼입: (1) column_name 이 null 인 미매핑 필드 1~2건(엔티티는 있으나 물리 매핑 미완 → 질의 시 되묻기), (2) description 이 null 인 결측 1~2건, (3) 같은 attribute 를 attribute_raw 별칭(매출/판매액/거래액)으로 다르게 표기한 표기 흔들림 2건, (4) 테넌트마다 같은 엔티티를 두어 교차 테넌트 침범 검증이 가능하게, (5) pii=true 인 필드 1~2건. JSON 배열만.{"type":"object","properties":{"tenant_id":{"type":"string"},"entity":{"type":"string"},"attribute":{"type":"string"},"attribute_raw":{"type":["string","null"]},"table_name":{"type":"string"},"column_name":{"type":["string","null"]},"dtype":{"type":"string","enum":["string","number","date","enum","id"]},"description":{"type":["string","null"]},"pii":{"type":"boolean"}},"required":["tenant_id","entity","attribute","table_name","dtype","pii"]}온톨로지가 가리키는 실제 업무 데이터. 자연어 질의가 실행돼 답이 나오는 대상입니다. 금액 결측·0원 경계·마스터에 없는 고객 orphan·상태 원문 표기 흔들림(취소/cancelled)·서로 다른 테넌트 행을 섞어, 규칙이 결측·경계에 안 깨지고 교차 테넌트 조회를 막는지 보여줍니다.
| tenant_id | order_id | customer_id | region | amount | status | status_raw | order_date |
|---|---|---|---|---|---|---|---|
| t_A | o_001 | c_A1 | 서울 | 52,000 | paid | 결제완료 | 2026-06-03 |
| t_A | o_002 | c_A2 | 부산 | 0 | cancelled | 취소 | 2026-06-05 |
| t_A | o_003 | c_A1 | 서울 | — | paid | — | 2026-06-07 |
| t_A | o_004 | c_ZZ | 대구 | 31,000 | paid | 결제완료 | 2026-06-08 |
| t_A | o_005 | — | — | 18,000 | refunded | 환불 | 2026-06-09 |
아래 JSON Schema 에 맞는 주문 업무 데이터 40행 JSON 배열로 생성합니다. 테넌트 2~3개, 며칠치.
{"type":"object","properties":{"tenant_id":{"type":"string"},"order_id":{"type":"string"},"customer_id":{"type":["string","null"]},"region":{"type":["string","null"]},"amount":{"type":["number","null"]},"status":{"type":"string","enum":["paid","cancelled","refunded"]},"status_raw":{"type":["string","null"]},"order_date":{"type":"string"}},"required":["tenant_id","order_id","status","order_date"]}
쉬운 데이터만 만들지 말 것. 하드 케이스 의도적 혼입: (1) amount 가 null 인 결측 2~3건, (2) amount 가 0 인 경계 1~2건, (3) customer_id 가 마스터에 없는 orphan 이거나 null 인 행 2건(집계 시 미매핑으로 알려야 함), (4) status_raw 를 취소/환불/cancelled 처럼 원문 표기로 흔들어 status enum 과 분리, (5) 서로 다른 tenant_id 행을 섞어 교차 테넌트 조회 차단을 검증 가능하게. JSON 배열만.{"type":"object","properties":{"tenant_id":{"type":"string"},"order_id":{"type":"string"},"customer_id":{"type":["string","null"]},"region":{"type":["string","null"]},"amount":{"type":["number","null"]},"status":{"type":"string","enum":["paid","cancelled","refunded"]},"status_raw":{"type":["string","null"]},"order_date":{"type":"string"}},"required":["tenant_id","order_id","status","order_date"]}| Python (FastAPI) | 온톨로지 해석·자연어에서 SQL 변환·읽기 전용 실행을 맡는 API 서버(자격요건 FastAPI 스택 반영) |
| PostgreSQL | 고객사 업무 데이터와 온톨로지 정의를 담는 관계형 저장소, 테넌트별 격리·인덱싱·읽기 전용 롤 분리(자격요건 반영) |
| LangGraph | 질문 해석에서 온톨로지 매핑, SQL 생성, 규칙 검증, 답변 생성까지 노드로 잇는 멀티 에이전트 그래프(우대 반영) |
| Domain Driven Design | 온톨로지(도메인 모델)를 코어로 두고 질의·실행 계층을 인프라 관심사와 분리하는 설계(자격요건 반영) |
| WebSocket | 에이전트의 질의 해석·실행 과정을 지연 없이 실무자 화면으로 흘리는 스트리밍(주요업무 Socket/Stream 반영) |
| SparkSQL | 대용량 업무 데이터 질의를 분산 실행하는 경로로 확장(우대 반영) |
✓ pass → 그대로 멈춰도 완결입니다. 더 키우려면 2주차(답이 질문 의도에 맞는지 검토)로 진행합니다.
✓ pass → 여기서 멈춰도 완결입니다. 더 키우면 3주차(멀티테넌트 물리 격리와 스트리밍)로 진행합니다.
자연어 질문 하나에 대한 질의 결과. 생성 SQL·사용한 온톨로지 필드(used_fields)·근거 행(evidence_rows)·해결 여부(resolved)·되묻기 사유(refusal_reason)를 한 스키마에 담아, 온톨로지 밖 필드를 지어내지 않았는지, SQL 이 읽기 전용인지, 근거 행이 같은 테넌트인지 객관 검증합니다.
{"type":"object","properties":{"tenant_id":{"type":"string"},"question":{"type":"string"},"resolved":{"type":"boolean"},"sql":{"type":["string","null"]},"used_fields":{"type":"array","items":{"type":"string"}},"evidence_rows":{"type":"array","items":{"type":"string"}},"answer":{"type":"string"},"refusal_reason":{"type":"string","enum":["not_in_ontology","ambiguous","unmapped_field","none"]}},"required":["tenant_id","question","resolved","used_fields","evidence_rows","answer","refusal_reason"]}[
{
"input_ref": "t_A: ‘지난달 지역별 매출 합계 알려줘’ (정의된 질문)",
"output": {
"tenant_id": "t_A",
"question": "지난달 지역별 매출 합계 알려줘",
"resolved": true,
"sql": "SELECT region, SUM(amount) AS total FROM order_fact WHERE tenant_id='t_A' AND status='paid' AND order_date >= '2026-06-01' AND order_date < '2026-07-01' GROUP BY region",
"used_fields": [
"order.region",
"order.amount",
"order.status",
"order.order_date"
],
"evidence_rows": [
"o_001",
"o_006",
"o_007"
],
"answer": "지난달 결제 완료 기준 지역별 매출은 서울 126,000원, 부산 26,000원입니다. 금액이 비어 있는 주문(o_003)은 합계에서 제외했습니다.",
"refusal_reason": "none"
}
},
{
"input_ref": "t_A: ‘재고 얼마나 남았어?’ (엔티티는 있으나 컬럼 미매핑)",
"output": {
"tenant_id": "t_A",
"question": "재고 얼마나 남았어?",
"resolved": false,
"sql": null,
"used_fields": [],
"evidence_rows": [],
"answer": "‘재고(inventory)’ 엔티티는 온톨로지에 정의됐지만 물리 컬럼 매핑이 아직 없어 조회할 수 없습니다. 데이터 연결을 담당하는 분께 inventory_fact 매핑을 요청해 주세요.",
"refusal_reason": "unmapped_field"
}
},
{
"input_ref": "t_A: ‘요즘 잘 나가?’ (모호한 질문)",
"output": {
"tenant_id": "t_A",
"question": "요즘 잘 나가?",
"resolved": false,
"sql": null,
"used_fields": [],
"evidence_rows": [],
"answer": "‘잘 나가’ 의 기준이 모호합니다. 매출 합계인지, 주문 건수인지, 어느 기간인지 알려주시면 정확히 조회하겠습니다.",
"refusal_reason": "ambiguous"
}
}
][자동 검증] 1. 출력이 스키마를 통과하는가(형식·필수 필드). 2. sql 이 있으면 읽기 전용인가(SELECT 로 시작하고 INSERT·UPDATE·DELETE·DROP 등 쓰기·DDL 이 섞이지 않았는지 정적 검사). 3. used_fields 의 각 필드가 그 tenant_id 온톨로지에 실재하는가(지어낸 필드 차단), sql 의 WHERE 에 tenant_id 조건이 있는가. 4. evidence_rows 가 전부 같은 tenant_id 행인가(교차 테넌트 근거 차단), 온톨로지 밖·모호·미매핑이면 resolved=false 이고 refusal_reason 이 그 사유로 채워지는가. [수동 검증] 1. 도메인 검토자 페르소나 2명이 SQL·답이 질문 의도에 맞는지 라벨링 → bootstrap Cohen's κ(두 사람 답이 우연 이상으로 일치하는 정도, 0~1) 하한 ≥ 0.6. 2. 정답 SQL 대비 결과 일치·거짓 응답 5% 사람 점검.