- 발행일
트리플을 우리말 문장으로 읽는다 — 술어 18종 문장 틀과 받침 조사
트리플을 우리말 문장으로 읽는다
온톨로지 화면의 옆 패널에는 고른 클래스의 관계가 목록으로 나옵니다. 이런 모양이에요.
이 클래스가 참조하는 클래스
dct:creator → 기관
dct:publisher → 기관
dcat:distribution → 배포
kadif:dataFrame → 데이터 프레임
무엇과 이어져 있는지는 압니다. 그런데 그 이음이 무슨 뜻인지는 안 나옵니다. dct:creator가 "이 자원을 기관이 만든다"인지 "이 자원이 기관을 만든다"인지, 화살표 방향은 알려주지만 문장으로는 안 읽혀요. 표준 용어에 익숙한 사람에게는 자명한데, 이 화면은 그렇지 않은 사람도 봅니다.
온톨로지는 원래 주어·서술어·목적어 문장의 집합입니다. 트리플이라고 부르는 게 그 뜻이고요. 그러면 문장으로 보여주면 됩니다.
기계 틀 하나로는 안 된다
| 방법 | 장점 | 포기하는 것 | 판단 |
|---|---|---|---|
| ① 목록 그대로 두고 술어에 툴팁 | 변경 최소 | 툴팁은 하나씩 올려 봐야 함. 관계가 스무 개면 스무 번 | 기각 |
② 일반 틀 하나 — A의 <속성 국문명>은 B입니다 | 술어가 늘어도 자동 | 속성 명칭이 명사형이라 뜻이 안 잡힘. "A의 배포는 B입니다"가 무슨 말인지 모름 | 기각 |
| ③ 술어마다 문장을 손으로 다듬고, 나머지는 ②로 | 자주 나오는 관계가 읽힘 | 새 술어는 손이 가고, 손 안 대면 ②로 떨어짐 | 채택 |
| ④ 서버에서 문장을 만들어 내려보냄 | 여러 화면이 공유 | 서버를 안 고쳐도 되는 일이다. 필요한 값이 이미 응답에 다 있음 | 기각 |
②가 왜 안 되는지가 이 작업의 출발점입니다. 표준의 속성 명칭은 대부분 명사예요 — distribution(배포), creator(생성자), conformsTo(준수 대상). 이걸 "A의 배포는 B입니다" 틀에 넣으면 문법은 맞는데 뜻이 안 잡힙니다. 사람이 읽을 문장은 동사가 있어야 합니다.
// 온톨로지는 주어·서술어·목적어 문장의 집합이다 — 선(트리플)을 우리말 문장으로 풀어준다.
// 기계 틀("A의 생성자는 B입니다")은 속성 명칭이 명사형이라 뜻이 안 잡혀, 술어마다 문장을
// 손으로 다듬었다. 여기 없는 새 술어는 아래 일반 틀로 떨어진다.
const TRIPLE_SENTENCES = {
"dct:creator": (s, t) => `${N(s)}${josa(s, "은/는")} ${N(t)}${josa(t, "이/가")} 만듭니다`,
"dct:publisher": (s, t) => `${N(s)}${josa(s, "은/는")} ${N(t)}${josa(t, "이/가")} 발행합니다`,
"dct:rightsHolder": (s, t) => `${N(s)}의 권리는 ${N(t)}${josa(t, "이/가")} 갖습니다`,
"dcat:distribution": (s, t) => `${N(s)}${josa(s, "은/는")} ${N(t)} 형태로 제공됩니다`,
"kadif:dataFrame": (s, t) => `${N(s)}${josa(s, "은/는")} ${N(t)}${josa(t, "으로/로")} 구성됩니다`,
...
};
dct:rightsHolder를 보면 주어가 아예 바뀝니다 — "A의 권리는 B가 갖습니다". 틀 하나로는 못 나오는 문장이에요.
조사는 받침을 세서 고른다
문장을 조립하는 순간 한국어의 조사가 걸립니다. 「데이터셋」은 "이", 「카탈로그」는 "가"예요.
// 받침 유무로 조사를 고른다("은/는"처럼 받침용/무받침용 쌍) — 한글이 아니면 병기.
function josa(word, pair) {
const [a, b] = pair.split("/");
const code = word.charCodeAt(word.length - 1);
if (code < 0xac00 || code > 0xd7a3) return `${a}(${b})`;
const batchim = (code - 0xac00) % 28;
if (pair === "으로/로") return batchim && batchim !== 8 ? a : b; // ㄹ 받침은 "로"
return batchim ? a : b;
}
한글 음절은 유니코드에서 0xAC00부터 규칙적으로 배열돼 있습니다. (코드 - 0xAC00) % 28이 종성 인덱스이고, 0이면 받침이 없어요. 라이브러리 없이 다섯 줄입니다.
예외가 하나 있습니다. 으로/로는 ㄹ 받침일 때 "로"입니다. "프레임으로"인데 "데이터 엘리먼트로"가 아니라 "…엘리먼트로"— 종성 ㄹ(인덱스 8)만 무받침처럼 굴어요. 그래서 그 자리만 조건이 하나 더 붙습니다.
한글이 아닐 때는 은(는)처럼 병기합니다. 이 화면에는 이름이 CURIE로만 있는 노드가 있어서, Dataset은(는)으로 나가요. 어색하지만 틀린 조사를 확정해서 붙이는 것보다 낫다고 봤습니다.
같은 술어는 한 문장으로 묶는다
문장으로 풀고 나니 새 문제가 생겼습니다. 관계가 몰리는 개념은 문장이 스무 줄이 됩니다.
「행위자」는 「국토교통부」가 운영합니다
「행위자」는 「한국도로공사」가 운영합니다
「행위자」는 「경찰청」이 운영합니다
... (17줄 더)
같은 술어·같은 방향은 상대 이름을 「A·B·C」로 묶어 한 문장으로 냅니다.
// 한 클래스가 낀 트리플을 문장 목록으로. 같은 술어·같은 방향은 상대 이름을 「A·B·C」로
// 묶어 한 문장으로 낸다 — 행위자처럼 관계가 몰리는 개념은 낱개로 펴면 스무 줄이 된다.
행위자 클래스가 20줄에서 6줄이 됐습니다. 정보량은 그대로인데 읽는 단위가 "관계 하나"에서 "관계 종류 하나"로 바뀌어요.
링크의 양끝이 부르는 쪽마다 다르다
이 함수는 두 곳에서 부릅니다. 온톨로지 렌더러 안에서 한 번, 데이터 맵의 상태 객체에서 한 번.
// ⚠️ links 의 source/target 은 부르는 쪽마다 꼴이 다르다 — 렌더러 안에서는 노드 객체이고
// 데이터 맵의 state.rootData 에서는 id 문자열이다. 양쪽 다 받게 id 로 맞춰 본다.
D3의 힘 시뮬레이션은 링크의 source/target을 초기화하면서 id 문자열에서 노드 객체로 바꿔치기합니다. 그래서 시뮬레이션을 거친 데이터와 원본 데이터가 같은 필드에 다른 타입을 담고 있어요.
한쪽만 지원하면 다른 쪽에서 조용히 빈 문장이 나옵니다. 오류가 아니라 undefined가 이름 자리에 들어가는 종류라, 화면에 「undefined」가 뜨거나 문장이 통째로 안 나오거나 합니다.
id 문자열로 통일해 조회하는 쪽으로 맞췄습니다.
곁들여 — 라벨을 원 안으로
같은 화면에서 엿새 전에 한 작업인데 성격이 이어집니다. 노드 라벨이 원 바깥에 두 줄로 붙어 있었어요. 국문 명칭과 CURIE가 원 밑에 늘어서니 노드가 많아질수록 글자끼리 겹쳤습니다.
라벨을 원 안으로 넣었습니다. 17개 노드 예외 없이요.
여기서 신경 쓴 건 배치를 안 흔드는 것이었습니다.
COLLIDE_R 고정이라 노드 간격·배치 불변, 화살표 끊기는 nodeR이 rDraw를 읽어 추종
글자가 안 들어가는 노드는 원을 키워야 하는데, 그 커진 반지름을 충돌 판정에도 쓰면 배치가 통째로 다시 잡힙니다. 그래서 충돌 반지름(COLLIDE_R)은 고정하고 그리는 반지름(rDraw)만 개별로 키웠어요. 화살표를 원 경계에서 끊는 계산은 rDraw를 따라가게 해서 촉이 원 안으로 파고들지 않게 했습니다.
긴 CURIE는 접두사에서 줄을 바꿉니다(kadif: / ResearchProject). 다만 최종 원이 작아질 때만 나눠요 — 안 나눠도 들어가면 한 줄이 낫습니다.
검증
- 술어 18종 각각에 대해 실제 데이터로 문장이 나오는 것 확인. 조사가 어색한 자리를 찾는 게 목적이라 눈으로 읽었습니다.
- 받침 있는 이름·없는 이름·ㄹ 받침 이름·한글 아닌 이름 네 경우로
josa확인. - 관계가 몰리는 클래스(행위자)에서 20줄 → 6줄 확인.
- 데이터 맵 쪽에서도 같은 문장이 나오는 것 확인 — 링크 양끝 타입이 다른 자리라 여기가 안 되면 통째로 빕니다.
- 기본 설명 ↔ 문장 읽기 전환 후 다른 클래스를 골라도 읽는 방식이 유지되는 것 확인.
- 서버 코드·엔드포인트 변경 0. 기존 응답에 있던 값을 다시 배치한 것뿐입니다.
- 노드 라벨은 17개 전부에서 글자가 원 안에 담기는 것과, 라벨 변경 전후로 배치가 안 바뀌는 것을 확인했습니다.
남은 것 · 한계
- 문장 틀이 코드에 있습니다. 열흘 뒤에 저는 화면이 만들어 내는 글자를 없애자는 원칙으로 M/O 표기와 열 이름을 전부 등록값으로 옮겼는데, 이 문장 틀은 정확히 그 반대입니다. 술어의 국문 명칭은 DB에 있지만 "문장"에 해당하는 칸은 없어서 코드에 뒀어요. 술어가 늘면 여기만 옛날에 남습니다. 언젠가 데이터로 가야 할 자리라고 보지만 지금은 안 갔습니다.
- 18종 밖의 술어는 여전히 기계 틀입니다. 기각했던 ②로 떨어져요. "A의 배포는 B입니다"가 안 읽힌다고 써놓고 새 술어에는 그 문장이 나갑니다.
- 한글이 아닌 이름은
은(는)병기입니다. CURIE만 있는 노드가 주어면 문장이 어색해요. 표기를 고르는 대신 판단을 유보한 건데, 유보한 티가 화면에 그대로 납니다. - 묶은 문장의 상한이 없습니다. 「A·B·C」로 묶는데 상대가 마흔 개면 한 문장이 마흔 개 이름을 답니다. 20줄이 6줄이 됐다는 건 그 데이터에서 그랬다는 뜻이지, 더 몰린 경우를 안 봤습니다.
- 커밋을 못 나눴습니다. 패널 스크롤 수정이 같은 함수의 같은 줄에 섞여서, 기능 추가와 레이아웃 수정이 한 커밋에 들어갔어요. 나중에 되짚을 때 어느 쪽이 원인인지 가리기 어려운 자리를 만들었습니다.
- 라벨을 원 안으로 넣은 건 노드 17개 기준입니다. 노드가 더 늘면
rDraw로 키운 원들이 서로 겹치기 시작할 텐데,COLLIDE_R은 고정이라 그때는 다시 봐야 합니다.
관련 글: 등록된 적 없는 ASN.1을 파싱하고 있었다 · 시맨틱 웹의 이해 — 공부한 개념이 그대로 실무 도메인이 된 경우