개념 해설 · 2026-09-01

OWL을 꼭 Reasoner로 써야 할까?

세법 온톨로지에서 공리와 SPARQL, Python의 실행 경계를 에이전트에게 물으며 다시 그린 기록

ontologyknowledge-graphretrieval

Agent에게 이렇게 물었다. “Property Chain을 쓸 거면 Pellet이나 HermiT를 운영 경로에 넣어야 하나?”

질문의 출발은 단순했다. 거래 하나에서 양도인과 그 배우자를 찾고 싶었다. 거래 TX001이 PersonA를 양도인으로 가리키고, PersonA가 PersonB의 배우자라는 사실이 있다. 그래프에서 두 간선만 따라가면 되는 일처럼 보였다.

그런데 이 경로를 그때그때 SPARQL로 쓰는 것과, “양도인의 배우자”라는 관계를 OWL 공리로 선언하는 것은 같은 일이 아니었다. Agent의 답도 여기에서 갈렸다. 결과가 같은지보다, 다음 사람이 그 관계를 무엇으로 이해하게 될지가 다르다는 것이었다.


한 경로를 찾는 일과 한 관계를 선언하는 일

처음 적어 본 사실은 이 정도였다.

TX001  hasTransferor  PersonA
PersonA spouseOf      PersonB

SPARQL에서는 양도인 변수 하나를 거쳐 배우자를 찾으면 된다.

SELECT ?spouse WHERE {
  :TX001 :hasTransferor ?transferor .
  ?transferor :spouseOf ?spouse .
}

이 질의는 현재 그래프에서 조건에 맞는 경로를 찾는다. 조회하는 쪽이 “양도인의 배우자”라는 의미를 알고 있다면 충분하다. 다음 화면이나 다음 규칙도 같은 의미가 필요하면, 같은 두 패턴을 또 쓴다.

OWL에서는 다른 선택을 할 수 있다. hasTransferor와 spouseOf가 이어진 경로는 hasTransferorSpouse라는 관계를 뜻한다고 TBox에 선언한다.

hasTransferor ∘ spouseOf → hasTransferorSpouse

OWL 2의 property chain은 이처럼 여러 object property를 잇는 경로가 어떤 상위 property를 함의하도록 표현한다. Reasoner가 이 공리를 해석하면 TX001과 PersonB 사이에 hasTransferorSpouse라는 파생 관계를 얻을 수 있다. W3C Primer가 부모 관계 두 개로 조부모 관계를 선언하는 예를 드는 이유도 같다.

표면적으로는 새 triple 하나가 생긴다. 하지만 내가 더 크게 본 차이는 triple의 개수가 아니었다. “양도인의 배우자”가 화면 쿼리의 우연한 조합이 아니라, 여러 규칙과 설명이 함께 참조할 수 있는 도메인 관계가 된다.


Agent는 “그 공리를 누가 해석하나”를 되물었다

여기서 다시 물었다. “그럼 Reasoner가 없으면 저 선언은 장식인가?”

아니다. 다만 실행 계약이 달라진다. Reasoner를 붙이면 OWL 의미론을 지원하는 엔진이 공리에서 파생 사실을 만든다. 반대로 Python이 TBox에서 허용한 property chain 목록을 읽고 SPARQL traversal을 수행한다면, Python이 그 선택된 공리들을 실행한다.

둘 다 관계를 따라가고, 둘 다 결과를 만든다. 그래서 둘을 “추론과 비추론”으로 나누면 실제 차이를 놓친다. 하나는 OWL 의미론을 Reasoner에게 맡기는 방식이고, 다른 하나는 애플리케이션이 지원 범위를 정해 명시적으로 실행하는 방식이다.

이 차이는 이름을 정확히 붙일 때 드러난다. Python 엔진이 property chain 두 개와 inverse relation 몇 개를 읽는다고 해서 그 엔진이 곧 일반 OWL Reasoner가 되는 것은 아니다. 클래스 동치, 카디널리티, disjointness, open-world 가정, property hierarchy의 제약까지 어떤 공리를 어느 정도 지원하는지 별도 계약이 필요하다. “OWL을 실행한다”보다 “이 엔진은 이 TBox의 이 공리 집합을 해석한다”가 정직한 설명이다.

