본문 바로가기
TYLER SONGBlog
← Blog index
CS학습8 min read

ERD: 엔티티와 관계로 데이터 구조를 설계하는 다이어그램

ERD(Entity-Relationship Diagram)는 데이터베이스에 저장할 대상(엔티티)과 그 사이의 관계를 그림으로 표현한 설계 도구다. 카디널리티와 식별/비식별 관계를 정의해 테이블 구조와 외래 키(FK)의 위치를 결정한다. 이 글은 ERD의 구성 요소부터 SQL(PostgreSQL) DDL로의 변환, 실무 설계 시 주의점까지 다룬다.

By 송민성

1. 개념

ERD(Entity-Relationship Diagram, 개체-관계 다이어그램)는 데이터베이스에 저장할 대상과 대상 사이의 관계를 도형과 선으로 나타낸 모델이다. 1976년 Peter Chen이 제안한 개체-관계 모델(ER Model)을 시각화한 것이다.

구성 요소는 세 가지다.

요소영문설명테이블 변환 시
개체Entity저장·관리할 대상 (회원, 주문, 상품)테이블
속성Attribute개체가 가진 특성 (이름, 가격)컬럼
관계Relationship개체 간 연결 (회원이 주문을 한다)외래 키(FK) 또는 연결 테이블

관계에는 두 가지 핵심 속성이 있다.

  • 카디널리티(Cardinality): 한쪽 개체 하나가 상대 개체 몇 개와 연결되는지를 나타낸다. 1:1, 1:N, N:M이 있다.
  • 참여도(Participation): 관계 참여가 필수인지(Mandatory) 선택인지(Optional)를 나타낸다. 필수 여부는 FK의 NOT NULL 여부로 이어진다.

표기법은 여러 가지가 있다. 실무에서는 닭발 표기법(Crow's Foot Notation, IE 표기법)이 가장 흔하다. 선 끝의 세 갈래가 "다수(N)"를 뜻한다.

설계 단계는 보통 세 층으로 나눈다.

단계내용
개념적 모델(Conceptual)개체와 관계만 표현. 속성은 생략하거나 최소화
논리적 모델(Logical)속성, 키, 정규화 반영. 특정 DBMS에 독립적
물리적 모델(Physical)데이터 타입, 인덱스, 제약 조건 등 DBMS 종속 요소 포함

2. 왜 사용하는가

  • 구현 전 오류 발견: 테이블을 만든 뒤 구조를 바꾸면 마이그레이션 비용이 크다. 그림 단계에서 중복·누락·잘못된 관계를 먼저 잡을 수 있다.
  • 소통 수단: 개발자, 기획자, DBA가 같은 그림을 보며 "주문 하나에 상품이 여러 개 들어가는가?" 같은 업무 규칙을 확인한다.
  • 정규화(Normalization)의 출발점: 개체와 속성을 분리하는 과정에서 데이터 중복과 이상 현상(Anomaly)을 줄인다.
  • 문서화: 스키마의 의도를 코드(DDL)보다 빠르게 파악할 수 있다. 신규 인원 온보딩과 유지보수에 쓰인다.

3. 동작 원리

ERD는 실행되는 것이 아니라 변환 규칙에 따라 스키마가 된다. 핵심 변환 규칙은 다음과 같다.

1) 개체 → 테이블, 속성 → 컬럼, 식별자 → 기본 키(PK)

개체를 유일하게 구분하는 속성(또는 속성 조합)이 기본 키(Primary Key)가 된다.

2) 1:N 관계 → N쪽 테이블에 FK

"회원 1명이 주문 N개를 한다"면 orders 테이블에 member_id FK를 둔다. FK는 항상 "다(N)" 쪽에 놓인다.

3) N:M 관계 → 연결 테이블(Junction Table)

관계형 모델은 컬럼 하나에 다수 값을 넣을 수 없으므로(제1정규형 위반), N:M은 두 개의 1:N으로 쪼갠다. "주문 ↔ 상품"은 order_items 테이블이 중간에서 두 FK를 가지며, 보통 두 FK의 조합을 복합 PK로 쓴다. 수량, 주문 당시 가격 같은 관계 자체의 속성도 이 테이블에 들어간다.

