본문 바로가기
Data Analysis/AI

Protégé에서 프로덕션까지: Python으로 온톨로지 통합하기 (Pankaj Kumar)

by Hagrid 2026. 4. 12.
반응형

요약 정리

핵심 내용 (TL;DR)

  • Owlready2 라이브러리를 사용하면 OWL 온톨로지를 Python 네이티브 객체처럼 다룰 수 있다. Protege에서 설계한 온톨로지를 Python에 로드하고, HermiT 추론기를 돌려 자동 분류까지 수행한 뒤, Flask REST API로 서빙하는 전체 파이프라인을 보여준다.
  • "DineWise"라는 레스토랑 추천 앱 시나리오를 통해, if/else 없이 추론기가 자동으로 채식 요리를 분류하고, 이를 API 엔드포인트로 노출하는 실용적 패턴을 다룬다.

주요 포인트

온톨로지와 Python 통합

  • Owlready2는 OWL 온톨로지를 Python 클래스/인스턴스로 매핑해줌
  • RDF/XML, OWL/XML, NTriples 포맷 자동 인식
  • SQLite3 기반 최적화된 쿼드스토어로 대용량 온톨로지도 처리 가능

DineWise 시나리오

  • Restaurant, Dish, Cuisine 등의 온톨로지 클래스 정의
  • MargheritaPizza를 VegetarianDish로 자동 분류하는 예시
  • 규칙 기반 분류가 하드코딩 로직을 대체

기술 스택과 구현

  • Owlready2로 .owl 파일 로드 → 데이터 프로그래밍 방식 추가 → HermiT 추론기 실행 → Flask API 4개 엔드포인트 구축
  • /restaurants, /restaurants/, /recommend, /dishes/vegetarian

프로덕션 패턴

  • 추론 결과 캐싱
  • 인덱스 구축으로 조회 성능 향상
  • 추론 범위 제한으로 리소스 관리

핵심 인사이트

  • "온톨로지는 단순한 문서가 아니라, 실제 애플리케이션을 구동하는 실행 가능한 지식이다."

Owlready2로 OWL 온톨로지를 Python API로 서빙하는 법

if/else를 수십 개 쌓아서 "이건 채식 요리고, 저건 아니고"를 분류해본 적 있는가? 조건이 20개를 넘어가면 유지보수가 지옥이 된다. 온톨로지 추론기는 이 문제를 근본적으로 다르게 푼다.

Pankaj Kumar가 Medium에 올린 글은 Protege에서 설계한 OWL 온톨로지를 Python으로 가져와서, 추론까지 돌리고, Flask API로 서빙하는 전 과정을 코드와 함께 보여준다. "DineWise"라는 레스토랑 추천 앱이 예제다.

Owlready2가 뭔데?

Owlready2는 OWL 2.0 온톨로지를 Python 객체처럼 다루게 해주는 라이브러리다. Java 기반 OWL API(예: Apache Jena)를 써본 사람이라면, Python에서 onto.Restaurant처럼 클래스에 바로 접근할 수 있다는 게 얼마나 편한지 알 것이다.

from owlready2 import *
onto = get_ontology("file://dinewise.owl").load()

이 두 줄이면 온톨로지 전체가 Python 네임스페이스에 들어온다. 클래스, 프로퍼티, 인스턴스 전부 . 접근이 가능하다. 내부적으로는 SQLite3 기반 쿼드스토어를 써서 큰 온톨로지도 메모리 문제 없이 처리한다.

추론기가 if/else를 대체하는 방식

이 글에서 가장 흥미로운 부분은 HermiT 추론기를 돌리는 대목이다.

Protege에서 "고기 재료가 없는 요리는 VegetarianDish다"라고 정의해두면, 추론기가 알아서 MargheritaPizza를 VegetarianDish로 분류한다.

with onto:
    sync_reasoner_hermit()

이게 전부다. 별도의 분류 로직을 코드로 짤 필요가 없다. 새로운 요리가 추가되면? 추론기를 다시 돌리면 된다. 분류 기준이 바뀌면? Protege에서 온톨로지만 수정하면 된다. 코드는 건드릴 일이 없다.

