본문 바로가기

파일 저장과 DBMS

같은 회원 정보를 여러 파일에 복사할 때 생기는 불일치를 SQL로 재현하고, DBMS가 제약과 트랜잭션, 권한과 복구로 대신 해 주는 일을 정리합니다. 이 교재 전체가 함께 쓰는 board_lab의 첫 테이블 members를 만들어 메타데이터로 확인하고, 데이터와 정보, 스키마와 인스턴스, 데이터 독립성의 뜻을 구별합니다.

데이터는 기록해 둔 사실이고, 정보는 그 사실을 어떤 질문에 맞게 해석한 결과입니다. 개인 메모 파일 하나로 시작하는 것은 쉽지만, 같은 회원 정보를 여러 CSV 파일에 복사하는 순간 어느 파일의 값이 최신인지 정해야 하는 문제가 생깁니다. 여러 사람이 동시에 고치고 장애가 나면 복구해야 하는 서비스라면, 파일 이름을 잘 짓는 규칙만으로는 데이터의 규칙을 지킬 수 없습니다.

이 교재는 회원 게시판 데이터베이스 board_lab 하나를 처음부터 끝까지 조금씩 키워 갑니다. 이 글에서는 회원 한 명을 식별하는 members 테이블을 만들어 이메일과 표시 이름을 한곳에서 관리하기 시작하고, 뒤의 게시글과 태그, 통계는 모두 이 테이블을 참조합니다. 장마다 서로 상관없는 예제 스키마로 다시 시작하지 않으므로, 앞에서 만든 테이블과 데이터가 뒤의 예제의 전제가 됩니다.

표로 보는 관계형 데이터의 단위

관계형 데이터베이스는 값을 표처럼 조직합니다. 데이터베이스(database)는 함께 관리할 테이블들의 이름 공간이고, 테이블(table)은 같은 종류의 사실을 모은 집합입니다. 가로 한 줄인 행(row)은 회원 한 명이나 게시글 하나를 뜻하고, 세로 항목인 열(column)은 이메일이나 제목처럼 모든 행이 함께 갖는 값의 의미와 타입을 정합니다. 칸 하나에 실제로 저장된 [email protected]이나 30이 값(value)입니다.

idemailname
1[email protected]안동민
2[email protected]이하나

게시글 테이블에는 다음과 같은 행이 있다고 해 봅시다. 게시글 101은 author_id=1, 제목 SQL 기초, published_at=2026-07-01 10:30:00이고, 게시글 102는 author_id=1, 제목 NULL 복습, published_at=NULL입니다.

members.id는 회원 행 하나하나를 구별하는 기본 키(primary key, PK)이고, posts.author_id는 회원 테이블의 기본 키를 가리키는 외래 키(foreign key, FK)입니다. 그래서 게시글 101과 102가 모두 1을 가져도 중복된 회원이 생긴 것이 아니라, 두 게시글을 모두 회원 1이 썼다는 관계를 나타냅니다. published_at의 NULL은 0이나 빈 문자열이 아니라, 아직 게시되지 않아 값이 없다는 뜻입니다.

SQL(structured query language)은 이 구조를 만들고, 데이터를 넣고, 질문하는 언어입니다. CREATE는 데이터베이스나 테이블의 구조를 만들고, INSERT는 행을 더하며, SELECT는 저장된 행과 열을 읽습니다. 처음에는 문법을 모두 외우기보다, 지금 쓰는 문장이 구조를 만드는지, 행을 바꾸는지, 결과를 읽는지를 구별하는 것부터 익힙니다.

복사본이 둘이면 어느 쪽이 맞는가

운영팀과 통계팀이 같은 회원의 이메일을 각자의 표에 복사해 두었는데, 한쪽만 이름을 고쳤다고 해 봅시다. 처음 실습한다면 MySQL 8.4 실행 환경에서 서버와 연결을 준비한 뒤 다음 SQL을 실행합니다. 임시 테이블(temporary table)은 연결이 끝나면 사라지므로 실습 데이터를 남기지 않습니다.

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;
source_name | email           | name
team_a      | [email protected] | 안동민
team_b      | [email protected] | 동민

