업무 시스템을 만들다 보면 거의 항상 부딪히는 벽이 있어요. 고객마다 관리하고 싶은 항목이 다르다는 거예요. A 고객은 "담당자"와 "예산"이 필요하고, B 고객은 "연령대"와 "촬영 가능 지역"이 필요해요. 처음엔 컬럼 몇 개 추가하면 될 것 같지만, 고객이 늘어날수록 테이블은 누더기가 되고, "이 항목 하나만 추가해주세요"라는 요청에 매번 마이그레이션과 배포가 따라붙어요.
행사·캐스팅 에이전시의 맞춤형 CRM을 구축한 사례에서도 똑같은 문제가 핵심이었어요. 가수·강연자·광고모델처럼 분야마다 필요한 필드가 전부 달랐거든요. 이 글에서는 그때 택한 JSONB 동적 필드 설계를 정리해볼게요.
컬럼을 늘리는 방식이 무너지는 지점
가장 흔한 출발은 그냥 컬럼을 늘리는 거예요. 처음 몇 개는 괜찮아요. 그런데 고객이 다섯, 열로 늘어나면 한 테이블에 안 쓰는 컬럼이 수십 개가 돼요. 어떤 고객에겐 필수인 컬럼이 다른 고객에겐 항상 비어 있죠. "고객마다 다른데 한 테이블에 다 넣는다"는 전제 자체가 어긋나기 시작해요.
다음으로 떠올리는 게 고객마다 테이블을 따로 만드는 방식이에요. 깔끔해 보이지만, 검색·통계·권한 같은 공통 기능을 테이블 수만큼 중복으로 짜야 해서 유지보수가 폭발해요. 결국 문제는 하나로 모여요. "바뀌는 것"을 코드와 스키마에 박아두면, 바뀔 때마다 코드와 스키마를 건드려야 한다는 거예요.
바뀌는 것은 데이터로, 고정된 것만 스키마로
그래서 관점을 뒤집어요. 필드 목록처럼 자주 바뀌는 것은 스키마가 아니라 데이터로 다루는 거예요. 구조는 둘로 나뉘어요.
- 필드 정의: 카테고리별로 어떤 필드가 있는지(이름·타입·검색 여부)를 별도 테이블에 데이터로 저장해요.
- 실제 값: 각 레코드의 커스텀 값은 JSONB 컬럼 하나에 담아요.
이러면 "필드를 추가"하는 일이 "스키마 변경"이 아니라 "데이터 한 줄 추가"가 돼요. 마이그레이션도, 배포도 필요 없어요.
# 필드 정의는 데이터, 카테고리마다 다르게
FieldDefinition.create!(category: 가수, name: "장르", field_type: "select", searchable: true)
# 실제 값은 JSONB 한 컬럼에
person.custom_fields # => { "장르" => "발라드", "구성" => "솔로" }JSONB 검색은 인덱스가 받쳐줘야 해요
여기서 가장 많이 나오는 걱정이 "JSONB로 검색하면 느리지 않냐"예요. 맞아요, 인덱스 없이 JSONB 내부를 뒤지면 풀스캔이 나요. 그래서 GIN 인덱스가 핵심이에요. JSONB 컬럼에 GIN 인덱스를 걸면 내부 키·값으로 거르는 질의도 인덱스를 타요.
CREATE INDEX idx_people_custom_fields ON people USING gin (custom_fields);그리고 모델 쪽에는 자주 쓰는 검색 패턴을 스코프로 감싸 둬요. 특정 필드가 있는지, 값이 포함되는지, 범위 안인지 같은 패턴이요. 이렇게 하면 검색 화면은 카테고리의 필드 정의만 읽어서, 그 카테고리에 맞는 조건 폼을 동적으로 그릴 수 있어요.
모든 걸 JSONB에 넣지는 않아요
동적 필드가 강력하다고 전부 JSONB에 밀어넣으면 안 돼요. 관계가 중요한 데이터(계약·정산·사용자처럼 무결성과 조인이 필요한 것)는 여전히 정규화된 테이블로 둬요. JSONB는 **"고객마다 다르고 자주 바뀌는 속성"**에만 써요. 이 경계를 분명히 하는 게 설계의 핵심이에요. 자유도와 무결성을 맞바꾸는 지점을 의식적으로 정하는 거죠.
이건 자동화 도입 ROI를 계산하는 일과도 닮았어요. 유연성은 공짜가 아니라서, 어디까지 유연하게 둘지를 정하는 게 곧 설계예요.
정리
고객마다 다른 데이터를 다루는 문제는 "컬럼을 얼마나 늘릴까"가 아니라 "무엇을 데이터로 뺄까"의 문제예요. 바뀌는 것을 데이터로 빼면, 고객의 변경 요청이 개발 업무가 아니라 운영 업무가 돼요. 이 설계가 실제로 어떻게 동작하는지는 맞춤형 CRM 구축 사례에서 화면으로 볼 수 있어요. 데이터를 옮기는 과정은 엑셀을 버리지 않고 옮기는 법에서 이어서 다뤄요.
자주 묻는 질문
JSONB 동적 필드는 언제 쓰는 게 좋나요?
고객·카테고리마다 관리 항목이 다르고 그 목록이 자주 바뀔 때 적합해요. 반대로 모든 레코드가 같은 구조를 갖고 관계 무결성이 중요하다면 정규화된 컬럼이 낫습니다.
동적 필드로도 빠른 검색이 가능한가요?
네. JSONB 컬럼에 GIN 인덱스를 걸면 내부 값으로 거르는 검색도 인덱스를 탑니다. 자주 검색하는 필드만 검색 대상으로 표시해 인덱스가 받쳐주는 범위에서 다루는 게 핵심이에요.
필드를 추가할 때 배포가 필요한가요?
아니요. 필드는 데이터로 관리되므로 관리자 화면에서 추가하면 바로 반영됩니다. 스키마 변경이나 재배포가 없습니다.