ERD를 테이블로 변환하는 방법: 다대다 관계와 약한 개체의 키 설계
1장. ERD의 선 하나를 실제 테이블로 옮기면 문제가 시작된다#
학생과 강좌 사이에는 다음과 같은 관계가 있습니다.
한 학생은 여러 강좌를 수강할 수 있습니다.
한 강좌에도 여러 학생이 등록할 수 있습니다.
ERD에서는 다음처럼 간단하게 표현할 수 있습니다.
erDiagram
STUDENT }o--o{ COURSE : enrolls그림만 보면 어렵지 않습니다.
하지만 실제 데이터베이스 테이블을 만들려고 하면 질문이 생깁니다.
학생 S001이 데이터베이스 강좌를 신청했습니다.
다음 학기에 같은 과목을 다시 수강하면 어떻게 저장해야 할까요?
수강 신청일은 학생의 속성일까요?
강좌의 속성일까요?
성적은 어디에 저장해야 할까요?
수강을 취소했다가 다시 신청한다면 기존 행을 수정해야 할까요, 새로운 행을 만들어야 할까요?
ERD에서는 선 하나로 표현했던 관계가 실제 데이터베이스에서는 여러 설계 문제로 확장됩니다.
ERD를 테이블로 변환한다는 것은 그림을 그대로 옮기는 일이 아닙니다.
그림에 담긴 업무 규칙을 기본키, 외래키, 연결 테이블, NULL 허용 여부, UNIQUE, 복합키와 이력 구조로 구체화하는 과정입니다.
2장. ER 모델은 현실의 대상을 데이터 구조로 바꾸는 방법이다#
ER은 Entity-Relationship의 약자입니다.
핵심은 두 가지입니다.
- 개체
- 관계
그리고 개체와 관계에는 속성이 존재합니다.
대학 시스템을 예로 들면 다음과 같은 대상이 있을 수 있습니다.
학생
학과
과목
개설강좌
교수이들은 개체가 될 수 있습니다.
그리고 다음과 같은 관계를 가집니다.
학생은 학과에 소속된다.
학생은 강좌를 수강한다.
교수는 강좌를 담당한다.Mermaid로 표현하면 다음과 같습니다.
erDiagram
DEPARTMENT ||--o{ STUDENT : has
STUDENT ||--o{ ENROLLMENT : registers
COURSE ||--o{ ENROLLMENT : includes이 그림 하나에는 여러 업무 규칙이 들어 있습니다.
학생은 학과에 속합니다.
학생은 여러 수강 기록을 가질 수 있습니다.
하나의 강좌에도 여러 수강 기록이 존재할 수 있습니다.
하지만 그림만 보고 데이터베이스를 만들기 전에 각각의 의미를 정확하게 해석해야 합니다.
3장. 개체와 인스턴스를 구분하자#
다음 구조가 있다고 하겠습니다.
학생
- 학번
- 이름이것은 학생이라는 개체 타입입니다.
실제 데이터는 다음처럼 존재합니다.
S001 | 김하늘
S002 | 이바다S001 | 김하늘은 학생이라는 개체 타입에 속한 하나의 개체 인스턴스입니다.
관계형 데이터베이스에서는 보통 다음처럼 대응합니다.
개체 타입
→ 테이블
개체 인스턴스
→ 테이블의 한 행
속성
→ 열따라서 학생 개체는 다음과 같은 테이블로 표현할 수 있습니다.
CREATE TABLE student (
student_id VARCHAR(20) PRIMARY KEY,
name VARCHAR(100) NOT NULL
);ERD에서 개체를 찾는 것은 관계형 데이터베이스에서 테이블의 후보를 찾는 과정과 연결됩니다.
4장. 모든 명사가 개체가 되는 것은 아니다#
업무 설명에 등장하는 명사를 모두 테이블로 만들면 안 됩니다.
예를 들어 다음 문장이 있습니다.
학생은 이름과 생년월일을 가지며 학과에 소속된다.
여기서 다음 단어들이 등장합니다.
학생
이름
생년월일
학과하지만 모두 같은 수준의 개체는 아닙니다.
학생과 학과는 독립적으로 관리할 필요가 있는 대상입니다.
반면 이름과 생년월일은 학생을 설명하는 속성입니다.
다음처럼 표현할 수 있습니다.
erDiagram
DEPARTMENT ||--o{ STUDENT : contains
STUDENT {
string student_id PK
string name
date birth_date
string department_id FK
}
DEPARTMENT {
string department_id PK
string department_name
}ER 모델링에서 가장 중요한 판단 가운데 하나는 이것입니다.
이 정보를 독립적인 대상으로 관리해야 하는가, 아니면 다른 대상을 설명하는 값인가?
5장. 속성은 저장 방법에 따라 여러 형태로 나뉜다#
학생에게 다음 정보가 있다고 하겠습니다.
학번
이름
생년월일
주소
전화번호
나이모두 속성처럼 보이지만 성격은 다릅니다.
| 속성 구분 | 예 | 설계에서 확인할 내용 |
|---|---|---|
| 단순 속성 | 학번 | 더 나눌 필요가 있는가 |
| 복합 속성 | 주소 | 검색·검증 단위로 나눌 것인가 |
| 단일값 속성 | 생년월일 | 한 대상에 하나만 존재하는가 |
| 다중값 속성 | 전화번호 | 여러 값을 허용하는가 |
| 저장 속성 | 생년월일 | 실제 값을 저장 |
| 유도 속성 | 나이 | 다른 값에서 계산 가능한가 |
| 키 속성 | 학번 | 개체를 식별할 수 있는가 |
속성의 성격을 잘못 판단하면 테이블 구조도 잘못 만들어질 수 있습니다.
6장. 다중값 속성은 별도 개체처럼 풀어낼 수 있다#
학생 한 명에게 여러 전화번호가 있다고 해보겠습니다.
다음처럼 한 열에 모두 넣는 방법도 생각할 수 있습니다.
S001 | 010-1111-2222, 02-123-4567하지만 이렇게 하면 전화번호 하나를 검색하거나 수정하기 어렵습니다.
번호별 유형을 관리하기도 어렵습니다.
그래서 전화번호를 별도 테이블로 분리할 수 있습니다.
erDiagram
STUDENT ||--o{ STUDENT_PHONE : has
STUDENT {
string student_id PK
string name
}
STUDENT_PHONE {
string student_id FK
string phone_number
string phone_type
}실제 테이블은 다음처럼 만들 수 있습니다.
CREATE TABLE student_phone (
student_id VARCHAR(20) NOT NULL,
phone_number VARCHAR(30) NOT NULL,
phone_type VARCHAR(20),
PRIMARY KEY (student_id, phone_number),
FOREIGN KEY (student_id)
REFERENCES student(student_id)
);학생 한 명에게 여러 전화번호가 있어도 각 번호를 독립적인 행으로 관리할 수 있습니다.
7장. 카디널리티는 양쪽에서 질문해야 한다#
학생과 학과 관계를 보겠습니다.
학생 한 명은 몇 개 학과에 속할 수 있을까요?
한 개라고 가정하겠습니다.
학과 하나에는 몇 명의 학생이 들어갈 수 있을까요?
여러 명입니다.
그러면 다음 관계가 됩니다.
학과 1 : N 학생Mermaid로 표현하면 다음과 같습니다.
erDiagram
DEPARTMENT ||--o{ STUDENT : contains하지만 1:N만으로 모든 업무 규칙이 표현된 것은 아닙니다.
학생이 학과 없이 존재할 수 있는지까지 확인해야 합니다.
8장. 최대 개수와 최소 개수는 다른 문제다#
다음 두 규칙을 비교해 보겠습니다.
첫 번째 규칙입니다.
학생은 반드시 한 학과에 속해야 한다.
두 번째 규칙입니다.
학생은 최대 한 학과에 속하지만 학과가 아직 없어도 된다.
둘 다 최대 개수는 1입니다.
하지만 최소 개수가 다릅니다.
첫 번째는:
최소 1
최대 1두 번째는:
최소 0
최대 1데이터베이스 구현도 달라집니다.
필수라면:
department_id VARCHAR(20) NOT NULL선택이라면:
department_id VARCHAR(20)처럼 NULL 허용 여부가 달라질 수 있습니다.
ERD의 카디널리티는 최대 연결 개수와 최소 참여 개수를 함께 읽어야 합니다.
9장. 1:N 관계는 N쪽에 외래키를 둔다#
학과와 학생은 다음 관계입니다.
erDiagram
DEPARTMENT ||--o{ STUDENT : contains
DEPARTMENT {
string department_id PK
string department_name
}
STUDENT {
string student_id PK
string name
string department_id FK
}한 학과에는 여러 학생이 있습니다.
각 학생은 하나의 학과를 참조합니다.
관계형 데이터베이스에서는 N쪽인 학생 테이블에 학과의 키를 외래키로 둡니다.
CREATE TABLE department (
department_id VARCHAR(20) PRIMARY KEY,
department_name VARCHAR(100) NOT NULL
);
CREATE TABLE student (
student_id VARCHAR(20) PRIMARY KEY,
name VARCHAR(100) NOT NULL,
department_id VARCHAR(20) NOT NULL,
FOREIGN KEY (department_id)
REFERENCES department(department_id)
);이 구조에서는 여러 학생이 같은 학과코드를 가질 수 있습니다.
S001 → CSE
S002 → CSE
S003 → MAT10장. 1:1 관계는 외래키만 추가한다고 끝나지 않는다#
학과와 학과장의 관계를 생각해 보겠습니다.
한 학과에는 학과장이 한 명입니다.
한 교수도 동시에 한 학과의 학과장만 맡을 수 있다고 가정하겠습니다.
erDiagram
PROFESSOR o|--o| DEPARTMENT : chairs학과 테이블에 학과장 사번을 둘 수 있습니다.
학과
- 학과코드
- 학과명
- 학과장사번그리고 교수 테이블을 참조하는 외래키를 설정합니다.
하지만 외래키만으로는 다음 데이터를 막지 못할 수 있습니다.
컴퓨터공학과 → E001
수학과 → E001같은 교수 E001이 두 학과의 학과장으로 들어갔습니다.
1:1을 강제하려면 다음과 같은 제약이 필요합니다.
chair_professor_id VARCHAR(20) UNIQUE학과장이 반드시 있어야 한다면 NOT NULL도 검토합니다.
즉 1:1 관계에서는 다음을 함께 확인해야 합니다.
FOREIGN KEY
UNIQUE
NOT NULL 여부11장. 학생과 강좌의 N:M 관계를 그대로 테이블로 만들 수는 없다#
학생과 강좌는 다음 관계를 가집니다.
erDiagram
STUDENT }o--o{ COURSE : enrolls학생 한 명은 여러 강좌를 수강합니다.
강좌 하나에도 여러 학생이 등록합니다.
학생 테이블에 강좌ID를 넣으면 어떻게 될까요?
학생
- 학번
- 이름
- 강좌ID학생이 하나의 강좌만 가지는 구조처럼 보입니다.
강좌 테이블에 학생ID를 넣어도 마찬가지입니다.
따라서 N:M 관계는 일반적으로 연결 테이블을 만들어 해결합니다.
12장. N:M 관계는 두 개의 1:N 관계로 바꾼다#
학생과 강좌 사이에 수강이라는 연결 테이블을 만듭니다.
erDiagram
STUDENT ||--o{ ENROLLMENT : registers
COURSE ||--o{ ENROLLMENT : receives
STUDENT {
string student_id PK
string name
}
COURSE {
string course_id PK
string course_name
}
ENROLLMENT {
string student_id FK
string course_id FK
}구조는 다음과 같습니다.
학생 1 : N 수강
강좌 1 : N 수강수강 테이블에는 다음처럼 데이터가 들어갑니다.
| 학번 | 강좌ID |
|---|---|
| S001 | L001 |
| S001 | L002 |
| S002 | L001 |
이제 학생 S001은 여러 강좌를 가질 수 있습니다.
강좌 L001에도 여러 학생이 연결될 수 있습니다.
13장. 연결 테이블에는 관계 자체의 속성이 들어간다#
수강에는 단순히 학생과 강좌의 연결만 있는 것이 아닙니다.
다음 정보가 존재할 수 있습니다.
신청일
수강상태
성적
취소일이 값들은 어디에 두어야 할까요?
성적은 학생의 속성이 아닙니다.
학생 S001은 여러 강좌에서 서로 다른 성적을 받을 수 있기 때문입니다.
강좌의 속성도 아닙니다.
한 강좌 안에서도 학생마다 성적이 다릅니다.
성적은 다음 조합에 의해 결정됩니다.
학생 + 강좌즉 수강 관계의 속성입니다.
Mermaid로 표현하면 다음과 같습니다.
erDiagram
STUDENT ||--o{ ENROLLMENT : registers
COURSE ||--o{ ENROLLMENT : receives
STUDENT {
string student_id PK
string name
}
COURSE {
string course_id PK
string course_name
}
ENROLLMENT {
string student_id FK
string course_id FK
date registered_at
string status
string grade
}관계 자체에도 중요한 데이터가 존재할 수 있다는 점이 핵심입니다.
14장. 과목과 실제 개설 강좌를 구분해야 한다#
데이터베이스라는 과목이 있다고 하겠습니다.
과목코드는 다음과 같습니다.
DB101하지만 DB101은 매 학기 개설될 수 있습니다.
2026년 1학기 1분반
2026년 2학기 1분반
2027년 1학기 2분반따라서 과목과 개설강좌를 분리할 수 있습니다.
erDiagram
SUBJECT ||--o{ COURSE : opens
SUBJECT {
string subject_code PK
string subject_name
}
COURSE {
string course_id PK
string subject_code FK
string semester
string section
}데이터는 다음처럼 표현할 수 있습니다.
| 강좌ID | 과목코드 | 학기 | 분반 |
|---|---|---|---|
| L001 | DB101 | 2026-1 | 01 |
| L002 | DB101 | 2026-2 | 01 |
학생이 같은 과목을 다시 수강해도 서로 다른 개설강좌를 참조하게 됩니다.
S001 → L001
S001 → L00215장. 전체 학사 관계를 ERD로 보면 구조가 선명해진다#
지금까지 내용을 하나의 ERD로 표현하면 다음과 같습니다.
erDiagram
DEPARTMENT ||--o{ STUDENT : contains
SUBJECT ||--o{ COURSE : opens
STUDENT ||--o{ ENROLLMENT : registers
COURSE ||--o{ ENROLLMENT : includes
DEPARTMENT {
string department_id PK
string department_name
}
STUDENT {
string student_id PK
string name
string department_id FK
}
SUBJECT {
string subject_code PK
string subject_name
}
COURSE {
string course_id PK
string subject_code FK
string semester
string section
}
ENROLLMENT {
string student_id FK
string course_id FK
date registered_at
string status
string grade
}이 그림을 보면 역할이 분명합니다.
SUBJECT는 교과목 자체입니다.
COURSE는 특정 학기에 실제로 개설된 강좌입니다.
STUDENT는 학생입니다.
ENROLLMENT는 특정 학생과 특정 강좌 사이에서 발생한 수강 관계입니다.
16장. 수강 테이블의 기본키는 업무 규칙에 따라 달라진다#
다음 수강 기록이 있습니다.
S001 | L001한 학생이 같은 개설강좌를 한 번만 신청할 수 있다면 다음 복합키가 가능합니다.
PRIMARY KEY
(student_id, course_id)그런데 취소 후 재신청을 별도의 기록으로 남겨야 한다면 문제가 생깁니다.
첫 번째 신청:
S001 | L001재신청:
S001 | L001키가 같습니다.
두 행을 저장할 수 없습니다.
즉 ERD의 N:M 관계만으로는 키를 결정할 수 없습니다.
같은 관계가 업무에서 여러 번 발생할 수 있는지 확인해야 합니다.
17장. 취소와 재신청을 사건으로 저장하면 별도의 ID가 필요할 수 있다#
다음 흐름을 생각해 보겠습니다.
신청
↓
취소
↓
재신청기존 수강 행의 상태만 변경하면 최초 신청 사실이나 취소 이력을 잃을 수 있습니다.
이력을 모두 남기려면 각 신청을 독립적인 사건으로 볼 수 있습니다.
erDiagram
STUDENT ||--o{ ENROLLMENT : makes
COURSE ||--o{ ENROLLMENT : target
ENROLLMENT {
bigint enrollment_id PK
string student_id FK
string course_id FK
date registered_at
string status
}데이터는 다음처럼 저장할 수 있습니다.
| 신청ID | 학생 | 강좌 | 신청일 | 상태 |
|---|---|---|---|---|
| E001 | S001 | L001 | 03-01 | 취소 |
| E002 | S001 | L001 | 03-10 | 신청 |
이제 두 신청을 구별할 수 있습니다.
하지만 여기서 또 하나의 규칙이 필요합니다.
같은 학생이 같은 강좌에 동시에 두 개의 활성 신청을 가져도 되는가?
별도의 ID는 행을 식별할 뿐입니다.
업무상 중복을 자동으로 막아주지는 않습니다.
18장. 현재 상태와 사건 이력은 다른 모델이다#
수강 데이터를 다음처럼 저장할 수도 있습니다.
한 학생 + 한 강좌 = 현재 상태 한 행예:
S001 | L001 | 수강중취소하면:
S001 | L001 | 취소재신청하면:
S001 | L001 | 수강중현재 상태를 확인하기는 쉽습니다.
하지만 과거 이력은 사라질 수 있습니다.
반대로 모든 사건을 저장하면:
신청
취소
재신청이력을 모두 알 수 있지만 현재 상태를 계산해야 합니다.
ERD를 설계할 때도 다음을 먼저 정해야 합니다.
현재 상태를 저장하는가?
사건의 이력을 저장하는가?
둘은 비슷해 보여도 완전히 다른 데이터 모델이 될 수 있습니다.
19장. 약한 개체는 소유자 없이는 식별하기 어려운 개체다#
직원과 부양가족 관계를 생각해 보겠습니다.
직원 E001에게 두 명의 부양가족이 있습니다.
1 | 김가람
2 | 김나래직원 E002에게도 가족번호 1번이 있습니다.
1 | 이다온가족번호 1만 보면 어느 가족인지 알 수 없습니다.
E001의 가족 1
E002의 가족 1직원의 사번이 함께 있어야 식별할 수 있습니다.
이처럼 소유자 없이 독립적으로 식별하기 어려운 개체를 약한 개체라고 설명할 수 있습니다.
20장. 약한 개체는 소유자의 키를 자신의 키에 포함한다#
직원과 부양가족을 Mermaid로 표현해 보겠습니다.
erDiagram
EMPLOYEE ||--o{ DEPENDENT : has
EMPLOYEE {
string employee_id PK
string name
}
DEPENDENT {
string employee_id PK, FK
int dependent_no PK
string name
string relation_type
}부양가족의 기본키는 다음 조합입니다.
employee_id
+
dependent_no즉:
(E001, 1)
(E001, 2)
(E002, 1)각 조합은 서로 다릅니다.
SQL에서는 다음과 같이 표현할 수 있습니다.
CREATE TABLE employee (
employee_id VARCHAR(20) PRIMARY KEY,
name VARCHAR(100) NOT NULL
);
CREATE TABLE dependent (
employee_id VARCHAR(20) NOT NULL,
dependent_no INTEGER NOT NULL,
name VARCHAR(100) NOT NULL,
relation_type VARCHAR(20),
PRIMARY KEY (employee_id, dependent_no),
FOREIGN KEY (employee_id)
REFERENCES employee(employee_id)
);21장. 약한 개체의 부분키는 소유자 범위 안에서만 유일하면 된다#
부양가족의 dependent_no가 다음처럼 존재할 수 있습니다.
E001 | 1
E001 | 2
E002 | 1
E002 | 2dependent_no = 1이 여러 번 등장합니다.
문제가 아닙니다.
가족번호는 전체 조직에서 유일한 값이 아니라 한 직원 안에서만 유일한 값입니다.
따라서 기본키가 다음처럼 구성됩니다.
(employee_id, dependent_no)이처럼 약한 개체에서는 소유자 키와 부분키를 함께 고려해야 합니다.
22장. 약한 개체라고 무조건 ON DELETE CASCADE는 아니다#
직원에게 부양가족이 종속되어 있으니 직원을 삭제하면 부양가족도 모두 삭제하면 될 것처럼 보입니다.
기술적으로는 다음과 같이 만들 수 있습니다.
FOREIGN KEY (employee_id)
REFERENCES employee(employee_id)
ON DELETE CASCADE하지만 이것이 항상 올바른 것은 아닙니다.
과거 급여 기록이나 복지 지급 기록에 부양가족 정보가 필요할 수 있습니다.
퇴사한 직원을 삭제하더라도 일정 기간 기록을 보존해야 할 수도 있습니다.
따라서 다음 두 가지는 구분해야 합니다.
식별 구조상 부모에게 종속된다.
부모 삭제 시 데이터도 즉시 삭제한다.존재 종속과 삭제 정책은 같은 개념이 아닙니다.
23장. 다중값 속성과 약한 개체는 겉모습이 비슷해도 의미가 다르다#
학생전화와 부양가족은 둘 다 별도 테이블로 나눌 수 있습니다.
erDiagram
STUDENT ||--o{ STUDENT_PHONE : has
EMPLOYEE ||--o{ DEPENDENT : has하지만 의미는 다릅니다.
학생전화는 학생에게 여러 값이 존재하는 다중값 속성을 관계형 구조로 풀어낸 것으로 볼 수 있습니다.
부양가족은 이름, 관계, 생년월일 등 자체 속성을 가지는 독립적인 업무 대상이지만 소유자 키가 필요한 약한 개체로 볼 수 있습니다.
테이블이 따로 생긴다는 결과만 보고 같은 개념으로 취급해서는 안 됩니다.
24장. 관계 속성은 “누구의 사실인가”를 물어보면 된다#
다음 속성을 살펴보겠습니다.
학생 이름
강좌 정원
수강 신청일
성적학생 이름은 학생의 사실입니다.
강좌 정원은 강좌의 사실입니다.
수강 신청일은 특정 학생이 특정 강좌를 신청했다는 관계의 사실입니다.
성적 역시 특정 학생과 특정 강좌의 관계에서 발생한 값입니다.
이 질문을 활용하면 속성 위치를 결정하기 쉽습니다.
이 값은 누구의 사실인가?
학생만 알아도 결정되는가?
강좌만 알아도 결정되는가?
아니면 학생과 강좌를 모두 알아야 결정되는가?
학생과 강좌를 함께 알아야 결정된다면 연결 테이블에 들어갈 가능성이 높습니다.
25장. 관계에 시간 개념이 들어가면 구조가 달라질 수 있다#
학과와 학과장의 관계가 현재 시점에는 1:1이라고 하겠습니다.
erDiagram
PROFESSOR o|--o| DEPARTMENT : chairs하지만 과거 학과장 이력도 보관해야 한다면 상황이 달라집니다.
2024년 E001
2025년 E002
2026년 E003현재 학과장만 저장한다면 학과 테이블에 외래키 하나를 둘 수 있습니다.
이력을 저장한다면 별도 관계 테이블이 필요할 수 있습니다.
erDiagram
DEPARTMENT ||--o{ DEPARTMENT_CHAIR_HISTORY : has
PROFESSOR ||--o{ DEPARTMENT_CHAIR_HISTORY : serves
DEPARTMENT_CHAIR_HISTORY {
string department_id FK
string professor_id FK
date start_date
date end_date
}현재 관계가 1:1이라도 시간까지 포함하면 여러 개의 관계 기록이 존재할 수 있습니다.
26장. ERD의 선은 실제 SQL 제약으로 검증해야 한다#
다음 ERD가 있다고 하겠습니다.
erDiagram
DEPARTMENT ||--o{ STUDENT : contains이 그림에서는 학생이 정확히 하나의 학과를 가져야 한다고 해석했습니다.
그런데 실제 테이블을 다음처럼 만들었다면:
department_id VARCHAR(20)NULL이 허용됩니다.
학과 없는 학생을 저장할 수 있습니다.
그림과 실제 데이터베이스가 서로 다른 규칙을 갖게 됩니다.
반대로 학과 없이 학생을 등록할 수 있는 업무인데 NOT NULL을 넣었다면 정상 데이터를 거부할 수 있습니다.
ERD와 테이블 구조는 반드시 같은 업무 규칙을 표현해야 합니다.
27장. 외래키가 있다고 1:1이나 필수 관계가 자동으로 만들어지는 것은 아니다#
외래키는 기본적으로 다음을 보장합니다.
값이 있다면 존재하는 부모를 참조한다.
하지만 다음을 자동으로 보장하지는 않습니다.
값이 반드시 존재해야 한다.
한 번만 사용되어야 한다.첫 번째는 NOT NULL과 연결됩니다.
두 번째는 UNIQUE와 연결될 수 있습니다.
예를 들어 학과장의 관계가 필수 1:1이라면 다음 세 규칙이 함께 필요할 수 있습니다.
FOREIGN KEY
NOT NULL
UNIQUEERD의 관계 기호 하나가 실제로는 여러 데이터베이스 제약으로 나뉘는 것입니다.
28장. Mermaid ERD에서 전체 모델을 하나로 연결해 보기#
학생·학과·과목·강좌·수강·전화번호·직원·부양가족을 한 모델로 묶으면 다음과 같이 표현할 수 있습니다.
erDiagram
DEPARTMENT ||--o{ STUDENT : contains
STUDENT ||--o{ STUDENT_PHONE : has
SUBJECT ||--o{ COURSE : opens
STUDENT ||--o{ ENROLLMENT : registers
COURSE ||--o{ ENROLLMENT : includes
EMPLOYEE ||--o{ DEPENDENT : has
DEPARTMENT {
string department_id PK
string department_name
}
STUDENT {
string student_id PK
string name
date birth_date
string department_id FK
}
STUDENT_PHONE {
string student_id PK, FK
string phone_number PK
string phone_type
}
SUBJECT {
string subject_code PK
string subject_name
}
COURSE {
string course_id PK
string subject_code FK
string semester
string section
}
ENROLLMENT {
bigint enrollment_id PK
string student_id FK
string course_id FK
date registered_at
string status
string grade
}
EMPLOYEE {
string employee_id PK
string name
}
DEPENDENT {
string employee_id PK, FK
int dependent_no PK
string name
string relation_type
}이 정도의 ERD가 되면 단순히 상자와 선을 보는 것이 아니라 실제 테이블 구조까지 함께 읽을 수 있습니다.
29장. ERD를 SQL 테이블로 변환해 보기#
핵심 테이블을 실제 SQL로 표현하면 다음과 같습니다.
CREATE TABLE department (
department_id VARCHAR(20) PRIMARY KEY,
department_name VARCHAR(100) NOT NULL
);
CREATE TABLE student (
student_id VARCHAR(20) PRIMARY KEY,
name VARCHAR(100) NOT NULL,
birth_date DATE,
department_id VARCHAR(20) NOT NULL,
FOREIGN KEY (department_id)
REFERENCES department(department_id)
);
CREATE TABLE subject (
subject_code VARCHAR(20) PRIMARY KEY,
subject_name VARCHAR(100) NOT NULL
);
CREATE TABLE course (
course_id VARCHAR(20) PRIMARY KEY,
subject_code VARCHAR(20) NOT NULL,
semester VARCHAR(20) NOT NULL,
section VARCHAR(20) NOT NULL,
FOREIGN KEY (subject_code)
REFERENCES subject(subject_code)
);
CREATE TABLE enrollment (
enrollment_id BIGINT PRIMARY KEY,
student_id VARCHAR(20) NOT NULL,
course_id VARCHAR(20) NOT NULL,
registered_at DATE NOT NULL,
status VARCHAR(20) NOT NULL,
grade VARCHAR(5),
FOREIGN KEY (student_id)
REFERENCES student(student_id),
FOREIGN KEY (course_id)
REFERENCES course(course_id)
);여기서 enrollment_id를 별도로 둔 이유는 수강 신청 사건을 각각 식별할 수 있게 하기 위해서입니다.
같은 학생과 강좌 조합의 중복 활성 신청을 금지해야 한다면 별도의 업무 제약을 추가해야 합니다.
30장. ERD는 정상 데이터보다 예외 데이터를 넣어봐야 제대로 검증된다#
다음 학생을 등록합니다.
S001 | 김하늘 | CSECSE 학과가 존재한다면 정상입니다.
이번에는 다음 학생을 등록해 보겠습니다.
S002 | 이바다 | XXXXXX라는 학과가 존재하지 않는다면 외래키가 거부해야 합니다.
수강도 마찬가지입니다.
S999 | L001학생 S999가 없다면 허용하면 안 됩니다.
S001 | L999강좌 L999가 없다면 역시 허용하면 안 됩니다.
정상 사례만 입력하면 대부분의 ERD는 그럴듯해 보입니다.
잘못된 입력과 경계 사례를 넣어봐야 관계와 제약의 문제를 찾을 수 있습니다.
31장. 재수강은 관계의 식별 범위를 검증하는 좋은 테스트다#
다음 설계가 있다고 하겠습니다.
수강 PK
(student_id, subject_code)학생 S001이 DB101을 한번 수강합니다.
S001 | DB101다음 해 같은 과목을 재수강합니다.
S001 | DB101저장할 수 없습니다.
따라서 질문해야 합니다.
우리가 구별하려는 것은 과목인가, 개설 강좌인가, 수강 사건인가?
개설 강좌라면 course_id가 필요할 수 있습니다.
수강 사건이라면 enrollment_id가 필요할 수 있습니다.
ERD의 관계를 테이블로 바꿀 때 가장 중요한 것은 한 행을 어떤 사실로 정의할 것인가입니다.
32장. 삭제 정책도 관계 설계에 포함해야 한다#
학생 S001에게 과거 수강 기록이 있습니다.
학생을 삭제하면 어떻게 해야 할까요?
다음 선택지가 있습니다.
학생 삭제를 거부한다.
수강 기록도 함께 삭제한다.
학생 상태만 변경하고 기록은 보존한다.
개인정보를 별도 처리하고 수강 이력은 유지한다.업무마다 답이 다릅니다.
학사 기록이라면 과거 수강 내역을 보존해야 할 가능성이 큽니다.
따라서 무조건 ON DELETE CASCADE를 사용하는 것은 위험할 수 있습니다.
외래키를 만드는 것과 삭제 정책을 정하는 것은 별도의 설계 문제입니다.
33장. N:M 연결 테이블은 다양한 업무에 반복해서 등장한다#
학생과 강좌뿐 아니라 다양한 시스템에 같은 구조가 존재합니다.
주문과 상품#
erDiagram
ORDER ||--|{ ORDER_ITEM : contains
PRODUCT ||--o{ ORDER_ITEM : appears_in
ORDER_ITEM {
string order_id FK
string product_id FK
int quantity
decimal unit_price
}수량과 주문 당시 단가는 주문과 상품의 관계에서 발생합니다.
사용자와 역할#
erDiagram
USER ||--o{ USER_ROLE : has
ROLE ||--o{ USER_ROLE : assigned
USER_ROLE {
string user_id FK
string role_id FK
date granted_at
}권한 부여일은 사용자 자체나 역할 자체의 속성이 아니라 관계의 속성입니다.
프로젝트와 직원#
erDiagram
PROJECT ||--o{ PROJECT_MEMBER : has
EMPLOYEE ||--o{ PROJECT_MEMBER : participates
PROJECT_MEMBER {
string project_id FK
string employee_id FK
string role
date joined_at
date left_at
}프로젝트 역할과 투입일은 직원과 프로젝트 사이의 관계에 속합니다.
34장. ERD를 작성할 때는 업무 문장을 먼저 만들어야 한다#
ERD 도구를 먼저 열고 선부터 그리면 관계의 실제 의미를 놓치기 쉽습니다.
먼저 업무 문장으로 적어보는 것이 좋습니다.
예를 들어 다음과 같습니다.
학생은 반드시 하나의 학과에 소속된다.
학과에는 학생이 없을 수도 있다.
학생은 여러 개설 강좌에 수강 신청할 수 있다.
개설 강좌에는 수강생이 없을 수도 있다.
수강 신청에는 신청일과 상태가 있다.
취소 후 재신청은 새로운 신청 기록으로 남긴다.
학생은 여러 전화번호를 가질 수 있다.
부양가족은 직원 안에서 번호로 식별한다.이렇게 정리한 다음 ERD를 그리면 선과 기호가 단순한 그림이 아니라 실제 업무 규칙을 나타내게 됩니다.
35장. ERD를 테이블로 변환할 때 확인해야 할 핵심 질문#
ERD를 실제 관계형 테이블로 바꿀 때는 다음 질문을 확인하는 것이 좋습니다.
- 독립적으로 관리할 개체는 무엇인가?
- 각 개체를 무엇으로 식별할 것인가?
- 관계는 1:1, 1:N, N:M 중 무엇인가?
- 관계가 없어도 개체가 존재할 수 있는가?
- 관계 자체에 속성이 존재하는가?
- 같은 관계가 여러 번 발생할 수 있는가?
- 과거 관계의 이력을 보존해야 하는가?
- 부모가 삭제되면 자식 데이터는 어떻게 해야 하는가?
- 다중값 속성은 별도 테이블로 분리해야 하는가?
- 약한 개체의 부분키는 어느 범위에서 유일한가?
이 질문에 답하지 않은 상태에서 ERD를 테이블로 변환하면 정상적인 첫 데이터는 들어갈 수 있어도 시간이 지나면서 구조적인 문제가 드러날 가능성이 높습니다.
36장. 핵심 정리#
ERD는 데이터베이스 테이블을 예쁘게 그리는 그림이 아닙니다.
현실의 대상과 관계를 정의하고 데이터베이스가 지켜야 할 업무 규칙을 시각적으로 표현하는 설계 도구입니다.
관계형 데이터베이스로 변환할 때 기본적인 원칙은 다음과 같습니다.
강한 개체
→ 독립 테이블
약한 개체
→ 소유자 키 + 부분키
1:1 관계
→ 한쪽 외래키 + 필요 시 UNIQUE
1:N 관계
→ N쪽에 외래키
N:M 관계
→ 연결 테이블
다중값 속성
→ 별도 테이블그러나 실제 설계에서는 이보다 더 중요한 질문이 있습니다.
관계는 필수인가?
NULL을 허용할 것인가?
같은 관계가 반복될 수 있는가?
관계에 어떤 속성이 있는가?
현재 상태와 이력을 어떻게 구분할 것인가?
삭제 이후의 관계는 어떻게 처리할 것인가?특히 N:M 관계에서는 연결 테이블을 단순한 외래키 모음으로 보면 안 됩니다.
수강의 신청일과 성적, 주문항목의 수량과 주문 당시 가격, 프로젝트 참여자의 역할처럼 관계 자체가 중요한 업무 데이터의 주체가 될 수 있습니다.
약한 개체도 단순히 부모 아래에 있는 자식 데이터라는 뜻이 아닙니다.
자신만으로 완전히 식별되지 않고 소유자의 키와 부분키를 함께 사용한다는 점이 핵심입니다.
좋은 ERD는 그림을 보는 순간 다음 질문에 답할 수 있어야 합니다.
어떤 행이 존재할 수 있는가?
어떤 행은 절대로 존재해서는 안 되는가?
같은 관계가 다시 발생하면 새로운 데이터인가 기존 데이터의 수정인가?
대상이 삭제되더라도 과거 관계는 남아야 하는가?
그리고 그 답이 실제 기본키, 외래키, UNIQUE, NOT NULL과 관계 테이블 구조에서도 동일하게 유지되어야 합니다.
ERD가 현실의 업무 규칙을 정확하게 설명하고, 실제 테이블이 그 규칙을 강제할 수 있을 때 비로소 개념 모델과 관계형 데이터베이스가 제대로 연결됩니다.