같은 이메일에 두 이름이 나오지만, 어느 행도 스스로 자기가 오래된 값이라고 말해 주지 않습니다. 두 행 모두 문법적으로 올바르므로 데이터베이스는 아무 오류도 내지 않습니다. 문제는 어느 이름이 맞는지 정할 기준, 곧 버전이나 소유권, 제약이 없다는 데 있습니다. 이 조회에는 ORDER BY가 없으므로 두 행이 나오는 순서도 보장되지 않습니다.

DATA OWNERSHIP · ONE CURRENT FACT

같은 회원의 현재값을 두 복사본이 주장하면 충돌한다

두 팀의 값은 각각 유효해도 함께 읽으면 이름이 다릅니다. 현재 회원 정보의 소유자를 members 한 행으로 정하고 다른 소비자는 참조하거나 다시 만듭니다.

충돌하는 복사본과 단일 원천에서 읽는 두 소비자 같은 이메일에 다른 이름을 가진 두 복사본은 최신값 근거가 없으며, 현재 회원 정보의 소유자를 members 한 행으로 정하면 두 팀은 그 행에서 이름을 조회합니다. 비교 비교 조회 조회 분산 복사본: 같은 사실의 현재값이 충돌 team_a 복사본[email protected]이름: 안동민 team_b 복사본[email protected]이름: 동민 최신값 근거 없음같은 email · 다른 name 단일 원천: 한 행에서 현재 정보를 읽기 members현재 정보의 소유자내부 식별자 id = 1 · PK업무 후보 키 email · UNIQUE[email protected]현재 표시 이름: 안동민 team_a 조회현재 이름: 안동민team_b 조회현재 이름: 안동민
충돌하는 복사본과 단일 원천에서 읽는 두 소비자 같은 이메일에 다른 이름을 가진 두 복사본은 최신값 근거가 없으며, 현재 회원 정보의 소유자를 members 한 행으로 정하면 두 팀은 그 행에서 이름을 조회합니다. 조회 조회 분산 복사본 team_a 복사본[email protected]이름: 안동민 team_b 복사본[email protected]이름: 동민 같은 email로두 복사본 비교 최신값 근거 없음같은 email다른 name 단일 원천에서현재 정보를 읽기 members현재 정보의 소유자id = 1 · PK업무 후보 키 emailUNIQUE[email protected]현재 이름: 안동민 team_a 조회현재 이름: 안동민team_b 조회현재 이름: 안동민

점선은 같은 이메일의 복사본을 비교하는 관계, 실선은 단일 원천의 현재 이름을 읽는 관계입니다. 아래는 소비 방식을 바꾸는 설계 예이며 위의 임시 복사 테이블이 자동 동기화된다는 뜻은 아닙니다. 이름 변경 책임은 members의 해당 행에 있고 내부 참조는 숫자 PK를 사용합니다.

백업·캐시·읽기 복제는 소유자와 동기화 규칙이 있으면 유용합니다. DBMS 사용 자체가 복사본의 최신성이나 좋은 모델을 자동 보장하지는 않습니다.

중복 자체가 언제나 나쁜 것은 아닙니다. 백업, 읽기 전용 복제본, 캐시도 복사본이지만, 무엇이 원본이고 언제 어떻게 따라가는지가 정해져 있습니다. 문제는 두 복사본이 모두 자기가 원본이라고 주장하는 상태입니다. 그래서 하나의 사실은 소유하는 테이블을 하나로 정하고, 다른 곳은 키로 그 테이블을 참조하거나 필요할 때 다시 만들어 낼 수 있게 해야 합니다. 복사본이 늘어날수록 고쳐야 할 곳도 늘어나기 때문입니다.

DBMS가 대신 해 주는 일

DBMS(database management system)는 SQL을 실행하는 프로그램에 그치지 않습니다. 스키마와 제약으로 허용되는 값의 모양을 정하고, 트랜잭션과 잠금으로 여러 사용자가 동시에 바꾸는 일을 조정하며, 계정과 권한으로 접근을 제한하고, 로그와 백업으로 장애가 났을 때 복구할 수 있게 합니다. 애플리케이션에서 입력을 검사하는 것도 필요하지만, 모든 쓰기 경로가 함께 지켜야 하는 규칙은 데이터베이스의 제약이 마지막 방어선이 됩니다.

