파일 저장과 DBMS
중복 파일이 만드는 불일치·동시성·복구 문제를 재현하고 회원 게시판의 단일 데이터 원천을 시작합니다.
데이터는 관찰한 사실이고 정보는 그 사실을 질문에 맞게 해석한 결과입니다.
개인 메모 파일 하나는 시작하기 쉽지만 같은 회원 정보를 여러 CSV에 복사하는 순간 어느 값이 최신인지 결정해야 합니다.
여러 사용자가 동시에 수정하고 장애 뒤 복구해야 하는 서비스라면 파일 이름 규칙만으로는 데이터 규칙을 지키기 어렵습니다.
전체 교재는 board_lab 하나를 누적합니다.
첫 문서에서는 회원 한 명을 식별하는 members를 만들고 이메일과 표시 이름을 한곳에서 관리합니다.
이후 게시글·태그·통계는 이 원천을 참조하며 장마다 무관한 예제 스키마로 다시 시작하지 않습니다.
관계형 데이터의 가장 작은 단위
관계형 데이터베이스는 값을 표처럼 조직하지만, 용어마다 책임이 있습니다.
데이터베이스는 함께 관리할 테이블의 이름 공간이고, 테이블은 같은 종류의 사실을 모은 집합입니다.
가로 한 줄인 행은 한 회원이나 한 게시글을 뜻하고, 세로 항목인 열은 이메일·제목처럼 모든 행이 공유하는 값의 의미와 타입을 정합니다.
칸 하나에 실제로 저장된 [email protected]이나 30이 값입니다.
| id | name | |
|---|---|---|
| 1 | [email protected] | 안동민 |
| 2 | [email protected] | 이하나 |
게시글의 예시 값은 다음과 같습니다.
- 게시글
id=101:author_id=1, 제목SQL 기초,published_at=2026-07-01 10:30:00 - 게시글
id=102:author_id=1, 제목NULL 복습,published_at=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는 저장된 행과 열을 읽습니다.
처음에는 구문을 모두 외우기보다 ‘구조를 만드는가, 행을 바꾸는가, 결과를 읽는가’를 구분합니다.
파일 분산 저장의 한계
처음 실습한다면 MySQL 8.4 실행 환경에서 서버와 연결을 준비한 뒤 이 문서의 SQL을 실행합니다. 아래 결과는 각 예제의 전제를 만족할 때 기대하는 모양이며, 저장된 행과 실행 순서에 따라 ID와 건수는 달라질 수 있습니다.
운영팀과 리포트팀이 같은 이메일을 각자 표에 복사하고 한쪽만 이름을 수정합니다.
두 행 모두 문법적으로 유효해 파일이나 테이블 자체는 오류를 내지 않습니다.
어떤 표시 이름이 맞는지 정할 버전·소유권·제약이 없다는 것이 오류입니다.
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 ('[email protected]', '안동민');
INSERT INTO team_b_member VALUES ('[email protected]', '동민');
SELECT 'team_a' AS source_name, email, name FROM team_a_member
UNION ALL
SELECT 'team_b', email, name FROM team_b_member;동일 이메일에 두 이름이 나오지만 어느 행도 스스로 오래됐다고 말하지 않습니다. 이 조회에는 ORDER BY가 없으므로 출력 행 순서는 보장하지 않습니다.
복사본을 더 만들수록 보정할 위치도 늘어납니다.
예상 오류source_name | email | name
team_a | [email protected] | 안동민
team_b | [email protected] | 동민중복 자체가 항상 나쁜 것은 아닙니다.
백업·읽기 복제·캐시는 책임자와 동기화 규칙이 있습니다.
문제는 두 복사본이 모두 원본이라고 주장하는 상태입니다.
데이터 소유 테이블을 하나로 정하고 다른 결과는 키로 참조하거나 재생성할 수 있어야 합니다.
DATA OWNERSHIP · ONE CURRENT FACT
같은 회원의 현재값을 두 복사본이 주장하면 충돌한다
두 팀의 값은 각각 유효해도 함께 읽으면 이름이 다릅니다. 현재 회원 정보의 소유자를 members 한 행으로 정하고 다른 소비자는 참조하거나 다시 만듭니다.
점선은 같은 이메일의 복사본을 비교하는 관계, 실선은 단일 원천의 현재 이름을 읽는 관계입니다. 아래는 소비 방식을 바꾸는 설계 예이며 위의 임시 복사 테이블이 자동 동기화된다는 뜻은 아닙니다. 이름 변경 책임은 members의 해당 행에 있고 내부 참조는 숫자 PK를 사용합니다.
백업·캐시·읽기 복제는 소유자와 동기화 규칙이 있으면 유용합니다. DBMS 사용 자체가 복사본의 최신성이나 좋은 모델을 자동 보장하지는 않습니다.
동시 수정, 권한, 감사, 복구 역시 같은 원천을 중심으로 설계합니다.
DBMS가 제공하는 기능
DBMS는 SQL 실행기만이 아닙니다.
스키마와 제약으로 허용 값의 모양을 정하고, 트랜잭션과 잠금으로 동시 변경의 경계를 만들며, 계정·권한으로 접근을 제한하고, 로그·백업으로 장애 복구 근거를 남깁니다.
애플리케이션 검증도 필요하지만 모든 쓰기 경로가 공유해야 하는 핵심 무결성을 지키는 데에는 DB 제약이 마지막 방어선이 됩니다.
이 교재는 MySQL 8.4와 InnoDB를 기준으로 하지만 테이블·행·열·기본 키·외래 키라는 관계형 개념은 다른 RDBMS에도 이어집니다.
제품 선택은 유행이나 한 문장의 우열이 아니라 일관성 요구, 쿼리 형태, 운영 역량, 비용, 생태계, 복구 목표를 함께 평가합니다.
NoSQL도 데이터 모델과 제약이 없는 저장소가 아니라 다른 절충안을 선택한 제품군입니다.
단일 데이터 원천 구성
UTF-8 전체 문자를 다루는 데이터베이스를 만들고 이메일을 유일한 업무 식별 후보로 둡니다.
내부 참조에는 변경 가능한 이메일 대신 숫자 PK를 사용합니다.
이 표가 회원 현재 정보의 소유자가 됩니다.
첫 DDL을 읽는 순서아래 CREATE TABLE의 괄호 안은 열 이름 → 타입 → 추가 규칙 순서입니다. BIGINT UNSIGNED는 음수 없는 정수, VARCHAR(60)은 최대 60문자 문자열이고, NOT NULL은 값 부재를 허용하지 않습니다. DEFAULT는 열을 생략했을 때 사용할 값입니다. AUTO_INCREMENT는 번호를 서버가 생성하게 하고, 마지막의 PRIMARY KEY와 UNIQUE KEY는 각각 행 식별과 이메일 중복 금지를 선언합니다.
INSERT는 열 목록에 적힌 값만 넣습니다. 따라서 아래 입력의 네 값은 이메일·암호 해시·이름·생성 시각에 순서대로 대응하고, 생략한 id는 생성되며 status는 ACTIVE가 됩니다. {demo}hash-min은 암호 해시 저장 위치를 보여 주는 가짜 값이지 실제 암호를 처리하는 코드가 아닙니다.
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 ('[email protected]', '{demo}hash-min', '안동민', '2026-07-01 09:00:00');
SELECT id, email, name, status, created_at
FROM members
WHERE email = '[email protected]';같은 이메일의 현재 이름은 회원 한 행에서 읽고 다른 테이블은 id를 참조할 준비가 됩니다.
id | email | name | status | created_at
1 | [email protected] | 안동민 | ACTIVE | 2026-07-01 09:00:00.000000이메일의 UNIQUE 제약이 한 사람의 이메일이 영원히 바뀌지 않는다는 뜻은 아닙니다.
로그인 식별 정책과 정규화 정책이 확정돼야 하며 대소문자 비교는 정렬 규칙에 영향을 받습니다.
내부 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로 옮기지 않습니다.
복사본·동시 쓰기 주체·세밀한 쿼리·무결성·복구 요구가 생기는 지점을 표시하고, 그 요구를 감당할 한 원천과 운영 책임을 선택합니다.
데이터·스키마·인스턴스를 연결하기
데이터는 관찰·기록한 사실이고 정보는 목적에 맞게 해석한 결과입니다. 회원별 게시글 세 행은 데이터이며 “이번 주 활성 회원 수”는 집계 기준을 적용한 정보입니다. DB가 값을 저장해 준다는 것과 업무상 의미를 올바르게 정했다는 것은 다릅니다.
members(id,email,name) 같은 구조·제약의 정의를 스키마, 오늘 실제로 저장된 회원 행들을 인스턴스라 합니다. 인스턴스는 INSERT/UPDATE/DELETE로 변하지만 스키마 변경은 열·타입·제약의 설계 변경입니다. 관계 모델의 형식 용어와 집합/SQL 차이는 관계 모델에서 작은 결과로 비교합니다.
3단계 스키마는 사용자별 필요한 모습을 외부 단계, 전체 논리 구조를 개념 단계, 파일·인덱스 등 저장 구조를 내부 단계로 설명하는 추상 모델입니다. MySQL의 스키마가 데이터베이스 이름을 뜻하는 용법이나 SQL VIEW 하나와 세 단계 전체를 동일시하지 않습니다.
예를 들어 운영자는 이메일을 포함한 회원 목록을, 공개 화면은 이름만 봅니다. 전체 논리 회원 구조는 하나이고 내부 인덱스를 바꿔도 공개 목록의 의미가 유지되도록 합니다. 내부 변경과 논리 모델을 분리하는 성질이 물리적 데이터 독립성입니다. 전체 논리 구조 변경 뒤에도 사용자별 뷰/응용 계약을 유지하는 목표가 논리적 데이터 독립성입니다. 모든 스키마 변경이 자동으로 응용 프로그램과 호환된다는 보장은 아닙니다.
데이터 사전은 테이블·열·제약 등의 메타데이터입니다. INFORMATION_SCHEMA는 이를 조회하는 인터페이스이며 사용자 테이블의 업무 행과 구분합니다. DBMS는 SQL의 해석·계획과 실행, 페이지/인덱스 저장, 트랜잭션/권한 등을 함께 조정합니다. 실행 계획을 읽는 것과 저장 파일을 직접 수정하는 것은 다른 작업입니다.
DBA는 접근·구성·백업/회복 등 관리 책임을, 응용 개발자는 업무 규칙을 SQL과 프로그램으로 연결하는 책임을, 최종 사용자는 허용된 서비스로 조회·입력하는 역할을 맡습니다. 한 사람이 여러 역할을 수행하더라도 서비스 계정에 관리 권한을 모두 주지 않습니다.
확인 문제: 회원 이름 조회의 열을 유지한 채 내부 인덱스만 바꾸는 사례와, 이메일 열을 삭제해 응용 조회가 깨지는 사례는 같은 독립성 성공인가요?
힌트와 풀이
외부 계약이 실제로 유지되는지를 보세요. 첫 사례는 물리 구조 변경에도 논리 조회를 유지한 예입니다. 둘째는 필요한 논리 열을 없애 계약이 깨졌으므로 논리적 데이터 독립성이 자동 확보된 사례가 아닙니다.
연습 문제
[email protected] 회원을 추가한 뒤 같은 이메일로 다른 이름을 다시 삽입해 보세요.
오류 번호와 키 이름을 기록하고, name만 같은 다른 이메일은 왜 허용되는지 식별자 관점에서 설명하세요.
이 연습은 SELECT @@session.autocommit이 1이고 별도의 트랜잭션을 시작하지 않은 연결에서 문장을 한 개씩 실행하는 전제입니다. 성공한 첫 INSERT는 이미 커밋되므로 다음 중복 오류가 첫 행까지 지우지는 않습니다. 오류 뒤 세 번째 문장을 실제로 실행했는지와 그 문장의 결과는 따로 확인합니다. 자동 커밋을 끈 상태에서 여러 문장을 한 단위로 묶는 실습은 트랜잭션과 멱등성에서 다룹니다. MySQL 8.4의 자동 커밋 설명은 각 문장 단위와 명시적 트랜잭션을 구별합니다.
해설과 예시 답안
첫 행은 정상 삽입되고 두 번째는 uq_members_email을 위반합니다.
이름은 동명이인이 가능하므로 UNIQUE가 아닙니다.
INSERT INTO members (email, password_hash, name, created_at)
VALUES ('[email protected]', '{demo}hash-hana', '이하나', '2026-07-02 10:00:00');
INSERT INTO members (email, password_hash, name, created_at)
VALUES ('[email protected]', '{demo}another-hash', '하나', '2026-07-03 10:00:00');
-- 동명이인은 다른 email이라 허용된다.
INSERT INTO members (email, password_hash, name, created_at)
VALUES ('[email protected]', '{demo}hash-jun', '이하나', '2026-07-03 11:00:00');두 번째 문장만 1062로 오류가 발생하고 첫째·셋째 행이 남는지 확인합니다.
실습 클라이언트가 오류 뒤 나머지 문장을 계속 실행했는지도 따로 기록합니다.
핵심 정리
- 데이터 복사본의 문제는 중복보다 단일 원본과 동기화 규칙의 부재입니다.
- DBMS는 스키마·동시성·권한·복구 경계를 제공하지만 좋은 모델을 자동 생성하지 않습니다.
- 내부 참조용 PK와 변경 가능한 업무 식별 후보를 구분합니다.
- 메타데이터와 실제 제약 오류를 실행 증거로 남깁니다.
다음 문서는 MySQL 실행 환경을 확인합니다. members까지 준비했다면 이어지는 PK·FK 문서에서 회원과 게시글의 관계를 만듭니다.