데이터 모델링은 업무 규칙을 구조로 바꾸는 일이다
데이터 모델링은 현실의 업무를 데이터 구조로 옮기는 과정이다. 여기서 중요한 것은 "무엇을 저장할 것인가"만이 아니라 "무엇이 무엇과 어떤 관계를 맺는가"를 이해하는 일이다.
예를 들어 쇼핑몰에서 "사용자는 상품을 주문하고, 주문은 결제를 통해 완료된다"라는 문장이 있다고 하자. 이 문장에는 여러 데이터 후보가 숨어 있다.
사용자
상품
주문
결제
주문 상태
주문 일시
결제 금액
이 후보를 아무 기준 없이 테이블로 만들면 나중에 쿼리가 복잡해지고, 중복 데이터가 늘어나며, 변경에 약한 구조가 된다. 좋은 데이터 모델은 코드보다 오래 살아남는다. API는 바뀌고 화면은 개편되지만, "주문이 무엇인가", "결제가 언제 완료된 것으로 볼 것인가" 같은 데이터의 의미는 서비스의 중심에 남는다.
그래서 데이터 모델링은 단순한 설계 문서 작업이 아니다. 이후의 성능, 유지보수, 장애 대응, 통계 분석 방식까지 영향을 주는 초기 의사결정이다.
AI 생성 이미지
요구사항 문장에서 엔티티, 속성, 관계를 찾기
데이터 모델링의 출발점은 요구사항 문장이다. 다음 문장을 보자.
사용자는 여러 상품을 장바구니에 담을 수 있고, 장바구니에서 선택한 상품으로 주문을 생성한다. 주문이 생성되면 결제를 시도할 수 있으며, 결제 성공 후 주문 상태는 결제 완료로 변경된다.
이 문장에서 먼저 찾을 것은 엔티티다. 엔티티는 독립적으로 식별하고 관리할 필요가 있는 대상이다. 위 예시에서는 사용자, 상품, 장바구니, 주문, 결제가 엔티티 후보가 된다.
다음은 속성이다. 속성은 엔티티가 가지는 값이다. 사용자는 이메일, 이름, 가입일을 가질 수 있다. 상품은 이름, 가격, 판매 상태를 가질 수 있다. 주문은 주문 번호, 주문 일시, 주문 상태, 총금액을 가질 수 있다.
마지막은 관계다. 관계는 엔티티 사이의 연결이다.
사용자는 여러 주문을 생성할 수 있다.
주문은 여러 상품을 포함할 수 있다.
주문은 결제 정보 또는 결제 시도와 연결될 수 있다.
상품은 여러 주문에 포함될 수 있다.
이 과정을 정리하면 다음과 같다.
| 구분 | 질문 | 예시 |
|---|---|---|
| 엔티티 | 독립적으로 관리해야 하는 대상인가? | 사용자, 주문, 상품 |
| 속성 | 그 대상이 가져야 하는 값인가? | 이메일, 주문 상태, 상품 가격 |
| 관계 | 대상 사이에 규칙 있는 연결이 있는가? | 사용자는 주문을 생성한다 |
실무에서는 명사라고 해서 모두 엔티티가 되는 것은 아니다. "주문 상태"는 독립 엔티티가 될 수도 있지만, 단순히 주문 테이블의 속성으로 충분할 수도 있다. 판단 기준은 별도로 관리할 규칙이 있는지다. 주문 상태별 전환 규칙, 관리자 설정, 표시명, 정렬 순서가 필요하다면 별도 테이블이 될 수 있다. 단순히 PENDING, PAID, CANCELLED 정도만 필요하다면 속성으로 두는 편이 단순하다.
반대로 처음에는 속성처럼 보였던 값이 나중에는 엔티티가 되기도 한다. 예를 들어 결제 수단을 단순 문자열로 저장하다가, 카드사별 정책, 간편결제 제공자, 환불 가능 여부 같은 규칙을 관리해야 한다면 별도 테이블로 분리할 수 있다. 데이터 모델링은 한 번에 정답을 맞히는 일이 아니라, 업무 의미와 변경 가능성을 보며 적절한 구조를 선택하는 일에 가깝다.
식별자는 데이터의 기준점을 만든다
엔티티를 정했다면 각 데이터를 어떻게 구분할지 결정해야 한다. 이를 식별자라고 한다. 관계형 데이터베이스에서는 보통 기본 키(primary key)가 이 역할을 한다.
사용자 테이블에서 이메일을 기본 키로 쓸 수도 있다. 하지만 이메일은 변경될 수 있다. 사용자가 이메일을 바꾸면 그 사용자를 참조하는 주문, 결제, 배송지 데이터에도 영향을 준다. 그래서 실무에서는 내부 식별자로 user_id 같은 값을 두고, 이메일은 별도의 고유 속성으로 관리하는 경우가 많다.
users
- id
- email
- name
- created_at식별자는 한 번 정하면 여러 테이블과 코드에 퍼진다. 따라서 "사람이 읽기 쉬운 값"보다 "변하지 않고 참조하기 좋은 값"을 우선하는 편이 안전하다.
다만 모든 곳에 무조건 숫자 ID만 쓰라는 뜻은 아니다. 외부 시스템과 연동하는 결제 거래 번호, 국가 코드, 상품 SKU처럼 업무적으로 안정적인 식별자가 있다면 중요한 고유 키로 다뤄야 한다. 핵심은 식별자의 변경 가능성과 참조 범위를 먼저 따져보는 것이다.
여기서 기본 키와 고유 키를 구분해 두면 좋다. 기본 키는 그 행을 대표해서 다른 데이터가 참조하는 기준점이다. 고유 키는 중복되면 안 되는 값을 보장하는 장치다. 이메일은 중복되면 안 되지만 변경될 수 있으므로, 기본 키보다는 고유 키로 두는 편이 안전한 경우가 많다.
일대일, 일대다, 다대다 관계
관계형 모델에서 가장 자주 만나는 관계는 일대일, 일대다, 다대다다. 이 관계를 잘못 표현하면 데이터 중복이나 쿼리 복잡도가 크게 늘어난다.
일대일 관계
일대일 관계는 한 행이 다른 테이블의 한 행과만 연결되는 구조다. 예를 들어 사용자와 사용자 프로필을 나눌 수 있다.
users
- id
- email
user_profiles
- user_id
- display_name
- bio사용자 프로필을 반드시 별도 테이블로 나눠야 하는 것은 아니다. 대부분의 사용자 조회에서 프로필이 항상 필요하다면 같은 테이블에 두는 편이 단순하다. 반대로 프로필 데이터가 크거나, 자주 접근하지 않거나, 권한을 다르게 관리해야 한다면 분리할 이유가 생긴다.
일대다 관계
일대다 관계는 한 사용자가 여러 주문을 갖는 경우처럼 가장 흔한 구조다. 이때 "다" 쪽 테이블에 외래 키를 둔다.
users
- id
- email
orders
- id
- user_id
- status
- ordered_atorders.user_id는 이 주문이 어느 사용자에게 속하는지 알려준다. 외래 키는 단순한 연결 값이 아니다. 데이터베이스가 참조 무결성을 검사할 수 있게 해주는 장치다. 존재하지 않는 사용자 ID로 주문이 생성되는 것을 막을 수 있고, 사용자를 삭제할 때 주문을 어떻게 처리할지도 정책으로 정할 수 있다.
실무에서는 외래 키 제약을 데이터베이스에 걸지 않고 애플리케이션 코드에서만 관리하는 경우도 있다. 대량 쓰기 성능, 배포 방식, 레거시 구조 때문일 수 있다. 하지만 그 선택은 책임을 코드와 운영 절차가 대신 진다는 뜻이다. 작은 서비스나 정합성이 중요한 업무에서는 외래 키를 적극적으로 고려하는 편이 안전하다.
다대다 관계
다대다 관계는 주문과 상품처럼 양쪽이 서로 여러 개를 가질 수 있는 관계다. 하나의 주문에는 여러 상품이 들어가고, 하나의 상품은 여러 주문에 포함될 수 있다.
이 관계를 직접 컬럼 하나로 표현하려고 하면 문제가 생긴다.
orders
- id
- product_ids # "1,2,3" 같은 문자열이런 구조는 처음에는 쉬워 보인다. 하지만 특정 상품을 포함한 주문을 찾기 어렵고, 상품 수량이나 주문 당시 가격을 저장하기도 어렵다. 인덱스를 제대로 활용하기도 힘들다.
그래서 다대다 관계는 보통 연결 테이블로 표현한다.
orders
- id
- user_id
- status
products
- id
- name
- price
order_items
- order_id
- product_id
- quantity
- unit_priceorder_items는 주문과 상품을 연결하면서 수량과 주문 당시 가격도 함께 저장한다. 여기서 unit_price가 중요하다. 상품의 현재 가격은 바뀔 수 있지만, 과거 주문의 결제 금액은 바뀌면 안 된다. 즉 주문 항목은 단순 연결이 아니라 "주문 시점의 상품 정보 일부"를 보존하는 역할도 한다.
모델링 선택은 쿼리 복잡도를 바꾼다
같은 도메인도 모델링 방식에 따라 쿼리의 모양이 달라진다. 예를 들어 "사용자의 최근 주문 목록과 각 주문에 담긴 상품 종류 수를 보여준다"는 기능을 생각해 보자.
정규화된 구조에서는 주문과 주문 항목을 조인하고 집계해야 한다.
SELECT o.id, o.status, COUNT(oi.product_id) AS item_kind_count
FROM orders o
JOIN order_items oi ON oi.order_id = o.id
WHERE o.user_id = ?
GROUP BY o.id, o.status
ORDER BY o.id DESC;이 구조는 데이터 중복이 적고, 주문 항목을 정확히 표현하기 좋다. 대신 조회 시 조인과 집계가 필요하다. 만약 상품 종류 수가 아니라 실제 구매 수량의 합이 필요하다면 COUNT가 아니라 SUM(oi.quantity)를 써야 한다. 화면에서 말하는 "상품 수"가 상품 종류 수인지, 총 수량인지도 모델링과 쿼리에서 분명히 정해야 한다.
반대로 주문 테이블에 item_kind_count나 total_quantity를 미리 저장할 수도 있다.
orders
- id
- user_id
- status
- item_kind_count
- total_quantity이러면 목록 조회는 단순해진다. 하지만 주문 항목이 변경될 때 미리 저장한 값도 함께 맞춰야 한다. 하나라도 누락되면 주문 항목의 실제 값과 주문 테이블의 값이 달라진다.
어느 쪽이 항상 정답은 아니다. 주문 생성 후 항목이 거의 바뀌지 않고 목록 조회가 매우 많다면 집계 값을 저장하는 선택이 합리적일 수 있다. 반대로 주문 항목 변경이 잦고 정확성이 중요하다면 계산해서 보여주는 편이 안전할 수 있다.
좋은 모델링은 "중복을 절대 만들지 않는다"가 아니다. 중복을 만들 때 어떤 정합성 비용을 치르는지 알고 선택하는 것이다.
테이블, 문서, 키-값 구조는 표현 방식이 다르다
데이터 모델링은 관계형 데이터베이스에만 해당하지 않는다. 같은 주문 데이터를 PostgreSQL이나 MySQL의 테이블로 저장할 수도 있고, MongoDB 같은 문서 데이터베이스에 주문 문서로 저장할 수도 있으며, Redis에 임시 주문 상태를 키-값으로 저장할 수도 있다.
관계형 모델에서는 주문, 주문 항목, 상품을 분리하고 조인으로 연결하는 방식이 자연스럽다. 주문 항목 검색, 통계, 정합성 검사가 중요할 때 유리하다.
문서 모델에서는 주문 문서 안에 주문 항목 배열을 포함할 수 있다.
{
"order_id": 1001,
"user_id": 10,
"status": "PAID",
"items": [
{ "product_id": 1, "quantity": 2, "unit_price": 15000 },
{ "product_id": 2, "quantity": 1, "unit_price": 9000 }
]
}이 구조는 주문 하나를 통째로 읽고 쓰는 기능에 잘 맞는다. 하지만 특정 상품이 포함된 모든 주문을 자주 검색하거나, 주문 항목 단위로 복잡한 집계를 해야 한다면 불리할 수 있다.
키-값 구조는 더 단순하다.
cart:user:10 -> { 상품 목록과 수량 }장바구니처럼 임시성이 강하고 빠른 읽기·쓰기가 중요한 데이터에는 적합하다. 그러나 영구 주문 기록이나 결제 정합성을 책임지는 저장소로는 부족한 점이 많다.
따라서 모델링은 저장소 선택과 연결된다. 먼저 업무 규칙과 접근 패턴을 이해하고, 그다음 테이블, 문서, 키-값 중 어떤 표현이 자연스러운지 판단해야 한다.
논리 모델과 물리 모델을 구분하라
논리 모델은 업무 관점의 데이터 구조다. 사용자, 주문, 결제, 상품이 무엇이고 서로 어떤 관계를 갖는지 정의한다. 이 단계에서는 특정 DBMS의 컬럼 타입이나 인덱스 세부 설정보다 데이터의 의미가 중요하다.
물리 모델은 실제 데이터베이스에 구현하기 위한 구조다. 테이블명, 컬럼명, 데이터 타입, 인덱스, 파티션, 샤딩 키, 저장 엔진 같은 선택이 포함된다.
초기 설계에서 너무 빨리 물리 모델로 들어가면 중요한 질문을 놓치기 쉽다.
주문은 결제 실패 후에도 유지되는가?
결제는 한 주문에 여러 번 시도될 수 있는가?
상품 가격이 바뀌면 기존 주문 금액도 바뀌어야 하는가?
사용자가 탈퇴하면 주문 기록은 삭제되는가, 익명화되는가?
이 질문들은 데이터 타입보다 먼저 결정되어야 한다. BIGINT를 쓸지 UUID를 쓸지보다 "이 데이터가 무엇을 의미하고 얼마나 오래 보존되어야 하는지"가 먼저다.
물론 물리 모델을 늦게까지 무시해도 안 된다. 대량 트래픽, 조회 패턴, 개인정보 암호화, 보관 기간 같은 현실적인 조건은 결국 설계에 반영되어야 한다. 다만 순서가 중요하다. 먼저 의미를 정하고, 그 의미를 잘 지키는 구현 방식을 고른다.
설계 초기에 확인해야 할 질문들
데이터 모델링을 할 때는 다음 질문을 반복해서 던지면 좋다.
이 데이터는 독립적으로 식별해야 하는가?
이 값은 시간이 지나도 바뀌지 않는가?
현재 값만 필요한가, 과거 이력도 필요한가?
관계가 일대일, 일대다, 다대다 중 어디에 가까운가?
조회가 많은가, 변경이 많은가?
중복 저장하면 어떤 정합성 문제가 생기는가?
삭제는 실제 삭제인가, 비활성화인가, 익명화인가?
나중에 분석이나 감사 목적으로 다시 필요할 가능성이 있는가?
특히 이력은 초기에 놓치기 쉽다. 상품 가격, 주문 상태, 결제 시도, 배송 상태는 모두 시간이 흐르며 변한다. 현재 값만 저장하면 화면 구현은 쉽지만, 나중에 "그 시점에 왜 이 금액이 청구되었는가"를 설명하지 못할 수 있다.
좋은 모델은 변경을 견딘다
좋은 데이터 모델은 현재 화면에만 맞춘 구조가 아니다. 업무 규칙을 명확히 담고, 변경이 생겼을 때 어디를 수정해야 하는지 예측 가능한 구조다.
처음부터 완벽한 모델을 만들 수는 없다. 서비스가 성장하면 요구사항은 바뀌고, 조회 패턴도 달라진다. 중요한 것은 변경 가능성이 높은 부분과 안정적인 부분을 구분하는 것이다.
사용자, 주문, 결제처럼 서비스의 핵심 기록은 의미를 신중히 정의해야 한다. 반면 화면 노출 순서, 임시 필터 조건, 캐시용 집계 값은 나중에 바뀔 수 있다는 전제로 설계할 수 있다.
데이터 모델링의 목표는 복잡한 다이어그램을 만드는 것이 아니다. 개발자와 기획자, 운영자가 같은 의미로 데이터를 이해하게 만드는 것이다. 이 공통 이해가 있어야 SQL도 단순해지고, API도 안정적이며, 장애가 발생했을 때 어떤 데이터가 맞는지 판단할 수 있다.
다음 장에서 다룰 관계형 데이터베이스와 SQL은 이 모델을 실제로 다루는 가장 대표적인 도구다. 하지만 그 전에 기억해야 할 원칙은 하나다. 데이터 구조는 코드의 부산물이 아니라, 애플리케이션이 지켜야 할 업무 규칙의 뼈대다.
이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.