이 교재는 MySQL 8.4와 그 기본 저장 엔진인 InnoDB를 기준으로 합니다. 그래도 테이블, 행, 열, 기본 키, 외래 키라는 관계형의 개념은 PostgreSQL이나 Oracle 같은 다른 관계형 DBMS에도 그대로 이어집니다. 어느 제품을 쓸지는 유행이나 한 문장짜리 우열이 아니라, 일관성의 요구와 쿼리의 모양, 운영할 수 있는 역량, 비용, 생태계, 복구 목표를 함께 따져 정합니다. NoSQL 데이터베이스도 데이터 모델과 제약이 없는 저장소가 아니라, 관계형과 다른 절충을 고른 제품들입니다.

회원 정보를 한곳에 두기

이제 회원 정보의 원본이 될 테이블을 만듭니다. 모든 유니코드 문자를 담을 수 있도록 데이터베이스의 문자 집합을 utf8mb4로 정하고, 이메일은 중복될 수 없게 하되, 다른 테이블이 회원을 가리킬 때는 바뀔 수 있는 이메일 대신 숫자 기본 키를 쓰게 합니다.

CREATE TABLE의 괄호 안은 열 이름, 타입, 추가 규칙의 순서로 읽습니다. BIGINT UNSIGNED는 음수가 없는 큰 정수이고, VARCHAR(60)은 최대 60글자의 문자열이며, NOT NULL은 값이 없는 것을 허용하지 않습니다. DEFAULT는 열을 생략했을 때 쓸 값이고, AUTO_INCREMENT는 번호를 서버가 만들게 합니다. 마지막의 PRIMARY KEY와 UNIQUE KEY는 각각 행의 식별과 이메일의 중복 금지를 선언합니다.

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 | email           | name   | status | created_at
1  | [email protected] | 안동민 | ACTIVE | 2026-07-01 09:00:00.000000

INSERT는 열 목록에 적은 열에만 값을 넣으므로, 네 값은 이메일, 암호 해시, 이름, 생성 시각에 차례로 들어가고, 생략한 id는 서버가 1로 만들며 status는 기본값 ACTIVE가 됩니다. {demo}hash-min은 암호 해시가 저장될 자리를 보여 주는 가짜 값일 뿐, 실제로 암호를 처리하는 코드가 아닙니다. created_at은 소수점 아래 여섯 자리까지 저장하는 DATETIME(6)이므로 마이크로초까지 표시됩니다.

이메일에 UNIQUE 제약을 걸었다고 한 사람의 이메일이 영원히 바뀌지 않는다는 뜻은 아닙니다. 이메일은 바뀔 수 있으므로, 다른 테이블이 이메일을 직접 참조하면 이메일이 바뀔 때마다 그 테이블들도 함께 고쳐야 합니다. 바뀌지 않는 내부 기본 키를 따로 두면 이메일을 바꿔도 참조가 흔들리지 않습니다. 이메일의 대소문자를 같은 값으로 볼지는 정렬 규칙(collation)이 정하는데, utf8mb4_0900_ai_ci의 ci는 대소문자를 구별하지 않는다는 뜻이어서 [email protected]과 [email protected]은 같은 값으로 취급되어 함께 들어갈 수 없습니다.

만든 구조를 메타데이터로 확인한다

테이블이 보인다는 것만으로는 충분하지 않습니다. 문자 집합과 정렬 규칙, 저장 엔진, 키가 의도대로 만들어졌는지를 데이터베이스의 메타데이터에서 확인합니다. INFORMATION_SCHEMA는 테이블과 열, 인덱스의 정의를 SQL로 조회하게 해 주는 인터페이스입니다. 아래의 둘째 조회는 statistics의 행을 인덱스마다 하나로 묶고(GROUP BY), GROUP_CONCAT으로 그 인덱스를 이루는 열 이름을 seq_in_index의 순서대로 쉼표로 이어 붙입니다. 묶어서 계산하는 문법은 4장 3편에서 자세히 다루므로, 지금은 결과만 확인합니다.

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로 나와야 하고, 둘째 조회에서는 id로 이루어진 PRIMARY와 email로 이루어진 uq_members_email이 모두 non_unique 0인 유일한 키로 나와야 합니다. MySQL 8.0부터는 INFORMATION_SCHEMA의 결과 머리글이 TABLE_NAME처럼 대문자로 표시될 수 있으므로, 머리글의 대소문자가 다르다고 놀랄 필요는 없습니다. GUI 화면을 캡처하는 대신 이런 SQL의 결과를 남겨 두면, 다른 환경에서도 같은 규칙인지 바로 비교할 수 있습니다.

