오늘 이 블로그에 온톨로지 글이 다섯 편 올라갔다. 어휘와 규칙을 나눈 글, 표현수준 스펙트럼을 다룬 글, 도메인 온톨로지와 구조 온톨로지를 여섯 렌즈로 쪼갠 글, 그래프와 벡터DB가 각각 무엇을 포기하는지 비교한 글, 그리고 가용성·탐색성·신뢰성·기계판독성 네 축이 사실은 사슬이라고 주장한 글.

다섯 편 모두 추상 층에서 끝났다. 이 글은 그 다섯 개의 축을 실제 데이터 두 벌 위에 얹어본다. 한국 공공데이터(data.go.kr)와 금융감독원 전자공시 OpenDART. 둘 다 공개돼 있고, 둘 다 표준이 있고, 둘 다 “기업”을 다룬다. 그러니 붙이면 될 것 같다.

붙지 않는다. 그리고 어디서 안 붙는지가 온톨로지 설계에서 정확히 무엇을 먼저 결정해야 하는지를 알려준다.

1. 두 세계 모두 이미 표준을 갖고 있다 — 서로 다른 층에

먼저 오해를 걷어내야 한다. “한국 공공데이터는 표준이 없다”는 말은 사실이 아니다. 법률 층위부터 있다.

「공공데이터의 제공 및 이용 활성화에 관한 법률」 제23조(공공데이터의 표준화) 제1항은 행정안전부장관이 표준을 제정·시행해야 할 대상을 이렇게 못 박는다.

  1. 공공데이터의 제공형태 및 제공기술
  2. 공공데이터 제공을 위한 분류체계
  3. 그 밖에 공공데이터의 제공 및 이용 활성화를 위하여 필요한 사항

이 세 호를 온톨로지 어휘로 옮기면 제공형태(format) 축과 분류체계(classification) 축이다. 그리고 그 아래로 실제 표준들이 층층이 깔려 있다.

  • 공공데이터 제공 표준 — 근거는 위 제23조. 행정안전부 고시 제2020-5호로 「공공데이터 개방 표준」이 시행됐고(표준번호 eGOV-D01.0030), 이후 제·개정이 계속 누적되고 있다.
  • 공공데이터 공통표준용어 — 「공공기관의 데이터베이스 표준화 지침」에 근거하여 공통표준단어공통표준도메인 의 조합으로 용어를 만든다. 도메인은 연월일C8, 금액N15, 여부C1 처럼 자료형과 길이까지 규정한다.
  • 행정표준코드code.go.kr. 근거는 「전자정부법」 제50조. 코드값의 국가 단위 통제어휘다.
  • 공공데이터 품질관리 수준진단·평가 — 근거는 같은 법 제22조(공공데이터의 품질관리). 제3항은 행안부장관이 “정기적으로 사회적·경제적 파급효과가 큰 제공대상 공공데이터에 대한 품질 진단·평가를 실시하고 그 결과를 공표할 수 있다”고 규정한다.

여기까지 보면 국내 공공데이터 표준 스택은 어휘 층과 규칙 층이 이미 법정으로 분리돼 있다. 오늘 올라온 「어휘와 그 어휘가 지켜야 할 규칙으로 나눠보면」이 RDFS와 SHACL로 설명한 그 구분이, 공통표준단어(어휘)와 공통표준도메인(제약)으로 이미 구현돼 있는 셈이다. 국내 행정 데이터 표준은 이 지점에서 상당히 앞서 있다.

DART 쪽은 어떤가. 여기서 놀라운 게 나온다.

OpenDART는 XBRL 택사노미재무제표양식 API(/api/xbrlTaxonomy.json)를 공개한다. 응답 필드가 sj_div(재무제표구분), account_id(계정ID), account_nm, label_kor, 그리고 IFRS Reference 다. IFRS Reference의 출력 예시는 개발가이드에 이렇게 적혀 있다.

K-IFRS 1001 문단 54 (9), K-IFRS 1007 문단 45

이건 그냥 코드표가 아니다. 클래스마다 규범적 근거 조항이 붙어 있는 온톨로지다. account_id는 전역 식별자고, label_kor은 라벨이고, IFRS Reference는 그 개념의 정의 출처다. SKOS로 옮기면 skos:notation / skos:prefLabel / skos:definition 의 출처에 각각 대응한다. 표현수준 스펙트럼에서 이건 단순 용어집보다 확실히 위쪽이다.

