포스트

DB 컬럼에 JSON을 넣으면 생기는 일

가변 속성을 JSON 컬럼에 담을 때의 유연성과, 추출 함수 쿼리·인덱싱 한계·정규화 경계를 가르친다.

DB 컬럼에 JSON을 넣으면 생기는 일

스키마를 고정하기 어려운 가변 속성을 다룬 주가 있었다. 상품마다 옵션이 다르고 새 속성이 수시로 생긴다. 매번 컬럼을 추가하기 부담스러우면 JSON 컬럼이 후보로 떠오른다. 하지만 유연함에는 대가가 있다.

JSON 컬럼은 왜 매력적인가, 그리고 왜 위험한가

관계형 모델은 컬럼이 고정이다. 새 속성이 생길 때마다 ALTER TABLE을 하면 운영 부담이 크고, 속성이 수십 개의 선택적 값이면 컬럼 대부분이 null로 비게 된다. JSON 컬럼은 이걸 한 컬럼으로 흡수한다.

1
2
3
4
5
CREATE TABLE product (
  id        BIGINT PRIMARY KEY,
  name      VARCHAR(200) NOT NULL,
  attributes JSON          -- {"color":"red","size":"L","weight":1.2}
);

문제는 DB 엔진이 JSON 내부를 “불투명한 텍스트 덩어리”로 본다는 점이다. WHERE name = ?은 인덱스를 타지만, JSON 안의 color로 거르려면 매 행마다 JSON을 파싱해 값을 추출해야 한다. 추출 함수로 쿼리할 수 있긴 하다.

1
2
3
4
-- MySQL: 추출 함수 또는 ->> 연산자
SELECT id, name
FROM product
WHERE attributes->>'$.color' = 'red';

하지만 이 쿼리는 인덱스가 없으면 풀 테이블 스캔이다. 데이터가 적을 땐 멀쩡하다가, 행이 수백만으로 늘면 조용히 느려진다.

인덱싱 한계 — 함수 기반/생성 컬럼

JSON 경로 자체에는 일반 인덱스를 못 건다. 우회로는 추출 결과를 별도 컬럼으로 끄집어내는 것이다. MySQL은 생성 컬럼(generated column)에 인덱스를 건다.

1
2
3
4
ALTER TABLE product
  ADD COLUMN color VARCHAR(30)
    GENERATED ALWAYS AS (attributes->>'$.color') STORED,
  ADD INDEX idx_color (color);

여기서 핵심 모순이 드러난다. 자주 검색하는 속성은 결국 컬럼으로 끄집어내야 한다. 그렇다면 그건 처음부터 정규 컬럼이었어야 한다. JSON 컬럼은 “검색하지 않는, 표시·보관 위주의 가변 속성”에 맞다.

flowchart TD
  A[가변 속성 등장] --> Q{이 속성으로<br/>검색·필터하나?}
  Q -- 예 --> C[정규 컬럼<br/>+ 인덱스]
  Q -- 아니오 --> J[JSON 컬럼<br/>표시·보관용]
  J --> R{나중에 검색 필요?}
  R -- 예 --> G[생성 컬럼으로<br/>추출 후 인덱스]

운영 함정

무결성 검증의 부재. JSON 컬럼은 키 오타, 타입 불일치(숫자여야 할 값이 문자열), 누락된 필수 키를 막아 주지 않는다. DB는 그냥 텍스트로 저장한다. 애플리케이션이 직렬화/역직렬화 시점에 검증하지 않으면 깨진 데이터가 쌓이고, 읽는 쪽에서 매번 방어 코드를 짜야 한다.

부분 수정의 비용. JSON의 키 하나만 바꿔도 보통은 컬럼 전체를 다시 써야 한다. 행이 크고 갱신이 잦으면 쓰기 증폭이 생긴다. 자주 갱신되는 값이라면 더더욱 정규 컬럼이 맞다.

핵심 요약

  • JSON 컬럼은 가변·선택적 속성을 한 컬럼으로 흡수해 잦은 ALTER를 피한다.
  • DB는 JSON 내부를 불투명하게 보므로, 경로 검색은 인덱스 없이는 풀스캔이다.
  • 검색·필터·정렬에 쓰는 속성은 정규 컬럼(또는 생성 컬럼)으로 빼라. JSON은 보관·표시용이 정석이다.
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.