CREATE DATABASE IF NOT EXISTS를 썼다고 지금의 데이터베이스가 utf8mb4라는 보장은 없습니다. 같은 이름의 데이터베이스가 이미 다른 문자 집합으로 있었다면 이 문장은 아무것도 바꾸지 않고 넘어갑니다. 그래서 메타데이터로 실제 설정을 확인하고, 엉뚱한 스키마에 접속하지 않았는지 SELECT DATABASE();의 결과도 함께 기록해 두는 습관을 들입니다.

더 깊이: INFORMATION_SCHEMA는 무엇을 읽는가

MySQL 8.0부터 테이블과 열, 인덱스의 정의는 트랜잭션으로 저장되는 데이터 사전(data dictionary) 테이블에 들어갑니다. 예전 버전은 이 정보를 메타데이터 파일과 트랜잭션을 지원하지 않는 테이블, 저장 엔진마다 따로 둔 사전에 나누어 저장했습니다. 데이터 사전 테이블은 보호되어 있어 일반 서버에서는 직접 조회할 수 없고, INFORMATION_SCHEMA의 TABLES, COLUMNS, STATISTICS 같은 테이블이 그 위에 만든 뷰로 동작합니다. 그래서 위의 조회는 서버가 쿼리를 실행할 때 쓰는 정의를 그대로 읽습니다.

같은 테이블 안에서도 정의를 담은 열과 통계를 담은 열은 성격이 다릅니다. TABLES의 TABLE_ROWS, AUTO_INCREMENT, DATA_LENGTH와 STATISTICS의 CARDINALITY 같은 열은 테이블 내용에 따라 바뀌는 값이라서, 서버가 캐시해 두고 information_schema_stats_expiry가 정한 시간 동안 캐시한 값을 그대로 돌려줍니다. 기본값은 86400초(24시간)이므로, 방금 행을 많이 넣었어도 AUTO_INCREMENT나 TABLE_ROWS가 예전 값으로 보일 수 있습니다. 캐시를 지금 갱신하려면 ANALYZE TABLE을 실행하고, 세션에서 이 변수를 0으로 두면 매번 저장 엔진에서 최신 값을 가져옵니다.

캐시를 갱신해도 InnoDB의 TABLE_ROWS는 옵티마이저가 쓰는 추정치일 뿐입니다. 공식 문서는 이 값이 실제 행 수와 40%에서 50%까지 차이 날 수 있다고 적고, 정확한 수가 필요하면 SELECT COUNT(*)를 쓰라고 안내합니다. 메타데이터 조회로 확인할 것은 엔진과 정렬 규칙, 키의 구성 같은 정의이고, 행 수 같은 통계는 참고용으로만 읽습니다.

운영에서 함께 생각할 것

로컬에서 실습하는 데이터베이스는 개념을 재현하는 환경일 뿐, 운영의 설계를 대신하지 않습니다. 운영에서는 관리자 계정으로 애플리케이션을 연결하지 않고, 필요한 스키마와 명령만 허락한 계정을 따로 만들어 씁니다. 백업은 파일이 있다는 것으로 검증되지 않으며, 실제로 복구해 보는 연습으로 검증합니다. 이때 잃어도 되는 데이터의 시간 범위를 RPO(recovery point objective), 복구에 걸려도 되는 시간을 RTO(recovery time objective)라고 합니다.