물론 한계도 있다. HermiT는 Java로 작성되어 있어서 JVM이 필요하다. 추론 시간이 온톨로지 크기에 따라 급격히 늘어날 수 있고, 대규모 프로덕션에서는 추론 결과를 캐싱하거나 추론 범위를 제한하는 전략이 필수다.

 

Flask API: 4개 엔드포인트로 지식을 서빙하다

추론이 끝난 온톨로지를 Flask로 감싸면 REST API가 된다. 글에서 다루는 엔드포인트는 네 가지다:

엔드포인트 기능
/restaurants 전체 레스토랑 목록
/restaurants/<id> 특정 레스토랑 상세
/recommend 스마트 추천 (추론 기반)
/dishes/vegetarian 채식 요리 목록

 

/dishes/vegetarian이 핵심이다. 이 엔드포인트는 VegetarianDish.instances()를 호출하는데, 이 인스턴스 목록은 개발자가 수동으로 태깅한 게 아니라 추론기가 자동으로 분류한 결과다.

@app.route('/dishes/vegetarian')
def get_vegetarian_dishes():
    dishes = list(onto.VegetarianDish.instances())
    return jsonify([dish.name for dish in dishes])

 

실제 프로덕션에 쓸 수 있을까?

솔직히 말하면, 이 글의 예제를 그대로 프로덕션에 넣기는 어렵다. 글에서도 언급하는 프로덕션 패턴이 있다:

  1. 추론 결과 캐싱: 매 요청마다 추론기를 돌리면 안 된다. 서버 시작 시 한 번 돌리고 결과를 저장해야 한다.
  2. 인덱스 구축: 자주 조회하는 패턴(예: 특정 요리 타입)에 대해 딕셔너리 형태의 인덱스를 미리 만든다.
  3. 추론 범위 제한: 전체 온톨로지 대신 필요한 서브셋만 추론 대상으로 잡는다.

그래도 이 접근법이 빛나는 영역이 있다. 분류 체계가 복잡하고 자주 바뀌는 도메인 -- 의료, 법률, 식품 규제 같은 분야에서는 온톨로지 기반 추론이 하드코딩보다 유지보수 비용이 훨씬 낮다.

 

최근에는 OWLAPY 같은 새 라이브러리도 등장했고, 온톨로지와 LLM을 결합한 GraphRAG 패턴도 주목받고 있다. 온톨로지가 "학술적 도구"에서 "실용적 인프라"로 넘어가는 과도기인 셈이다.

Q: 그러면 언제 온톨로지를 쓰고, 언제 그냥 DB 스키마를 쓰는 게 나을까? 도메인 지식이 자주 바뀌고, 그 변경이 분류 로직에 영향을 주는 경우라면 온톨로지가 맞다. 단순 CRUD라면 오버엔지니어링이다.

 

원문: From Protege to Production: Integrating Your Ontology with Python - Pankaj Kumar, Jan 10, 2026


태그: #Owlready2 #Python온톨로지 #OWL추론 #FlaskAPI #지식그래프 #시맨틱웹 #HermiT추론기


한글 번역

Protege에서 프로덕션까지: Python으로 온톨로지 통합하기

서론

이전 글에서 우리는 Protege를 사용하여 DineWise 온톨로지를 구축했다 -- 레스토랑, 요리, 요리 종류(cuisine), 그리고 그것들 사이의 관계를 정의했다. 우리는 클래스를 만들고, 프로퍼티를 정의하고, 도메인 전문 지식을 실행 가능한 지식으로 캡처하는 규칙을 추가했다.

하지만 솔직하자. 온톨로지가 학술 도구에 갇혀 있으면 아무 의미가 없다. 진짜 마법은 그 지식을 실제 애플리케이션에 통합할 때 일어난다.

이 글에서 우리는 그 다리를 건넌다. 우리의 Protege 온톨로지를 가져와서 Python 기반 REST API로 변환할 것이다. 이 API는 추론된 지식(inferred knowledge)을 사용하여 스마트한 레스토랑 추천을 제공한다.

