동적 속성 저장 - 희소 열, EAV 및 JSONB 설명
Takashi Yamamoto
Infrastructure Engineer · Leapcell

소개
끊임없이 진화하는 데이터 관리 환경에서 애플리케이션은 종종 매우 가변적이고 예측 불가능한 속성을 가진 데이터를 처리하는 문제에 직면합니다. 고정된 열 구조를 가진 기존 관계형 데이터베이스 스키마는 이러한 요구 사항에 효율적으로 적응하는 데 어려움을 겪을 수 있습니다. 예를 들어, 다양한 제품 범주(예: 전자 제품, 의류, 서적)가 고유한 특성 세트를 가지고 있거나 사용자가 사용자 정의 필드를 정의할 수 있는 사용자 프로필 시스템을 상상해 보세요. 가능한 모든 속성을 단일의 넓은 테이블에 강제로 넣으면 수많은 빈 열이 생기고 저장이 비효율적이게 됩니다. 반면, 각 속성 세트에 대해 별도의 테이블을 만들면 스키마 관리의 악몽이 될 수 있습니다. 이 문서는 희소 열, EAV(Entity-Attribute-Value) 및 JSONB라는 세 가지 주요 데이터베이스 설계 패턴을 탐구하며, 각각 고유한 장단점과 이상적인 사용 사례를 통해 동적 속성을 효과적으로 저장하고 쿼리하기 위한 고유한 전략을 제공합니다.
동적 속성 관리를 위한 핵심 개념
패턴을 자세히 살펴보기 전에 동적 속성 저장을 이해하는 데 중요한 몇 가지 핵심 개념을 정의해 보겠습니다.
- 고정 스키마: 미리 정의된 정적 열 집합을 가진 테이블의 데이터베이스 설계입니다. 이것이 전통적인 관계형 모델입니다.
- 동적 속성: 사전에 알려지지 않았거나 동일한 유형의 개체 간에 크게 달라지는 개체의 속성입니다.
- 데이터 희소성: 테이블의 데이터가 상당 부분을
NULL
값으로 구성하는 상황으로, 특정 행에 대해 많은 열이 사용되지 않음을 나타냅니다. - 유연성: 스키마 변경 없이 새 속성을 추가하거나 기존 속성을 수정하는 용이성입니다.
- 쿼리 성능: 동적 속성을 기반으로 데이터를 검색하고 필터링하는 속도와 효율성입니다.
- 저장 효율성: 데이터 저장을 위해 디스크 공간이 얼마나 효과적으로 활용되는지, 낭비를 최소화합니다.
동적 속성을 위한 설계 패턴
1. 희소 열: 열 집합 활용
희소 열은 주로 SQL Server에서 사용되는 기능으로, 물리적 저장 공간을 차지하지 않고 NULL
값을 저장하도록 설계되었습니다. COLUMN_SET
(테이블의 모든 희소 열에 대한 형식화되지 않은 XML 표현)과 결합하면 단일의 넓은 테이블 내에서 다양한 열 집합을 관리하는 방법을 제공합니다.
원리: NULL
값을 명시적으로 저장하는 대신, 희소 열은 값이 없음을 나타내는 메타데이터를 저장합니다. 값이 존재하면 정상적으로 저장됩니다. COLUMN_SET
을 사용하면 애플리케이션에서 단일 XML 문서로 행의 모든 희소 열 값을 읽고 쓸 수 있습니다.
구현 (SQL Server):
CREATE TABLE Products ( ProductID INT PRIMARY KEY, ProductName VARCHAR(255), -- 일반 속성 Price DECIMAL(10, 2), -- 동적 속성을 위한 희소 열 ScreenSizeInches DECIMAL(3, 1) SPARSE NULL, -- 전자 제품용 ProcessorType VARCHAR(50) SPARSE NULL, -- 전자 제품용 Material VARCHAR(50) SPARSE NULL, -- 의류용 Author VARCHAR(100) SPARSE NULL, -- 서적용 -- 모든 희소 열에 대한 쉬운 액세스를 위한 열 집합 AllDynamicAttributes XML COLUMN_SET FOR ALL_SPARSE_COLUMNS ); -- 전자 제품 삽입 INSERT INTO Products (ProductID, ProductName, Price, ScreenSizeInches, ProcessorType) VALUES (1, 'Laptop X', 1200.00, 15.6, 'Intel i7'); -- 의류 제품 삽입 INSERT INTO Products (ProductID, ProductName, Price, Material) VALUES (2, 'T-Shirt', 25.00, 'Cotton'); -- 제품 열 집합 쿼리 SELECT ProductID, ProductName, AllDynamicAttributes FROM Products WHERE ProductID = 1;
애플리케이션 시나리오:
- 많은 수의 열이 행의 일부에만 적용될 때(데이터 희소성 > 20-40%).
- 고정적이고 알려진 속성이 데이터의 대부분을 구성할 때.
- 잠재적인 동적 속성 집합이 비교적 안정적이며 스키마 생성 시 정의될 수 있을 때.
- 주로 SQL Server 환경 내에서 사용될 때.
장점:
- 스키마 단순성: 모든 속성이 한 테이블에 있어 JOIN이 단순화됩니다.
- 유형 안전성: 각 희소 열은 정의된 데이터 유형을 유지합니다.
- 성능: 핵심 속성 및 특정 희소 열에 대한 쿼리에 성능을 발휘할 수 있으며, 직접 인덱스를 생성할 수 있습니다.
- 저장 최적화:
NULL
값이 공간을 소비하지 않습니다.
단점:
- 벤더 종속성: 주로 SQL Server 기능입니다.
- 스키마 진화: 새 동적 속성을 추가해도 여전히
ALTER TABLE
이 필요합니다. - 확장성: 수백 또는 수천 개의 희소 열이 있는 테이블은 다루기 어려워지며 열 제한에 도달할 수 있습니다.
COLUMN_SET
오버헤드:COLUMN_SET
을 읽거나 쓸 때 XML 파싱이 필요합니다.
2. EAV (Entity-Attribute-Value): 유연한 삼총사
EAV는 "개방형 스키마" 또는 "수직 테이블" 설계라고도 하며, 세 개의 열(엔티티 ID, 속성 이름, 값)을 사용하여 동적 속성을 저장합니다.
원리: 열이 속성을 나타내는 대신, 행은 개체에 대한 속성-값 쌍을 나타냅니다. 각 개체는 임의 개수의 속성-값 쌍을 가질 수 있습니다.
구현 (일반 SQL):
CREATE TABLE Products ( ProductID INT PRIMARY KEY, ProductName VARCHAR(255) -- ... 기타 일반 속성 ); CREATE TABLE ProductAttributes ( ProductAttributeID INT PRIMARY KEY IDENTITY(1,1), ProductID INT NOT NULL, AttributeName VARCHAR(100) NOT NULL, AttributeValue VARCHAR(MAX) NOT NULL, -- 모든 값을 문자열 형식으로 저장 FOREIGN KEY (ProductID) REFERENCES Products(ProductID) ); -- 전자 제품의 동적 속성 삽입 INSERT INTO ProductAttributes (ProductID, AttributeName, AttributeValue) VALUES (1, 'ScreenSizeInches', '15.6'), (1, 'ProcessorType', 'Intel i7'); -- 의류 제품의 동적 속성 삽입 INSERT INTO ProductAttributes (ProductID, AttributeName, AttributeValue) VALUES (2, 'Material', 'Cotton'); -- 특정 속성이 있는 제품 쿼리 SELECT p.ProductName, pa.AttributeName, pa.AttributeValue FROM Products p JOIN ProductAttributes pa ON p.ProductID = pa.ProductID WHERE p.ProductID = 1; -- 속성을 열로 가져오기(피벗과 유사) - 더 복잡한 쿼리 SELECT p.ProductName, MAX(CASE WHEN pa.AttributeName = 'ScreenSizeInches' THEN pa.AttributeValue END) AS ScreenSizeInches, MAX(CASE WHEN pa.AttributeName = 'ProcessorType' THEN pa.AttributeValue END) AS ProcessorType, MAX(CASE WHEN pa.AttributeName = 'Material' THEN pa.AttributeValue END) AS Material FROM Products p LEFT JOIN ProductAttributes pa ON p.ProductID = pa.ProductID GROUP BY p.ProductID, p.ProductName;
애플리케이션 시나리오:
- 매우 동적이고 예측 불가능한 속성 집합.
- 속성 집합이 자주 변경되거나 성장할 때.
- 보고 요구 사항이 복잡한 분석 쿼리보다는 키-값 조회로 자주 충족될 때, 특정 열 형식이 필요한 경우.
- 스키마 없는 또는 유연한 데이터 모델이 중요할 때.
장점:
- 최대 유연성: 새 속성에 대한 스키마 변경이 필요 없습니다.
- 확장성: 개체당 거의 무제한의 동적 속성을 처리할 수 있습니다.
- 저장 효율성: 기존의 속성-값 쌍만 저장하며
NULL
은 없습니다.
단점:
- 복잡한 쿼리: 특정 속성을 검색하거나 여러 속성에 걸쳐 쿼리하는 것은 종종 자체 JOIN, 피벗팅 또는 복잡한 서브쿼리가 필요하며, 이는 성능 저하로 이어집니다.
- 유형 안전성 부족: 모든 값은 일반적으로 일반 문자열 형식(
VARCHAR(MAX)
)으로 저장되며, 애플리케이션 수준에서 변환 및 유효성 검사가 필요합니다. - 데이터 무결성 문제: 동적 속성에 대한 데이터 유형, 제약 조건 및 관계를 적용하기 어렵습니다.
- 성능 오버헤드: 광범위한 JOIN 및 대규모 테이블 스캔은 특히 많은 속성이나 개체가 있을 때 쿼리 성능에 심각한 영향을 미칠 수 있습니다.
3. JSONB: 관계형 내의 네이티브 문서 저장소
JSONB (JSON Binary)는 PostgreSQL(및 MySQL의 JSON, SQL Server의 JSON과 같이 기능이 다양한 유사 구현도 존재)에서 사용 가능한 네이티브 JSON 데이터 형식입니다. JSON 데이터를 분해된 이진 형식으로 저장하여 JSON 문서 내의 키 및 값에 대한 효율적인 인덱싱 및 쿼리를 가능하게 합니다.
원리: 별도의 열이나 행 대신, 개체에 대한 모든 동적 속성은 전용 열의 단일 JSON 문서에 저장됩니다. 데이터베이스 엔진은 이 구조화된 데이터와 상호 작용하기 위한 함수와 연산자를 제공합니다.
구현 (PostgreSQL):
CREATE TABLE Products ( ProductID INT PRIMARY KEY, ProductName VARCHAR(255), Price DECIMAL(10, 2), DynamicAttributes JSONB ); -- 전자 제품 삽입 INSERT INTO Products (ProductID, ProductName, Price, DynamicAttributes) VALUES (1, 'Laptop X', 1200.00, '{"ScreenSizeInches": 15.6, "ProcessorType": "Intel i7"}'); -- 의류 제품 삽입 INSERT INTO Products (ProductID, ProductName, Price, DynamicAttributes) VALUES (2, 'T-Shirt', 25.00, '{"Material": "Cotton", "Size": "M"}'); -- 특정 속성 쿼리 SELECT ProductID, ProductName, DynamicAttributes->>'ScreenSizeInches' AS ScreenSize FROM Products WHERE ProductID = 1; -- 속성별 필터링 SELECT ProductID, ProductName, DynamicAttributes FROM Products WHERE DynamicAttributes->>'Material' = 'Cotton'; -- 빠른 쿼리를 위한 특정 JSONB 경로에 인덱스 생성 (Gin 인덱스) CREATE INDEX idx_products_material ON Products USING GIN ((DynamicAttributes->'Material')); -- 특정 속성 업데이트 UPDATE Products SET DynamicAttributes = jsonb_set(DynamicAttributes, '{ScreenSizeInches}', '13.3'::jsonb) WHERE ProductID = 1;
애플리케이션 시나리오:
- 속성이 종종 블록으로 또는 부분적으로 함께 쿼리될 때.
- 스키마 유연성과 쿼리 성능 간의 좋은 균형이 필요할 때.
- JSON 데이터와 자연스럽게 작동하는 애플리케이션과의 통합.
- 동적 속성이 일부 내부 구조(예: 중첩된 객체 또는 배열)를 가질 때.
- PostgreSQL이 선택된 데이터베이스일 때.
장점:
- 스키마 유연성: 테이블 스키마를 변경하지 않고 새 속성을 추가하거나 제거할 수 있습니다.
- 우수한 쿼리 성능: JSONB는 키 및 값의 인덱싱을 허용하여 중첩 데이터에 대한 쿼리를 크게 가속화합니다.
- 단일 열: 동적 데이터에 대한 단일 열의 단순성.
- 풍부한 데이터 유형: JSON은 다양한 데이터 유형(문자열, 숫자, 부울, 배열, 객체)을 지원합니다.
- 반구조화된 데이터: 복잡하고 중첩된 데이터 구조를 저장할 수 있습니다.
단점:
- 데이터베이스별: 기능 및 성능은 다른 SQL 데이터베이스 간에 매우 다양합니다. PostgreSQL의 JSONB는 특히 강력합니다.
- 네이티브 관계형 제약 조건 부족: 데이터베이스 수준의 유형 적용 및 외래 키 제약 조건은 JSONB 내의 데이터에 직접 적용되지 않습니다. 유효성 검사는 일반적으로 애플리케이션 계층 또는 고급 CHECK 제약 조건을 통해 이루어집니다.
- 증가된 쿼리 복잡성: JSONB 연산자를 포함하는 쿼리는 처음에는 표준 SQL보다 덜 직관적일 수 있습니다.
- 임의 키에 대한 제한된 인덱싱: 특정 키는 인덱싱할 수 있지만, (알려지지 않은 집합인 경우) 가능한 모든 키를 인덱싱하는 것은 불가능합니다.
결론
동적 속성을 저장하기 위해 희소 열, EAV 및 JSONB 중에서 선택하는 것은 보편적으로 "최고"인 패턴을 선택하는 것이 아니라, 어떤 패턴이 애플리케이션의 요구 사항, 데이터 특성, 쿼리 패턴 및 선택한 데이터베이스 시스템과 가장 효과적으로 일치하는지에 대한 문제입니다. 희소 열은 적당히 희소하고 미리 정의된 속성에 대해 유형 안전성 및 SQL Server 특정 저장소 이점을 제공합니다. EAV는 매우 휘발성이 높고 예측 불가능한 속성 집합에 대해 궁극적인 유연성을 제공하지만, 종종 쿼리 성능과 데이터 무결성에 상당한 비용을 지불합니다. JSONB는 스키마 유연성, 효율적인 쿼리(특히 인덱싱을 통해) 및 반구조화된 데이터 지원을 제공하여 강력한 균형을 이루며, PostgreSQL과 같은 데이터베이스에서 최신 애플리케이션을 위한 강력한 솔루션입니다. 유연성, 성능, 유형 안전성 및 스키마 복잡성의 절충점을 신중하게 고려함으로써 개발자는 동적 데이터를 효율적으로 관리하기 위한 가장 적절한 전략을 선택할 수 있습니다.