회사 업무·인력관리 온톨로지 설계 — 직원·조직·스킬을 그래프로 모델링하기

@JavaPark · 2026년 6월 16일 · 24 min read

회사 업무·인력관리 온톨로지 설계 커버 — 직원·조직·스킬을 그래프로 모델링하기

안녕하세요, 자바파커입니다.

"백엔드 결제 모듈 맡길 수 있는, 지금 여유 있는 사람 누구죠?" — 이 한 줄에 답하려고 엑셀 세 개를 vlookup으로 엮어본 적, 혹시 있으신가요?

인사·조직 데이터는 보통 이렇게 흩어져 있습니다. 조직도는 PPT, 스킬은 자기소개 시트, 프로젝트 투입 현황은 또 다른 엑셀. 사람은 알지만 "이 조건을 모두 만족하는 사람" 을 물으면 한참 손으로 엮어야 답이 나옵니다.

결론부터 말씀드리면 — 인력관리 데이터는 표가 아니라 그래프로 모델링해야 진짜 질문에 답합니다. 직원·부서·직무·스킬·프로젝트는 서로 얽힌 관계망이고, 관계망을 표로 누르면 그 관계가 사라지기 때문이에요.

이번 글에서는 회사 업무와 인력관리 도메인의 온톨로지를 처음부터 설계해봅니다. 제가 블로그 검색 온톨로지를 설계할 때 썼던 방법, 답할 질의에서 거꾸로 푸는 방식을 그대로 HR 도메인에 적용해볼게요. 1) 답할 질의 정의 → 2) 클래스 도출 → 3) 속성·관계 정리 → 4) HR 특유의 함정 회피, 4단계로 갑니다.


1단계: 답할 질의를 먼저 적는다

설계의 출발점은 조직도 그리기가 아니라 답하고 싶은 질문 리스트입니다. 어떤 질문에 답할 시스템인지 모르고 클래스부터 뽑으면, 만들어 놓고 보니 정작 핵심 질문을 못 푸는 그래프가 됩니다.

인력관리 도메인에서 실제로 답하고 싶은 질의를 적어봤습니다.

# 질의 예시 필요한 정보
Q1 "결제 모듈 맡길 수 있는, 지금 여유 있는 사람" 스킬 + 현재 투입률(가용성)
Q2 "이 프로젝트에 투입된 인원과 각자의 역할" 프로젝트-사람 투입 + 역할
Q3 "Kubernetes 경험자 중 플랫폼팀 소속" 스킬 + 조직 소속
Q4 "다음 분기에 프로젝트 롤오프되는 인원" 투입 기간(종료일)
Q5 "이 사람의 보고 라인 전체" 보고 관계 (조직 계층)
Q6 "시니어 백엔드 직무의 승진 요건을 충족한 사람" 직무 요건 + 보유 스킬/경력
Q7 "결제 운영 시스템에 접근 권한이 있는 사람" 역할 기반 권한 (RBAC)
Q8 "AWS 자격증을 보유했지만 현재 안 쓰는 사람" 자격증 + 현재 투입 도메인

이 8개 질의가 곧 온톨로지가 통과해야 할 테스트 케이스입니다. 설계 끝에 "이 8개를 다 표현할 수 있는가?"로 검증할 거예요.

팁: 질의는 "사람 목록을 뽑는 질문" 위주로 적되, Q5(보고 라인)·Q7(권한)처럼 관계를 타고 들어가는 질문을 꼭 한두 개 섞으세요. 단순 필터 질문만 적으면 표로도 풀려서 굳이 그래프를 쓸 이유가 안 보입니다.


2단계: 클래스 후보 뽑기 — 명사를 모은다

질의 안에서 명사를 추출합니다. 명사는 곧 클래스 후보예요.

명사 뽑기: 사람/직원(Employee), 결제 모듈·플랫폼팀(부서/조직), 역할(프로젝트 내 역할), 스킬(Kubernetes·백엔드), 프로젝트, 투입(기간·투입률), 직무/직급(시니어 백엔드), 승진 요건, 자격증(AWS), 권한/접근

여기서 클래스로 승격할 것 vs 속성으로 남길 것을 골라냅니다. 기준은 단순합니다. ① 여러 엔티티와 N:N으로 연결되는가, ② 그 명사 자체에 메타데이터가 붙는가, ③ 검색의 진입점이 되는가. 하나라도 YES면 클래스입니다.

