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에서만 얻는다.
핵심 포인트
마이그레이션에서 jsonb 컬럼 추가 — t.jsonb :settings, default: {}, null: false
모델에 store_accessor :settings, :theme, :language 선언
타입이 필요하면 attribute :theme, :string 으로 캐스팅 추가
검색이 필요하면 GIN 인덱스 — add_index :users, :settings, using: :gin
내부 키로 조회 — settings->>theme 연산자, 또는 포함 검색 settings @> {...} 연산자 사용
장점
- ✓ 컬럼 추가 없이 유동적 데이터 관리
- ✓ GIN 인덱스로 JSON 내부 고속 검색 (PostgreSQL)
- ✓ store_accessor로 일반 속성처럼 접근
단점
- ✗ jsonb는 PostgreSQL 전용 (SQLite는 json만)
- ✗ 핵심 검색 데이터를 다 넣으면 쿼리가 지저분
- ✗ 제약조건/외래키를 jsonb 내부에는 못 건다