Devin.KR

데이터베이스와 DBMS - 파일로 관리하던 시절의 문제에서 출발하기

개발자KR 조회 8

이 장에서 배우는 것

SQL 연구소는 도서 판매를 시작하면서 회원, 도서, 주문 정보를 엑셀 파일 몇 개로 관리했다. 주문이 늘고 담당자가 여러 명으로 늘어나면서 파일 하나로는 감당이 안 되는 문제들이 나타났다. 이 장은 그 문제들이 정확히 무엇인지 짚고, 데이터베이스와 DBMS(database management system)가 이를 어떻게 해결하는지, 그리고 데이터가 3단계 스키마로 어떻게 나뉘어 관리되는지를 본다. 마지막으로 이 책 전체에서 계속 쓸 온라인 서점 데이터베이스를 SQLite로 직접 만들어 본다.

  • 파일로 데이터를 관리할 때 생기는 중복·불일치·동시 접근 문제를 설명할 수 있다
  • 데이터베이스와 DBMS를 구분하고 DBMS가 맡는 역할을 말할 수 있다
  • 외부·개념·내부 3단계 스키마와 데이터 독립성의 의미를 설명할 수 있다
  • SQL 연구소의 온라인 서점 스키마를 SQLite에 직접 만들 수 있다

문제 상황

SQL 연구소는 처음에 회원 정보를 회원명단.xlsx 한 파일로 관리했다. 영업팀은 이 파일을 복사해 자기 폴더에 두고 회원 등급을 손으로 갱신했고, 배송팀도 배송 상태를 적으려고 같은 파일을 또 복사했다. 몇 달 뒤 마케팅팀이 등급별 할인 쿠폰을 발송하려고 파일을 열었더니, 회원 101번 김하늘의 등급이 영업팀 파일에는 SILVER, 배송팀 파일에는 BASIC으로 적혀 있었다. 두 파일 다 최근에 수정된 파일이라 어느 쪽이 맞는 값인지 파일만 봐서는 알 수 없었다.

파일마다 흩어진 회원 등급 정보가 서로 어긋난다

비슷한 시기에 재고 담당자 두 명이 동시에 재고현황.xlsx를 열어 각자 다른 책의 재고를 수정하고 저장했다. 나중에 저장한 사람의 파일이 먼저 저장한 사람의 수정 내용을 통째로 덮어써서, 먼저 반영한 재고 차감분이 사라져 버렸다. 이 세 가지 상황 — 같은 데이터가 여러 파일에 중복되고, 그 값들이 서로 어긋나고, 동시에 접근한 수정이 사라지는 것 — 은 파일 하나만 쓸 때는 잘 드러나지 않다가 파일과 사용 인원이 늘어날수록 커지는 문제다.

파일 처리 방식의 한계

중복과 불일치

파일 처리 방식에서는 팀마다, 프로그램마다 필요한 데이터를 자기 파일에 따로 담아 둔다. 회원 이름과 등급이 영업팀 파일, 배송팀 파일, 마케팅팀 파일에 각각 저장되는 식이다. 같은 값을 여러 곳에 두면 어느 한 곳만 갱신되고 나머지는 그대로 남는 일이 반드시 생긴다. 값이 갈라지고 나면 어느 파일이 최신인지 파일 자체는 알려주지 않으므로, 결국 사람이 파일을 일일이 대조해서 맞는 값을 추정해야 한다.

동시 접근 문제

스프레드시트 파일은 보통 한 번에 한 사람이 열어 편집하고 저장하는 방식으로 동작한다. 두 사람이 각자 파일을 열어 서로 다른 부분을 고치더라도, 나중에 저장하는 쪽이 파일 전체를 덮어쓰기 때문에 먼저 저장된 수정 내용이 통째로 사라질 수 있다. 프로그램이 파일을 직접 열고 닫는 구조에서는 "지금 이 파일을 누가 쓰고 있는가"를 관리하는 주체가 없으므로, 이 문제는 파일 형식을 바꾼다고 해결되지 않는다. 누가 언제 무엇을 고치는지 조율하는 별도의 장치가 필요하다.

데이터베이스와 DBMS