명사 클래스로? 이유
Employee (직원) O 핵심 엔티티
OrgUnit (조직/부서) O 계층 구조 + 자체 메타데이터(이름, 코드, 상위 조직)
Job (직무) O "백엔드 엔지니어" 같은 직무 정의, 요건이 붙음
JobGrade (직급/레벨) O 시니어/주니어 등급, 승진 경로의 노드
Skill (스킬) O 직원·직무와 N:N, 향후 상위/하위 스킬 관계 가능
Project (프로젝트) O 여러 직원이 투입, 자체 메타데이터(기간, 고객)
Assignment (투입) O 사람-프로젝트 사이의 "관계에 데이터가 붙음" (뒤에서 설명)
Certification (자격증) O 발급기관·만료일 메타데이터, 직원과 N:N
AccessRole (접근 역할) O RBAC의 권한 묶음, 직원·시스템과 N:N
시니어, AWS, 다음 분기 X 등급명/자격명/날짜는 위 클래스의 속성 또는 인스턴스

9개 클래스가 나왔습니다. 많아 보여도 괜찮아요 — 다음 단계에서 정리됩니다. 여기서 가장 중요한 건 Assignment를 클래스로 끌어올린 것입니다. 왜 그런지가 이 도메인의 핵심이라 4단계에서 따로 다룹니다.


3단계: 속성과 관계 도출

클래스가 정해지면 속성(클래스 자체가 갖는 값)과 관계(클래스끼리 연결)를 채웁니다.

속성 정리

Employee
  ├─ employeeId   (식별자)
  ├─ name         (이름)
  ├─ email        (연락처)
  ├─ hireDate     (입사일)
  └─ status       (재직/휴직/퇴사)

OrgUnit
  ├─ code         (식별자)
  ├─ name         (조직명)
  └─ type         (본부/팀/파트)

Job
  ├─ code         (식별자)
  └─ title        (직무명: 백엔드 엔지니어 등)

JobGrade
  ├─ code         (식별자: G3, G4 …)
  └─ name         (표시명: 시니어 등)

Skill
  ├─ name         (식별자: Kubernetes 등)
  └─ category     (인프라/백엔드/PM 등)

Project
  ├─ code         (식별자)
  ├─ name         (프로젝트명)
  ├─ startDate
  └─ endDate

Assignment            ← 사람-프로젝트 투입(관계를 클래스로)
  ├─ role             (PM/리드/멤버 — 프로젝트 내 역할)
  ├─ allocation       (투입률: 0.5 = 50%)
  ├─ startDate
  └─ endDate

Certification
  ├─ name             (식별자: AWS SAA 등)
  ├─ issuer           (발급기관)
  └─ ...

AccessRole
  ├─ code             (식별자: payment-ops 등)
  └─ description

식별자는 가능하면 사람이 읽을 수 있는 키(code, name)를 씁니다. employeeId만 빼면 대부분 자연키로 충분해요.

관계 도출

1단계의 질의 8개를 다시 보면서, 각 질의가 어떤 관계를 타고 도달하는지 따져봅니다.

관계 출발 도착 카디널리티 어느 질의를 위한 건가
belongsTo Employee OrgUnit N:1 Q3
reportsTo Employee Employee N:1 Q5
parentUnit OrgUnit OrgUnit N:1 Q5 (조직 계층)
hasJob Employee Job N:1 Q6
atGrade Employee JobGrade N:1 Q6
hasSkill Employee Skill N:N Q1, Q3, Q8
requiresSkill Job Skill N:N Q6 (직무 요건)
assignedVia Employee Assignment 1:N Q1, Q2, Q4
onProject Assignment Project N:1 Q2, Q4
holds Employee Certification N:N Q8
hasAccessRole Employee AccessRole N:N Q7
grantsAccessTo AccessRole Project/System N:N Q7

reportsTobelongsTo일부러 분리한 점에 주목하세요. 매트릭스 조직에서는 "소속 조직"과 "보고 대상"이 다를 수 있습니다(팀에 소속이지만 보고는 PM에게). 하나로 합치면 매트릭스 구조를 표현하지 못합니다.

