데이터 베이스/설계 2편, 실무에서 반드시 마주치는 9가지 설계 패턴

Ch08. JSON 설계 - JSON 활용

webmaster 2026. 8. 1. 09:22

사례 1: 카테고리별 다른 상품 속성

쇼핑몰에서 가장 흔히 접하는 문제다. 스마트폰, 노트북, 의류, 식품 등 카테고리마다 필요한 속성이 완전히 다르다. 이 문제를 JSON으로 어떻게 해결하는지 살펴보자.

 

테이블 설계

DROP TABLE IF EXISTS product_catalog;

CREATE TABLE product_catalog (
    product_id BIGINT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(200) NOT NULL,
    category VARCHAR(100) NOT NULL,
    base_price INT NOT NULL,
    attributes JSON,
    created_at DATETIME NOT NULL DEFAULT NOW()
);
  • 공통 속성인 "name", "category", "base_price"는 일반 컬럼으로, 카테고리마다 다른 속성은 "attributes": "JSON" 컬럼으로 관리한다.

다양한 카테고리 상품 등록

-- 스마트폰
INSERT INTO product_catalog (name, category, base_price, attributes) VALUES
('갤럭시 S24 울트라', '스마트폰', 1650000, 
	JSON_OBJECT(
		'screen_size', 6.8,
		'storage_options', JSON_ARRAY(256, 512, 1024),
        'colors', JSON_ARRAY('티타늄 그레이', '티타늄 블랙','티타늄 바이올렛'),
        'ram', 12,
        '5g', true,
        'camera', JSON_OBJECT('main', 200, 'ultra_wide', 12, 'telephoto', 50),
        'battery', 5000,
        'os', 'Android 14'
));

-- 노트북
INSERT INTO product_catalog (name, category, base_price, attributes) VALUES
('맥북 프로 14 M3', '노트북', 2390000, 
	JSON_OBJECT(
		'screen_size', 14.2,
		'storage_options', JSON_ARRAY(512, 1024, 2048),
		'colors', JSON_ARRAY('스페이스 그레이','실버'),
		'ram_options', JSON_ARRAY(18, 36),
		'cpu', 'Apple M5 Pro',
		'gpu', 'Apple M5 Pro 14코어',
		'battery_life', 17,
		'weight', 1.55,
		'ports', JSON_ARRAY('HDMI', 'USB-C', 'MagSafe', 'SD카드')
));

-- 의류
INSERT INTO product_catalog (name, category, base_price, attributes) VALUES
('오버핏 맨투맨', '의류', 45000, 
	JSON_OBJECT(
		'sizes', JSON_ARRAY('S', 'M', 'L', 'XL', 'XXL'),
		'colors', JSON_ARRAY('블랙', '화이트', '그레이','네이비'),
		'material', '면 100%',
		'gender', 'unisex',
		'season', JSON_ARRAY('봄','가을'),
		'care_instructions', JSON_ARRAY('세탁기 사용 가능', '표백제 사용 금지','낮은 온도 다림질')
));

-- 식품
INSERT INTO product_catalog (name, category, base_price, attributes) VALUES
('유기농 아보카도', '식품', 8900,
	JSON_OBJECT(
		'origin', '멕시코',
		'unit', '개',
		'quantity', 3,
		'organic', true,
		'storage', '냉장 보관',
		'shelf_life_days', 7,
		'nutrition', 
			JSON_OBJECT(
				'calories', 160, 'fat', 15,
                'protein', 2,
				'carbs', 9,
				'fiber', 7
			),
		'allergens', JSON_ARRAY()
));

-- 가전제품
INSERT INTO product_catalog (name, category, base_price, attributes) VALUES
('LG 스탠바이미', '가전', 1290000, 
	JSON_OBJECT(
		'screen_size', 27,
		'resolution', '1920x1080',
		'panel_type', 'IPS',
		'touch_screen', true,
		'battery_life', 3,
		'weight', 8.5,
		'speakers', JSON_OBJECT('type', '내장', 'output', '20W'),
		'connectivity', JSON_ARRAY('Wi-Fi', 'Bluetooth', 'HDMI', 'USB'),
		'os', 'webOS'
));

 

카테고리별 속성 조회

