MySQL 해부 - Docker 설치부터 운영 기본까지
Docker로 MySQL 8.4를 설치하고 server, database, table, index, transaction 구조와 최소 SQL 예제, 백업과 복원, 서비스 계정, utf8mb4 문자셋 같은 운영 기본을 정리한 입문 단편입니다.
기준 버전은 MySQL 8.4 (LTS, 최신 패치 8.4.10)이고, 실습은 mysql:8.4 Docker image를 씁니다. LTS는 장기 지원 버전을 뜻합니다.
MySQL이 푸는 문제
애플리케이션 데이터를 CSV나 JSON 파일로 직접 관리하면 세 가지 한계가 있습니다. 두 프로세스가 같은 파일에 동시에 쓰면 한쪽 내용이 유실되고, 쓰는 도중에 프로세스가 죽으면 반쯤 쓰인 파일이 남고, 데이터가 커지면 한 건을 찾을 때도 파일 전체를 읽어야 해서 조회가 느려집니다.
관계형 데이터베이스(RDB)는 이 세 문제를 전담하는 별도의 프로그램입니다. 데이터 한 건이 row, 항목 하나가 column인 table 구조로 저장하고, 동시 쓰기는 server가 순서를 조정하고, 중간에 끊긴 작업은 transaction 단위로 되돌리고, 조회는 index로 전체를 읽지 않고 찾습니다. 이 database에 명령을 내리는 언어가 SQL이고, table을 만들고 데이터를 넣고 조회하는 문장이 전부 SQL입니다.
MySQL과 PostgreSQL은 둘 다 SQL로 다루는 오픈소스 RDB이고, 이 글에서 다루는 기본 개념은 양쪽에 그대로 적용됩니다. MySQL은 웹 서비스에서 가장 널리 쓰이는 오픈소스 RDB이고, PostgreSQL은 PostgreSQL 편에서 따로 다룹니다.
설치와 첫 실행
실습은 Docker container로 합니다. container 실행 방법 자체는 Docker 기초 4편에서 다뤘으므로 여기서는 명령만 적습니다.
1
docker run -d --name my-mysql -p 3306:3306 -e MYSQL_ROOT_PASSWORD=my-secret-pw -v mysql-data:/var/lib/mysql mysql:8.4
버전은 8.4를 씁니다. 차기 LTS인 MySQL 9.7이 2026년 4월 21일에 나왔지만 출시 3개월밖에 되지 않았고, 8.4 LTS는 2024년 4월 출시로 2029년 4월까지 Premier Support, 2032년 4월까지 Extended Support가 보장되며, 8.0은 2026년 4월에 지원이 종료(EOL)되었으므로(마지막 패치 8.0.46) 새로 시작한다면 8.4가 기준입니다. root는 기본 관리자 계정이고 공식 image에서는 MYSQL_ROOT_PASSWORD 환경 변수가 사실상 필수이며(MYSQL_RANDOM_ROOT_PASSWORD 같은 대안 변수를 쓰지 않는 한 이 변수 없이는 시작되지 않습니다), MYSQL_DATABASE, MYSQL_USER, MYSQL_PASSWORD를 함께 주면 시작할 때 database와 일반 사용자를 자동으로 만들어 줍니다. 기본 port는 3306이고, host에서 이미 MySQL이 실행 중이어서 port가 충돌하면 -p 3307:3306처럼 다른 host port를 씁니다. 첫 실행은 초기화에 수십 초가 걸리므로 docker logs my-mysql 출력에 ‘ready for connections’가 나타난 뒤 접속합니다. ERROR 2002가 나면 아직 초기화 중이므로 잠시 후 다시 시도합니다.
1
docker exec -it my-mysql mysql -uroot -p
mysql은 MySQL이 기본으로 제공하는 명령줄 client입니다. 비밀번호를 물으면 위에서 정한 my-secret-pw를 입력하고, 접속 직후에는 다음 명령으로 상태를 확인합니다. 참고로 client 접속은 되는데 애플리케이션의 언어 드라이버에서만 오류가 나면 인증 방식 문제일 수 있습니다. MySQL 8.0부터 기본 인증 플러그인이 caching_sha2_password인데 구형 드라이버는 이를 지원하지 않고, 8.4에서는 구식 mysql_native_password 플러그인이 기본적으로 비활성화되어 있으므로 드라이버를 최신 버전으로 올리는 것이 해결책입니다.
1
2
3
SELECT VERSION(); -- server 버전
SHOW DATABASES; -- database 목록
STATUS; -- 접속 정보와 문자셋 요약, 종료는 EXIT;
구조
| 구성요소 | 설명 |
|---|---|
| server (mysqld) | 데이터를 저장하고 SQL을 처리하는 본체 프로세스입니다. client는 여기에 접속합니다. |
| database | table을 묶는 단위입니다. MySQL에서는 schema와 동의어입니다. |
| table / row / column | 데이터가 저장되는 표 구조입니다. 데이터 한 건이 row, 항목 하나가 column입니다. |
| storage engine | table 데이터를 디스크에 실제로 읽고 쓰는 모듈입니다. 기본은 InnoDB입니다. |
| index | 특정 column 값으로 row를 빨리 찾기 위한 자료구조입니다. 조회할 때 table 전체를 읽지 않게 해 줍니다. |
| transaction | 여러 SQL 문장을 전부 반영되거나 전부 취소되는 하나의 작업 단위로 묶는 기능입니다. |
확인 명령은 네 개면 됩니다. SHOW DATABASES;는 database 목록을 보여 주고, USE 이름;은 이후 명령이 적용될 database를 선택하고, SHOW TABLES;는 현재 database의 table 목록을, DESCRIBE 테이블명;은 그 table의 column 구성을 보여 줍니다.
최소 예제
게시판 하나를 만들어 봅니다. 아래 SQL은 client에 그대로 붙여 넣으면 실행됩니다. PRIMARY KEY는 row를 유일하게 식별하는 column이고, AUTO_INCREMENT 덕분에 id는 넣는 순서대로 1부터 자동으로 채워지므로, 아래에서 WHERE id = 2로 특정 글을 지목할 수 있습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
CREATE DATABASE board;
USE board;
CREATE TABLE posts (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
author VARCHAR(50) NOT NULL,
title VARCHAR(200) NOT NULL,
is_notice BOOLEAN NOT NULL DEFAULT FALSE,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
INSERT INTO posts (author, title) VALUES
('alice', '첫 글입니다'), ('bob', '질문 있습니다'), ('alice', '공지 초안입니다');
SELECT * FROM posts WHERE author = 'alice'; -- 조건 조회
SELECT author, COUNT(*) FROM posts GROUP BY author; -- 작성자별 글 수 집계
UPDATE posts SET title = '질문 있습니다 (해결)' WHERE id = 2;
CREATE INDEX idx_posts_author ON posts (author); -- author 조회용 index
이어서 transaction입니다. 먼저 UPDATE posts SET is_notice = TRUE WHERE id = 1;을 실행해 id 1번 글을 공지로 만들어 둡니다. 이제 id 3번 글을 공지로 올리면서 기존 공지를 내리는 작업은 UPDATE 두 개가 함께 반영되어야 합니다.
1
2
3
4
BEGIN;
UPDATE posts SET is_notice = FALSE WHERE is_notice = TRUE;
UPDATE posts SET is_notice = TRUE WHERE id = 3;
COMMIT;
두 문장을 낱개로 실행하다가 사이에서 접속이 끊기면 공지가 하나도 없는 중간 상태가 남습니다. BEGIN과 COMMIT으로 묶으면 COMMIT 시점에 두 변경이 함께 반영되고, 중간에 문제가 생기면 ROLLBACK 한 번으로 전부 취소됩니다. 연관된 쓰기를 중간 상태 없이 처리하는 것이 transaction의 존재 이유입니다.
UI 훑기
명령줄 client만으로 실습은 충분하지만, table 구조와 데이터를 한 화면에서 보려면 GUI가 편합니다. 공식 도구는 MySQL Workbench이고, 여러 종류의 DB를 함께 쓴다면 DBeaver가 무난합니다. 접속 정보(host 127.0.0.1, port 3306, user root)로 연결한 뒤 두 가지를 확인합니다. 왼쪽 트리에서 board database 아래 posts table을 열면 위에서 넣은 row가 표로 보이고, table 정보 탭에서는 DESCRIBE로 확인했던 column 구성과 index 목록이 같은 내용으로 나옵니다.
실전 운영
데이터는 volume에 둡니다. MySQL의 데이터 디렉토리는 container 안 /var/lib/mysql이고, 처음 실행 명령처럼 named volume을 여기에 마운트해야 container를 지워도 데이터가 남습니다. volume 개념은 Docker 기초 7편에서 다뤘습니다.
백업은 mysqldump로 만들고, 실제로 복원까지 한 번 해 봐야 그 백업이 동작한다고 말할 수 있습니다. 아래 두 번째 명령이 복원이며, 덤프한 SQL 파일을 mysql client의 표준 입력으로 전달하는 방식입니다.
1
2
docker exec my-mysql sh -c 'exec mysqldump --all-databases -uroot -p"$MYSQL_ROOT_PASSWORD"' > backup.sql
docker exec -i my-mysql sh -c 'exec mysql -uroot -p"$MYSQL_ROOT_PASSWORD"' < backup.sql
접속 정보는 코드 밖에서 주입합니다. 비밀번호를 소스 코드에 적지 않고 환경 변수나 배포 도구의 secret 기능으로 전달해야 코드 저장소에 유출되지 않습니다. 또한 애플리케이션이 root로 접속하게 두지 않고, 필요한 권한만 가진 서비스용 계정을 만들어 씁니다. 아래 계정 이름에서 @ 뒤는 접속을 허용할 host이고 %는 모든 host를 뜻합니다.
1
2
CREATE USER 'app'@'%' IDENTIFIED BY 'app-password';
GRANT SELECT, INSERT, UPDATE, DELETE ON board.* TO 'app'@'%';
문자셋은 utf8mb4를 씁니다. MySQL 8.0부터 기본 문자셋이 utf8mb4이고 collation은 utf8mb4_0900_ai_ci이므로 그대로 두면 됩니다. collation은 문자열을 비교하고 정렬할 때 쓰는 규칙입니다. 이름이 비슷한 ‘utf8’(utf8mb3)은 3바이트라 이모지를 저장하지 못하므로 일부러 바꾸지 않습니다.
자주 겪는 문제
한글이나 이모지가 ? 또는 깨진 문자로 저장되는 경우가 있습니다. table이나 접속의 문자셋이 utf8mb4가 아닌 것이 원인입니다. SHOW VARIABLES LIKE 'character_set%';로 현재 설정을 확인하고, table과 client 접속 문자셋을 모두 utf8mb4로 맞추면 해결됩니다.
다른 container에서 localhost:3306으로 접속하면 connection refused가 납니다. container 안에서 localhost는 그 container 자신을 가리키므로 MySQL이 없는 곳에 접속을 시도한 것이 원인입니다. 같은 Docker network에 두고 host 자리에 container 이름(my-mysql)을 쓰면 해결됩니다. host의 mysql client로 접속할 때도 localhost 대신 mysql -h 127.0.0.1 -P 3306 -uroot -p처럼 127.0.0.1을 명시해야 하는데, localhost를 쓰면 TCP 대신 소켓 파일로 접속을 시도해 실패하기 때문입니다.
container를 지우고 다시 만들었더니 database가 비어 있는 경우도 있습니다. volume 없이 실행해서 데이터가 container 내부에만 있었던 것이 원인입니다. -v mysql-data:/var/lib/mysql처럼 volume을 마운트해 다시 실행하면 이후로는 데이터가 유지되고, 이미 지운 데이터는 백업이 없으면 복구할 수 없으므로 위의 백업 절차를 먼저 갖춰야 합니다.
관련 글
- PostgreSQL 편: 같은 문제를 PostgreSQL 기준으로 실습한 글입니다.
- Docker 기초 시리즈: container 실행과 volume 등 이 글의 실습 전제를 다룹니다.