왜 Python인가?

"왜 Java가 아닌가?"라고 물을 수 있다. 결국 대부분의 온톨로지 도구는 Java 생태계(Apache Jena, OWL API 등)를 위해 만들어졌다.

답은 간단하다: Owlready2

Owlready2는 게임 체인저(역주: 판도를 바꾸는 도구)다. 이것은 OWL 온톨로지를 네이티브 Python 객체처럼 다룰 수 있게 해주는 Python 라이브러리다. Java 브릿지도, 복잡한 설정도, XML 파싱도 필요 없다. Python답게 import하고, 온톨로지를 로드하고, 마치 원래 Python 코드인 것처럼 작업하면 된다.

환경 설정

먼저 필요한 것들:

pip install owlready2 flask

그리고 Java가 필요하다 (추론기가 Java 기반이므로):

# macOS
brew install openjdk

# Ubuntu
sudo apt-get install default-jdk

1단계: 온톨로지 로드

Protege에서 우리 DineWise 온톨로지를 OWL 파일로 내보냈다고 가정한다. 이제 Python으로 가져온다:

from owlready2 import *

# 온톨로지 로드
onto = get_ontology("file://dinewise.owl").load()

# 무엇이 들어있는지 확인
print("클래스:", list(onto.classes()))
print("프로퍼티:", list(onto.properties()))
print("개체(individuals):", list(onto.individuals()))

여기서 일어나는 일: Owlready2가 OWL 파일을 읽고 모든 것을 Python 객체로 변환한다. onto.Restaurant는 실제 Python 클래스가 된다. onto.MargheritaPizza는 인스턴스가 된다. onto.hasCuisine은 프로퍼티가 된다.

2단계: 데이터를 프로그래밍 방식으로 추가

Protege에서 모든 것을 수동으로 추가했지만, 이제 코드로 할 수 있다:

with onto:
    # 새 레스토랑 생성
    new_restaurant = onto.Restaurant("PythonBistro")
    new_restaurant.hasName = ["Python Bistro"]
    new_restaurant.hasLocation = ["Tech Valley"]

    # 새 요리 생성
    pasta = onto.Dish("PythonPasta")
    pasta.hasName = ["Python Special Pasta"]
    pasta.hasIngredient = [onto.Tomato, onto.Basil, onto.Pasta]

    # 레스토랑에 연결
    new_restaurant.servesDish = [pasta]

with onto: 블록을 주목하라. 이것은 Owlready2에게 모든 변경이 이 특정 온톨로지에 속한다고 알려준다. 이것 없이는 Owlready2가 변경 사항을 어디에 저장해야 할지 모른다.

3단계: 추론기 실행

여기서 마법이 일어난다. 우리의 온톨로지에는 "고기 재료가 없는 요리는 VegetarianDish다"와 같은 규칙이 있다. 추론기를 돌려서 이 규칙들을 적용한다:

with onto:
    sync_reasoner_hermit()

이 한 줄이 HermiT 추론기를 실행한다. 추론기는:

  • 모든 규칙과 공리(axiom)를 확인
  • 논리적 결론을 도출
  • 온톨로지를 추론된 사실로 업데이트

예를 들어, MargheritaPizza가 토마토, 모짜렐라, 바질만 재료로 가지고 있다면 (고기 없음), 추론기가 자동으로 VegetarianDish로 분류한다. if/else가 필요 없다. 논리가 온톨로지에 내장되어 있다.

4단계: Flask REST API 구축

이제 이 지식을 API로 노출한다:

from flask import Flask, jsonify, request
from owlready2 import *

app = Flask(__name__)

# 앱 시작 시 온톨로지 로드 및 추론
onto = get_ontology("file://dinewise.owl").load()
with onto:
    sync_reasoner_hermit()

@app.route('/restaurants')
def get_restaurants():
    restaurants = list(onto.Restaurant.instances())
    return jsonify([{
        'name': r.hasName[0] if r.hasName else r.name,
        'location': r.hasLocation[0] if r.hasLocation else None,
        'cuisine': [c.name for c in r.hasCuisine] if r.hasCuisine else []
    } for r in restaurants])