-- 스마트폰 정보 조회
SELECT
	name,
	base_price,
	attributes->>'$.screen_size' AS screen_size,
	attributes->>'$.ram' AS ram,
	attributes->>'$.battery' AS battery,
	attributes->>'$.camera.main' AS main_camera
FROM product_catalog
WHERE category = '스마트폰';

쿼리 결과

-- 식품 영양정보 조회
SELECT
    name,
    attributes->>'$.origin' AS origin,
    attributes->>'$.nutrition.calories' AS calories,
    attributes->>'$.nutrition.protein' AS protein,
    attributes->>'$.organic' AS organic
FROM product_catalog
WHERE category = '식품';

쿼리 결과

 

사례 2: 상품 옵션과 재고 관리

쇼핑몰에서 하나의 상품이 여러 옵션(색상, 사이즈 조합)을 가지는 경우가 많다. 이를 JSON으로 관리하는 방법을 알아보자.

DROP TABLE IF EXISTS product_variant;
CREATE TABLE product_variant (
    product_id BIGINT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(200) NOT NULL,
    base_price INT NOT NULL,
    variants JSON NOT NULL,
    created_at DATETIME NOT NULL DEFAULT NOW()
);

 

옵션 데이터 등록

INSERT INTO product_variant (name, base_price, variants) VALUES
('나이키 에어맥스', 189000, 
	JSON_ARRAY(
		JSON_OBJECT('sku', 'AM-BLK-250', 'color', '블랙', 'size', 250, 'stock', 15, 'price_adjust', 0),
		JSON_OBJECT('sku', 'AM-BLK-255', 'color','블랙', 'size', 255, 'stock', 23, 'price_adjust', 0),
		JSON_OBJECT('sku', 'AM-BLK-260', 'color', '블랙', 'size', 260, 'stock', 18, 'price_adjust', 0),
		JSON_OBJECT('sku', 'AM-BLK-265', 'color', '블랙', 'size', 265, 'stock', 12, 'price_adjust', 0),
		JSON_OBJECT('sku', 'AM-WHT-250', 'color', '화이트', 'size', 250, 'stock', 8, 'price_adjust', 0),
		JSON_OBJECT('sku', 'AM-WHT-255', 'color', '화이트', 'size', 255, 'stock', 20, 'price_adjust', 0),
		JSON_OBJECT('sku', 'AM-WHT-260', 'color', '화이트', 'size', 260, 'stock', 5, 'price_adjust', 0),
		JSON_OBJECT('sku', 'AM-LTD-260', 'color', '한정판 골드', 'size', 260, 'stock', 3, 'price_adjust', 50000)
));
  • JSON 배열의 경우에는 다루기가 까다롭다.
  • 이렇게 만들어진 JSON 배열을 마치 테이블처럼 편리하게 다루는 방법을 알아보자.

JSON_TABLE로 옵션 펼치기

MySQL 8.0의 "JSON_TABLE" 함수를 사용하면 JSON 배열을 별도의 테이블처럼 펼칠 수 있다.

JSON_TABLE(
    json_doc,
    path
    COLUMNS (
        column_name data_type PATH json_path [ERROR | EMPTY handling],
        ...
    )
)
  • "JSON_TABLE"은 JSON 데이터를 SQL 세계로 끌어오는 "가상 테이블 생성 함수"다.
  • json_doc: JSON 컬럼 또는 JSON 값
  • path: JSON_TABLE이 반복할 기준 경로, 보통 배열을 가리킨다.
    • $[*]: 루트에 있는 배열의 모든 요소
    • $.orders[*] : orders 배열의 모든 요소
  • COLUMNS : JSON → 컬럼 매핑 정의
    • 예) "size INT PATH '$.size'" ("$.size"를 INT형 size 컬럼으로 매핑한다.)
  • 오류 처리 옵션 (기본값: "NULL ON EMPTY", "NULL ON ERROR")
    • ERROR ON ERROR: 변환 실패 시 오류
    • NULL ON ERROR: 실패 시 NULL
    • ERROR ON EMPTY: 값 없음 → 오류
    • NULL ON EMPTY: 값 없음 → NULL