4) 1:1 관계 → 한쪽에 UNIQUE FK

FK 컬럼에 UNIQUE 제약을 걸어 "하나만" 연결되도록 보장한다. 보통 선택적(Optional) 쪽에 FK를 둔다.

5) 식별 관계 vs 비식별 관계

  • 식별 관계(Identifying): 부모의 PK가 자식 PK의 일부가 된다. 자식이 부모 없이 존재할 수 없을 때 쓴다. (예: order_items)
  • 비식별 관계(Non-identifying): 부모의 PK가 자식의 일반 컬럼(FK)이 된다. (예: orders.member_id)

6) 참여도 → NULL 허용 여부

필수 관계는 FK에 NOT NULL, 선택 관계는 NULL 허용으로 표현한다.

4. 코드 예제

쇼핑몰 ERD(회원 1:N 주문, 주문 N:M 상품)를 PostgreSQL DDL로 변환한 예다.

text
MEMBER ||--o{ ORDERS : places
ORDERS ||--|{ ORDER_ITEM : contains
PRODUCT ||--o{ ORDER_ITEM : "is included in"
sql
CREATE TABLE member ( member_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, email VARCHAR(255) NOT NULL UNIQUE, name VARCHAR(100) NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE TABLE product ( product_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, name VARCHAR(200) NOT NULL, price NUMERIC(12, 2) NOT NULL CHECK (price >= 0) ); -- 1:N (비식별 관계): 회원 1명 - 주문 N개 CREATE TABLE orders ( order_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, member_id BIGINT NOT NULL REFERENCES member(member_id), ordered_at TIMESTAMPTZ NOT NULL DEFAULT now() ); -- N:M 해소용 연결 테이블 (식별 관계, 복합 PK) CREATE TABLE order_item ( order_id BIGINT NOT NULL REFERENCES orders(order_id) ON DELETE CASCADE, product_id BIGINT NOT NULL REFERENCES product(product_id), quantity INT NOT NULL CHECK (quantity > 0), unit_price NUMERIC(12, 2) NOT NULL, -- 주문 시점 가격 스냅샷 PRIMARY KEY (order_id, product_id) ); -- FK 컬럼 인덱스 (PostgreSQL은 FK에 인덱스를 자동 생성하지 않는다) CREATE INDEX idx_orders_member_id ON orders(member_id); CREATE INDEX idx_order_item_product_id ON order_item(product_id); -- 관계를 따라 조인하는 조회 SELECT m.name, o.order_id, p.name AS product, oi.quantity, oi.unit_price FROM member m JOIN orders o ON o.member_id = m.member_id JOIN order_item oi ON oi.order_id = o.order_id JOIN product p ON p.product_id = oi.product_id WHERE m.email = 'user@example.com';

order_item.unit_price를 따로 두는 이유는, 상품 가격이 나중에 바뀌어도 과거 주문 금액이 변하지 않게 하기 위해서다. 의도적인 비정규화이며, 이력 보존이라는 업무 규칙에서 나온 결정이다.

5. 시간 복잡도 또는 성능 특성

ERD 자체에는 시간 복잡도가 없다. 대신 ERD에서 내린 결정이 쿼리 성능에 영향을 준다.

  • 조인 비용: 정규화를 많이 할수록 테이블이 늘고 조인이 많아진다. 조인 성능은 FK 컬럼 인덱스 유무에 크게 좌우된다. PostgreSQL은 PK와 UNIQUE에는 인덱스를 자동 생성하지만, 참조하는 쪽 FK 컬럼에는 만들지 않는다.
  • B-tree 인덱스 조회: 인덱스를 통한 단건 조회는 O(log n)이다.
  • FK 검증 비용: INSERT/UPDATE/DELETE 때마다 참조 무결성을 확인한다. 부모 행을 삭제할 때 자식 테이블 FK에 인덱스가 없으면 자식 테이블 전체를 스캔할 수 있다.
  • 복합 PK 순서: (order_id, product_id) 복합 PK 인덱스는 order_id 단독 조건에는 쓰이지만 product_id 단독 조건에는 효율적으로 쓰이지 않는다. 그래서 위 예제에서 product_id에 별도 인덱스를 만들었다.
  • 비정규화 트레이드오프: 조회가 많은 경로에서 조인을 줄이려고 중복 컬럼을 두면 읽기는 빨라지지만 쓰기 시 정합성 관리 비용이 생긴다.

6. 실무 사용 사례

  • 신규 서비스 스키마 설계: 요구사항에서 명사를 뽑아 개체 후보로 삼고, 동사를 관계 후보로 삼는다.
  • 레거시 DB 문서화: 운영 중인 DB에서 ERD를 역으로 추출(리버스 엔지니어링)해 구조를 파악한다. DBeaver, DataGrip, pgAdmin 같은 도구가 이를 지원한다.
  • 코드형 다이어그램: Mermaid, dbdiagram.io(DBML), PlantUML로 ERD를 텍스트로 관리하면 Git에서 변경 이력을 리뷰할 수 있다.
  • ORM 모델과 대응: JPA, Prisma, Django ORM의 모델 관계(@OneToMany, ForeignKey, ManyToMany)는 ERD의 관계를 코드로 옮긴 것이다. ERD를 먼저 그리면 ORM 모델 설계가 쉬워진다.
  • 마이그레이션 리뷰: 스키마 변경 PR에 ERD 변경분을 함께 올리면 영향 범위 파악이 쉽다.

7. 주의할 점

  • N:M을 그대로 두지 않는다: 물리 모델에서는 반드시 연결 테이블로 해소해야 한다. 관계 속성(수량, 가입일 등)이 있으면 연결 테이블이 사실상 독립 개체가 된다.
  • PK 선택: 이메일, 전화번호 같은 자연키(Natural Key)는 변경될 수 있어 PK로 위험하다. 대리키(Surrogate Key)를 PK로 쓰고 자연키는 UNIQUE로 보장하는 방식이 흔하다.
  • FK 인덱스 누락: 위에서 설명했듯 PostgreSQL에서는 직접 만들어야 한다.
  • ON DELETE 동작 명시: CASCADE, RESTRICT, SET NULL 중 업무 규칙에 맞게 정한다. 무심코 CASCADE를 쓰면 부모 삭제 시 데이터가 연쇄 삭제된다.
  • 과도한 정규화/비정규화: 정규화는 이상 현상을 막는 수단이지 목적이 아니다. 읽기 패턴을 보고 필요한 곳만 의도적으로 비정규화하고, 그 이유를 문서에 남긴다.
  • ERD와 실제 스키마의 괴리: 운영 중 스키마가 바뀌는데 ERD를 갱신하지 않으면 문서가 오히려 혼란을 준다. 코드형 다이어그램이나 자동 추출로 동기화한다.
  • 관계 방향 혼동: FK는 N쪽에 위치한다. 1쪽에 FK를 두면 관계 표현이 깨진다.
  • 다형 관계의 한계: target_type, target_id처럼 여러 테이블을 가리키는 컬럼은 FK 제약을 걸 수 없어 참조 무결성을 DB가 보장하지 못한다.

8. 핵심 정리

  • ERD는 개체(Entity), 속성(Attribute), 관계(Relationship)로 데이터 구조를 표현하며, 개념 → 논리 → 물리 단계로 구체화된다.
  • 변환 규칙: 개체는 테이블, 1:N은 N쪽 FK, N:M은 연결 테이블, 1:1은 UNIQUE FK.
  • 카디널리티와 참여도는 FK의 UNIQUE와 NOT NULL 제약으로 이어진다.
  • 식별 관계는 부모 PK가 자식 PK에 포함되고, 비식별 관계는 일반 FK로 들어간다.
  • PostgreSQL은 FK 컬럼에 인덱스를 자동 생성하지 않으므로 조인과 삭제 성능을 위해 직접 만든다.
  • ERD는 한 번 그리고 끝내는 그림이 아니라 스키마와 함께 갱신해야 하는 문서다.