티스토리 뷰
작성일: 2026년 1월 9일
카테고리: Ontology, RDF, Semantic Web
키워드: RDF, 트리플, Turtle, 시맨틱 웹, 지식 표현
시리즈: 온톨로지 + AI 에이전트: 세무 컨설팅 시스템 아키텍처 (5부/총 20부)
대상 독자: 온톨로지에 입문하는 시니어 개발자
핵심 질문
(주어, 술어, 목적어)만으로 무엇을 표현할 수 있는가?
이 질문에 답하기 위해 이번 편에서 다루는 내용:
- 왜 관계형 테이블이 아닌 그래프인가?
- 트리플의 세 구성 요소와 URI의 역할
- 실제 재무 데이터를 트리플로 변환하는 전략
- Turtle 문법과 네임스페이스의 설계 의도
요약
RDF(Resource Description Framework)는 웹에서 데이터를 표현하는 W3C 표준이다. 모든 정보를 "주어-술어-목적어" 형태의 트리플로 표현하며, 이 트리플들이 모여 지식그래프를 형성한다. 이 글에서는 실제 세무 데이터(재무상태표, 손익계산서)를 RDF 트리플로 변환하는 과정을 다룬다. Python의 RDFLib 라이브러리를 사용하여 JSON 데이터를 RDF로 변환하고 저장하는 실습을 진행한다.
이전 내용 복습
Part A에서 배운 핵심 개념:
- 온톨로지: 지식을 구조화하는 체계 (TBox + ABox)
- 지식그래프: 트리플 기반의 그래프 데이터 구조
- AI 에이전트: 도구를 사용해 자율적으로 작업 수행
- 시스템 아키텍처: 5개 계층 (수집 → 변환 → 저장 → 분석 → 출력)
이제 Part B에서 실제로 지식 표현 기술을 구현한다. 첫 단계는 RDF다.
실제 세무 데이터 살펴보기
실습에서 사용할 데이터는 A노무법인의 실제 재무 데이터다. 먼저 원본 JSON 구조를 살펴본다.
재무상태표 (Balance Sheet)
{
"name": "2024",
"type": "BS",
"contents": {
"title": "재무상태표",
"company": "A노무법인",
"당기": "제 4기 2024년 12월 31일 현재",
"전기": "제 3기 2023년 12월 31일 현재",
"year": "2024",
"sum": {
"자산총계": [2194433171, 2579529430, 0],
"부채총계": [486333117, 1212553609, 0],
"자본총계": [1708100054, 1366975821, 0],
"유동자산": [766807888, 1220566005, 0],
"비유동자산": [1427625283, 1358963425, 0],
"유동부채": [486333117, 608248031, 0],
"비유동부채": [0, 604305578, 0]
},
"data": {
"자산": {
"유동자산": {
"당좌자산": {
"보통예금": [685741852, 1183577300, 0],
"외상매출금": [0, 4450000, 0],
"미수금": [10200481, 3085740, 0],
"선급금": [35754157, 2860480, 0]
}
},
"비유동자산": {
"유형자산": {
"토지": [113646000, 113646000, 0],
"건물": [103673263, 106406953, 0],
"비품": [66717992, 56904134, 0]
}
}
}
},
"당기순이익": [341124233, 630485442]
}
}
데이터 구조 특징:
- 배열의 인덱스:
[당기, 전기, 전전기](3개년) - 계층 구조: 자산 > 유동자산 > 당좌자산 > 보통예금
- 숫자 단위: 원 (정수)
손익계산서 (Income Statement)
{
"name": "2024",
"type": "IS",
"contents": {
"title": "손익계산서",
"company": "A노무법인",
"당기": "제 4(당)기 2024년 1월 1일부터 2024년 12월 31일까지",
"year": "2024",
"sum": {
"매출액": [5297933879, 4733703948, 3012297097],
"영업이익": [420100720, 741223433, 472096906],
"당기순이익": [341124233, 630485442, 514169773],
"판매비와관리비": [4877833159, 3992480515, 2540200191]
},
"data": {
"매출액": {
"수수료수입": [5297933879, 4733703948, 3012297097]
},
"판매비와관리비": {
"임원급여": [1200000000, 1200000000, 918560000],
"직원급여": [2115389334, 1632617894, 832105490],
"복리후생비": [252091099, 145149876, 99712973],
"광고선전비": [189845118, 166949273, 130572667]
}
}
}
}
왜 그래프인가?
관계형 데이터베이스의 테이블 구조는 스키마가 고정되어 있다. 재무상태표에 새 계정과목이 추가되면 ALTER TABLE이 필요하다. 반면 그래프 구조는 새로운 관계를 언제든 추가할 수 있다.
| 관계형 DB | 그래프 DB |
|---|---|
| 테이블 + 외래키 조인 | 노드 + 엣지 |
| 스키마 변경 = DDL 작업 | 새 트리플 추가만으로 확장 |
| 다대다 관계 = 조인 테이블 | 직접 연결 |
| NULL로 빈 칸 표현 | 해당 트리플 자체가 없음 |
세무 분석에서는 "A회사가 B회사를 지배한다", "C계정이 D계정의 하위 항목이다"처럼 관계 자체가 핵심 정보다. 이런 관계를 가장 자연스럽게 표현하는 방법이 그래프다.
RDF란 무엇인가?
기본 개념: 트리플
RDF의 핵심은 트리플(Triple)이다. 모든 정보를 세 부분으로 표현한다:
주어 (Subject) ── 술어 (Predicate) ── 목적어 (Object)예시:
A노무법인 ── 매출액 ── 52.9억 원
A노무법인 ── 업종 ── 노무법인
직원A ── 근무처 ── A노무법인URI: 전 세계에서 유일한 식별자
RDF에서 모든 개념은 URI(Uniform Resource Identifier)로 식별된다.
http://example.org/company/A노무법인
http://example.org/financial/revenue
http://example.org/employee/직원AURI를 사용하면:
- 유일성: 전 세계에서 같은 개념은 같은 URI로 표현
- 연결성: 다른 시스템의 데이터와 연결 가능
- 확장성: 새로운 개념 추가 시 충돌 없음
네임스페이스와 접두사
긴 URI를 매번 쓰기 번거로우므로 접두사(Prefix)를 사용한다.
@prefix company: <http://example.org/company/> .
@prefix fin: <http://example.org/financial/> .
# 이제 짧게 쓸 수 있다
company:A노무법인 fin:revenue 5297933879 .
Turtle 형식으로 RDF 작성하기
Turtle이란?
Turtle(Terse RDF Triple Language)은 RDF를 사람이 읽기 쉬운 형태로 작성하는 문법이다.
기본 문법
# 주석은 #로 시작
# 접두사 선언
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
@prefix company: <http://example.org/company/> .
@prefix fin: <http://example.org/financial/> .
# 트리플 작성 (마침표로 끝남)
company:A노무법인 rdf:type company:Company .
company:A노무법인 rdfs:label "A노무법인" .
company:A노무법인 fin:industry "노무법인" .
같은 주어에 대한 여러 술어
세미콜론(;)을 사용하면 같은 주어에 대해 여러 술어를 연결할 수 있다.
company:A노무법인
rdf:type company:Company ;
rdfs:label "A노무법인" ;
fin:industry "노무법인" ;
fin:establishedYear 2021 .
같은 주어-술어에 대한 여러 목적어
쉼표(,)를 사용하면 같은 주어-술어에 대해 여러 목적어를 나열할 수 있다.
company:A노무법인
fin:hasEmployee company:직원A , company:직원B , company:직원C .
데이터 타입 지정
# 정수
fin:amount "5297933879"^^xsd:integer .
# 날짜
fin:date "2024-12-31"^^xsd:date .
# 문자열 (기본값, 생략 가능)
rdfs:label "A노무법인"^^xsd:string .
rdfs:label "A노무법인" . # 동일
실습: 재무상태표를 RDF로 변환하기
변환 전략
JSON의 계층 구조를 RDF 트리플로 변환하는 전략:
- 회사 → 클래스 인스턴스로 표현
- 재무제표 → 회사에 연결된 별도 리소스
- 계정과목 → 재무제표에 연결된 속성
- 연도별 값 → 각 연도별 재무제표 리소스
변환 결과 (Turtle)
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
@prefix tax: <http://example.org/tax/> .
@prefix fin: <http://example.org/financial/> .
@prefix acc: <http://example.org/account/> .
# 회사 정보
tax:Company_A노무법인
rdf:type tax:Company ;
rdfs:label "A노무법인" ;
tax:companyType "노무법인" ;
tax:fiscalPeriod 4 .
# 2024년 재무상태표
fin:BS_A노무법인_2024
rdf:type fin:BalanceSheet ;
fin:belongsTo tax:Company_A노무법인 ;
fin:fiscalYear 2024 ;
fin:fiscalPeriod "제 4기" ;
fin:reportDate "2024-12-31"^^xsd:date ;
# 자산
acc:totalAssets 2194433171 ;
acc:currentAssets 766807888 ;
acc:nonCurrentAssets 1427625283 ;
# 유동자산 상세
acc:cashAndDeposits 685741852 ;
acc:accountsReceivable 0 ;
acc:otherReceivables 10200481 ;
acc:prepaidExpenses 35754157 ;
# 비유동자산 상세
acc:land 113646000 ;
acc:buildings 103673263 ;
acc:equipment 66717992 ;
# 부채
acc:totalLiabilities 486333117 ;
acc:currentLiabilities 486333117 ;
acc:nonCurrentLiabilities 0 ;
# 자본
acc:totalEquity 1708100054 ;
acc:paidInCapital 30000000 ;
acc:retainedEarnings 1678100054 .
# 2023년 재무상태표
fin:BS_A노무법인_2023
rdf:type fin:BalanceSheet ;
fin:belongsTo tax:Company_A노무법인 ;
fin:fiscalYear 2023 ;
fin:fiscalPeriod "제 3기" ;
fin:reportDate "2023-12-31"^^xsd:date ;
acc:totalAssets 2579529430 ;
acc:currentAssets 1220566005 ;
acc:nonCurrentAssets 1358963425 ;
acc:totalLiabilities 1212553609 ;
acc:currentLiabilities 608248031 ;
acc:nonCurrentLiabilities 604305578 ;
acc:totalEquity 1366975821 .
손익계산서 RDF 변환
# 2024년 손익계산서
fin:IS_A노무법인_2024
rdf:type fin:IncomeStatement ;
fin:belongsTo tax:Company_A노무법인 ;
fin:fiscalYear 2024 ;
fin:periodStart "2024-01-01"^^xsd:date ;
fin:periodEnd "2024-12-31"^^xsd:date ;
# 수익
acc:revenue 5297933879 ;
acc:serviceIncome 5297933879 ;
# 비용
acc:sellingAndAdminExpenses 4877833159 ;
acc:executiveSalaries 1200000000 ;
acc:employeeSalaries 2115389334 ;
acc:welfareExpenses 252091099 ;
acc:advertisingExpenses 189845118 ;
# 이익
acc:operatingIncome 420100720 ;
acc:incomeBeforeTax 413490303 ;
acc:incomeTax 72366070 ;
acc:netIncome 341124233 .
# 2023년 손익계산서
fin:IS_A노무법인_2023
rdf:type fin:IncomeStatement ;
fin:belongsTo tax:Company_A노무법인 ;
fin:fiscalYear 2023 ;
acc:revenue 4733703948 ;
acc:operatingIncome 741223433 ;
acc:netIncome 630485442 .
# 2022년 손익계산서
fin:IS_A노무법인_2022
rdf:type fin:IncomeStatement ;
fin:belongsTo tax:Company_A노무법인 ;
fin:fiscalYear 2022 ;
acc:revenue 3012297097 ;
acc:operatingIncome 472096906 ;
acc:netIncome 514169773 .
Python으로 RDF 생성하기
RDFLib 설치
pip install rdflib
JSON을 RDF로 변환하는 코드
from rdflib import Graph, Namespace, Literal, URIRef
from rdflib.namespace import RDF, RDFS, XSD
import json
# 네임스페이스 정의
TAX = Namespace("http://example.org/tax/")
FIN = Namespace("http://example.org/financial/")
ACC = Namespace("http://example.org/account/")
def create_graph():
"""RDF 그래프 생성 및 네임스페이스 바인딩"""
g = Graph()
g.bind("tax", TAX)
g.bind("fin", FIN)
g.bind("acc", ACC)
return g
def add_company(g, company_name: str):
"""회사 정보를 그래프에 추가"""
company_uri = TAX[f"Company_{company_name}"]
g.add((company_uri, RDF.type, TAX.Company))
g.add((company_uri, RDFS.label, Literal(company_name)))
return company_uri
def convert_balance_sheet(g, company_uri, bs_data: dict):
"""재무상태표 JSON을 RDF로 변환"""
contents = bs_data["contents"]
year = int(contents["year"])
company_name = "A노무법인" # 익명화
# 재무상태표 리소스 생성
bs_uri = FIN[f"BS_{company_name}_{year}"]
g.add((bs_uri, RDF.type, FIN.BalanceSheet))
g.add((bs_uri, FIN.belongsTo, company_uri))
g.add((bs_uri, FIN.fiscalYear, Literal(year, datatype=XSD.integer)))
# 요약 항목 추가 (당기 = 인덱스 0)
summary = contents["sum"]
mapping = {
"자산총계": ACC.totalAssets,
"부채총계": ACC.totalLiabilities,
"자본총계": ACC.totalEquity,
"유동자산": ACC.currentAssets,
"비유동자산": ACC.nonCurrentAssets,
"유동부채": ACC.currentLiabilities,
"비유동부채": ACC.nonCurrentLiabilities,
}
for kor_name, predicate in mapping.items():
if kor_name in summary:
value = summary[kor_name][0] # 당기 값
g.add((bs_uri, predicate, Literal(value, datatype=XSD.integer)))
return bs_uri
def convert_income_statement(g, company_uri, is_data: dict):
"""손익계산서 JSON을 RDF로 변환"""
contents = is_data["contents"]
year = int(contents["year"])
company_name = "A노무법인"
# 손익계산서 리소스 생성
is_uri = FIN[f"IS_{company_name}_{year}"]
g.add((is_uri, RDF.type, FIN.IncomeStatement))
g.add((is_uri, FIN.belongsTo, company_uri))
g.add((is_uri, FIN.fiscalYear, Literal(year, datatype=XSD.integer)))
# 요약 항목 추가
summary = contents["sum"]
mapping = {
"매출액": ACC.revenue,
"영업이익": ACC.operatingIncome,
"당기순이익": ACC.netIncome,
"판매비와관리비": ACC.sellingAndAdminExpenses,
"법인세등": ACC.incomeTax,
}
for kor_name, predicate in mapping.items():
if kor_name in summary:
value = summary[kor_name][0] # 당기 값
g.add((is_uri, predicate, Literal(value, datatype=XSD.integer)))
# 판관비 상세 항목
if "data" in contents and "판매비와관리비" in contents["data"]:
expenses = contents["data"]["판매비와관리비"]
expense_mapping = {
"임원급여": ACC.executiveSalaries,
"직원급여": ACC.employeeSalaries,
"복리후생비": ACC.welfareExpenses,
"광고선전비": ACC.advertisingExpenses,
}
for kor_name, predicate in expense_mapping.items():
if kor_name in expenses:
value = expenses[kor_name][0]
g.add((is_uri, predicate, Literal(value, datatype=XSD.integer)))
return is_uri
# 실행 예시
if __name__ == "__main__":
# 그래프 생성
g = create_graph()
# JSON 파일 로드 (실제 경로로 수정)
with open("재무상태표-bs.json", "r", encoding="utf-8") as f:
bs_data = json.load(f)
with open("손익계산서-is.json", "r", encoding="utf-8") as f:
is_data = json.load(f)
# 회사 추가
company_uri = add_company(g, "A노무법인")
# 재무제표 변환
convert_balance_sheet(g, company_uri, bs_data)
convert_income_statement(g, company_uri, is_data)
# Turtle 형식으로 출력
print(g.serialize(format="turtle"))
# 파일로 저장
g.serialize("tax_knowledge_graph.ttl", format="turtle")
print(f"\n총 {len(g)} 개의 트리플이 생성되었습니다.")
실행 결과
@prefix acc: <http://example.org/account/> .
@prefix fin: <http://example.org/financial/> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix tax: <http://example.org/tax/> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
tax:Company_A노무법인 a tax:Company ;
rdfs:label "A노무법인" .
fin:BS_A노무법인_2024 a fin:BalanceSheet ;
acc:currentAssets 766807888 ;
acc:currentLiabilities 486333117 ;
acc:nonCurrentAssets 1427625283 ;
acc:nonCurrentLiabilities 0 ;
acc:totalAssets 2194433171 ;
acc:totalEquity 1708100054 ;
acc:totalLiabilities 486333117 ;
fin:belongsTo tax:Company_A노무법인 ;
fin:fiscalYear 2024 .
fin:IS_A노무법인_2024 a fin:IncomeStatement ;
acc:advertisingExpenses 189845118 ;
acc:employeeSalaries 2115389334 ;
acc:executiveSalaries 1200000000 ;
acc:incomeTax 72366070 ;
acc:netIncome 341124233 ;
acc:operatingIncome 420100720 ;
acc:revenue 5297933879 ;
acc:sellingAndAdminExpenses 4877833159 ;
acc:welfareExpenses 252091099 ;
fin:belongsTo tax:Company_A노무법인 ;
fin:fiscalYear 2024 .
트리플 구조 시각화
생성된 트리플을 그래프로 시각화하면:
graph TB
Company["tax:Company_A노무법인"]
subgraph BS["재무상태표 2024"]
A1["자산 21.9억"]
L1["부채 4.8억"]
E1["자본 17억"]
end
subgraph IS["손익계산서 2024"]
R1["매출 52.9억"]
O1["영업이익 4.2억"]
N1["순이익 3.4억"]
end
Company --> BS
Company --> IS
style Company stroke:#2563eb,stroke-width:3px
style BS stroke:#16a34a,stroke-width:2px
style IS stroke:#ea580c,stroke-width:2px
RDF의 장점
1. 유연한 스키마
새로운 속성을 추가해도 기존 데이터에 영향 없음:
# 나중에 추가해도 됨
fin:BS_A노무법인_2024
acc:inventories 0 ; # 재고자산 (원래 없던 속성)
acc:intangibleAssets 114616667 . # 무형자산
2. 데이터 연결
다른 데이터셋과 연결 가능:
# 업종 분류 코드 연결
tax:Company_A노무법인
tax:industryCode <http://example.org/ksic/N> . # 한국표준산업분류
# 외부 지식베이스 연결 (개념적 예시)
tax:Company_A노무법인
owl:sameAs <http://dbpedia.org/resource/Labor_law_firm> .
3. 추론 가능
규칙을 정의하면 새로운 사실을 추론:
# 규칙: 부채비율 = 부채총계 / 자본총계 × 100
# 부채비율 200% 초과 시 "고위험" 분류
# 추론 결과
fin:BS_A노무법인_2024
fin:debtRatio 28.47 ; # 486333117 / 1708100054 × 100
fin:riskLevel "저위험" .
핵심 정리
| 개념 | 설명 | 예시 |
|---|---|---|
| 트리플 | RDF의 기본 단위 | (A노무법인, 매출액, 52.9억) |
| URI | 리소스의 고유 식별자 | http://example.org/company/A노무법인 |
| 접두사 | URI 축약 표현 | tax:Company_A노무법인 |
| Turtle | RDF 직렬화 형식 | .ttl 파일 |
| RDFLib | Python RDF 라이브러리 | from rdflib import Graph |
JSON → RDF 변환 핵심
- 리소스 식별: 각 개체에 URI 부여
- 관계 정의: 적절한 술어(predicate) 선택
- 데이터 타입: 숫자, 날짜 등 타입 명시
- 계층 표현: 중첩 구조는 별도 리소스로 분리
다음 단계 미리보기
6부: OWL로 세무 용어 정의하기 - TBox 설계
5부에서 데이터(ABox)를 RDF로 표현했다. 하지만 "재무상태표란 무엇인가?", "영업이익은 어떻게 계산하는가?"와 같은 스키마(TBox)는 아직 정의하지 않았다.
6부에서는 OWL(Web Ontology Language)을 사용하여:
- 클래스 계층 정의 (재무제표 > 재무상태표, 손익계산서)
- 속성의 도메인/레인지 정의
- 계산 규칙 표현 (영업이익 = 매출 - 판관비)
- 제약 조건 정의 (자산총계 = 부채총계 + 자본총계)
를 다룬다.
참고 자료
RDF 표준
RDFLib
관련 시리즈
'실제 경험과 인사이트를 AI와 함께 정리한 글' 카테고리의 다른 글
| SPARQL 쿼리 마스터하기 (0) | 2026.01.09 |
|---|---|
| OWL로 세무 용어 정의하기: TBox 설계 (0) | 2026.01.09 |
| 세무 AI 시스템 아키텍처 설계: 전체 시스템을 어떻게 구성하는가 (0) | 2026.01.09 |
| AI 에이전트 개념: 에이전트가 '도구'를 사용한다는 것의 의미 (0) | 2026.01.09 |
| 지식그래프 입문: RDB vs Graph DB, 언제 무엇을 쓰는가 (0) | 2026.01.09 |
- Total
- Today
- Yesterday
- authentication
- SHACL
- AI
- LLM
- troubleshooting
- Go
- Development Tools
- frontend
- Tax Analysis
- architecture
- AGENTS.md
- EnvironmentAgnostic
- react
- AI agent
- knowledge graph
- backend
- Tailwind CSS
- security
- Claude Opus 4.5
- Developer Tools
- authorization
- Ontology
- api gateway
- imprun.dev
- claude code
- CLAUDE.md
- GPT-5.1
- Rag
- Kubernetes
- Next.js
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | ||||
| 4 | 5 | 6 | 7 | 8 | 9 | 10 |
| 11 | 12 | 13 | 14 | 15 | 16 | 17 |
| 18 | 19 | 20 | 21 | 22 | 23 | 24 |
| 25 | 26 | 27 | 28 | 29 | 30 | 31 |