SELECT
	p.name,
	p.base_price,
	v.sku,
	v.color,
	v.size,
	v.stock,
	(p.base_price + v.price_adjust) AS final_price
FROM product_variant p,
JSON_TABLE(p.variants, '$[*]' COLUMNS (
	sku VARCHAR(50) PATH '$.sku',
	color VARCHAR(50) PATH '$.color',
	size INT PATH '$.size',
	stock INT PATH '$.stock',
	price_adjust INT PATH '$.price_adjust'
)) AS v;

쿼리 결과

  • 이렇게 하면 p와 v는 조인을 통해서 결과를 생성한다.

특정 조건의 옵션 찾기

-- 재고가 10개 이하인 옵션 찾기
SELECT
    p.name,
    v.sku,
    v.color,
    v.size,
    v.stock
FROM product_variant p,
JSON_TABLE(p.variants, '$[*]' COLUMNS (
    sku VARCHAR(50) PATH '$.sku',
    color VARCHAR(50) PATH '$.color',
    size INT PATH '$.size',
    stock INT PATH '$.stock'
    )) AS v
WHERE v.stock <= 10;

쿼리 결과

색상별 총 재고 집계

SELECT
    p.name,
    v.color,
    SUM(v.stock) AS total_stock,
    COUNT(*) AS size_count
FROM product_variant p,
JSON_TABLE(p.variants, '$[*]' COLUMNS (
    color VARCHAR(50) PATH '$.color',
    stock INT PATH '$.stock'
)) AS v
GROUP BY p.name, v.color;

쿼리 결과

사례 3: 사용자 설정 저장

사용자마다 다른 설정을 저장해야 할 때 JSON이 유용하다. 알림 설정, UI 설정, 개인화 설정 등이 대표적이다.

DROP TABLE IF EXISTS user_settings;
CREATE TABLE user_settings (
    user_id BIGINT PRIMARY KEY,
    username VARCHAR(100) NOT NULL,
    settings JSON,
    created_at DATETIME NOT NULL DEFAULT NOW(),
    updated_at DATETIME NOT NULL DEFAULT NOW() ON UPDATE NOW()
);

 

사용자 설정 등록

INSERT INTO user_settings (
    user_id, 
    username, 
    settings
) 
VALUES 
(
    1, 
    'kim_user', 
    JSON_OBJECT(
        'notifications', JSON_OBJECT(
            'email', true,
            'push', true,
            'sms', false,
            'marketing', false,
            'frequency', 'realtime'
        ),
        'display', JSON_OBJECT(
            'theme', 'dark',
            'language', 'ko',
            'timezone', 'Asia/Seoul',
            'items_per_page', 20
        ),
        'privacy', JSON_OBJECT(
            'profile_visible', true,
            'show_online_status', false,
            'allow_friend_requests', true
        )
    )
),
(
    2, 
    'lee_user', 
    JSON_OBJECT(
        'notifications', JSON_OBJECT(
            'email', true,
            'push', false,
            'sms', true,
            'marketing', true,
            'frequency', 'daily'
        ),
        'display', JSON_OBJECT(
            'theme', 'light',
            'language', 'en',
            'timezone', 'America/New_York',
            'items_per_page', 50
        ),
        'privacy', JSON_OBJECT(
            'profile_visible', false,
            'show_online_status', false,
            'allow_friend_requests', false
        )
    )
);

 

특정 설정 조회

SELECT
    username,
    settings->>'$.display.theme' AS theme,
    settings->>'$.display.language' AS language,
    settings->>'$.notifications.email' AS email_notification
FROM user_settings;

쿼리 결과

특정 설정만 업데이트

-- 테마만 변경
UPDATE user_settings
SET settings = JSON_SET(settings, '$.display.theme', 'auto')
WHERE user_id = 1;

-- 결과 확인
SELECT username, settings->>'$.display.theme' AS theme
FROM user_settings WHERE user_id = 1;

쿼리 결과

사례 4: 로그와 이벤트 데이터

사용자 행동 로그, 시스템 이벤트 등 구조가 다양한 데이터를 저장할 때 JSON이 효과적이다.

 

테이블 설계

