이 문서를 통해 아래 질문들에 답할 수 있게 됩니다.
- 관계형 DB에서 테이블, 행, 컬럼은 각각 어떤 의미를 가지는가?
- 화면 DTO를 그대로 테이블로 만들면 왜 문제가 생기는가?
- 백엔드 개발자는 테이블을 어떤 기준으로 나누어야 하는가?
개요
관계형 DB의 기본 단위는 테이블, 행, 컬럼이다. 하지만 이것을 단순 저장 형태로만 이해하면 설계가 화면이나 API 응답에 끌려간다. 테이블은 비즈니스 사실의 집합이고, 행은 하나의 식별 가능한 사실이며, 컬럼은 그 사실의 속성이다.
백엔드 개발자는 “어떤 JSON을 저장할까”보다 “어떤 사실을 어떤 불변식으로 관리할까”를 먼저 생각해야 한다. 이후 성능 때문에 projection이나 snapshot을 만들 수는 있지만, 그것은 원본 모델을 대체하는 설계가 아니라 읽기 요구를 위한 파생 모델이어야 한다.
원리
회원 테이블은 회원이라는 사실의 집합이다.
CREATE TABLE members (
id bigint PRIMARY KEY,
email varchar(255) NOT NULL,
name varchar(100) NOT NULL,
created_at timestamp NOT NULL
);한 행은 한 명의 회원을 나타낸다. email, name, created_at은 회원의 속성이다.
주문은 별도 사실이다.
CREATE TABLE orders (
id bigint PRIMARY KEY,
member_id bigint NOT NULL REFERENCES members(id),
status varchar(20) NOT NULL,
total_price bigint NOT NULL,
created_at timestamp NOT NULL
);회원과 주문을 한 테이블에 합치면 주문이 여러 개인 회원을 표현하기 어렵고 중복이 생긴다.
화면 중심 설계의 함정
화면에 회원 이름, 최근 주문, 포인트, 쿠폰 수가 한 번에 보인다고 하나의 테이블로 만들면 안 된다.
-- 좋지 않은 냄새
CREATE TABLE member_dashboard (
member_id bigint PRIMARY KEY,
member_name varchar(100),
recent_order_id bigint,
point_balance bigint,
coupon_count int
);이 테이블은 원본 데이터인지 캐시인지 projection인지 불분명하다. 다만 dashboard를 빠르게 보여주기 위한 read model 자체가 나쁜 것은 아니다. 이름, 원본 테이블, 갱신 주기, 불일치 허용 시간, 재생성 방법이 명확하면 실무적으로 좋은 선택이 될 수 있다.
CREATE TABLE member_dashboard_snapshots (
member_id bigint PRIMARY KEY,
member_name varchar(100) NOT NULL,
recent_order_id bigint,
point_balance bigint NOT NULL,
coupon_count int NOT NULL,
source_version bigint NOT NULL,
refreshed_at timestamp NOT NULL
);이 경우 중요한 질문은 “정규화되어 있는가” 하나가 아니다. “원본은 어디인가”, “언제 갱신되는가”, “몇 분까지 오래된 값을 허용하는가”, “깨졌을 때 다시 만들 수 있는가”가 더 실전적인 질문이다.
컬럼의 의미
컬럼 하나에는 가능한 한 하나의 의미를 둔다.
-- 좋지 않다
address varchar(500)주소 검색, 우편번호, 상세 주소가 필요하면 분리한다.
zip_code varchar(20),
address1 varchar(255),
address2 varchar(255)반대로 과도한 분리는 조인과 복잡도를 늘린다. 실제 검색과 검증 요구사항이 기준이다.
실전 팁
- 테이블은 화면이 아니라 비즈니스 사실 단위로 나눈다.
- API 응답용 projection과 원본 테이블을 구분한다.
- 컬럼은 타입과 제약으로 의미를 드러낸다.
- 중복이 필요하면 snapshot인지 cache인지 이름으로 드러낸다.
- ERD에는 관계뿐 아니라 주요 조회 방향도 표시한다.
- projection/read model에는 원본, 갱신 방식, staleness 허용 범위, 재계산 방법을 문서화한다.
- 개인 프로젝트에서도 “정규화된 원본 + 필요한 조회용 projection” 구조를 한 번 구현해 보면 기업형 read model 사고를 연습할 수 있다.
위험 신호!
- 화면 하나당 테이블 하나를 만든다.
- 여러 값을 콤마 문자열로 한 컬럼에 저장한다.
- JSON 컬럼에 핵심 도메인 데이터를 모두 넣는다.
- 원본과 캐시 테이블 이름이 구분되지 않는다.
- 컬럼 의미를 코드 주석 없이는 알 수 없다.
- projection 테이블이 원본처럼 수정되거나, 원본과 불일치했을 때 복구 방법이 없다.
- JSON 컬럼을 쓰면서 검색, 제약, 마이그레이션, 부분 업데이트 비용을 검토하지 않는다.
확인 질문
- 테이블을 화면 기준으로 만들면 왜 위험한가?
- 화면은 자주 바뀌고 여러 비즈니스 사실을 조합하므로 데이터 중복과 갱신 이상이 생기기 쉽기 때문이다.
- 행은 무엇을 나타내야 하는가?
- 식별 가능한 하나의 비즈니스 사실이나 엔티티 인스턴스를 나타내야 한다.
- Projection 테이블을 둘 때 필요한 것은 무엇인가?
- 원본 데이터, 갱신 방식, 불일치 복구 방법, 사용 목적을 명확히 해야 한다.
- 화면 요구에 맞춘 테이블이 항상 나쁜 것은 아닌 이유는 무엇인가?
- 원본 모델이 따로 있고 read model의 갱신 방식과 불일치 허용 범위가 명확하다면, 조회 성능과 API 단순화를 위한 의도적인 projection이 될 수 있기 때문이다.