데이터베이스는 여러 응용 프로그램과 여러 사용자가 공유할 목적으로 통합해 저장한 데이터의 집합이다. 회원, 도서, 주문 데이터를 팀마다 따로 두지 않고 하나로 모아 두면, 값은 한 군데에만 존재하고 모든 프로그램이 그 한 곳을 본다. DBMS는 이 데이터베이스를 만들고 운영하는 소프트웨어로, 응용 프로그램은 파일을 직접 열지 않고 DBMS에 요청을 보내 데이터를 읽고 쓴다.

여러 응용 프로그램은 DBMS를 거쳐야 데이터베이스에 접근한다

DBMS가 응용 프로그램과 저장 장치 사이에 자리 잡으면서 맡는 역할은 여러 가지다. 같은 데이터를 여러 테이블에 나눠 담아 중복을 줄이고, 정해진 형식에 어긋난 값이 들어오지 못하게 막는다. 이 값이 걸러지는 방식은 키와 무결성 제약을 다루는 장에서 자세히 본다. 여러 사용자가 동시에 요청을 보내도 서로의 수정이 사라지지 않도록 순서를 조율하는데, 이 부분은 동시성 제어와 회복을 다루는 장에서 다시 살펴본다. 그리고 SQL이라는 언어로 원하는 데이터를 조건에 맞춰 찾아오는 질의 처리 기능을 제공하는데, 이는 SQL 기본 조회부터 이어지는 여러 장에서 익힌다.

파일 처리 방식과 DBMS 방식이 같은 문제를 다루는 방식
항목파일 처리 방식DBMS 방식
같은 데이터의 저장 위치필요한 프로그램마다 각자 파일에 복사한 곳에 저장하고 여러 프로그램이 공유
값이 어긋났을 때사람이 파일을 대조해서 확인저장된 값이 하나뿐이라 어긋날 일 자체가 적음
동시 접근나중에 저장한 파일이 이전 수정을 덮어씀DBMS가 요청 순서를 조율
형식에 안 맞는 값파일에 그대로 저장됨DBMS가 저장 전에 걸러냄

3단계 스키마와 데이터 독립성

스키마는 데이터가 어떤 구조로 짜여 있는지를 나타낸 설계도다. DBMS는 이 설계도를 한 장이 아니라 세 층으로 나눠 관리한다. 외부 스키마는 회원용 화면, 관리자용 화면처럼 각 응용 프로그램이나 사용자 그룹이 보는 데이터의 범위를 정의한다. 개념 스키마는 조직 전체가 공유하는 전체 논리 구조로, 어떤 테이블이 있고 테이블 사이에 어떤 관계가 있는지를 담는다. 내부 스키마는 그 데이터가 디스크에 실제로 어떤 파일과 인덱스 형태로 저장되는지를 정의한다.

외부·개념·내부 스키마는 같은 데이터를 서로 다른 시각에서 보여준다

이렇게 층을 나누는 이유는 한 층의 변화가 다른 층에 번지지 않게 하기 위해서다. 이를 데이터 독립성이라 부른다. 내부 스키마, 즉 저장 방식을 바꿔도 개념 스키마와 그 위의 응용 프로그램은 영향을 받지 않아야 하는데 이를 물리적 데이터 독립성이라 한다. 개념 스키마에 새 테이블이나 열이 추가돼도 이미 쓰고 있는 외부 스키마, 즉 기존 응용 프로그램의 화면과 질의는 그대로 동작해야 하는데 이를 논리적 데이터 독립성이라 한다. 예를 들어 서점 데이터베이스에 review 테이블을 새로 추가해도 기존 주문 화면 프로그램은 고칠 필요가 없다.

3단계 스키마가 보는 대상
스키마보는 대상설명
외부 스키마개별 응용 프로그램·사용자회원 앱에는 주문 내역만, 관리자 화면에는 전체 데이터가 보이는 식으로 시야를 제한
개념 스키마조직 전체member, book, orders 등 모든 테이블과 그 관계를 담은 전체 논리 구조
내부 스키마DBMS·저장 장치테이블이 디스크에 파일과 인덱스로 저장되는 실제 방식

완성 코드

schema.sql

-- SQL 연구소 온라인 서점 스키마
CREATE TABLE member (
    id         INTEGER PRIMARY KEY,
    email      TEXT NOT NULL,
    name       TEXT NOT NULL,
    grade      TEXT NOT NULL,
    region     TEXT,
    joined_on  TEXT NOT NULL
);