한 장으로 그리면

        [OrgUnit] ──parentUnit──▶ [OrgUnit]   (조직 계층, self-loop)
            ▲
            │ belongsTo
            │
[Skill] ◀─hasSkill─ [Employee] ─reportsTo─▶ [Employee]   (보고 라인)
   ▲                    │  │
   │ requiresSkill      │  ├─hasJob──▶ [Job] ─requiresSkill─▶ [Skill]
   │                    │  ├─atGrade─▶ [JobGrade]
  [Job]                 │  ├─holds───▶ [Certification]
                        │  └─hasAccessRole─▶ [AccessRole] ─grantsAccessTo─▶ [Project]
                        │
                  assignedVia
                        │
                        ▼
                  [Assignment] ─onProject─▶ [Project]
                  (role, allocation, 기간)

이 그림이 곧 인력관리 온톨로지의 첫 버전입니다.


4단계: HR 도메인에서 꼭 빠지는 함정 5가지

여기까지 와도 실제 데이터를 부어 보면 깨지는 지점이 있습니다. 인사 도메인 특유의 함정 5가지를 짚습니다.

함정 1) "투입"을 단순 관계로 두면 안 된다 — 관계의 사물화(Reification)

가장 흔한 실수입니다. Employee ─assignedTo▶ Project로 끝내고 싶은 유혹이 큽니다. 하지만 투입에는 역할·투입률·기간이 붙습니다. "김개발이 결제 프로젝트에 PM으로 50%, 7월까지" — 이 정보는 사람의 속성도, 프로젝트의 속성도 아닙니다. 관계 그 자체의 속성이에요.

이럴 때 관계를 하나의 클래스(Assignment)로 끌어올립니다. 이걸 사물화(reification) 또는 n-항 관계 모델링이라고 합니다. 같은 사람이 같은 프로젝트에 시기를 달리해 두 번 투입될 수도 있으니, Assignment를 노드로 둬야 Q4("다음 분기 롤오프")가 깔끔하게 풀립니다.

판정 기준: "A가 B와 관계 맺는데, 그 관계에 언제·얼마나·어떤 자격으로가 붙는가?" 붙으면 관계를 클래스로 올리세요.

함정 2) 직무(Job)·직급(JobGrade)·역할(Role)을 한 덩어리로 뭉개기

한국 조직에서 특히 자주 섞입니다. 셋은 전부 다른 축입니다.

개념 무엇인가 예시
직무 (Job) 무슨 일을 하는가 백엔드 엔지니어, PM
직급 (Grade) 어느 레벨인가 주니어 / 시니어 / 리드
역할 (Role) 이 프로젝트에서 무슨 역할 이 프로젝트의 PM, 멤버

"시니어 백엔드 엔지니어가 A프로젝트에서는 멤버, B프로젝트에서는 리드"가 자연스럽게 표현돼야 합니다. 직무·직급은 사람에 거의 고정으로 붙고(hasJob/atGrade), 역할은 투입(Assignment.role)에 붙는다 — 이렇게 소속 위치가 다릅니다. 한 필드로 합치면 Q2도 Q6도 둘 중 하나는 못 풉니다.

함정 3) 조직 계층과 보고 라인을 같다고 가정하기

함정 2와 짝입니다. belongsTo(조직 소속)와 reportsTo(보고 대상)를 동일시하면 매트릭스·겸직·파견 구조가 전부 깨집니다. 둘 다 self-reference가 가능한 별도 관계로 두세요. 평소엔 일치하더라도, 다를 수 있는 두 축은 처음부터 분리하는 게 정석입니다.

함정 4) 권한을 사람에 직접 박기

"김개발 → 결제시스템 접근 가능"을 직접 연결하고 싶지만, 사람이 100명이면 권한 부여가 100번 흩어집니다. 가운데 AccessRole을 두는 RBAC 모델이 정답이에요.

Employee ─hasAccessRole▶ AccessRole ─grantsAccessTo▶ System/Project

이렇게 두면 Q7("이 시스템에 접근 가능한 사람")이 역방향 탐색 한 번으로 풀리고, 권한 회수도 역할 단위로 끝납니다. 인사이동이 잦은 조직일수록 이 구조의 효과가 큽니다.

함정 5) 시점(時點)을 무시하기 — "현재"만 담으면 이력이 사라진다

