LODY/정리

데이터베이스 / 관계형 모델과 SQL 기본

관계형 모델과 SQL 기본

🟣 한 곳에 다 적으면 왜 무너지나

주문 데이터를 관리한다고 해 보자. 가장 소박한 방법은 표 하나에 전부 몰아 적는 것이다. 주문 한 건마다 회원 이름, 회원 등급, 주문한 상품 이름, 상품 가격, 주문 수량을 한 줄에 나란히 적는다. 처음엔 이게 제일 편하다. 한 줄만 보면 그 주문에 관한 모든 것이 다 있다.

문제는 같은 사실이 여러 줄에 반복된다는 데서 시작한다. 한 회원이 주문을 열 번 하면 그 회원의 이름과 등급이 열 줄에 똑같이 적힌다. 회원이 등급을 올리면 열 줄을 전부 고쳐야 하고, 한 줄이라도 놓치면 같은 회원인데 등급이 두 개인 모순이 생긴다. 회원이 아직 주문을 한 번도 안 했으면 그 회원을 적을 자리조차 없다. 이렇게 하나의 사실을 여러 곳에 중복 저장할 때 갱신·삽입·삭제에서 생기는 모순을 이상 현상이라 부른다.

해법의 방향은 분명하다. 회원에 관한 사실은 회원 표에 한 번만 적고, 주문에 관한 사실은 주문 표에 적되, 둘을 연결할 고리만 남긴다. 데이터를 주제별로 잘게 쪼개 중복을 없애는 이 발상이 관계형 모델의 출발점이다.


🟣 관계형 모델: 표, 행, 그리고 키

관계형 데이터베이스는 데이터를 표(테이블)로 담는다. 표의 각 행은 하나의 개체를, 각 열은 그 개체의 속성을 나타낸다. 회원 표의 한 행은 회원 한 명이고, 열은 이름·등급·가입일 같은 속성이다. 학술적으로는 표를 릴레이션, 행을 튜플, 열을 애트리뷰트라 부르는데, 실무에서는 표와 행, 열이라는 말로 충분하다.

이 모델의 핵심은 행을 구별하는 방법에 있다. 표 안의 어떤 행이든 유일하게 찾아낼 수 있어야 한다. 이 역할을 하는 열(또는 열의 조합)이 기본키다. 회원 번호가 기본키라면 같은 회원 번호를 가진 행이 둘 있을 수 없고, 회원 번호가 비어 있는 행도 있을 수 없다. 유일성과 비어 있지 않음, 이 두 조건이 기본키의 뜻 전부다.

표를 쪼개 놓으면 흩어진 표를 다시 이어야 하는데, 그 고리가 외래키다. 외래키는 다른 표의 기본키를 가리키는 열이다. 주문 표에 회원 이름을 통째로 적는 대신 회원 번호만 적고, 이 회원 번호가 회원 표의 어떤 행을 가리키는지로 연결한다.

주문 표주문번호(기본키)회원번호(외래키) · 주문일회원 표회원번호(기본키)이름 · 등급회원번호로 가리킴

이렇게 나누면 회원 등급을 고칠 때 회원 표의 한 행만 바꾸면 된다. 그 회원의 주문이 몇 건이든 주문 표는 손댈 필요가 없다. 앞에서 본 이상 현상이 뿌리부터 사라진다.


🟣 정규화: 쪼개는 데에도 규칙이 있다

표를 어디까지 어떻게 쪼갤지에도 기준이 있다. 이 기준을 단계별로 정리한 것이 정규화다. 무작정 잘게 나누는 게 아니라, 한 열의 값이 무엇에 의존하는지를 따져 나눈다.

첫 단계는 한 칸에 값 하나만 담는 것이다. 주문한 상품 여러 개를 한 칸에 쉼표로 몰아 적지 않고, 상품 하나가 한 행이 되게 편다. 이것이 1정규형이다. 다음 단계는 기본키의 일부에만 딸린 속성을 떼어내는 것이다. 주문 상세 표의 키가 주문번호와 상품번호의 조합인데 상품 이름은 상품번호만으로 정해진다면, 상품 이름은 이 표에 있을 이유가 없다. 상품 표로 옮긴다. 이것이 2정규형이다. 마지막으로 기본키가 아닌 속성이 다른 일반 속성에 딸려 가는 경우를 없앤다. 회원 표에 등급과 등급별 할인율이 같이 있다면, 할인율은 회원이 아니라 등급에 딸린 값이다. 등급 표로 뺀다. 이것이 3정규형이다.

세 단계를 관통하는 한 문장은 이것이다. 모든 속성은 오직 기본키에만 의존해야 한다. 이 원칙을 지키면 같은 사실이 두 곳에 적히는 일이 구조적으로 막힌다.

정규화가 늘 정답은 아니다. 표를 많이 쪼갤수록 조회할 때 다시 이어 붙이는 조인이 늘어난다. 조회 성능이 절실한 자리에서는 일부러 중복을 허용해 표를 합치기도 하는데, 이를 반정규화라 한다. 정규화로 무결성을 얻고 반정규화로 성능을 얻는 사이에서 무엇을 택할지는 그 데이터를 얼마나 자주 바꾸고 얼마나 자주 읽는지에 달려 있다. 이 트레이드오프는 뒤의 최적화 장에서 다시 만난다.


🟣 DDL: 표의 구조를 정의한다

이제 이 모델을 실제 SQL로 옮긴다. SQL은 크게 세 갈래다. 표의 구조를 정의하는 DDL, 데이터를 조회하고 바꾸는 DML, 변경을 확정하거나 되돌리는 TCL이다. 먼저 구조부터 만든다.