CREATE TABLE author (
    id       INTEGER PRIMARY KEY,
    name     TEXT NOT NULL,
    country  TEXT
);

CREATE TABLE publisher (
    id    INTEGER PRIMARY KEY,
    name  TEXT NOT NULL
);

CREATE TABLE category (
    id         INTEGER PRIMARY KEY,
    name       TEXT NOT NULL,
    parent_id  INTEGER REFERENCES category(id)
);

CREATE TABLE book (
    id             INTEGER PRIMARY KEY,
    isbn           TEXT NOT NULL,
    title          TEXT NOT NULL,
    publisher_id   INTEGER REFERENCES publisher(id),
    category_id    INTEGER REFERENCES category(id),
    price          INTEGER NOT NULL,
    published_on   TEXT NOT NULL,
    pages          INTEGER
);

CREATE TABLE book_author (
    book_id    INTEGER REFERENCES book(id),
    author_id  INTEGER REFERENCES author(id),
    role       TEXT NOT NULL,
    PRIMARY KEY (book_id, author_id, role)
);

CREATE TABLE inventory (
    book_id     INTEGER PRIMARY KEY REFERENCES book(id),
    stock       INTEGER NOT NULL,
    updated_at  TEXT NOT NULL
);

CREATE TABLE coupon (
    id             INTEGER PRIMARY KEY,
    code           TEXT NOT NULL,
    discount_rate  REAL NOT NULL,
    valid_until    TEXT NOT NULL
);

CREATE TABLE orders (
    id          INTEGER PRIMARY KEY,
    member_id   INTEGER REFERENCES member(id),
    ordered_at  TEXT NOT NULL,
    status      TEXT NOT NULL,
    coupon_id   INTEGER REFERENCES coupon(id)
);

CREATE TABLE order_item (
    order_id    INTEGER REFERENCES orders(id),
    line_no     INTEGER NOT NULL,
    book_id     INTEGER REFERENCES book(id),
    qty         INTEGER NOT NULL,
    unit_price  INTEGER NOT NULL,
    PRIMARY KEY (order_id, line_no)
);

CREATE TABLE payment (
    id        INTEGER PRIMARY KEY,
    order_id  INTEGER REFERENCES orders(id),
    method    TEXT NOT NULL,
    amount    INTEGER NOT NULL,
    paid_at   TEXT NOT NULL
);

CREATE TABLE shipment (
    id            INTEGER PRIMARY KEY,
    order_id      INTEGER REFERENCES orders(id),
    carrier       TEXT NOT NULL,
    shipped_at    TEXT,
    delivered_at  TEXT
);

CREATE TABLE review (
    id          INTEGER PRIMARY KEY,
    member_id   INTEGER REFERENCES member(id),
    book_id     INTEGER REFERENCES book(id),
    rating      INTEGER NOT NULL,
    body        TEXT,
    created_at  TEXT NOT NULL
);

-- 표본 데이터
INSERT INTO member (id, email, name, grade, region, joined_on) VALUES
    (1, 'haneul@example.com', '김하늘', 'SILVER', '서울', '2024-03-02');

INSERT INTO publisher (id, name) VALUES (1, '한빛인쇄');

INSERT INTO category (id, name, parent_id) VALUES
    (1, '컴퓨터', NULL),
    (2, '데이터베이스', 1);

INSERT INTO author (id, name, country) VALUES (1, '이서연', '대한민국');

INSERT INTO book (id, isbn, title, publisher_id, category_id, price, published_on, pages) VALUES
    (1, '979-11-0000-001-1', '데이터베이스 개론', 1, 2, 28000, '2026-01-15', 356);

INSERT INTO book_author (book_id, author_id, role) VALUES (1, 1, 'AUTHOR');

INSERT INTO inventory (book_id, stock, updated_at) VALUES (1, 42, '2026-09-01 09:00:00');

INSERT INTO orders (id, member_id, ordered_at, status, coupon_id) VALUES
    (1, 1, '2026-09-01 10:15:00', 'PAID', NULL);

INSERT INTO order_item (order_id, line_no, book_id, qty, unit_price) VALUES
    (1, 1, 1, 1, 28000);