인사 데이터의 절반은 시간입니다. 지금 누가 어디 소속인지뿐 아니라, 언제부터 언제까지 그랬는지가 중요하죠. hasSkill·belongsTo처럼 변하는 관계에 최소한 시작일을, 이력이 필요한 관계(투입·소속 이력)는 함정 1처럼 사물화해서 기간을 답니다.

다만 모든 걸 이력화하면 과대 설계입니다. 감사·정산에 꼭 필요한 관계(투입·급여·권한 부여)만 시점을 남기고, 나머지는 현재 값만 두세요. 설계 단계에서 가장 비싼 실수는 과소 설계가 아니라 과대 설계입니다.


검증 — 질의 8개를 다 표현할 수 있는가?

설계가 끝났으면 1단계의 질의로 다시 검증합니다. 그래프 질의어(Cypher 류)로 머릿속에 그려봅니다.

질의 경로 표현 가능?
Q1 결제 맡길 여유 인력 Employee ─hasSkill▶ Skill(결제), 단 SUM(assignedVia.allocation) < 1
Q2 프로젝트 투입 인원·역할 Project ◀onProject◀ Assignment ◀assignedVia◀ Employee (+ role)
Q3 K8s + 플랫폼팀 Employee ─hasSkill▶ Skill(K8s) AND ─belongsTo▶ OrgUnit(플랫폼팀)
Q4 다음 분기 롤오프 Assignment(endDate in 분기) ─assignedVia◀ Employee
Q5 보고 라인 전체 Employee ─reportsTo▶* Employee (가변 길이 탐색)
Q6 승진 요건 충족자 Job ─requiresSkill▶ Skill ⊆ Employee ─hasSkill▶ Skill
Q7 시스템 접근 가능자 System ◀grantsAccessTo◀ AccessRole ◀hasAccessRole◀ Employee
Q8 자격증 있지만 안 쓰는 사람 Employee ─holds▶ Cert(AWS) AND NOT (현재 Assignment.domain = 인프라)

8개 모두 표현 가능합니다. 이 시점에서 설계가 일단락된 거예요. 하나라도 안 되면 그 질의를 위한 관계를 추가하거나 — 정 안 되면 검색 범위 밖으로 빼는 결정을 내립니다.


잠깐 — 지금까지 만든 건 "스키마"이지 "데이터"가 아닙니다

여기까지 따라오셨다면 한 가지 짚고 넘어가야 합니다. 우리가 1~4단계에서 설계한 건 온톨로지 "스키마"(schema)이지, 실제 인사 데이터(인스턴스)가 아닙니다. 이 둘을 헷갈리면 "설계는 끝났는데 왜 검색이 안 되지?" 하고 멈칫하게 됩니다.

구분 정체 HR 예시 비유
스키마 클래스·속성·관계의 정의(틀) Employee ─hasSkill▶ Skill 관계가 있다 붕어빵
인스턴스 그 틀을 따르는 실제 개체(데이터) 김개발 ─hasSkill▶ Kubernetes 찍어낸 붕어빵

학술 용어로는 스키마를 TBox(용어 정의), 인스턴스를 ABox(사실 주장)라고 부릅니다. 관계형 DB로 치면 스키마는 테이블 정의(DDL), 인스턴스는 **행(row)**입니다.

# 스키마 (틀 — 거의 안 변함, 작음)
class Employee { employeeId, name, hireDate }
class Skill    { name, category }
relation hasSkill : Employee → Skill

# 인스턴스 (실제 데이터 — 계속 늘고 바뀜, 많음)
emp:kim   a Employee ; name "김개발" ; hireDate "2021-03-02" .
skill:k8s a Skill    ; name "Kubernetes" .
emp:kim   hasSkill   skill:k8s .      ← 스키마의 hasSkill를 실제로 채운 한 줄

이 둘의 관계에서 기억할 세 가지:

  1. 인스턴스는 스키마를 반드시 따른다. 스키마에 hasSkill : Employee → Skill만 있는데 인스턴스에서 김개발 ─hasSkill▶ 결제프로젝트(Skill이 아닌 Project)를 연결하면 스키마 위반입니다. 스키마가 합법 범위를 규정해요.
  2. 수가 비대칭이다. 스키마는 클래스 9개·관계 12개로 작고 거의 고정. 인스턴스는 직원 수백·투입 수천 건으로 계속 변합니다. → 스키마 설계에 시간을 쓰고, 인스턴스는 자동 적재하는 게 정석입니다.
  3. 검증 층이 다르다. 바로 위에서 한 "질의 8개 표현 가능?"은 스키마 검증이고, 실제 직원 데이터를 그래프에 넣고 Cypher를 돌려 답을 받아보는 건 인스턴스 검증입니다.