정리하면 이렇다.

온톨로지 층 공공데이터(행안부 스택) DART(OpenDART)
개체 식별자 표준화 대상 아님 corp_code 있음 (DART 내부 발번)
통제어휘 공통표준용어 + 행정표준코드 (법정) 재무 계정과목 한정
제약·자료형 공통표준도메인 (여부C1 등) XBRL 스키마
분류체계 법 제23조 1항 2호로 명시 위임 induty_code 등 (후술)
개념 정의의 출처 고시·지침 문서 IFRS Reference (기계판독 필드)

공공데이터는 컬럼 이름을 표준화했고, DART는 계정과목을 온톨로지화했다. 둘 다 진짜다. 그런데 둘 중 어느 쪽도 “이 법인이 저 법인과 같은 법인인가” 를 표준화하지 않았다.

2. 조인 키가 없다 — 그것도 의도적으로

OpenDART에서 기업을 식별하는 값은 최소 네 개다.

  • corp_code — 8자리. DART 고유번호. 공시 조회의 사실상 기본키.
  • stock_code — 6자리 종목코드. 상장사만 값이 있다.
  • jurir_no — 법인등록번호.
  • bizr_no — 사업자등록번호.

뒤의 두 개가 국가 식별자다. 공공데이터 쪽 데이터셋들이 기업을 붙잡을 때 쓰는 키도 대개 사업자등록번호다. 그러면 corp_code ↔ bizr_no 매핑표만 있으면 두 세계가 붙는다.

그 매핑표가 없다.

OpenDART의 고유번호 API(/api/corpCode.xml)는 전체 기업 목록을 Zip으로 한 번에 내려준다. 그 안에 들어 있는 필드는 이게 전부다.

corp_code       고유번호 (8자리)
corp_name       정식명칭
corp_eng_name   영문명칭
stock_code      종목코드 (6자리, 상장사만)
modify_date     최종변경일자

jurir_nobizr_no도 없다. 이 두 값을 얻으려면 기업개황 API(/api/company.json)를 회사 하나당 한 번씩 호출해야 한다.

설계 관점에서 이 사실의 의미는 크다. 두 세계를 잇는 조인은 파일 하나를 받아 처리하는 O(1) 작업이 아니라 기업 수만큼 API를 두드리는 O(N) 작업이다. 그리고 그 O(N) 산출물은 어디에도 표준으로 존재하지 않으므로, 각자가 만들고 각자가 관리한다.

여기서 첫 번째 설계 원칙이 나온다.

개체 해소(entity resolution) 결과를 파생 캐시가 아니라 1급 개체로 모델링하라.

corp_codebizr_noowl:sameAs로 잇고 싶어진다. 하지 마라. owl:sameAs는 전이적이고 대칭적이며 무조건적이다. 한 번 잘못 이으면 그 오류가 추론을 타고 그래프 전체로 번진다. 실제로는 조건부 동일성이다 — 누가, 언제, 어떤 근거로 같다고 판단했는가.

PROV-O를 쓰면 이렇게 된다. 매핑 자체가 prov:Entity이고, 그것을 만든 행위가 prov:Activity이며, 수행 주체가 prov:Agent(자동 매칭이면 prov:SoftwareAgent)다. 오늘의 「네 축은 사슬이다」 글에서 신뢰성이 결국 출처 그래프로 환원된다고 한 지점이 정확히 여기다. 매핑에 출처가 없으면 그 매핑을 신뢰할 방법이 원리적으로 없다.

3. 식별자에는 시간이 있다

stock_code는 상장사만 값이 있다. 뒤집으면 이 필드는 속성이 아니라 상태의 함수다. 상장하면 생기고 상장폐지되면 사라진다. 이전상장하면 바뀔 수 있다.

그런데 corpCode 벌크 파일이 제공하는 시간 정보는 modify_date 하나다. “마지막으로 뭔가 바뀐 날”이지 “이 값이 언제부터 언제까지 유효했는가”가 아니다. 즉 시점 조인(as-of join)이 불가능하다. 2023년 공시를 2026년 종목코드로 해석하면 조용히 틀린다.