INSERT INTO payment (id, order_id, method, amount, paid_at) VALUES
    (1, 1, 'CARD', 28000, '2026-09-01 10:15:30');

줄별 해설

member, author, publisher처럼 성격이 다른 데이터는 서로 다른 테이블에 나눠 담았다. 각 테이블의 id 열은 PRIMARY KEY로 지정해 그 테이블 안에서 한 행을 유일하게 가리키는 값으로 삼았다.

book 테이블의 publisher_id와 category_id는 REFERENCES로 각각 publisher(id), category(id)를 가리킨다. 출판사 이름이나 분류 이름을 book에 직접 적지 않고 번호만 저장해서, 출판사 이름이 바뀌어도 publisher 테이블 한 행만 고치면 된다.

category 테이블의 parent_id는 자기 자신인 category(id)를 가리킨다. 상위 분류가 없는 "컴퓨터"는 parent_id를 NULL로 두고, 하위 분류인 "데이터베이스"는 parent_id에 1을 넣어 2단계 계층을 표현했다.

book_author와 order_item은 두 테이블을 이어 주는 역할만 한다. 책 한 권에 저자가 여럿일 수 있고 저자 한 명이 책을 여러 권 쓸 수 있으므로, 이 다대다 관계는 book이나 author 어느 한쪽에 욱여넣을 수 없다. 그래서 book_id, author_id, role 세 값을 묶어 PRIMARY KEY로 삼는 별도 테이블을 뒀다. order_item도 마찬가지로 order_id와 line_no를 묶어 한 주문 안의 각 줄을 구분한다.

member의 region, book의 pages, shipment의 shipped_at과 delivered_at은 NOT NULL을 붙이지 않았다. 가입할 때 지역을 안 밝힌 회원이 있을 수 있고, 페이지 수가 아직 확정되지 않은 도서가 있을 수 있고, 아직 발송하지 않은 주문은 배송일을 가질 수 없기 때문이다.

표본 데이터는 member, publisher, category, author, book 순서로 넣었다. book이 publisher와 category를 참조하므로, 참조당하는 테이블에 행이 먼저 있어야 참조하는 테이블에 행을 넣을 수 있다.

실행 결과

$ sqlite3 bookstore.db < schema.sql
$ sqlite3 bookstore.db ".tables"
author        coupon        member        payment
book          inventory     order_item    publisher
book_author   category      orders        review
shipment
$ sqlite3 bookstore.db "SELECT id, title, price FROM book;"
1|데이터베이스 개론|28000

실무에서 자주 틀리는 것

저자 이름을 book 테이블에 직접 적어 넣기

틀린 코드:

CREATE TABLE book (
    id INTEGER PRIMARY KEY,
    title TEXT NOT NULL,
    author_name TEXT NOT NULL,
    price INTEGER NOT NULL
);

저자 한 명이 책을 여러 권 쓰면 책마다 author_name에 이름을 다시 입력해야 한다. 어떤 책에는 "이서연", 다른 책에는 "이 서연"처럼 띄어쓰기가 달라지면 같은 사람인지 프로그램이 알 수 없다. 파일 시절에 겪은 중복·불일치 문제가 테이블 하나 안에서 그대로 재현되는 셈이다.

고친 코드:

CREATE TABLE author (
    id INTEGER PRIMARY KEY,
    name TEXT NOT NULL,
    country TEXT
);

CREATE TABLE book_author (
    book_id INTEGER REFERENCES book(id),
    author_id INTEGER REFERENCES author(id),
    role TEXT NOT NULL,
    PRIMARY KEY (book_id, author_id, role)
);

숫자 값을 문자열 열에 저장하기

틀린 코드:

CREATE TABLE book (
    id INTEGER PRIMARY KEY,
    title TEXT NOT NULL,
    price TEXT NOT NULL
);
INSERT INTO book (id, title, price) VALUES (2, '샘플 도서', '28,000원');

입력한 사람마다 "28000원", "28,000", "이만팔천원"처럼 형식이 제각각이라 가격순 정렬이나 합계 계산이 그대로 되지 않는다.

고친 코드:

CREATE TABLE book (
    id INTEGER PRIMARY KEY,
    title TEXT NOT NULL,
    price INTEGER NOT NULL
);
INSERT INTO book (id, title, price) VALUES (2, '샘플 도서', 28000);

