계층 구조 데이터 표현 방식과 리팩토링 전략
초기 사쿠밀(SakuMil)은 인접 리스트(Adjacency List)로 폴더 계층화 기능을 구현했습니다. 이는 부모 ID 참조의 단순한 구조로 쓰기 작업이 용이했으나, 1년 만에 고객 10배 성장과 함께 거대한 계층 구조가 형성되며 계층을 넘는 조회 시 심각한 N+1 쿼리 문제로 성능 저하가 발생했습니다.
이 문제 해결을 위해 두 가지 리팩토링 방안이 검토되었습니다.
1. 클로저 테이블 (Closure Table)
-
모든 조상-자손 경로를
ancestor_id,descendant_id,depth컬럼 포함 별도 테이블에 저장합니다. -
장점: 계층 읽기(조상/자손 조회)가 단일 SQL
JOIN쿼리로 매우 효율적이며, N+1 문제를 해결합니다. -
단점: 저장 공간 증가, 노드 생성/이동 시 경로 테이블 갱신 복잡성으로 구현 난이도가 높습니다.
2. 재귀 CTE (Recursive CTE) + 인접 리스트
-
기존 인접 리스트 구조 유지, SQL의
WITH RECURSIVE구문으로 계층 구조 재귀 쿼리.Rails 7.2.0인터페이스 지원. -
장점: 데이터 구조 변경 없이 단일 쿼리로 계층 읽기 가능하며, 인접 리스트의 쓰기 용이성을 유지합니다.
-
단점: DB 내 재귀 처리로 계층이 깊어질 경우 성능 저하 발생 가능.
성능 실험 및 리팩토링 결정
제품의 읽기 빈번 및 계층 제한 없음 워크로드 특성을 고려하여 자손 노드 탐색 성능 실험을 진행했습니다.
-
인접 리스트: 계층 깊이에 따라 쿼리 수가 기하급수적으로 증가하며 가장 느렸습니다.
-
재귀 CTE: 인접 리스트보다 빠르나, 깊이에 따라 성능이 점진적으로 저하되었습니다.
-
클로저 테이블: 계층 깊이에 상관없이 거의 일정한 매우 빠른 성능을 보였습니다. 깊이 13에서 인접 리스트 대비 931배, 재귀 CTE 대비 200배 이상 빠른 쿼리 실행 시간을 기록했습니다.
이러한 실험 결과를 바탕으로, 읽기 성능이 매우 중요한 제품 요구사항에 맞춰 클로저 테이블을 최종 리팩토링 방식으로 선택했습니다. 계층 구조의 비순환성(Acyclic) 유효성 검증의 중요성도 강조되었습니다.