그 계약이 없으면 위험한 일이 생긴다. TBox에는 owl:equivalentClass를 추가했는데 Python 엔진은 property chain만 읽을 수 있다. 설계자는 자동 분류된다고 믿고, 실행기는 그 공리를 지나친다. 조용한 누락이다.


거래 분류는 관계 하나로 끝나지 않았다

가상의 거래를 조금 더 채워 보자.

TX001
  hasTransferor → PersonA
  hasTransferee → PersonB
  hasAsset → ApartmentA
  hasDebt → LoanA

PersonA relativeOf PersonB

관계만 놓고 보면 hasTransferor ∘ relativeOf → hasRelatedParty 같은 property chain을 생각할 수 있다. 이 관계가 존재하는 Transfer를 RelatedPartyTransfer로 분류하는 class expression도 가능하다. 거래가 어떤 법적 맥락을 가질 수 있는지 후보를 넓히는 데는 꽤 좋은 언어다.

하지만 세법 온톨로지에서 그 다음 문장은 거의 항상 숫자나 시간으로 넘어갔다. 채무를 실제로 누가 인수했는지. 자산가액은 얼마인지. 양도가액이 시가의 일정 비율 아래인지. 보유기간은 며칠인지. 시행일 전후인지. 앞 거래와의 간격은 몇 개월인지.

SPARQL도 FILTER와 식으로 값을 비교할 수 있다. 그렇다고 세액 계산, 반올림, 기간 산정, 예외의 우선순위까지 한 쿼리나 OWL 공리로 묶는 편이 좋은지는 별개 문제였다. korean_tax는 임계값과 세율을 KB에 두되, 비교와 산술은 family-wealth의 reasoning 코드가 맡도록 경계를 두었다. 이 선택은 OWL이 약해서라기보다, 숫자 계산의 입력·중간값·예외를 테스트와 trace로 남겨야 했기 때문이다.


sameAs는 분류의 지름길이 아니었다

처음 OWL을 읽을 때는 owl:sameAs가 눈에 잘 들어왔다. URI 두 개가 같은 실체라고 선언하면 데이터를 합칠 수 있다. 하지만 거래를 특수관계인 거래로 읽는 문제에서 이 기능은 중심이 아니었다.

sameAs는 identity resolution의 선택이다. EntityA와 EntityB가 동일한 개인이라고 선언하면, 두 IRI가 가리키는 모든 사실을 같은 개인에 관한 사실로 받아들이게 된다. 별칭이나 이름이 비슷하다는 이유만으로 느슨하게 붙일 수 있는 관계가 아니다.

반면 “양도인의 배우자”, “특수관계인에 해당하는 당사자”, “채무를 인수한 수증자”는 동일성의 문제가 아니라 관계와 조건의 문제다. 이때 먼저 보는 것은 rdfs:subPropertyOf, owl:inverseOf, property chain, restriction, class expression이다. 자연인 A와 법인 B가 존재한다는 사실 자체는 약하다. 누가 누구의 배우자인지, 누가 법인을 지배하는지, 거래의 어느 자리에 들어왔는지가 법적 질문을 만든다.

그래서 sameAs를 중심으로 한 모델보다 property와 class가 만나는 자리를 먼저 보게 됐다. 동일 인물을 찾아야 할 때는 identity resolution의 근거를 따로 관리하고, 거래 유형을 찾을 때는 관계의 방향과 사건 구조를 읽는다. 두 문제를 한 predicate로 섞지 않는 편이 나중에 설명하기도 쉽다.


Reasoner의 결과보다 trace가 먼저 필요했던 이유

Reasoner가 TX001 rdf:type RelatedPartyDebtAssumptionGift를 붙여 주면 편리하다. 하지만 세무 서비스에서 바로 이어지는 질문은 “왜?”다.

