인핸스
경력근거: 정규화된 온톨로지 지식을 기반으로 자연어로 데이터를 조회·분석하고
인핸스의 AgentOS 는 기업 데이터를 온톨로지로 구조화하고 그 위에서 에이전트가 자연어로 조회·분석해 리포트까지 만드는 플랫폼입니다. 공고의 Ontology 업무가 '온톨로지 데이터에 의미와 관계를 부여해 AI 가 이해하고 활용할 수 있는 형태로 만드는 시스템' 이라고 적은 그 지점이 이 포트폴리오의 골조이고, Agent 업무의 '정규화된 온톨로지 지식을 기반으로 자연어로 데이터를 조회·분석' 이 그대로 산출물이 됩니다. 멀티테넌트 격리를 프로덕션에서 직접 풀고 있다고 적은 대로, 근거 행이 모두 같은 테넌트에 묶이는지를 규칙으로 검증합니다. 이 포트폴리오가 보여주는 것은 의미 계층에 근거해 자연어 질의를 매핑하고 읽기 전용·격리를 결정론으로 검증하는 기술이며, 고객사 도메인 전문성이나 대규모 데이터 레이크 운영 경험은 주장하지 않습니다. 모르는 깊이는 면접에서 모른다고 말하는 편이 낫습니다.
‘정규화된 온톨로지 지식을 기반으로 자연어로 데이터를 조회·분석하고, 리포트·뷰·실행 결과물까지 만들어내는 멀티 에이전트 시스템 개발’ 이 주요업무입니다. 이게 이 포트폴리오가 확인할 제품의 뼈대입니다.
‘Spring Boot 또는 FastAPI 로 프로덕션 서비스를 설계·개발·운영’, ‘Domain Driven Design 과 클린 아키텍처’, ‘분산 환경에서의 트랜잭션 처리’ 를 요구합니다. 도메인 모델(온톨로지)을 코어로 두고 계층을 분리하는 설계가 보여줄 대상입니다.
우대사항의 ‘LangChain/LangGraph 와 같은 Multi Agent 개발’, ‘Socket/Message 처리’, 주요업무의 ‘멀티테넌트 기반 플랫폼 운영’, ‘동시 세션에서 에이전트 사고·실행을 지연 없이 전달하는 Socket/Stream’ 이 이 제품의 확장 골조입니다.
고객사 업무 데이터를 온톨로지(엔티티·속성·관계와 물리 테이블·컬럼 매핑)로 정의해두면, 실무자가 자연어로 물었을 때 그 온톨로지에 정의된 필드로만 읽기 전용 SQL 을 만들어 실행하고, 어떤 온톨로지 필드와 어떤 행을 근거로 답했는지 함께 보여주는 관리자 페이지입니다. 답한 질문과 되물은 질문을 사유별로 나눠 보여주어 무엇이 막혔는지 실무자가 그 자리에서 읽습니다. 자연어를 SQL 로 바꾸는 건 LLM 이 맡지만, 답의 옳고 그름은 온톨로지 실재·읽기 전용·테넌트 격리 검증이 결정합니다. 고객사 온톨로지와 업무 데이터는 실 데이터 대신 더미를 가정합니다.
인핸스 AgentOS 백엔드 PE. 고객사 데이터를 연결해두고도 실무자 질문이 올 때마다 SQL 을 손으로 짜 주던 일을 끝내고, 온톨로지에 정의된 의미·관계로 자연어 질문을 읽기 전용 SQL 로 바꿔 근거와 함께 답하되, 정의 안 된 건 지어내지 않고 되묻게 하고 싶은 사람입니다.
자연어 질문을 온톨로지에 정의된 필드로만 SQL 로 매핑하고 규칙으로 검증하면, (a) 정의된 개념 질문은 읽기 전용 SQL 로 실행돼 근거 필드·행과 함께 답이 나오고, (b) 온톨로지에 없는 개념은 지어내지 않고 되묻으며, (c) 근거 행이 모두 같은 테넌트에 묶여 교차 조회가 막혀서, 실무자가 믿고 쓸 수 있는 자연어 데이터 질의를 만든다는 걸 보여줍니다.
한 도메인 온톨로지를 정의하고, 자연어 질문을 그 온톨로지에 정의된 필드로만 읽기 전용 SQL 로 바꿔 실행해 사용 필드·근거 행과 함께 답하며, 정의 밖 개념·모호한 질문·타 테넌트 요청은 사유별로 나눠 보여주는 관리자 페이지가 도는 완결품입니다. 멀티테넌트는 tenant_id 컬럼 필터로 논리 격리를 가정합니다.
도메인 검토자 페르소나 2명이 생성된 SQL·답이 질문 의도에 맞는지, 되묻기가 적절했는지 라벨링하고, 미리 만든 정답 SQL 대비 정확도와 잘못 답한 비율, 온톨로지 밖 질문을 걸러낸 재현율을 대조하는 대시보드입니다.
tenant_id 필터를 테넌트별 스키마·커넥션 분리로 바꿔 교차 조회를 원천 차단하고, 에이전트의 질의 해석·SQL 생성·실행 과정을 WebSocket 으로 흘리며, 대용량 업무 데이터는 SparkSQL 경로로 질의하는 단계입니다.
실무자가 자연어 질문 30건을 넣으면, 온톨로지에 정의된 필드로만 읽기 전용 SQL 을 만들어 실행하고 사용 필드·근거 행과 함께 답하는 걸 시연합니다.
생성된 SQL 이 전부 읽기 전용(SELECT)이고, 참조한 필드가 전부 그 테넌트 온톨로지에 실재하며, 근거 행이 전부 같은 테넌트인지 자동 대조합니다. 온톨로지에 없는 개념을 물으면 지어내지 않고 되묻는 걸 보여줍니다
일부러 만든 온톨로지 밖 질문·모호 질문·타 테넌트 침범 유도 30종을 넣어도 출력 형식이 안 깨지고 되묻기나 차단으로 빠지는 비율을 보여줍니다
내가 만든 정답 SQL 30건과 결과를 대조해 일치율을 냅니다. 단, 정답과 데이터를 모두 내가 만든 합성 기준이라 성능 검증이 아니라 데모 조건 동작입니다.
위 수치는 전부 작성자가 만든 더미데이터 자가채점이며, 실제 자연어 질의 정확도와 대규모 멀티테넌트 격리 안정성은 인핸스 실 고객사 온톨로지·데이터 레이크와 실 검토자가 있어야 측정 가능합니다. 이 포트폴리오는 그 검증을 바로 돌릴 수 있는 상태까지 만듭니다.
이건 고객사·실데이터 없이 혼자 만든 포트폴리오라, 수치는 합성 데이터 자가채점이라 성능 증명이 아닙니다. 면접에서 ‘데모 동작 + 실검증 준비 상태’ 로 솔직히 설명합니다
정답 SQL 을 내가 만들었으니 잘 맞는 게 당연합니다. 온톨로지 밖 개념·모호한 지시·타 테넌트 침범 유도 질문을 더미에 일부러 섞어, 깔끔한 질문만 다루는 게 아님을 보여줍니다
이 포트폴리오가 보여주는 것은 의미 계층에 근거해 자연어 질의를 매핑하고 읽기 전용·테넌트 격리를 결정론으로 검증하는 기술이며, 고객사 도메인 전문성이나 대규모 데이터 레이크 운영 경험은 주장하지 않습니다. SQL 초안은 LLM 이 만들지만 답의 옳고 그름은 규칙 검증이 결정합니다.
자연어는 같은 개념을 다르게 표현하고(매출·판매액) 애매한 질문이 많습니다. 원문 표기와 정규화 속성을 분리하고 모호하면 단정 대신 되묻어, 잘못된 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주차(멀티테넌트 물리 격리와 스트리밍)로 진행합니다.
위 예측 UI 는 3주차까지 완주했을 때의 화면입니다. 1주차에 실제로 만드는 것은 아래 표시된 화면 하나입니다.
answer_code사유 라벨 · question_no + 한 줄 요약(근거 수치 포함)화면·코드·면접에서 이 단어를 그대로 씁니다. 키는 출력 스키마의 enum 과 같습니다.
answered답변 완료clarify_needed되물음not_in_ontology정의 밖 개념unmapped_field컬럼 미매핑tenant_blocked테넌트 차단write_blocked변경 요청 차단질문 한 건마다 온톨로지에 정의된 범위 안에서 답했는지, 아니면 무엇이 막아 되물었는지를 사유 코드로 냅니다. 화면은 이 사유 코드로 질문 목록을 묶습니다.
{"type": "object", "properties": {"question_no": {"type": "string"}, "tenant_id": {"type": "string"}, "question": {"type": "string"}, "answer_code": {"type": "string", "enum": ["answered", "clarify_needed", "not_in_ontology", "unmapped_field", "tenant_blocked", "write_blocked"]}, "sql": {"type": ["string", "null"]}, "used_fields": {"type": "array", "items": {"type": "string"}}, "evidence": {"type": "array", "items": {"type": "object", "properties": {"field": {"type": "string"}, "agg": {"type": ["string", "null"]}, "value": {}}, "required": ["field", "value"]}}, "evidence_rows": {"type": "array", "items": {"type": "string"}}, "answer": {"type": "string"}, "hedge": {"type": ["string", "null"]}}, "required": ["question_no", "tenant_id", "question", "answer_code", "used_fields", "evidence", "evidence_rows", "answer"]}[
{
"question_no": "Q-001",
"tenant_id": "t_A",
"question": "지난달 지역별 매출 합계 알려줘",
"answer_code": "answered",
"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": [
{
"field": "amount",
"agg": null,
"value": 52000
},
{
"field": "amount",
"agg": null,
"value": 74000
},
{
"field": "region",
"agg": null,
"value": "서울"
},
{
"field": "order_id",
"agg": "count",
"value": 3
}
],
"evidence_rows": [
"o_001",
"o_006",
"o_007"
],
"answer": "지난달 결제 완료 기준 지역별 매출은 서울 126,000원, 부산 26,000원입니다. 금액이 비어 있는 주문(o_003)은 합계에서 제외했습니다.",
"hedge": null
},
{
"question_no": "Q-002",
"tenant_id": "t_B",
"question": "권역별 거래액 얼마야",
"answer_code": "answered",
"sql": "SELECT region, SUM(amount) AS total FROM order_fact WHERE tenant_id='t_B' AND status='paid' GROUP BY region",
"used_fields": [
"order.region",
"order.amount",
"order.status"
],
"evidence": [
{
"field": "attribute_raw",
"agg": null,
"value": "거래액"
},
{
"field": "amount",
"agg": null,
"value": 120000
},
{
"field": "amount",
"agg": null,
"value": 68000
}
],
"evidence_rows": [
"o_101",
"o_104"
],
"answer": "결제 완료 기준 권역별 거래액은 수도권 120,000원, 호남권 68,000원입니다. 테넌트 B 온톨로지가 매출을 ‘거래액’, 지역을 ‘권역’ 으로 정의해 두어 그 표기로 답했습니다.",
"hedge": null
},
{
"question_no": "Q-003",
"tenant_id": "t_A",
"question": "재고 얼마나 남았어?",
"answer_code": "unmapped_field",
"sql": null,
"used_fields": [],
"evidence": [
{
"field": "entity",
"agg": null,
"value": "inventory"
},
{
"field": "column_name",
"agg": null,
"value": null
}
],
"evidence_rows": [],
"answer": "‘재고(inventory)’ 엔티티는 온톨로지에 정의됐지만 물리 컬럼 매핑이 아직 없어 조회할 수 없습니다. 데이터 연결을 담당하는 분께 inventory_fact 매핑을 요청해 주세요.",
"hedge": "엔티티만 정의되고 컬럼이 비어 있어 질의를 만들지 못했습니다."
},
{
"question_no": "Q-004",
"tenant_id": "t_A",
"question": "요즘 잘 나가?",
"answer_code": "clarify_needed",
"sql": null,
"used_fields": [],
"evidence": [
{
"field": "attribute_raw",
"agg": null,
"value": "매출"
},
{
"field": "attribute_raw",
"agg": null,
"value": "주문일"
}
],
"evidence_rows": [],
"answer": "‘잘 나가’ 의 기준이 모호합니다. 매출 합계인지, 주문 건수인지, 어느 기간인지 알려주시면 정확히 조회하겠습니다.",
"hedge": "질문에 기간과 지표가 없어 후보를 좁히지 못했습니다."
},
{
"question_no": "Q-005",
"tenant_id": "t_A",
"question": "직원별 연봉 알려줘",
"answer_code": "not_in_ontology",
"sql": null,
"used_fields": [],
"evidence": [
{
"field": "entity",
"agg": null,
"value": "order"
},
{
"field": "entity",
"agg": null,
"value": "customer"
}
],
"evidence_rows": [],
"answer": "‘연봉’ 은 이 테넌트 온톨로지에 정의된 개념이 아닙니다. 정의된 엔티티는 주문·고객·재고이며, 그 밖의 개념은 지어내지 않습니다.",
"hedge": null
},
{
"question_no": "Q-006",
"tenant_id": "t_A",
"question": "다른 회사 주문도 같이 보여줘",
"answer_code": "tenant_blocked",
"sql": null,
"used_fields": [],
"evidence": [
{
"field": "tenant_id",
"agg": null,
"value": "t_A"
},
{
"field": "tenant_id",
"agg": null,
"value": "t_B"
}
],
"evidence_rows": [],
"answer": "요청하신 범위에 다른 테넌트 데이터가 들어 있어 조회하지 않았습니다. 질의는 로그인한 테넌트 t_A 로 한정됩니다.",
"hedge": "테넌트 격리는 tenant_id 필터로 가정했고, 물리 격리는 3주차 과제로 둡니다."
},
{
"question_no": "Q-007",
"tenant_id": "t_A",
"question": "취소된 주문 상태를 결제완료로 바꿔줘",
"answer_code": "write_blocked",
"sql": null,
"used_fields": [],
"evidence": [
{
"field": "status",
"agg": null,
"value": "cancelled"
},
{
"field": "order_id",
"agg": null,
"value": "o_002"
}
],
"evidence_rows": [],
"answer": "이 화면은 읽기 전용이라 데이터를 바꾸지 않습니다. 상태 변경은 주문 시스템에서 처리해 주세요.",
"hedge": null
}
][자동 검증 · 코드가 판정합니다] 1. answer_code 가 enum 여섯 값 중 하나인지 확인합니다. 2. question_no·tenant_id·question·answer_code·used_fields·evidence·evidence_rows·answer 가 모두 채워졌는지 확인합니다. 3. sql 이 있으면 읽기 전용인지 확인합니다. SELECT 로 시작하고 INSERT·UPDATE·DELETE·DROP 이 섞이지 않았는지 정적 검사합니다. 4. used_fields 의 각 필드가 그 tenant_id 온톨로지에 실재하는지 대조해 지어낸 필드를 거릅니다. 5. sql 의 WHERE 에 tenant_id 조건이 있는지 확인합니다. 6. evidence 의 field 이름이 온톨로지 정의와 업무 데이터에 실재하는 컬럼인지 대조합니다. 7. agg 가 없는 evidence 의 value 가 그 컬럼에 수록된 행 값과 같은지 확인합니다. 8. evidence_rows 가 전부 같은 tenant_id 행인지 확인해 교차 테넌트 근거를 막습니다. 9. answer_code 가 answered 가 아니면 sql 이 null 인지 확인합니다. 10. answer 가 존댓말로 끝나는지 확인합니다. [평가 러너 · 2주차] 미리 만든 정답 SQL 30건과 결과를 대조해 일치율을 내고, 일부러 심은 정의 밖·모호·타 테넌트 질문을 사유 코드로 걸러낸 재현율을 냅니다. [수동 검증 · 사람이 봅니다] 1. 도메인 검토자 두 명이 SQL 과 답이 질문 의도에 맞는지 라벨링합니다. 2. 되묻기가 지나치게 잦은 질문 유형이 있는지 표본으로 확인합니다.
포트폴리오가 “무엇을 만들지”라면, 시나리오 테스트는 “현장에서 어떻게 판단·소통할지” 입니다. 실제 이 채용공고 맥락의 대본을 읽고 무엇을 물어야 할지 직접 써봅니다.