DROP TABLE IF EXISTS event_log;
CREATE TABLE event_log (
	log_id BIGINT PRIMARY KEY AUTO_INCREMENT,
	event_type VARCHAR(100) NOT NULL,
	user_id BIGINT,
	event_data JSON NOT NULL,
	created_at DATETIME NOT NULL DEFAULT NOW(),
	INDEX idx_event_type (event_type),
	INDEX idx_user_id (user_id),
	INDEX idx_created_at (created_at)
);

 

다양한 이벤트 로그 등록

-- 페이지 조회 이벤트
INSERT INTO event_log (
    event_type, 
    user_id, 
    event_data
) 
VALUES (
    'page_view', 
    1, 
    JSON_OBJECT(
        'page', '/products/phone/galaxy-s24',
        'referrer', 'https://google.com',
        'duration_seconds', 45,
        'device', JSON_OBJECT(
            'type', 'mobile', 
            'os', 'iOS', 
            'browser', 'Safari'
        )
    )
);

-- 검색 이벤트
INSERT INTO event_log (
    event_type, 
    user_id, 
    event_data
) 
VALUES (
    'search', 
    1, 
    JSON_OBJECT(
        'query', '갤럭시 케이스',
        'filters', JSON_OBJECT(
            'category', '액세서리', 
            'price_max', 50000
        ),
        'results_count', 42,
        'clicked_position', 3
    )
);

-- 장바구니 추가 이벤트
INSERT INTO event_log (
    event_type, 
    user_id, 
    event_data
) 
VALUES (
    'add_to_cart', 
    1, 
    JSON_OBJECT(
        'product_id', 101,
        'product_name', '갤럭시 S24',
        'quantity', 1,
        'price', 1200000,
        'cart_total', 1200000
    )
);

-- 구매 완료 이벤트
INSERT INTO event_log (
    event_type, 
    user_id, 
    event_data
) 
VALUES (
    'purchase', 
    1, 
    JSON_OBJECT(
        'order_id', 10001,
        'items_count', 2,
        'total_amount', 1270000,
        'payment_method', 'card',
        'coupon_used', 'WELCOME10'
    )
);

-- 시스템 에러 이벤트
INSERT INTO event_log (
    event_type, 
    user_id, 
    event_data
) 
VALUES (
    'error', 
    NULL, 
    JSON_OBJECT(
        'error_code', 'DB_ERROR',
        'message', 'Failed to connect to database',
        'server', 'db1',
        'stack_trace', '...',
        'level', 'critical'
    )
);

 

이벤트 타입별 조회

-- 사용자 1의 검색 이벤트 분석
SELECT
    event_data->>'$.query' AS search_query,
    event_data->>'$.results_count' AS results,
    event_data->>'$.clicked_position' AS clicked_at,
    created_at
FROM event_log
WHERE event_type = 'search' AND user_id = 1;

쿼리 결과

-- 사용자 1의 전체 이벤트 분석
SELECT
    event_type,
    event_data->>'$.page' AS page,
    event_data->>'$.query' AS search_query,
    event_data->>'$.results_count' AS results,
    event_data->>'$.clicked_position' AS clicked_at,
    event_data->>'$.product_name' AS name,
    event_data->>'$.total_amount' AS amount,
    created_at
FROM event_log
WHERE user_id = 1;

쿼리 결과

-- 시스템 에러 조회
SELECT
    event_data->>'$.error_code' AS error_code,
    event_data->>'$.message' AS message,
    event_data->>'$.level' AS level,
    created_at
FROM event_log
WHERE event_type = 'error';

쿼리 결과

 

정리

주요 활용 사례는 다음과 같다.

  • 카테고리별 다른 상품 속성 관리
  • 상품 옵션과 재고 관리 (JSON_TABLE 활용)
  • 다국어 지원
  • 사용자 설정 저장
  • 주문 정보 스냅샷 보존
  • 로그와 이벤트 데이터

핵심 기법은 다음과 같다.

  • "JSON_TABLE" : JSON 배열을 테이블처럼 펼쳐서 조회
  • "JSON_OBJECT", "JSON_ARRAY" : 구조화된 JSON 생성
  • "JSON_SET", "JSON_INSERT" : 부분 업데이트
  • 중첩 경로 접근: "$.parent.child.value"