값이 없다는 것을 빈 문자열로 표현하기

틀린 코드:

INSERT INTO member (id, email, name, grade, region, joined_on)
VALUES (2, 'jin@example.com', '박진', 'BASIC', '', '2026-05-10');

지역을 아직 밝히지 않은 회원과 지역이 빈 문자열이라는 값을 가진 회원을 구분할 수 없다. 나중에 지역별로 회원 수를 세면 빈 문자열도 하나의 지역처럼 집계돼 결과가 왜곡된다.

고친 코드:

INSERT INTO member (id, email, name, grade, region, joined_on)
VALUES (2, 'jin@example.com', '박진', 'BASIC', NULL, '2026-05-10');

한눈에 보기

이 장에서 다룬 개념 정리
개념핵심 내용
파일 처리 방식의 한계같은 데이터가 여러 파일에 중복되고, 값이 어긋나고, 동시 저장이 서로를 덮어쓴다
데이터베이스여러 응용 프로그램이 공유할 목적으로 통합해 저장한 데이터의 집합
DBMS데이터베이스를 만들고 관리하는 소프트웨어. 요청 처리·값 검증·동시 접근 조율을 맡는다
3단계 스키마외부(응용별 시야)·개념(전체 논리 구조)·내부(실제 저장 방식)로 나눠 관리
데이터 독립성한 층의 변화가 다른 층에 번지지 않는 성질. 논리적/물리적 독립성으로 나뉜다

연습 문제

  1. SQL 연구소가 파일로 회원 등급을 관리하다가 겪은 불일치 사례를 하나 들고, 이를 데이터베이스로 옮기면 어떻게 해결되는지 서술하라.
  2. 저자와 책이 다대다 관계일 때 book_author 같은 별도 테이블이 필요한 이유를 설명하라.
  3. 회원이 찜한 책을 기록하는 wishlist 테이블을 CREATE TABLE 문으로 작성하라. 같은 회원이 같은 책을 두 번 찜할 수 없어야 하고, 찜한 날짜를 남겨야 하며, member와 book을 참조해야 한다.
  4. "회원 앱 화면에는 결제 수단 정보가 보이지 않는다"는 상황은 3단계 스키마 중 어느 단계에서 결정되는지 설명하라.

정답과 해설

1. 문제 상황에서 다룬 것처럼, 영업팀 파일과 배송팀 파일에 같은 회원 김하늘의 등급이 SILVER와 BASIC으로 다르게 적혀 있던 사례를 들 수 있다. 데이터베이스로 옮기면 등급 값을 member 테이블 한 곳에만 저장하고 모든 팀의 프로그램이 그 값을 함께 읽으므로, 값이 하나뿐이라 애초에 어긋날 여지가 없다.

2. 책 한 권에 저자가 여럿일 수 있고 저자 한 명이 여러 책을 쓸 수 있는 다대다 관계는 book이나 author 어느 한쪽 테이블에 상대편 정보를 직접 넣는 방식으로 표현할 수 없다. book_author처럼 양쪽의 키를 함께 가진 테이블을 따로 둬야 책 하나에 저자 여러 명을, 저자 한 명에 책 여러 권을 각각 행 하나씩으로 표현할 수 있다.

3. 다음과 같이 작성한다.

CREATE TABLE wishlist (
    member_id  INTEGER REFERENCES member(id),
    book_id    INTEGER REFERENCES book(id),
    added_on   TEXT NOT NULL,
    PRIMARY KEY (member_id, book_id)
);

member_id와 book_id를 묶어 PRIMARY KEY로 지정했으므로 같은 회원·같은 책 조합은 테이블에 한 행만 존재할 수 있어, 같은 책을 두 번 찜하는 입력 자체가 막힌다.

4. 외부 스키마 단계다. 개념 스키마에는 payment 테이블의 결제 수단 정보가 그대로 존재하지만, 회원 앱이라는 외부 스키마는 회원이 볼 필요가 있는 항목만 골라 보여 주도록 시야를 제한할 수 있다. 개념 스키마의 데이터를 바꾸지 않고도 외부 스키마 수준에서 어떤 항목을 드러낼지 정할 수 있다는 점이 이 예시가 보여 주는 것이다.

댓글 0

아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.

댓글을 남기려면 로그인이 필요합니다.