이 문서를 통해 아래 질문들에 답할 수 있게 됩니다.

  • 성장 가능한 스키마는 무엇을 미리 열어두고 무엇을 과하게 일반화하지 않는가?
  • 개인 프로젝트에서도 확장성을 보여주는 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 위험을 줄이기 위해서다.

참고 문서