개인정보를 지워 달라는 요청이 오면 현재 테이블뿐 아니라 캐시와 백업, 감사 기록에 남은 사본까지 함께 생각해야 합니다. 데이터베이스를 지우고 다시 만드는 명령을 실행하기 전에는 지금 접속한 곳이 실습 환경이 맞는지 확인합니다. 이 글에서 정한 기본 키와 이메일의 유일성, created_at의 뜻은 뒤의 모든 글이 기대므로, 왜 그렇게 정했는지를 스키마 결정 기록으로 남겨 둡니다.

DBMS를 고르는 기준

따져 볼 것질문
소유권같은 사실의 원본이 하나로 정해져 있는가
동시성여러 쓰기 주체가 같은 값을 바꿀 수 있는가
무결성모든 쓰기 경로가 함께 지켜야 할 규칙이 있는가
복구장애 뒤 어느 시점까지 어떤 절차로 되돌리는가
비용DBMS를 운영하는 비용이 데이터의 위험과 규모에 맞는가

작은 파일을 모두 데이터베이스로 옮길 필요는 없습니다. 혼자 쓰는 작은 업무라면 스프레드시트가 더 알맞을 수 있습니다. 복사본이 생기고, 쓰는 주체가 여럿이 되고, 세밀한 질문이 필요하고, 무결성과 복구가 중요해지는 지점에서, 그 요구를 감당할 하나의 원본과 운영 책임을 고릅니다.

데이터, 스키마, 인스턴스

회원별 게시글 세 행은 데이터이고, "이번 주의 활성 회원 수"는 집계 기준을 적용해 얻은 정보입니다. 데이터베이스가 값을 저장해 준다고 해서 그 값의 업무상 의미까지 올바르게 정해지는 것은 아닙니다.

members(id, email, name)처럼 구조와 제약을 정의한 것을 스키마(schema), 오늘 실제로 저장된 회원 행들을 인스턴스(instance)라고 합니다. 인스턴스는 INSERT, UPDATE, DELETE로 매일 바뀌지만, 스키마를 바꾸는 것은 열과 타입, 제약의 설계를 바꾸는 일입니다. 관계 모델의 형식적인 용어와 집합과 SQL의 차이는 관계 모델과 관계대수의 집합 연산에서 다시 비교합니다.

3단계 스키마 구조는 데이터베이스를 세 단계로 나누어 설명하는 추상 모델입니다. 사용자마다 필요한 모습을 외부 단계, 전체의 논리 구조를 개념 단계, 파일과 인덱스 같은 저장 구조를 내부 단계라고 합니다. 예를 들어 운영자는 이메일까지 포함한 회원 목록을, 공개 화면은 이름만 보지만, 전체의 논리적인 회원 구조는 하나입니다. MySQL에서 "스키마"라는 말이 데이터베이스의 이름을 뜻하기도 하므로, 이 이론의 세 단계와 혼동하지 않도록 합니다.

내부의 인덱스를 바꿔도 공개 화면의 회원 목록이 그대로 유지되는 성질을 물리적 데이터 독립성(physical data independence)이라고 합니다. 전체의 논리 구조를 바꿔도 사용자마다의 화면과 애플리케이션이 그대로 동작하게 하려는 목표는 논리적 데이터 독립성(logical data independence)입니다. 이는 목표일 뿐이어서, 모든 스키마 변경이 저절로 애플리케이션과 호환되는 것은 아닙니다.

테이블과 열, 제약 같은 메타데이터를 모은 것을 데이터 사전(data dictionary)이라고 하고, INFORMATION_SCHEMA는 이를 조회하는 창구입니다. 사람의 역할도 나뉩니다. 데이터베이스 관리자(DBA)는 접근과 구성, 백업과 복구를, 애플리케이션 개발자는 업무 규칙을 SQL과 프로그램으로 잇는 일을, 최종 사용자는 허락된 화면으로 조회하고 입력하는 일을 맡습니다. 한 사람이 여러 역할을 하더라도 서비스 계정에 관리 권한을 모두 주지는 않습니다.