@app.route('/restaurants/<id>')
def get_restaurant(id):
    restaurant = onto.search_one(iri="*" + id)
    if not restaurant:
        return jsonify({'error': 'Not found'}), 404
    return jsonify({
        'name': restaurant.hasName[0] if restaurant.hasName else restaurant.name,
        'dishes': [d.name for d in restaurant.servesDish] if restaurant.servesDish else []
    })

@app.route('/recommend')
def recommend():
    cuisine_type = request.args.get('cuisine')
    vegetarian = request.args.get('vegetarian', 'false').lower() == 'true'

    restaurants = list(onto.Restaurant.instances())
    results = []

    for r in restaurants:
        dishes = r.servesDish if r.servesDish else []
        if vegetarian:
            # 추론된 지식 사용!
            dishes = [d for d in dishes if onto.VegetarianDish in d.is_a]
        if cuisine_type:
            # 요리 종류 필터
            if any(c.name.lower() == cuisine_type.lower() for c in (r.hasCuisine or [])):
                results.append(r)
        else:
            if dishes:
                results.append(r)

    return jsonify([r.name for r in results])

@app.route('/dishes/vegetarian')
def get_vegetarian_dishes():
    # 이것이 핵심 -- 추론기가 자동 분류한 결과
    dishes = list(onto.VegetarianDish.instances())
    return jsonify([{
        'name': d.hasName[0] if d.hasName else d.name,
        'ingredients': [i.name for i in d.hasIngredient] if d.hasIngredient else []
    } for d in dishes])

if __name__ == '__main__':
    app.run(debug=True, port=5000)

스마트 추천이 작동하는 방식

/recommend?vegetarian=true 엔드포인트를 주목하라. 우리는 "이 요리가 채식인지" 확인하기 위해 재료 목록을 하드코딩하지 않는다. 대신 onto.VegetarianDish in d.is_a를 확인한다. 이것은 추론기가 이미 알아낸 사실이다.

이것이 온톨로지 기반 접근의 힘이다:

  • 새 요리 추가? 추론기가 알아서 분류한다
  • "비건"이라는 새 카테고리 추가? 온톨로지에 규칙 하나만 추가하면 된다
  • 비즈니스 로직 변경? 코드가 아닌 온톨로지를 수정한다

프로덕션 패턴

이것을 프로덕션에 가져갈 때 고려할 점:

추론 결과 캐싱:

# 나쁜 예: 매 요청마다 추론
@app.route('/dishes')
def get_dishes():
    sync_reasoner_hermit()  # 느림!
    ...

# 좋은 예: 시작 시 추론, 결과 캐싱
reasoned = False
def ensure_reasoned():
    global reasoned
    if not reasoned:
        with onto:
            sync_reasoner_hermit()
        reasoned = True

인덱스 구축:

# 자주 접근하는 데이터에 대한 인덱스
dish_index = {}
for dish in onto.Dish.instances():
    dish_index[dish.name] = {
        'type': [c.name for c in dish.is_a],
        'ingredients': [i.name for i in dish.hasIngredient or []]
    }

추론 범위 제한:

# 전체 온톨로지 대신 특정 클래스만 추론
with onto:
    sync_reasoner_hermit(infer_property_values=True,
                          infer_data_property_values=True)

결론

당신의 온톨로지는 단순한 문서가 아니다. 그것은 실제 애플리케이션을 구동할 수 있는 실행 가능한 지식이다.

이 튜토리얼에서 우리는:

  1. Owlready2로 OWL 온톨로지를 Python에 로드했다
  2. 프로그래밍 방식으로 데이터를 추가했다
  3. HermiT 추론기로 자동 분류를 수행했다
  4. Flask REST API로 추론된 지식을 서빙했다

GitHub 컴패니언 코드: github.com/cloudbadal007/from-protege-to-production-python


관련 자료 및 링크

원문 링크

관련 자료

반응형

댓글