CREATE TABLE로 표를 만들면서 각 열의 자료형과 함께 지켜야 할 규칙을 못박는다. 이 규칙이 제약조건이다.

CREATE TABLE members (
    member_id   INTEGER      PRIMARY KEY,
    name        VARCHAR(50)  NOT NULL,
    grade       VARCHAR(20)  NOT NULL DEFAULT 'BASIC',
    email       VARCHAR(100) UNIQUE,
    joined_at   DATE         NOT NULL
);
 
CREATE TABLE orders (
    order_id    INTEGER   PRIMARY KEY,
    member_id   INTEGER   NOT NULL,
    ordered_at  DATE      NOT NULL,
    FOREIGN KEY (member_id) REFERENCES members(member_id)
);

제약조건은 잘못된 데이터가 아예 들어오지 못하게 막는 문지기다. PRIMARY KEY는 유일성과 비어 있지 않음을 함께 건다. NOT NULL은 값이 반드시 있어야 함을, UNIQUE는 중복이 없어야 함을, DEFAULT는 값을 안 주면 채울 기본값을 정한다. 가장 중요한 것은 FOREIGN KEY다. 주문 표의 member_id가 회원 표에 실제로 있는 회원만 가리키도록 강제한다. 존재하지 않는 회원의 주문을 넣으려 하면 데이터베이스가 거부한다.

이 무결성 규칙을 애플리케이션 코드가 아니라 데이터베이스가 지킨다는 점이 중요하다. 어느 경로로 데이터가 들어오든, 심지어 개발자가 콘솔에서 직접 손대더라도 규칙은 지켜진다. 무결성의 최후 방어선을 데이터에 가장 가까운 곳에 둔다는 발상이다.


🟣 DML: 데이터를 넣고, 바꾸고, 조회한다

구조가 섰으면 데이터를 다룬다. 넣는 INSERT, 바꾸는 UPDATE, 지우는 DELETE, 그리고 가장 많이 쓰는 조회 SELECT가 있다.

INSERT INTO members (member_id, name, grade, email, joined_at)
VALUES (1, '박정환', 'GOLD', 'lody@example.com', '2024-03-13');
 
UPDATE members SET grade = 'VIP' WHERE member_id = 1;
 
DELETE FROM members WHERE member_id = 1;

UPDATEDELETE에서 WHERE 절을 빠뜨리면 표의 모든 행에 적용된다. 회원 한 명의 등급을 올리려다 전체 회원을 VIP로 만드는 사고가 여기서 난다. 그래서 변경 문은 실행 전에 같은 WHERESELECT를 먼저 돌려 대상 행 수를 확인하는 습관이 안전하다.

조회는 어떤 열을(SELECT), 어느 표에서(FROM), 어떤 조건으로(WHERE), 어떻게 정렬해(ORDER BY) 가져올지를 적는다.

SELECT name, grade
FROM members
WHERE grade = 'VIP'
ORDER BY joined_at DESC;

여기서 관계형 모델의 성격 하나가 드러난다. SQL은 "어떻게 찾아라"가 아니라 "무엇을 원하는지"만 적는 선언형 언어다. VIP 회원을 가입 역순으로 달라고만 하면, 어느 순서로 표를 훑고 어떤 방법으로 정렬할지는 데이터베이스가 알아서 정한다. 이 "알아서 정하는" 주체가 옵티마이저인데, 마지막 장의 주인공이다. 지금은 우리가 방법이 아니라 결과만 기술한다는 사실만 기억하면 된다.


🟣 정리

  • 하나의 사실을 여러 곳에 중복 저장하면 값을 바꾸거나 넣거나 지울 때 모순이 생긴다. 관계형 모델은 데이터를 주제별 표로 쪼개 이 이상 현상을 없앤다.
  • 기본키는 행을 유일하게 구별하고, 외래키는 쪼갠 표를 다시 잇는 고리다. 무결성 규칙은 애플리케이션이 아니라 데이터베이스가 지킨다.
  • 정규화는 "모든 속성은 기본키에만 의존한다"는 원칙으로 중복을 구조적으로 막는다. 조회 성능이 필요하면 반정규화로 일부 중복을 되사는 트레이드오프가 있다.
  • DDL로 구조와 제약조건을 정의하고, DML로 데이터를 넣고 바꾸고 조회한다. UPDATE·DELETEWHERE 누락은 전체 행 사고로 이어진다.
  • SQL은 방법이 아니라 결과를 기술하는 선언형 언어다. 실제 실행 방법은 옵티마이저가 정한다.

관계형 모델은 앱 개발과도 곧장 닿는다. 모바일 앱이 기기 안에 두는 SQLite나 Core Data도 결국 표와 기본키, 외래키로 이뤄진 관계형 저장소다. 서버 스키마를 정규화하는 감각이 로컬 캐시 설계에서 그대로 쓰였다. 면접에서 "정규화와 반정규화 중 무엇을 택하겠느냐"를 물으면, 정답을 고르는 문제가 아니라 읽기·쓰기 비율로 판단하는 문제라고 답하게 된 것도 이 장에서 얻은 것이다.

중복을 없애려고 표를 쪼갰더니 이제 새로운 숙제가 생겼다. 회원 이름과 그 회원의 주문 내역을 한 화면에 같이 보여주려면, 흩어진 두 표를 다시 하나로 이어야 한다. 다음 장의 조인이 그 일을 한다.