양도인은 누구였는가. 양수인은 누구였는가. 어떤 관계가 확인됐는가. 채무 인수는 계약서와 금융기관 자료에서 확인됐는가. 기준일과 자산가액은 무엇인가. 어느 TaxRule이 후보가 되었고, 어떤 조건이 충족되지 않았는가.

이 질문을 받으면 파생 class 하나만으로는 부족하다. Rule pipeline은 후보 검색과 적용성 평가를 나누고, 각 단계의 입력과 결과를 남긴다. 예를 들어 relationship resolution이 spouseOf를 찾았다는 사실, transaction pattern이 부담부증여 후보라는 사실, 계산기가 채무액과 시가를 어떤 값으로 읽었는지가 차례로 남는다.

여기서 OWL은 사라지지 않는다. TBox는 requiresRelationship, requiresTransactionPattern, appliesToAsset처럼 어떤 사실이 규칙에 필요한지 말한다. SPARQL은 그 사실을 그래프에서 찾는다. Python은 숫자와 시간 조건을 평가한다. Trace는 그 셋을 한 판정의 근거로 다시 묶는다.

Raw facts
  → RDF graph
  → TBox가 선언한 관계·개념
  → SPARQL 후보 탐색
  → Python 조건·계산 평가
  → Applicable / Not applicable / Insufficient facts
  → Explanation trace

이 파이프라인은 Reasoner를 배제하자는 설계가 아니다. 반복되는 관계 추론이 충분히 안정되고, 공리 지원 범위가 테스트되며, 파생 사실을 어떤 근거로 저장할지 정해진다면 Reasoner를 한 단계에 넣을 수 있다. 다만 결과 class만 남기고 중간의 판단 경로를 버리는 방식은 세법 시스템과 잘 맞지 않았다.


OWL을 실행기가 아니라 의미의 정본으로 볼 때

Agent에게 마지막으로 물었다. “그럼 OWL은 결국 문서인가, 실행 규칙인가?”

둘 중 하나로 고르면 답이 작아진다. OWL은 Person, Asset, Transfer, TaxRule이 무엇을 뜻하고 어떤 관계가 어떤 관계를 함의하는지 선언하는 의미의 정본이 될 수 있다. Reasoner는 그 정본을 실행하는 한 가지 방식이다. SPARQL과 Python rule engine도, 지원 범위를 분명히 할 수 있다면 다른 실행 방식이 될 수 있다.

내가 만들고 있는 세법 온톨로지에서는 이 구분이 오히려 편했다. RDF에는 사건 사실을 둔다. OWL에는 반복되는 개념과 관계의 뜻을 둔다. SPARQL에는 그래프에서 찾아야 할 질문을 둔다. Python에는 비교·산술·우선순위 같은 결정 절차를 둔다. NLI나 LLM은 자연어 조항에서 후보 조건을 읽는 보조 층으로 남긴다. 어느 층도 다른 층의 일을 몰래 대신하지 않게 하는 편이 중요했다.

그래서 질문도 조금 바뀌었다. “OWL을 얼마나 많이 추론하게 만들까?”보다 먼저 물을 일은 이것이다. 이 관계가 도메인의 뜻이라면 TBox에 둘 준비가 되었는가. 이 계산이 법적 결론을 바꾼다면 코드와 테스트와 trace에서 다시 읽을 수 있는가. 그리고 이 실행기가 TBox의 무엇을 실제로 해석하는가.


참고와 범위

OWL 2의 property chain, object property, class expression의 표현 범위는 W3C의 OWL 2 Primer와 OWL 2 Structural Specification를 참고했다. SPARQL의 graph pattern, property path, 표현식은 SPARQL 1.1 Query Language를 참고했다.

이 글의 TX001, PersonA·PersonB, 자산·채무는 설계 논의를 위한 가상 사례다. 여기서 말하는 분업은 korean_tax와 family-wealth를 만들며 채택한 현재의 설계 경계이며, 모든 OWL 프로젝트나 개별 세무 판단에 대한 일반 규칙은 아니다.