공공데이터 쪽도 같은 문제를 다른 모양으로 갖는다. 행정표준코드는 코드값의 통제어휘를 주지만, 개별 데이터셋이 그 코드를 어느 시점 판본으로 썼는지는 데이터 안에 없다.

두 번째 설계 원칙.

식별자 부여를 사실이 아니라 사건으로 모델링하라. 링크에 유효기간을 붙여라.

구체적으로는 링크를 직접 잇지 말고 중간에 부여 사건 노드를 둔다.

{
  "@type": "IdentifierAssignment",
  "assignedTo":   { "@id": "urn:corp:00126380" },
  "identifier":   { "scheme": "krx:stock_code", "value": "005930" },
  "validFrom":    "1975-06-11",
  "validTo":      null,
  "prov:wasGeneratedBy": { "@id": "urn:activity:dart-company-fetch-20260903" }
}

DCAT 3가 데이터셋 버전에 대해 dcat:previousVersion / dcat:hasCurrentVersion / dcat:DatasetSeries를 도입한 것도 같은 문제 인식이다. 카탈로그 층은 이미 시간을 1급으로 다루기 시작했는데, 개체 층은 아직 아니다.

4. -표준계정코드 미사용- — 정직한 실패의 교과서

이 글에서 가장 배울 게 많은 한 줄은 OpenDART 개발가이드에 있다. 단일회사 전체 재무제표 API(/api/fnlttSinglAcntAll.json)의 account_id 필드 설명이다.

계정ID — XBRL 표준계정ID ※ 표준계정ID가 아닐경우 "-표준계정코드 미사용-" 표시

읽고 잠깐 멈췄다. 표준 계정과목 온톨로지가 있고, 그것을 쓰지 않은 제출자가 있고, 그 경우 식별자 필드에 한국어 문장이 들어간다.

이걸 설계 결함으로 읽으면 오독이다. 대안이 무엇이었을지 생각해보면 안다. 빈 문자열을 넣으면 “값이 없음”과 “표준을 안 씀”이 구분되지 않는다. 임의 코드를 발번하면 존재하지 않는 표준 개념을 만들어내는 셈이다. 조용히 null을 넣으면 소비자는 데이터 누락으로 오해한다. DART는 셋 다 하지 않고, 값 공간 안에 “표준을 쓰지 않았다”는 상태를 명시적으로 넣었다.

이게 오늘 「네 축」 글에서 다룬 IETF aipref의 unknown과 같은 구조다. allow/disallow 이지선다에 unknown을 3번째 값으로 넣은 이유는, 모른다는 상태를 표현하지 못하면 소비자가 임의로 하나를 골라버리기 때문이다.

동시에 소비자 쪽 함정도 정확히 여기 있다. account_id는 문자열 타입이므로 스키마 검증은 통과한다. 타입은 맞고 조인만 조용히 실패한다. 기계판독성은 “파싱되는가”가 아니라 “의미가 보존되는가”라는 앞 글의 주장이 바로 이 자리에서 실물로 확인된다.

세 번째 설계 원칙.

“모른다”와 “해당 없음”과 “표준 밖”을 값 공간의 1급 시민으로 넣어라. 그리고 그것들을 식별자 타입과 구분되는 타입으로 넣어라.

공통표준도메인은 이걸 잘 못 한다. 여부C1은 Y/N이다. 모른다를 담을 자리가 도메인 정의 자체에 없다. 어휘 층 표준화가 앞서 있다는 앞의 평가는 유지하되, 이 한계도 같이 봐야 한다.

5. 분류체계는 코드값이 아니라 3튜플이다

DART 기업개황 응답에는 induty_code(업종코드)가 있다. 개발가이드의 설명은 — 확인해보면 — 명칭란과 설명란 모두 “업종코드”다. 어떤 코드체계의 몇 차 개정판인지 문서에 없다.

코드체계가 명시되지 않은 코드값은, 조인 관점에서는 값이 없는 것과 사실상 같다. 한국표준산업분류는 개정될 때마다 코드가 재배열되고 세분류가 신설·통합된다. 어느 판본인지 모르면 같은 숫자가 다른 산업을 가리킬 수 있다.

공공데이터 쪽은 이 점에서 낫다. 행정표준코드시스템은 코드체계를 명시적 관리 대상으로 두고, code.go.kr에서 코드체계 단위로 조회·다운로드하게 한다. 법 제23조 1항 2호가 “분류체계”를 명시 위임한 효과다.

