백엔드 데이터 인프라 7편. PostgreSQL의 토대인 관계형 모델 — 행·열·관계·기본 키·외래 키 핵심 개념을 풀어쓴 학습 노트.
이 글은 백엔드 데이터 인프라 시리즈 70편 중 7편이에요. 6편 접속 까지 "환경" 을 만들었으니, 이번 7편은 그 위에 "데이터를 어떻게 표현하느냐" 의 핵심 — 관계형 모델 의 기본 개념.
왜 입문에서 이 개념을 다시?
자바 백엔드 입문 시리즈에서 JPA·@Entity·Repository를 다뤘다면 — "테이블·행·열" 이 이미 익숙할 거예요. 다만 PG 깊이로 가려면 — 관계형 모델의 "왜 이렇게 설계됐는지" 한 번 정리하고 가는 게 도움. 8편 SQL 기초·9편 CREATE TABLE 부터는 이 개념 위에 직접 코드를 박아요.
관계형 모델의 기원
관계형 모델 = 1970년 IBM 연구원 Edgar F. Codd 가 제안. "데이터를 수학적 관계(relation, 즉 표) 로 표현하자" 라는 단순하지만 강력한 아이디어. 50년+ 지난 지금도 RDBMS의 토대.
핵심 4가지: - 데이터는 테이블 로 - 테이블 = 행(row) + 열(column) - 행 = 한 개체의 사실 - 행 사이 관계 = 외래 키로
테이블 — 한 종류의 사실 묶음
+----+----------+-------------------+---------------------+
| id | name | email | created_at |
+----+----------+-------------------+---------------------+
| 1 | Alice | alice@example.com | 2026-05-17 10:00:00 |
| 2 | Bob | bob@example.com | 2026-05-17 10:05:00 |
| 3 | Charlie | NULL | 2026-05-17 10:10:00 |
+----+----------+-------------------+---------------------+
이 "users 테이블" 이 표현하는 것 = "우리 시스템의 사용자들". 한 행 = 한 사용자.
용어: - 테이블(table) = 관계(relation) 또는 엔티티 집합 - 행(row) = 튜플(tuple) 또는 레코드 - 열(column) = 속성(attribute) 또는 필드
논문에선 "관계·튜플·속성" 이라 부르지만 — 실무에서는 "테이블·행·열" 이 압도적.
열 = 도메인 + 타입
각 열은 "무엇을 담느냐" 가 명확.
| 열 | 타입 | 의미 |
|---|---|---|
id |
BIGINT |
사용자의 고유 식별자 |
name |
TEXT |
이름 (한 사람) |
email |
TEXT |
이메일 (필수 X — NULL 허용) |
created_at |
TIMESTAMPTZ |
가입 시각 |
타입 = "이 열이 받을 수 있는 값의 범위". 32편 데이터 타입에서 PG 풍부한 타입을 깊이.
행 = 한 개체의 사실 묶음
한 행이 "한 개체" 를 의미. 한 행의 모든 열을 합치면 — "이 사용자에 대해 우리가 아는 모든 것".
원자성 (Atomicity, 한 셀에 하나의 값) — 한 셀(행과 열의 교차점) 에는 "단일 값" 만. 예: tags 열에 "red,blue,green" 같은 콤마 분리 값 박는 건 안티패턴.
❌ 나쁜 예
+----+--------------------+
| id | tags |
+----+--------------------+
| 1 | red,blue,green |
+----+--------------------+
✅ 좋은 예 (별도 테이블)
+----+-------+ +------+----------+
| id | name | | uid | tag |
+----+-------+ +------+----------+
| 1 | Alice | | 1 | red |
+----+-------+ | 1 | blue |
| 1 | green |
+------+----------+
이 "정규화" 원칙이 관계형의 핵심.
기본 키 (Primary Key)
행을 "고유하게 식별" 하는 열. 한 테이블에 1개.
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY, -- 자동 증가 + 기본 키
name TEXT NOT NULL,
email TEXT UNIQUE
);
기본 키 후보:
- 자동 증가 정수 (BIGSERIAL·IDENTITY) — 가장 흔함
- UUID (Universally Unique Identifier, 충돌 없는 식별자) — 분산 시스템 표준
- 자연 키 (이메일·주민번호 등) — 위험 (변할 수 있음)
자연 키는 "바뀔 수 있는 데이터" — 추천 X. 인공 키(자동 증가 또는 UUID) 가 안전.
외래 키 (Foreign Key)
다른 테이블의 기본 키를 "참조" 하는 열. 행 사이 관계 표현.
CREATE TABLE orders (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id),
amount INTEGER NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW()
);
user_id 가 — users.id 를 가리키는 외래 키. "이 주문은 어느 사용자의 것인가" 가 명확.
이게 참조 무결성 — DB가 "존재하지 않는 사용자 ID로 주문 만들기" 를 거부.
INSERT INTO orders (user_id, amount) VALUES (999, 10000);
-- ERROR: insert or update on table "orders" violates foreign key constraint
자바 백엔드 입문 46편 JPA 연관관계 가 자바 객체 ↔ 이 외래 키 매핑.
NULL — "값이 없음" 의 특별한 상태
SELECT email FROM users WHERE id = 3;
email
-------
NULL ← 값이 없음 (모름·미입력)
NULL 의 핵심 룰:
- NULL = NULL 은 참(true) 이 아님 — NULL
- WHERE email = NULL ❌ — 항상 거짓
- WHERE email IS NULL ✅ — 표준
- WHERE email IS NOT NULL ✅
- COUNT(email) = NULL 빼고 카운트
- COUNT(*) = 모든 행 포함
NULL 다루기가 SQL 입문자의 첫 함정.
1:1 · 1:N · N:M 관계
행 사이 관계 3가지.
1:1 — 한 사용자 ↔ 한 프로필
CREATE TABLE user_profiles (
user_id BIGINT PRIMARY KEY REFERENCES users(id),
bio TEXT
);
기본 키이자 외래 키 — "한 사용자에 한 프로필" 강제.
1:N — 한 사용자 ↔ 여러 주문
CREATE TABLE orders (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id),
...
);
users 1 : orders N. 가장 흔한 관계.
N:M — 여러 사용자 ↔ 여러 태그
CREATE TABLE tags (
id BIGSERIAL PRIMARY KEY,
name TEXT UNIQUE
);
CREATE TABLE user_tags (
user_id BIGINT REFERENCES users(id),
tag_id BIGINT REFERENCES tags(id),
PRIMARY KEY (user_id, tag_id)
);
중간 테이블(join table, 두 테이블을 잇는 연결 표) 로 분해. "한 사용자에 여러 태그, 한 태그에 여러 사용자".
정규화 — 중복 제거
정규화 = "같은 사실을 한 곳에만" 박기.
❌ 비정규화 (중복)
+----+--------+----------+----------+
| id | name | city | country |
+----+--------+----------+----------+
| 1 | Alice | Seoul | Korea |
| 2 | Bob | Seoul | Korea | ← Korea 가 중복
| 3 | Carlos | Madrid | Spain |
+----+--------+----------+----------+
✅ 정규화
users cities
+----+--------+--------+ +--------+----------+
| id | name |city_id | | id | name |
+----+--------+--------+ +--------+----------+
| 1 | Alice | 1 | | 1 | Seoul |
| 2 | Bob | 1 | | 2 | Madrid |
| 3 | Carlos | 2 | +--------+----------+
+----+--------+--------+
장점 = 데이터 일관성 + 저장 공간 절약. 단점 = JOIN 많아짐 (성능 트레이드오프).
실무 = 1NF·2NF·3NF 까지 따르는 게 표준. 너무 깊은 정규화(4NF·5NF) 는 오히려 복잡.
ACID — 관계형의 약속
Atomicity·Consistency·Isolation·Durability. 자바 백엔드 입문 43편 @Transactional 에서 다룬 트랜잭션 원칙. PG는 ACID 100% 보장.
- A 원자성 — 모두 성공 또는 모두 실패
- C 일관성 — 제약 조건 항상 만족
- I 격리성 — 동시 트랜잭션 서로 영향 X (38편 MVCC, 다중 버전 동시성 제어)
- D 영속성 — 커밋 후엔 안 사라짐 (pg_wal, PG의 변경 로그 디렉토리)
함정 5가지
(1) 콤마 분리 값을 한 셀에
tags TEXT -- 'red,blue,green'
쿼리·검색·갱신 모두 어려움. 별도 테이블 또는 배열 타입.
(2) NULL 비교
WHERE col = NULL -- ❌ 항상 거짓
WHERE col IS NULL -- ✅
(3) 자연 키를 기본 키로
CREATE TABLE users (
email TEXT PRIMARY KEY, -- ❌ 이메일 바뀌면 외래 키 다 깨짐
...
);
인공 키(자동 증가·UUID)가 안전.
(4) 외래 키 없이 "코드로만" 무결성
CREATE TABLE orders (
user_id BIGINT NOT NULL -- ❌ FK 없음 — 잘못된 ID 박혀도 DB가 못 잡음
);
DB 수준 FK + 앱 수준 검증 둘 다.
(5) 너무 깊은 정규화
5NF 까지 분해하면 — JOIN 폭주 + 코드 복잡. 실무 = 3NF + 가끔 의도적 비정규화 (성능·캐시).
(1) 데이터 = 테이블, 한 셀 = 단일 값. (2) 기본 키 = 행 고유 식별. (3) 외래 키 = 행 간 관계. (4) NULL ≠ NULL. (5) 1:1·1:N·N:M 세 종류 관계, N:M은 중간 테이블.
한 줄 정리 — 관계형 모델 = 테이블·행·열 + 기본 키·외래 키. 한 셀 단일 값(원자성)·NULL은 IS로 비교·인공 키 우선·FK로 무결성. 정규화는 3NF까지 표준.
시험 직전 한 번 더 — 관계형 모델 입문자가 매번 헷갈리는 것
- 테이블 = 한 종류의 사실 묶음
- 행(row) = 한 개체, 열(column) = 한 속성
- 원자성 = 한 셀에 단일 값
- 콤마 분리 값 박기 = 안티패턴
- 기본 키 = 행 고유 식별 (테이블에 1개)
- 인공 키 우선 — BIGSERIAL·UUID
- 자연 키 (이메일·주민번호) 위험 — 변할 수 있음
- 외래 키 = 다른 테이블의 기본 키 참조
- 참조 무결성 = 존재하지 않는 ID 박기 거부
REFERENCES users(id)문법- NULL = NULL 거짓 — IS NULL 사용
COUNT(col)= NULL 빼고,COUNT(*)= 모두- 관계 3가지 = 1:1·1:N·N:M
- N:M = 중간 테이블 (join table)
- 정규화 = 중복 제거, 같은 사실 한 곳에
- 1NF = 원자값, 2NF·3NF = 부분·이행 종속 제거
- 실무 = 3NF + 의도적 비정규화 (성능)
- ACID = Atomicity·Consistency·Isolation·Durability
- PG 는 ACID 100%
- E.F.Codd = 관계형 모델 창안 (1970)
- 50년+ 검증된 모델
- JOIN = 정규화의 비용 (15~16편)
- 8편 SQL 기초부터 코드로 실습
시리즈 다른 편 (앞뒤 글 모음)
이전 글:
- 2편 — PostgreSQL 설치 (Docker·brew·apt 3가지)
- 3편 — PostgreSQL 아키텍처 (클라이언트·서버·DB·테이블)
- 4편 — psql 첫 접속과 기본 명령
- 5편 — 데이터베이스 만들기 CREATE DATABASE
- 6편 — PostgreSQL 데이터베이스 접속·전환
다음 글: