최근에 회사에서 조직도 기능을 넣어서 관련된 방식을 찾던중 여러 가지를 찾았고 그중에서 closure table 방식에 대해서 알게 되어서 정리해보았습니다.
Closure Table: 계층 구조를 표현하는 효율적인 모델
복잡한 계층 구조를 관계형 데이터베이스에서 어떻게 표현할 수 있을까요?
카테고리, 조직도, 댓글처럼 트리 형태의 데이터를 다루다 보면, 효율적인 탐색과 관리가 중요해집니다.
계층 구조 표현의 어려움
관계형 데이터베이스(RDBMS)는 본질적으로 테이블 간 관계를 1:1, 1:N, N:M의 형태로 표현하는 데 최적화되어 있습니다. 하지만 트리 구조는 이보다 복잡하며, 일반적으로 다음과 같은 방법으로 표현됩니다.
- Adjacency List: 각 노드에 부모 ID를 저장
- Path Enumeration: 루트부터 현재 노드까지의 경로를 문자열로 저장
- Nested Set: 노드의 left, right 값을 통해 트리를 표현
- Closure Table: 조상-자손 관계를 별도 테이블로 저장
이 중에서 Closure Table은 조회 성능이 우수하며, 다양한 트리 관련 질의를 효율적으로 처리할 수 있는 구조입니다.
Closure Table이란?
Closure Table은 트리 내 모든 노드 간의 조상-자손 관계를 별도의 테이블에 저장하는 방식입니다. 한 노드가 여러 자식 또는 부모를 가질 수 있는 일반적인 계층 구조를 포함해, 다양한 형태의 트리를 지원할 수 있습니다.
예를 들어 A → B → C 구조가 있다고 하면, Closure Table에는 다음과 같은 정보가 저장됩니다.
| ancestor_id | descendant_id | depth |
|---|---|---|
| A | A | 0 |
| A | B | 1 |
| A | C | 2 |
| B | B | 0 |
| B | C | 1 |
| C | C | 0 |
자기 자신과의 관계도 포함되는 것이 특징이며, depth 컬럼은 두 노드 간의 거리(0이면 자기 자신)를 나타냅니다.
테이블 구조 예시
다음은 Closure Table을 적용한 카테고리 트리 예시입니다.
1. categories 테이블 (실제 노드)
CREATE TABLE categories (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL
);
2. category_closure 테이블 (조상-자손 관계 저장)
CREATE TABLE category_closure (
ancestor_id INT NOT NULL,
descendant_id INT NOT NULL,
depth INT NOT NULL,
PRIMARY KEY (ancestor_id, descendant_id),
FOREIGN KEY (ancestor_id) REFERENCES categories(id),
FOREIGN KEY (descendant_id) REFERENCES categories(id)
);
장점과 단점
장점
- 빠른 조회: 조상이나 자손을 조회할 때 재귀 쿼리 없이도 한 번의 조인으로 처리할 수 있습니다.
- 경로 전체 조회 가능: 루트에서 특정 노드까지의 경로, 또는 특정 노드의 서브트리를 쉽게 가져올 수 있습니다.
- 서브트리 카운트, 필터링 등 복잡한 트리 연산을 SQL만으로 처리할 수 있습니다.
단점
- 저장 공간 증가: N개의 노드가 있을 경우, 최악의 경우 N² 개의 행이 Closure Table에 저장됩니다.
- 삽입/삭제가 비쌈: 노드를 추가하거나 삭제할 경우, 관련된 모든 경로를 갱신해야 하므로 로직이 복잡해집니다.
- 변화가 많은 트리에 부적합: 구조 변경이 빈번한 경우에는 유지비용이 큽니다.
Closure Table 쿼리 예시
1. 특정 노드의 모든 자손 조회
SELECT c.*
FROM categories c
JOIN category_closure cc ON c.id = cc.descendant_id
WHERE cc.ancestor_id = 1;
2. 특정 노드의 모든 조상 조회
SELECT c.*
FROM categories c
JOIN category_closure cc ON c.id = cc.ancestor_id
WHERE cc.descendant_id = 5;
3. 루트 노드 조회
SELECT c.*
FROM categories c
WHERE NOT EXISTS (
SELECT 1
FROM category_closure cc
WHERE c.id = cc.descendant_id AND cc.depth > 0
);
언제 Closure Table을 사용할까?
다음과 같은 조건에서는 Closure Table이 적합합니다.
- 트리의 구조가 안정적이고 자주 변경되지 않을 때
- 자손/조상 트리를 빠르게 탐색해야 할 때
- 트리의 깊이가 깊거나 불균형적일 때
- 검색/조회 성능이 중요한 서비스 (카테고리, 메뉴, 조직도 등)
반면, 트리 구조가 자주 변경되거나 단순한 경우에는 오히려 Adjacency List 같은 단순한 구조가 더 적합할 수 있습니다.
정리
Closure Table은 계층형 데이터를 표현하는 여러 방법 중 가장 유연하고 성능이 뛰어난 방식 중 하나입니다.
단, 그만큼 복잡성과 유지 비용이 수반되므로, 적용 전에는 데이터의 변경 빈도와 조회 패턴을 충분히 고려할 필요가 있습니다.
데이터 트리 구조가 많고, 자주 조회되는 서비스라면 Closure Table은 충분히 도입할 만한 가치가 있다고 생각합니다.