네 번째 설계 원칙.

분류값을 스칼라가 아니라 (코드체계 IRI, 판본, 코드값) 3튜플로 저장하라.

SKOS의 skos:ConceptScheme이 정확히 이 자리다. 코드값은 skos:Concept이고, 그 개념은 반드시 어떤 ConceptSchemeskos:inScheme으로 소속된다. 판본이 다르면 다른 ConceptScheme이고, 둘 사이는 skos:exactMatch / skos:closeMatch로 잇는다. exactMatchcloseMatch를 나눠 둔 것이 요점이다 — 개정 전후 매핑은 대부분 정확 일치가 아니다.

6. 이 문제는 벡터로 안 풀린다

오늘 그래프 대 벡터DB를 비교한 글이 있었으니 짚고 넘어가야 한다. 기업명 문자열 유사도로 매칭하면 되지 않나?

안 된다. 그리고 안 되는 이유가 구조적이다.

법인 동일성은 유사도의 문제가 아니라 법적 사실의 문제다. “삼성전자”와 “삼성전자서비스”는 문자열로는 가깝고 법인으로는 완전히 다르다. “㈜엘지화학”과 “LG화학”은 문자열로 멀고 같은 법인이다. 상호 변경, 분할, 합병을 거치면 문자열 거리와 법적 동일성의 상관은 더 무너진다.

임베딩 거리는 틀렸을 때 틀렸다고 말하지 못한다. 0.87이라는 점수가 나오면 그건 참도 거짓도 아닌 0.87이다. 반면 사업자등록번호 일치는 참이거나 거짓이다. 이 축에서는 그래프 쪽이 포기하는 것(구축 비용)보다 벡터 쪽이 포기하는 것(판정 가능성)이 훨씬 비싸다.

실무적 절충은 이렇다. 벡터는 후보 생성에만 쓰고, 판정은 반드시 결정적 규칙과 국가 식별자로 한다. 그리고 그 판정 결과를 §2의 1급 개체로 적재한다.

7. 그래서 설계는 네 층으로 갈린다

앞의 다섯 원칙을 층으로 정리하면 이렇게 된다.

  1. 개체 층LegalEntity. 어떤 외부 식별자도 이 층의 URI가 되면 안 된다. corp_code도, bizr_no도 속성이지 정체성이 아니다. 내부 URI를 발번하라.
  2. 식별자 부여 층IdentifierAssignment. (체계, 값, 유효기간, 출처). §3.
  3. 관측 층 — 공시·데이터셋. DCAT 3의 dcat:Dataset / dcat:DatasetSeries. DART 쪽에서는 rcept_no(접수번호 14자리)가 개별 공시의 안정적 식별자다.
  4. 출처 층 — PROV-O. 위 세 층의 모든 노드가 어떤 Activity로 어떤 Agent에 의해 만들어졌는지.

이 순서가 중요하다. 흔한 실패는 관측 층부터 만들고(공시 데이터를 긁어 넣고) 개체 층을 나중에 정규화하는 것이다. 그러면 이미 적재된 관측들이 corp_code를 직접 참조하고 있어서, 나중에 개체 층을 넣을 때 전부 다시 써야 한다.

그리고 비상장사에서 이 설계가 시험받는다. stock_code가 없으므로 외부 조인의 유일한 다리는 jurir_no/bizr_no이고, 그 값은 벌크 파일에 없으며 기업개황 API로만 얻어진다. 상장사만 다루면 이 설계는 과잉으로 보인다. 비상장사를 포함하는 순간 필수가 된다.

결론 세 가지

하나. 한국 공공데이터 표준 스택은 어휘 층과 제약 층에서 앞서 있고, 개체 층이 비어 있다. 그 공백은 우연이 아니라 법 제23조가 위임한 표준 대상이 “제공형태”와 “분류체계” 두 축이기 때문이다. 온톨로지를 설계하는 사람이 채워야 할 자리는 표준이 이미 채운 자리가 아니라 이 빈자리다.

둘. DART는 계정과목 축에서는 이미 진짜 온톨로지를 갖고 있다. IFRS Reference까지 기계판독 필드로 제공한다. 그런데 기업 축에서는 내부 발번 식별자 하나뿐이고, 국가 식별자와의 다리는 벌크 파일에서 빠져 있다. 한 조직 안에서도 축마다 온톨로지 성숙도가 다르다. 어느 축이 성숙했는지부터 확인하고 시작해야 한다.