정리

  • 복사본의 문제는 중복 자체보다, 어느 쪽이 원본인지와 어떻게 맞출지가 정해지지 않은 데 있습니다.
  • DBMS는 스키마와 제약, 트랜잭션, 권한, 복구를 제공하지만 좋은 데이터 모델을 저절로 만들어 주지는 않습니다.
  • 다른 테이블이 참조하는 내부 기본 키와, 바뀔 수 있는 업무 식별자인 이메일을 구분합니다.
  • 만든 구조는 메타데이터로, 제약은 실제로 위반해 오류를 내 보는 것으로 확인합니다.
  • 스키마는 구조와 제약의 정의, 인스턴스는 지금 저장된 행이며, 데이터 독립성은 저장 구조나 논리 구조가 바뀌어도 사용자 쪽을 지키려는 목표입니다.

연습 문제

1. 같은 이메일, 같은 이름

[email protected] 회원을 추가한 뒤 같은 이메일로 다른 이름을 다시 넣어 보세요. 이어서 이메일은 다르지만 이름이 같은 회원도 넣어 보세요. 각 문장의 결과는 어떻게 되고, 이름은 왜 같아도 되나요? 연결은 자동 커밋(SELECT @@session.autocommit이 1)이고, 문장을 하나씩 실행한다고 합니다.

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');

INSERT INTO members (email, password_hash, name, created_at)
VALUES ('[email protected]', '{demo}hash-jun', '이하나', '2026-07-03 11:00:00');
해설 보기

첫 문장은 정상으로 들어가고, 둘째 문장은 uq_members_email을 어겨 오류 1062(Duplicate entry)로 거부되며, 셋째 문장은 이메일이 다르므로 들어갑니다. 자동 커밋에서는 문장마다 따로 커밋되므로, 둘째 문장의 오류가 이미 커밋된 첫 행을 지우지 않습니다(MySQL 8.4의 자동 커밋). 이름은 동명이인이 있을 수 있어 사람을 식별하지 못하므로 UNIQUE를 걸지 않습니다. 실습 도구가 오류가 난 뒤에도 나머지 문장을 계속 실행했는지는 따로 확인해야 합니다. 여러 문장을 하나로 묶는 방법은 트랜잭션과 멱등성에서 다룹니다.

2. 어느 쪽이 데이터 독립성인가

회원 이름을 조회하는 화면의 열은 그대로 두고 내부의 인덱스만 바꾼 경우와, 애플리케이션이 쓰던 이메일 열을 지워 조회가 깨진 경우는 둘 다 데이터 독립성이 지켜진 것인가요?

해설 보기

첫 경우는 저장 구조를 바꾸었는데도 논리적인 조회가 그대로 유지되었으므로 물리적 데이터 독립성이 지켜진 예입니다. 둘째 경우는 사용자 쪽이 필요로 하는 논리적인 열을 없애 조회가 깨졌으므로 논리적 데이터 독립성이 지켜지지 않았습니다. 데이터 독립성은 저절로 얻어지는 성질이 아니라, 뷰나 애플리케이션과의 약속을 유지하도록 변경을 설계해야 얻을 수 있습니다.

3. 이미 있던 데이터베이스

누군가 예전에 board_lab을 latin1 문자 집합으로 만들어 두었습니다. 그 상태에서 본문의 CREATE DATABASE IF NOT EXISTS board_lab CHARACTER SET utf8mb4 ...를 실행하면 데이터베이스의 문자 집합은 무엇이 되나요? 어떻게 확인할 수 있나요?

해설 보기

같은 이름의 데이터베이스가 이미 있으므로 이 문장은 새로 만들지도 바꾸지도 않고 경고만 남긴 채 넘어가며, 문자 집합은 그대로 latin1입니다. SELECT default_character_set_name, default_collation_name FROM information_schema.schemata WHERE schema_name = 'board_lab';으로 실제 값을 확인할 수 있습니다. ALTER DATABASE로 기본값을 바꿀 수는 있지만 이는 앞으로 만들 테이블의 기본값일 뿐, 이미 있는 테이블의 문자 집합까지 바꾸지는 않습니다. 그래서 IF NOT EXISTS를 쓴 뒤에는 메타데이터로 설정을 확인해야 합니다.

다음 글 기본 키와 외래 키에서는 회원 테이블에 이어 게시글 테이블을 만들고, 기본 키와 외래 키로 두 테이블의 관계를 지키는 방법을 확인합니다.