이 문서를 통해 아래 질문들에 답할 수 있게 됩니다.
- 성장 가능한 스키마는 무엇을 미리 열어두고 무엇을 과하게 일반화하지 않는가?
- 개인 프로젝트에서도 확장성을 보여주는 DB 설계는 어떤 모습인가?
- 스키마 변경 가능성을 낮추기보다 안전하게 변경하는 구조는 어떻게 만드는가?
개요
성장 가능한 스키마는 미래를 완벽히 예측한 스키마가 아니다. 변경이 생겼을 때 안전하게 진화할 수 있는 스키마다. 초기에 모든 가능성을 추상화하면 오히려 복잡하고 느린 구조가 된다.
백엔드 개발자는 과잉 일반화와 변경 불가능한 설계 사이의 균형을 잡아야 한다.
원리
처음부터 모든 엔티티를 다음처럼 일반화하면 위험하다.
CREATE TABLE objects (
id bigint PRIMARY KEY,
type varchar(50),
attributes jsonb
);유연해 보이지만 제약, 인덱스, 타입 검증, 조인이 어려워진다. DB가 지켜줄 수 있는 계약이 줄어든다.
반대로 너무 고정하면 작은 요구 변경에도 대형 migration이 필요하다.
성장 가능한 스키마는 “변경을 피하는 설계”가 아니라 “변경을 작게 쪼개 안전하게 반영하는 설계”다. 새 컬럼 추가, backfill, 코드 전환, 제약 추가, 구컬럼 제거를 한 번에 하지 않고 단계로 나눈다.
좋은 기본값
성장 가능한 스키마는 다음 기본을 가진다.
- 명확한 PK와 FK
- Not Null과 Unique
- 상태값과 전이 규칙
- 생성/수정 시각
- migration 이력
- 주요 조회 인덱스
- 삭제와 보관 정책
이 기본이 있으면 변경이 와도 데이터 의미를 유지하기 쉽다.
JSON 컬럼 사용
JSON 컬럼은 유용하지만 신중해야 한다.
적합한 경우:
- 드문 부가 속성
- 외부 API 원본 payload 보관
- schema가 자주 변하는 비핵심 metadata
- 검색과 정렬의 핵심 조건이 아닌 데이터
부적합한 경우:
- 자주 검색하는 핵심 필드
- 제약이 중요한 금액, 상태, 식별자
- 조인과 FK가 필요한 관계
- 자주 업데이트되는 큰 문서
JSON은 유연성을 주지만 DB 제약과 인덱스 설계를 어렵게 만들 수 있다.
JSON 필드는 승격 기준을 정해둔다.
- 검색 조건에 자주 쓰인다.
- 정렬이나 pagination key가 된다.
- Unique, FK, Check 같은 제약이 필요하다.
- 운영자가 ad-hoc 분석에서 자주 조회한다.
- 값 변경이 다른 테이블과 관계를 만든다.
이 중 하나라도 핵심 요구가 되면 명시 컬럼이나 별도 테이블로 승격하는 편이 낫다.
변경을 위한 구조
변경 가능성은 migration 전략으로 대응한다.
- 새 컬럼은 nullable로 추가하고 점진적으로 채운다.
- 상태값 추가는 구버전 코드 호환성을 확인한다.
- 큰 테이블은 backfill과 제약 추가를 나눈다.
- 제거는 충분한 관찰 기간 후 수행한다.
- 데이터 변환은 재실행 가능하게 만든다.
전형적인 expand/contract 흐름은 다음과 같다.
1. nullable 새 컬럼 추가
2. 새 코드가 두 컬럼을 함께 write
3. backfill로 기존 데이터 채움
4. 읽기 경로를 새 컬럼으로 전환
5. NOT NULL/Unique/Check 제약 추가
6. 구 컬럼 제거이 순서가 필요한 이유는 배포 중 구버전 코드와 신버전 코드가 동시에 존재할 수 있기 때문이다.
개인 프로젝트 기준
수준 있는 개인 프로젝트는 다음을 보여줄 수 있다.
- ERD와 핵심 제약 설명
- migration 파일
- 주요 목록 API 인덱스
- 중복 방지 Unique 제약
- 삭제 정책
- cursor pagination 한 곳 이상
- 대량 변경 시나리오에 대한 간단한 문서
이 정도만 있어도 단순 CRUD와 차이가 난다.
실전 팁
- 미래를 예측하기보다 안전한 변경 절차를 만든다.
- 핵심 도메인 필드는 명시 컬럼과 제약으로 둔다.
- JSON은 주변 metadata에 제한적으로 쓴다.
- 스키마 설계 의도를 README나 문서에 남긴다.
- 새 요구사항이 오면 기존 데이터 마이그레이션 비용을 먼저 계산한다.
- 스키마 변경 PR에는 데이터량, backfill 시간, lock 영향, 구버전 코드 호환성, rollback 전략을 적는다.
- JSON에서 명시 컬럼으로 승격하는 기준을 팀 규칙으로 둔다.
- 개인 프로젝트에서도 migration history와 restore/backfill 경험을 README에 남기면 실전성이 드러난다.
위험 신호!
- 모든 확장성을 JSON으로 해결한다.
- FK와 제약을 전혀 두지 않는다.
- 상태값 추가가 구버전 코드를 깨뜨린다.
- migration 없이 운영 DB를 직접 바꾼다.
- 왜 이 테이블 구조가 되었는지 아무도 설명하지 못한다.
- 새 컬럼 추가와 backfill, 제약 추가, 코드 전환을 한 배포에 모두 넣는다.
- JSON 필드가 핵심 검색 조건이 되었는데 여전히 검증과 인덱스 전략이 없다.
- rollback을 “git revert”로만 생각하고 이미 변경된 데이터 되돌림을 고려하지 않는다.
확인 질문
- 성장 가능한 스키마는 미래를 모두 예측하는 것과 어떻게 다른가?
- 모든 가능성을 미리 넣는 것이 아니라 변경이 생겼을 때 안전하게 진화할 수 있는 제약과 절차를 갖추는 것이다.
- JSON 컬럼을 핵심 필드에 남발하면 위험한 이유는 무엇인가?
- 타입 검증, 제약, 인덱스, 조인, 마이그레이션이 어려워져 DB가 데이터 계약을 지키기 힘들기 때문이다.
- 개인 프로젝트에서 DB 설계 역량을 보여주는 요소는 무엇인가?
- migration, 제약 조건, 인덱스, 삭제 정책, pagination, 설계 의도 문서화다.
- expand/contract 방식으로 스키마를 바꾸는 이유는 무엇인가?
- 운영 중 구버전/신버전 코드가 공존하고 기존 데이터 backfill과 제약 검증이 필요하므로, 변경을 단계로 나눠 장애와 rollback 위험을 줄이기 위해서다.