🧩

jsonb와 store_accessor

PostgreSQL의 jsonb 컬럼을 모델 속성처럼 다루기

PostgreSQL의 jsonb는 JSON을 바이너리로 저장하는 컬럼 타입이다. 일반 json 타입과 달리 저장할 때 한 번 파싱해두기 때문에, 읽을 때 다시 파싱하지 않는다.

진짜 차이는 인덱스다. jsonb는 GIN 인덱스를 걸 수 있어서 JSON 내부 키로 검색해도 빠르다. json 타입은 그게 안 된다.

store_accessor — jsonb를 속성처럼

jsonb 컬럼 하나에 여러 값을 넣고, 모델에서는 일반 컬럼처럼 읽고 쓰는 기능이다.

class User < ApplicationRecord
  store_accessor :settings, :theme, :language
end

user.theme = 'dark'
user.language = 'ko'
user.save
# settings = { "theme": "dark", "language": "ko" }

별도 컬럼을 만들지 않아도 settings 하나로 유동적인 설정값을 관리한다. 값이 늘 때마다 마이그레이션을 돌릴 필요가 없다.

언제 쓰나

스키마가 자주 바뀌거나 미리 정하기 애매한 데이터에 쓴다. 사용자 설정, 외부 API 응답 캐시, 메타데이터 같은 것.

반대로 자주 검색하고 조인하는 핵심 데이터는 그냥 컬럼으로 빼는 게 낫다. jsonb 안에 다 넣으면 쿼리가 금방 지저분해진다.

SQLite에서는

store_accessor 자체는 SQLite의 json(TEXT) 컬럼에서도 읽고 쓰기는 된다. 하지만 GIN 인덱스와 @>, ->> 연산자 기반 고속 검색은 PostgreSQL 전용이다. SQLite에서는 편의 기능만 누리고 검색 성능 이점은 없다.

json vs jsonb

항목 json jsonb
저장 방식텍스트 원본 그대로바이너리 파싱 후 저장
읽기 속도매번 파싱파싱 불필요 → 빠름
GIN 인덱스
내부 키 검색느림빠름

검색 연산자

User.where("settings->>'theme' = ?", 'dark')
Product.where("metadata @> ?", { category: 'book' }.to_json)

⚠ 이 프로젝트는 SQLite

SQLite에서는 jsonb 대신 t.json을 쓴다. store_accessor의 속성 관리 편의는 동작하지만, GIN 인덱스와 고속 검색은 PostgreSQL에서만 얻는다.

핵심 포인트

1

마이그레이션에서 jsonb 컬럼 추가 — t.jsonb :settings, default: {}, null: false

2

모델에 store_accessor :settings, :theme, :language 선언

3

타입이 필요하면 attribute :theme, :string 으로 캐스팅 추가

4

검색이 필요하면 GIN 인덱스 — add_index :users, :settings, using: :gin

5

내부 키로 조회 — settings->>theme 연산자, 또는 포함 검색 settings @> {...} 연산자 사용

장점

  • 컬럼 추가 없이 유동적 데이터 관리
  • GIN 인덱스로 JSON 내부 고속 검색 (PostgreSQL)
  • store_accessor로 일반 속성처럼 접근

단점

  • jsonb는 PostgreSQL 전용 (SQLite는 json만)
  • 핵심 검색 데이터를 다 넣으면 쿼리가 지저분
  • 제약조건/외래키를 jsonb 내부에는 못 건다

사용 사례

사용자 설정/환경설정 저장 외부 API 응답 캐시 스키마가 유동적인 메타데이터