흐름으로 보면 스키마 설계(이 글의 1~4단계) → 인스턴스 적재(인사 시스템·엑셀에서 데이터 주입) → 질의입니다. 이 글은 스키마까지를 다뤘고, 적재 단계는 기존 시리즈 3편에서 blog의 frontmatter를 파싱해 그래프에 인스턴스로 넣은 방식과 똑같이 — HR 도메인에서는 인사 DB·스프레드시트를 매핑해 — 채우면 됩니다.


표 대신 그래프, 언제 가치가 나오나

솔직히 말하면, 직원 30명에 프로젝트 몇 개 수준이면 엑셀로도 됩니다. 그래프 온톨로지가 진짜 빛나는 건 이럴 때예요.

  • N홉 질문이 일상일 때 — "내 밑의 밑까지 K8s 가능자"(Q5+Q3)처럼 관계를 여러 번 타고 들어가는 질문
  • 조건 교차가 많을 때 — 스킬 × 가용성 × 소속 × 권한을 동시에 거는 Q1, Q8
  • 이력·감사가 필요할 때 — 누가 언제 어떤 권한을 가졌는지 추적
  • LLM에게 물려줄 때 — 이 구조를 GraphRAG에 얹으면 "결제 맡길 여유 있는 사람?"을 자연어로 묻고 근거까지 답하게 만들 수 있습니다 (이건 다음 기회에 별도로 다뤄볼게요)

반대로 단순 명단·연락처 조회만 필요하면 굳이 그래프로 갈 이유가 없습니다. 답할 질의가 그래프를 정당화하지 못하면, 표가 정답입니다.


자주 묻는 질문 (FAQ)

Q1. 꼭 그래프 DB(Neo4j 등)를 써야 하나요? 관계형 DB로는 안 되나요? 됩니다. 위 온톨로지는 관계형 테이블로도 그대로 옮길 수 있어요(Assignment는 조인 테이블에 컬럼 추가). 다만 Q5(보고 라인 전체)처럼 깊이가 가변인 재귀 탐색이 잦으면 그래프 DB가 압도적으로 편합니다. 재귀 CTE를 매번 짜는 게 괜찮다면 RDB로 시작해도 무방합니다. 설계는 스택과 독립적이라는 게 이 방식의 장점이에요.

Q2. 직무·직급·역할이 회사마다 다른데 이 모델이 우리 회사에도 맞나요? 명칭은 바꿔도 세 개의 축(무슨 일 / 어느 레벨 / 이 프로젝트에서의 역할)이 다르다는 원칙은 거의 모든 조직에 적용됩니다. 직급 체계가 없는 수평 조직이면 JobGrade를 빼고, 프로젝트제가 아니면 Assignment 대신 OrgUnit 소속 이력만 남기세요. 1단계 질의 리스트를 자기 회사 버전으로 다시 적는 게 먼저입니다.

Q3. 개인정보·민감정보(급여, 평가)는 어떻게 다루나요? 온톨로지 모델에는 관계와 식별자만 두고, 급여·평가 같은 민감 속성은 접근 통제가 걸린 별도 저장소로 분리하는 걸 권합니다. 그래프에는 "Employee가 SalaryRecord를 가진다"는 관계만 남기고 실제 값은 권한 검증 후 조회하는 식이죠. Q7의 AccessRole 구조가 여기서도 그대로 쓰입니다.


인력관리 데이터를 표로만 관리하다 "그 사람이 누구였더라" 하며 시트를 뒤진 경험, 다들 한 번쯤 있으실 거예요. 여러분 회사에서 답하기 어려웠던 인사 질문 한두 개를 먼저 적어보세요. 그게 곧 온톨로지 설계의 출발점입니다. 댓글로 공유해주시면 같이 모델링해볼게요.

@JavaPark
AI 시대의 개발자 도구, 실전 경험을 공유합니다