파일 저장과 DBMS
중복 파일이 만드는 불일치·동시성·복구 문제를 재현하고 회원 게시판의 단일 데이터 원천을 시작합니다.
데이터는 관찰한 사실이고 정보는 그 사실을 질문에 맞게 해석한 결과입니다.
개인 메모 파일 하나는 시작하기 쉽지만 같은 회원 정보를 여러 CSV에 복사하는 순간 어느 값이 최신인지 결정해야 합니다.
여러 사용자가 동시에 수정하고 장애 뒤 복구해야 하는 서비스라면 파일 이름 규칙만으로는 데이터 규칙을 지키기 어렵습니다.
전체 교재는 board_lab 하나를 누적합니다.
첫 문서에서는 회원 한 명을 식별하는 members를 만들고 이메일과 표시 이름을 한곳에서 관리합니다.
이후 게시글·태그·통계는 이 원천을 참조하며 장마다 무관한 예제 스키마로 다시 시작하지 않습니다.
관계형 데이터의 가장 작은 단위
관계형 데이터베이스는 값을 표처럼 조직하지만, 용어마다 책임이 있습니다.
데이터베이스는 함께 관리할 테이블의 이름 공간이고, 테이블은 같은 종류의 사실을 모은 집합입니다.
가로 한 줄인 행은 한 회원이나 한 게시글을 뜻하고, 세로 항목인 열은 이메일·제목처럼 모든 행이 공유하는 값의 의미와 타입을 정합니다.
칸 하나에 실제로 저장된 min@example.com이나 30이 값입니다.
| id | name | |
|---|---|---|
| 1 | min@example.com | 안동민 |
| 2 | hana@example.com | 이하나 |
| id | author_id | title | published_at |
|---|---|---|---|
| 101 | 1 | SQL 기초 | 2026-07-01 10:30:00 |
| 102 | 1 | NULL 복습 | NULL |
members.id는 각 회원 행을 하나로 구별하는 기본 키(Primary Key, PK)입니다.
posts.author_id는 회원 테이블의 PK를 가리키는 외래 키(Foreign Key, FK)입니다.
따라서 게시글 101과 102가 같은 값 1을 가져도 중복 회원이 아니라 ‘두 게시글 모두 회원 1이 작성했다’는 관계가 됩니다.
published_at의 NULL은 0이나 빈 문자열이 아니라 아직 게시되지 않았다는 값의 부재입니다.
SQL(Structured Query Language)은 이 구조를 만들고 데이터를 넣고 질문하는 언어입니다.
CREATE는 데이터베이스나 테이블 구조를 만들고, INSERT는 행을 추가하며, SELECT는 저장된 행과 열을 읽습니다.
처음에는 구문을 모두 외우기보다 ‘구조를 만드는가, 행을 바꾸는가, 결과를 읽는가’를 구분합니다.
파일 분산 저장의 한계
운영팀과 리포트팀이 같은 이메일을 각자 표에 복사하고 한쪽만 이름을 수정합니다.
두 행 모두 문법적으로 유효해 파일이나 테이블 자체는 오류를 내지 않습니다.
어떤 표시 이름이 맞는지 정할 버전·소유권·제약이 없다는 것이 오류입니다.
CREATE DATABASE IF NOT EXISTS board_lab
CHARACTER SET utf8mb4
COLLATE utf8mb4_0900_ai_ci;
USE board_lab;
CREATE TEMPORARY TABLE team_a_member (
email VARCHAR(255),
name VARCHAR(80)
);
CREATE TEMPORARY TABLE team_b_member LIKE team_a_member;
INSERT INTO team_a_member VALUES ('min@example.com', '안동민');
INSERT INTO team_b_member VALUES ('min@example.com', '동민');
SELECT 'team_a' AS source_name, email, name FROM team_a_member
UNION ALL
SELECT 'team_b', email, name FROM team_b_member;동일 이메일에 두 이름이 나오지만 어느 행도 스스로 오래됐다고 말하지 않습니다.
복사본을 더 만들수록 보정할 위치도 늘어납니다.
예상 오류source_name | email | name
team_a | min@example.com | 안동민
team_b | min@example.com | 동민중복 자체가 항상 나쁜 것은 아닙니다.
백업·읽기 복제·캐시는 책임자와 동기화 규칙이 있습니다.
문제는 두 복사본이 모두 원본이라고 주장하는 상태입니다.
데이터 소유 테이블을 하나로 정하고 다른 결과는 키로 참조하거나 재생성할 수 있어야 합니다.
동시 수정, 권한, 감사, 복구 역시 같은 원천을 중심으로 설계합니다.
DBMS가 제공하는 기능
DBMS는 SQL 실행기만이 아닙니다.
스키마와 제약으로 허용 값의 모양을 정하고, 트랜잭션과 잠금으로 동시 변경의 경계를 만들며, 계정·권한으로 접근을 제한하고, 로그·백업으로 장애 복구 근거를 남깁니다.
애플리케이션 검증도 필요하지만 모든 쓰기 경로가 공유해야 하는 핵심 무결성은 DB 제약이 마지막 방어선이 됩니다.
이 교재는 MySQL 8.4와 InnoDB를 기준으로 하지만 테이블·행·열·기본 키·외래 키라는 관계형 개념은 다른 RDBMS에도 이어집니다.
제품 선택은 유행이나 한 문장의 우열이 아니라 일관성 요구, 쿼리 형태, 운영 역량, 비용, 생태계, 복구 목표를 함께 평가합니다.
NoSQL도 데이터 모델과 제약이 없는 저장소가 아니라 다른 절충안을 선택한 제품군입니다.
단일 데이터 원천 구성
UTF-8 전체 문자를 다루는 데이터베이스를 만들고 이메일을 유일한 업무 식별 후보로 둡니다.
내부 참조에는 변경 가능한 이메일 대신 숫자 PK를 사용합니다.
이 표가 회원 현재 정보의 소유자가 됩니다.
CREATE DATABASE IF NOT EXISTS board_lab
CHARACTER SET utf8mb4
COLLATE utf8mb4_0900_ai_ci;
USE board_lab;
CREATE TABLE members (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
email VARCHAR(255) NOT NULL,
password_hash VARCHAR(255) NOT NULL,
name VARCHAR(60) NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE',
created_at DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
PRIMARY KEY (id),
UNIQUE KEY uq_members_email (email)
) ENGINE = InnoDB;
INSERT INTO members (email, password_hash, name, created_at)
VALUES ('min@example.com', '{demo}hash-min', '안동민', '2026-07-01 09:00:00');
SELECT id, email, name, status, created_at
FROM members
WHERE email = 'min@example.com';같은 이메일의 현재 이름은 회원 한 행에서 읽고 다른 테이블은 id를 참조할 준비가 됩니다.
id | email | name | status | created_at
1 | min@example.com | 안동민 | ACTIVE | 2026-07-01 09:00:00.000000UNIQUE 이메일이 사람이 영원히 바뀌지 않는다는 뜻은 아닙니다.
로그인 식별 정책과 정규화 정책이 확정돼야 하며 대소문자 비교는 정렬 규칙에 영향을 받습니다.
내부 PK를 별도로 두면 이메일 변경이 모든 자식 FK를 흔들지 않습니다.
개인정보의 변경 이력이 필요하면 현재 테이블에 열을 계속 복사하지 말고 감사 요구에 맞는 이력 구조를 뒤에서 설계합니다.
실행 결과와 제약 확인
테이블이 보인다는 사실뿐 아니라 문자 집합, 정렬 규칙, 엔진, 키를 메타데이터에서 확인합니다.
같은 이메일 삽입이 실제로 거절되는지도 별도 트랜잭션에서 관찰합니다.
SELECT
table_schema,
table_name,
engine,
table_collation
FROM information_schema.tables
WHERE table_schema = 'board_lab'
AND table_name = 'members';
SELECT
index_name,
non_unique,
GROUP_CONCAT(column_name ORDER BY seq_in_index) AS columns_in_key
FROM information_schema.statistics
WHERE table_schema = 'board_lab'
AND table_name = 'members'
GROUP BY index_name, non_unique
ORDER BY index_name;엔진은 InnoDB, 정렬 규칙은 utf8mb4_0900_ai_ci이고 PRIMARY·uq_members_email 두 유일 키가 보여야 합니다.
GUI 화면 대신 SQL 결과를 남기면 다른 환경에서도 같은 규칙을 비교할 수 있습니다.
파일도 잠금·버전·암호화를 직접 구현할 수 있고 DBMS도 잘못 사용하면 불일치가 생깁니다.
차이는 공통 문제를 검증된 기능과 선언적 규칙으로 제공하느냐에 있습니다.
DBMS를 쓴다고 자동으로 좋은 모델이 생기지 않으므로 한 행의 정체성, 단일 원본, 변경 경계, 복구 목표를 먼저 정합니다.
스프레드시트는 작은 단일 사용자 업무에 적합할 수 있고 서버 DB는 운영 비용을 동반합니다.
같은 이메일로 두 번째 INSERT를 실행해 오류 1062를 관찰하고, 다른 이메일에 같은 name은 허용되는지 확인합니다.
이름이 식별자가 아닌 이유를 결과로 설명하세요.
임시 복사본의 한쪽만 UPDATE한 뒤 두 결과를 다시 비교하면 중앙 원천이 없을 때 보정 쿼리가 원본마다 필요함을 볼 수 있습니다.
DB를 지우고 다시 만들기 전에는 실제 환경인지 확인하는 습관을 들입니다.
운영에서는 루트 계정으로 애플리케이션을 연결하지 않고 필요한 스키마·명령만 허용한 계정을 사용합니다.
백업은 존재 여부가 아니라 복구 리허설로 검증하고, RPO는 잃을 수 있는 데이터 시간, RTO는 복구까지 걸리는 시간을 뜻합니다.
개인정보 내보내기·삭제 요구도 현재 테이블, 캐시, 백업, 감사의 수명주기를 함께 봐야 합니다.
이번 로컬 학습 DB는 개념을 재현하는 환경이며 운영 복구 설계를 대체하지 않습니다.
이메일 앞뒤 공백과 Unicode 정규화, 대소문자 정책은 입력 단계와 저장 제약에서 일치해야 합니다.
utf8mb4를 썼다고 모든 비교가 바이트 단위가 되는 것은 아니며 정렬 규칙이 동등성과 정렬을 정합니다.
CREATE DATABASE IF NOT EXISTS는 기존 스키마가 원하는 문자 집합이라는 보장이 아니므로 메타데이터를 반드시 확인합니다.
동일 이름의 다른 스키마에 접속하지 않도록 SELECT DATABASE()도 실행 기록에 남깁니다.
회원은 이후 모든 회원 게시판 문서가 공유하는 첫 원천입니다.
ch1-3에서 게시글이 author_id를 FK로 참조하고 ch5에서는 댓글의 답글 관계를 자기 참조로 확장합니다.
ch10 이후 이력·삭제 정책을 설계할 때도 현재 행과 과거 사실의 소유권을 분리합니다.
지금 정한 PK·이메일 유일 제약·created_at의 뜻이 뒤에서 바뀌지 않도록 스키마 결정 로그에 이유를 남깁니다.
DBMS 선택 기준
| 판단 축 | 확인할 질문 |
|---|---|
| 소유권 | 같은 사실의 단일 원본이 하나로 정해졌는가? |
| 동시성 | 여러 쓰기 주체가 같은 값을 바꿀 수 있는가? |
| 무결성 | 모든 쓰기 경로가 공유할 규칙이 있는가? |
| 복구 | 장애 뒤 어느 시점까지 어떤 절차로 복원하는가? |
| 비용 | DBMS 운영 비용이 데이터 위험과 규모에 맞는가? |
작은 파일을 무조건 DB로 옮기지 않습니다.
복사본·동시 쓰기 주체·세밀한 쿼리·무결성·복구 요구가 생기는 지점을 표시하고, 그 요구를 감당할 한 원천과 운영 책임을 선택합니다.
연습 문제
hana@example.com 회원을 추가한 뒤 같은 이메일로 다른 이름을 다시 삽입해 보세요.
오류 번호와 키 이름을 기록하고, name만 같은 다른 이메일은 왜 허용되는지 식별자 관점에서 설명하세요.
해설과 예시 답안
첫 행은 정상 삽입되고 두 번째는 uq_members_email을 위반합니다.
이름은 동명이인이 가능하므로 UNIQUE가 아닙니다.
INSERT INTO members (email, password_hash, name, created_at)
VALUES ('hana@example.com', '{demo}hash-hana', '이하나', '2026-07-02 10:00:00');
INSERT INTO members (email, password_hash, name, created_at)
VALUES ('hana@example.com', '{demo}another-hash', '하나', '2026-07-03 10:00:00');
-- 동명이인은 다른 email이라 허용된다.
INSERT INTO members (email, password_hash, name, created_at)
VALUES ('jun@example.com', '{demo}hash-jun', '이하나', '2026-07-03 11:00:00');두 번째 문장만 1062로 오류가 발생하고 첫째·셋째 행이 남는지 확인합니다.
실습 클라이언트가 오류 뒤 나머지 문장을 계속 실행했는지도 따로 기록합니다.
핵심 정리
- 데이터 복사본의 문제는 중복보다 단일 원본과 동기화 규칙의 부재입니다.
- DBMS는 스키마·동시성·권한·복구 경계를 제공하지만 좋은 모델을 자동 생성하지 않습니다.
- 내부 참조용 PK와 변경 가능한 업무 식별 후보를 구분합니다.
- 메타데이터와 실제 제약 오류를 실행 증거로 남깁니다.
다음 문서에서는 회원과 게시글을 PK·FK 관계로 분리하고 관계가 실제 제약으로 지켜지는지 확인합니다.