셋. 값 공간에 실패를 명시적으로 넣은 설계가 조용히 성공한 척하는 설계보다 낫다. -표준계정코드 미사용-은 보기 흉하지만 정직하다. 흉한 게 문제라면 타입을 나누면 된다. 없애면 안 된다.

References

  1. 「공공데이터의 제공 및 이용 활성화에 관한 법률」 제22조(공공데이터의 품질관리), 제23조(공공데이터의 표준화) — 국가법령정보센터. https://www.law.go.kr/LSW/lsInfoP.do?lsId=011895 (1차·법령)
  2. OpenDART 개발가이드 — 공시정보 > 기업개황 (/api/company.json). https://opendart.fss.or.kr/guide/detail.do?apiGrpCd=DS001&apiId=2019002 (1차·공식)
  3. OpenDART 개발가이드 — 공시정보 > 고유번호 (/api/corpCode.xml). https://opendart.fss.or.kr/guide/detail.do?apiGrpCd=DS001&apiId=2019018 (1차·공식)
  4. OpenDART 개발가이드 — 정기보고서 재무정보 > 단일회사 전체 재무제표 (/api/fnlttSinglAcntAll.json). account_id 필드 설명 인용 출처. https://opendart.fss.or.kr/guide/detail.do?apiGrpCd=DS003&apiId=2019020 (1차·공식)
  5. OpenDART 개발가이드 — 정기보고서 재무정보 > XBRL택사노미재무제표양식 (/api/xbrlTaxonomy.json). IFRS Reference 출력예시 출처. https://opendart.fss.or.kr/guide/detail.do?apiGrpCd=DS003&apiId=2020001 (1차·공식)
  6. OpenDART 개발가이드 — 정기보고서 재무정보 > 재무제표 원본파일(XBRL) (/api/fnlttXbrl.xml). https://opendart.fss.or.kr/guide/detail.do?apiGrpCd=DS003&apiId=2019019 (1차·공식)
  7. 공공데이터포털 — 공공데이터 공통표준용어(공통표준단어·공통표준도메인). https://www.data.go.kr/data/15156379/fileData.do (1차·공식)
  8. 공공데이터포털 — 공공데이터 제공 표준. https://www.data.go.kr/data/15156444/fileData.do (1차·공식)
  9. 행정표준코드관리시스템(근거: 「전자정부법」 제50조). https://www.code.go.kr (1차·공식)
  10. W3C, Data Catalog Vocabulary (DCAT) — Version 3, W3C Recommendation. https://www.w3.org/TR/vocab-dcat-3/ (1차·표준)
  11. W3C, PROV-O: The PROV Ontology, W3C Recommendation, 30 April 2013. https://www.w3.org/TR/prov-o/ (1차·표준)
  12. W3C, SKOS Simple Knowledge Organization System Reference, W3C Recommendation, 18 August 2009. https://www.w3.org/TR/skos-reference/ (1차·표준)

근거의 한계

이 글의 API 필드 설명은 모두 2026-09-03 시점의 OpenDART 개발가이드 페이지에서 직접 확인한 것이다. OpenDART는 공지 없이 필드가 추가·변경될 수 있으므로, 인용한 문구는 시점 한정으로 읽어야 한다. 특히 induty_code의 코드체계가 문서에 명시되어 있지 않다는 §5의 지적은 개발가이드 페이지 본문에 대한 확인이며, 별도 공지나 FAQ에 설명이 있을 가능성은 배제하지 못했다.

공공데이터 표준 문서들은 제·개정이 누적되는 문서라 여기서는 제정 근거와 구조만 인용하고 개정 차수·수록 항목 수 같은 변동 수치는 쓰지 않았다. 공공데이터 쪽에 “개체 식별자 표준이 없다”는 §1의 진술은 법 제23조가 위임한 표준 대상 목록에 그 축이 없다는 뜻이며, 개별 기관이 자체 식별자 규칙을 운영하지 않는다는 뜻은 아니다.

두 세계를 실제로 조인한 대규모 실증 결과는 이 글에 없다. §2의 O(N) 주장은 API 명세로부터의 연역이고, 매칭 정확도 같은 수치는 검증 없이 쓰지 않았다.