0. 들어가며
MySQL을 처음 배울 때는 데이터베이스가 하나의 통일된 시스템이라고 생각했습니다. 테이블을 만들고, 쿼리를 실행하면, MySQL이 알아서 데이터를 저장하고 조회해주는 것처럼 보였습니다. 하지만 성능 문제를 마주치면서, MySQL이 생각보다 훨씬 복잡한 구조로 되어 있다는 것을 알게 되었습니다.
특히 인상 깊었던 경험은 로그 데이터를 저장하는 테이블의 성능 문제였습니다. 시스템의 모든 사용자 행동을 기록하는 테이블이었는데, 데이터가 쌓일수록 INSERT 속도가 점점 느려졌습니다. 하루에 수백만 건씩 쌓이는 로그 데이터를 감당하기 어려워졌고, 결국 데이터베이스 전체의 성능에 영향을 주기 시작했습니다.
1. 스토리지 엔진이란 무엇인가
MySQL의 독특한 점은 플러그인 가능한 스토리지 엔진 아키텍처를 가지고 있다는 것입니다.
이것은 데이터를 실제로 디스크에 저장하고 읽어오는 부분을 별도의 모듈로 분리한 구조입니다.
MySQL 서버는 크게 두 계층으로 나뉩니다.
상위 계층은 MySQL 엔진입니다.
MysQL엔진은 SQL 파서, 옵티마이저, 실행 계획기 등을 포함합니다.
사용자의 쿼리를 받아서 분석하고, 최적화하고, 어떻게 실행할지 계획을 세웁니다.
이 부분은 어떤 스토리지 엔진을 사용하든 동일합니다.
하위 계층은 스토리지 엔진입니다.
스토리지 엔진 실제로 데이터를 디스크에 저장하고, 인덱스를 관리하고, 트랜잭션을 처리하는 부분입니다.
각 테이블마다 다른 스토리지 엔진을 사용할 수 있습니다.
같은 데이터베이스 안에서도 어떤 테이블은 InnoDB를 쓰고, 어떤 테이블은 MyISAM을 쓰고, 어떤 테이블은 ARCHIVE를 쓸 수 있습니다.
현재 MySQL에서 가장 많이 사용되는 스토리지 엔진은 세 가지입니다.
InnoDB, MyISAM, 그리고 ARCHIVE입니다.
InnoDB는 MySQL 5.5 버전부터 기본 엔진이 되었고, 대부분의 경우 InnoDB를 사용하면 됩니다
MyISAM은 오래된 엔진이지만 여전히 특정 상황에서 유용합니다.
ARCHIVE는 특수 목적의 엔진으로, 로그성 데이터에 최적화되어 있습니다.
지금부터 각 엔진을 더 자세히 살펴보겠습니다.
2. InnoDB - 현대적 범용 엔진의 표준
InnoDB는 MySQL의 기본 스토리지 엔진입니다.
2010년 MySQL 5.5 버전부터 기본값이 되었고, 현재 대부분의 MySQL 테이블이 InnoDB를 사용합니다.
InnoDB가 기본 엔진이 된 이유는 트랜잭션, 외래키, 행 수준 락, MVCC 등 현대적인 애플리케이션이 필요로 하는 거의 모든 기능을 제공하기 때문입니다.
InnoDB의 가장 중요한 특징은 ACID를 완벽하게 지원한다는 것입니다. Atomicity, Consistency, Isolation, Durability입니다.
트랜잭션이 성공하면 완전히 반영되고, 실패하면 완전히 롤백됩니다. 중간 상태는 존재하지 않습니다. 서버가 갑자기 다운되더라도, 재시작하면 커밋된 트랜잭션은 모두 복구되고 커밋되지 않은 트랜잭션은 롤백됩니다.
이것이 가능한 이유는 Write-Ahead Logging 방식을 사용하기 때문입니다. 데이터를 실제로 변경하기 전에 먼저 로그에 변경 내용을 기록합니다. 이 로그를 Redo Log라고 합니다. 만약 서버가 다운되면, Redo Log를 읽어서 커밋된 변경사항을 재현합니다. 반대로 Undo Log는 롤백을 위한 정보를 담고 있습니다. 트랜잭션을 롤백해야 할 때, Undo Log를 사용해서 이전 상태로 되돌립니다.
START TRANSACTION;
INSERT INTO orders (user_id, amount) VALUES (1, 10000);
UPDATE users SET balance = balance - 10000 WHERE id = 1;
-- 여기서 서버가 다운되면?
-- 아직 COMMIT 전이므로 Undo Log를 사용해 롤백
-- COMMIT 후라면 Redo Log를 사용해 복구
COMMIT;
InnoDB의 또 다른 특징은 행 수준 락(Row-level Locking)입니다.
데이터를 수정할 때, 테이블 전체를 잠그는 것이 아니라 해당 행만 잠급니다. 이것은 동시성을 크게 향상시킵니다. 수천 명의 사용자가 동시에 다른 행을 수정할 수 있습니다. 서로 다른 주문을 처리하거나, 서로 다른 사용자의 정보를 업데이트하는 작업은 서로를 막지 않습니다.
-- 트랜잭션 A
UPDATE users SET last_login = NOW() WHERE id = 1;
-- 트랜잭션 B (동시 실행 가능)
UPDATE users SET last_login = NOW() WHERE id = 2;
-- 트랜잭션 C (대기 필요)
UPDATE users SET last_login = NOW() WHERE id = 1;
트랜잭션 A와 B는 서로 다른 행을 수정하므로 동시에 실행됩니다. 하지만 트랜잭션 C는 A가 수정 중인 행을 다시 수정하려고 하므로 A가 커밋할 때까지 대기해야 합니다.
InnoDB는 MVCC(Multi-Version Concurrency Control)를 사용합니다. 이것은 앞서 트랜잭션 격리 레벨에서 다뤘던 개념입니다. 데이터의 여러 버전을 유지하여, 읽기 작업이 쓰기 작업을 막지 않고, 쓰기 작업도 읽기 작업을 막지 않게 합니다. 한 트랜잭션이 데이터를 수정하는 동안, 다른 트랜잭션은 이전 버전을 읽을 수 있습니다.
인덱스 구조도 중요합니다. InnoDB는 클러스터형 인덱스(Clustered Index)를 사용합니다. 이것은 데이터 자체가 Primary Key 순서로 정렬되어 저장된다는 의미입니다. Primary Key로 조회할 때 매우 빠릅니다. 하지만 Secondary Index는 Primary Key를 참조하는 방식이라서, Secondary Index로 조회하면 Primary Key를 먼저 찾고 다시 데이터를 찾는 두 단계가 필요합니다.
CREATE TABLE users (
id BIGINT PRIMARY KEY, -- 클러스터형 인덱스
email VARCHAR(100),
name VARCHAR(50),
INDEX idx_email (email) -- 세컨더리 인덱스
);
-- Primary Key 조회: 한 번에 데이터 접근
SELECT * FROM users WHERE id = 1;
-- Secondary Index 조회: 먼저 idx_email에서 id를 찾고, 다시 id로 데이터 접근
SELECT * FROM users WHERE email = 'user@example.com';
이런 구조 때문에 Primary Key 선택이 매우 중요합니다. UUID 같은 무작위 값을 Primary Key로 사용하면, 데이터가 무작위 순서로 삽입되어 성능이 떨어집니다. Auto Increment 정수 값처럼 순차적으로 증가하는 값이 더 효율적입니다.
InnoDB는 외래키 제약조건을 지원합니다. 참조 무결성을 데이터베이스 레벨에서 보장할 수 있습니다. 부모 테이블의 행을 삭제하려고 할 때, 자식 테이블에 참조하는 행이 있으면 에러가 발생합니다.
CREATE TABLE orders (
id BIGINT PRIMARY KEY,
user_id BIGINT,
amount INT,
FOREIGN KEY (user_id) REFERENCES users(id)
ON DELETE CASCADE
ON UPDATE CASCADE
);
-- users 테이블의 id=1 행을 삭제하면
-- orders 테이블의 user_id=1인 행들도 자동으로 삭제됨 (CASCADE)
DELETE FROM users WHERE id = 1;
InnoDB의 장점을 정리하면 완벽한 트랜잭션 지원, 높은 동시성, 크래시 복구 기능, 외래키 제약조건, 행 수준 락 등입니다.
단점도 있습니다. MyISAM이나 ARCHIVE에 비해 디스크 공간을 더 많이 사용하고, 복잡한 내부 구조로 인해 특정 상황에서는 성능이 떨어질 수 있습니다.
하지만 대부분의 경우 InnoDB가 최선의 선택입니다. 전자상거래, 금융, 회원 관리 등 데이터 정합성이 중요한 모든 애플리케이션에서 InnoDB를 사용해야 합니다. 특별한 이유가 없다면 InnoDB를 기본으로 사용하고, 특수한 경우에만 다른 엔진을 고려하는 것이 좋습니다.
3. MyISAM - 단순하고 빠른 읽기 전용 엔진
MyISAM은 MySQL의 전통적인 스토리지 엔진입니다.
MySQL 5.5 이전 버전에서는 기본 엔진이었습니다. 지금은 InnoDB에 밀려 사용 빈도가 줄었지만, 여전히 특정 상황에서는 MyISAM이 더 나은 선택일 수 있습니다.
MyISAM의 가장 큰 특징은 단순성입니다. 트랜잭션을 지원하지 않고, 외래키도 없으며, MVCC도 없습니다. 복잡한 동시성 제어 메커니즘 대신, 테이블 수준 락을 사용합니다. 한 번에 하나의 쓰기 작업만 가능하고, 읽기 작업은 여러 개가 동시에 가능합니다.
-- MyISAM 테이블 생성
CREATE TABLE product_catalog (
id BIGINT PRIMARY KEY,
name VARCHAR(200),
description TEXT,
price INT
) ENGINE=MyISAM;
-- 읽기는 동시에 여러 개 가능
SELECT * FROM product_catalog WHERE id = 1; -- 트랜잭션 A
SELECT * FROM product_catalog WHERE id = 2; -- 트랜잭션 B (동시 실행)
-- 쓰기는 한 번에 하나만 가능
UPDATE product_catalog SET price = 10000 WHERE id = 1; -- 트랜잭션 C
UPDATE product_catalog SET price = 20000 WHERE id = 2; -- 트랜잭션 D (대기)
테이블 수준 락은 단순하지만 제한적입니다. 누군가 한 행을 수정하는 동안, 다른 사람은 완전히 다른 행도 수정할 수 없습니다. 전체 테이블이 잠기기 때문입니다. 이것은 쓰기 작업이 많은 시스템에서는 심각한 병목이 됩니다.
하지만 읽기 작업만 있거나, 쓰기가 매우 드문 경우에는 MyISAM이 InnoDB보다 빠를 수 있습니다. MyISAM은 트랜잭션 로그를 관리하지 않고, MVCC를 위한 버전 관리도 하지 않으므로, 순수한 읽기 성능이 더 좋습니다. 특히 Full Table Scan이 필요한 경우, MyISAM이 더 효율적입니다.
-- 전체 테이블을 스캔하는 집계 쿼리
SELECT COUNT(*), AVG(price)
FROM product_catalog
WHERE category = 'electronics';
이런 쿼리는 어차피 전체 테이블을 읽어야 하므로, 트랜잭션이나 MVCC의 오버헤드가 없는 MyISAM이 더 빠를 수 있습니다.
MyISAM의 또 다른 특징은 인덱스 구조입니다. InnoDB의 클러스터형 인덱스와 달리, MyISAM은 비클러스터형 인덱스를 사용합니다. 데이터는 삽입 순서대로 저장되고, 인덱스는 데이터의 물리적 위치를 가리킵니다. Primary Key와 Secondary Index가 동등합니다.
-- MyISAM의 인덱스 구조
CREATE TABLE articles (
id BIGINT PRIMARY KEY,
title VARCHAR(200),
content TEXT,
created_at DATETIME,
INDEX idx_created_at (created_at)
);
-- Primary Key 조회나 Secondary Index 조회나 비슷한 성능
SELECT * FROM articles WHERE id = 1;
SELECT * FROM articles WHERE created_at > '2025-01-01';
MyISAM은 테이블 압축을 지원합니다. myisampack 유틸리티를 사용하면 테이블을 압축하여 읽기 전용으로 만들 수 있습니다. 압축된 테이블은 디스크 공간을 크게 절약하고, 읽기 성능도 향상될 수 있습니다. 데이터를 디스크에서 메모리로 읽어오는 양이 줄어들기 때문입니다.
하지만 압축하면 수정이 불가능해집니다. 완전히 읽기 전용이 됩니다. 그래서 히스토리 데이터나 아카이브 데이터처럼 더 이상 변경되지 않는 데이터에 사용합니다.
MyISAM의 가장 큰 문제는 크래시 복구입니다. 트랜잭션을 지원하지 않으므로, 서버가 갑자기 다운되면 데이터가 손상될 수 있습니다. 쓰기 작업 중간에 서버가 멈추면, 일부는 반영되고 일부는 반영되지 않은 불일치 상태가 될 수 있습니다.
-- MyISAM 테이블에서 대량 삽입 중 서버 다운
INSERT INTO logs SELECT * FROM temp_logs; -- 100만 건 삽입 중
-- 서버 재시작 후
-- 일부만 삽입되고 일부는 삽입 안 된 상태
-- 중복이나 누락이 발생할 수 있음
복구를 위해 myisamchk 도구를 사용해야 하는데, 이것은 시간이 오래 걸리고 항상 성공하는 것도 아닙니다.
그렇다면 MyISAM은 언제 사용해야 할까요? 가장 적합한 경우는 읽기 전용이거나 읽기 위주의 데이터입니다. 상품 카탈로그, 우편번호 데이터, 지역 정보처럼 한 번 로드하고 거의 변경되지 않는 참조 데이터에 적합합니다.
-- 읽기 전용 참조 데이터
CREATE TABLE postal_codes (
code VARCHAR(10) PRIMARY KEY,
city VARCHAR(100),
district VARCHAR(100)
) ENGINE=MyISAM;
-- 초기 데이터 로드 후 거의 변경 없음
-- 주로 조회만 함
SELECT city, district FROM postal_codes WHERE code = '06234';
또한 Full-text 검색이 필요한 경우에도 MyISAM을 고려할 수 있습니다. InnoDB도 Full-text 인덱스를 지원하지만, MyISAM의 Full-text 검색이 역사적으로 더 성숙하고 안정적입니다.
블로그 게시글이나 뉴스 기사 검색 같은 기능에서 사용할 수 있습니다.
하지만 현재는 대부분의 경우 InnoDB를 사용하는 것이 더 안전합니다. MyISAM의 장점이던 읽기 성능도 InnoDB가 많이 개선되어 큰 차이가 없어졌고, 크래시 복구 문제는 여전히 심각한 리스크입니다. 특별한 이유가 없다면 InnoDB를 사용하고, 정말로 읽기만 하는 특수한 경우에만 MyISAM을 고려하는 것이 좋습니다.
4. ARCHIVE - 압축과 저장에 특화된 로그 엔진
ARCHIVE는 매우 특수한 목적으로 설계된 스토리지 엔진입니다.
이름에서 알 수 있듯이, 대량의 데이터를 압축하여 아카이빙하는 데 최적화되어 있습니다. 로그 데이터, 히스토리 데이터, 감사 기록처럼 계속 쌓이기만 하고 거의 조회되지 않으며 수정되지 않는 데이터에 적합합니다.
ARCHIVE의 가장 큰 특징은 극단적인 압축률입니다. zlib 압축 알고리즘을 사용하여 데이터를 압축하는데, 보통 InnoDB 대비 10분의 1 수준으로 디스크 공간을 사용합니다. 텍스트 데이터가 많을수록 압축률이 더 좋습니다.
-- ARCHIVE 엔진으로 로그 테이블 생성
CREATE TABLE user_activity_log (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT,
action VARCHAR(100),
url VARCHAR(500),
ip_address VARCHAR(45),
user_agent TEXT,
created_at DATETIME
) ENGINE=ARCHIVE;
-- 같은 구조의 InnoDB 테이블
CREATE TABLE user_activity_log_innodb (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT,
action VARCHAR(100),
url VARCHAR(500),
ip_address VARCHAR(45),
user_agent TEXT,
created_at DATETIME
) ENGINE=InnoDB;
-- 1억 건의 데이터를 저장했을 때
-- InnoDB: 약 15GB
-- ARCHIVE: 약 1.5GB
압축은 삽입 시점에 이루어집니다. 데이터를 INSERT할 때 자동으로 압축되어 저장됩니다. 덕분에 INSERT 성능도 매우 좋습니다. 디스크에 쓸 데이터 양이 적기 때문입니다. 초당 수만 건의 INSERT도 무리 없이 처리할 수 있습니다.
하지만 ARCHIVE는 기능이 매우 제한적입니다. INSERT와 SELECT만 가능하고, UPDATE와 DELETE는 지원하지 않습니다. 한 번 저장된 데이터는 수정하거나 삭제할 수 없습니다. 오직 추가만 가능합니다.
-- 가능한 작업
INSERT INTO user_activity_log
VALUES (NULL, 1, 'LOGIN', '/login', '192.168.1.1', 'Mozilla/5.0...', NOW());
SELECT * FROM user_activity_log
WHERE user_id = 1 AND created_at > '2025-01-01';
-- 불가능한 작업
UPDATE user_activity_log SET action = 'LOGOUT' WHERE id = 1; -- 에러
DELETE FROM user_activity_log WHERE id = 1; -- 에러
인덱스도 제한적입니다. Auto Increment Primary Key만 가능하고, 다른 컬럼에는 인덱스를 생성할 수 없습니다. 그래서 Primary Key가 아닌 컬럼으로 조회하면 Full Table Scan이 발생합니다.
-- Primary Key 조회: 빠름
SELECT * FROM user_activity_log WHERE id = 12345;
-- 다른 컬럼 조회: Full Table Scan (느림)
SELECT * FROM user_activity_log WHERE user_id = 1;
이것은 심각한 제약처럼 보이지만, ARCHIVE의 사용 목적을 생각하면 이해가 됩니다. 로그 데이터는 보통 시간 순서대로 조회합니다. 최근 데이터를 빠르게 조회하고, 오래된 데이터는 드물게 조회하거나 배치로 처리합니다. 복잡한 검색이 필요하다면, 데이터를 추출해서 별도의 분석 시스템으로 옮기는 것이 일반적입니다.
트랜잭션도 지원하지 않습니다. 각 INSERT는 독립적으로 처리되고, 롤백할 수 없습니다. 하지만 로그 데이터는 어차피 트랜잭션이 필요 없습니다. 로그는 쌓이기만 하면 되고, 중간에 실패해도 재시도하거나 다음 로그를 기록하면 됩니다.
ARCHIVE는 테이블 수준 락을 사용합니다. MyISAM과 비슷하게, 쓰기 작업이 진행되는 동안 테이블 전체가 잠깁니다. 하지만 ARCHIVE는 INSERT만 하므로, 여러 INSERT가 동시에 발생해도 버퍼에 쌓였다가 배치로 처리됩니다. 락으로 인한 대기 시간이 짧습니다.
ARCHIVE 엔진의 내부 구조는 단순합니다. 데이터는 순차적으로 파일에 append됩니다. 삭제된 공간을 재사용하거나 빈 공간을 관리하는 복잡한 로직이 없습니다. 그냥 파일 끝에 계속 추가됩니다. 이것이 빠른 INSERT 성능의 비결입니다.
조회할 때는 압축된 데이터를 풀어서 읽습니다. 이것은 CPU 자원을 사용하지만, 디스크 I/O가 줄어들므로 전체적으로는 효율적일 수 있습니다. 특히 최근 서버는 CPU가 충분하고 디스크 I/O가 병목인 경우가 많으므로, CPU로 압축/해제 작업을 하는 것이 합리적입니다.
ARCHIVE는 언제 사용해야 할까요? 가장 적합한 경우는 로그성 데이터입니다. 웹 서버 액세스 로그, 애플리케이션 이벤트 로그, 사용자 행동 로그, 센서 데이터 등이 그렇습니다.
-- 센서 데이터 로그
CREATE TABLE sensor_readings (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
sensor_id INT,
temperature DECIMAL(5,2),
humidity DECIMAL(5,2),
pressure DECIMAL(7,2),
measured_at DATETIME
) ENGINE=ARCHIVE;
-- 매초마다 수천 개의 센서에서 데이터 수신
-- 데이터는 계속 쌓이기만 함
-- 실시간 조회는 거의 없고, 배치로 분석
또한 규정 준수를 위한 감사 로그에도 적합합니다. 금융 시스템이나 의료 시스템에서는 모든 작업을 기록하고 일정 기간 보관해야 합니다. 이런 데이터는 거의 조회되지 않지만 반드시 보관해야 하므로, 압축률이 높은 ARCHIVE가 유용합니다.
-- 감사 로그
CREATE TABLE audit_log (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT,
table_name VARCHAR(100),
operation VARCHAR(10),
old_value TEXT,
new_value TEXT,
created_at DATETIME
) ENGINE=ARCHIVE;
히스토리 데이터도 ARCHIVE에 적합합니다. 주문 내역, 결제 내역처럼 과거 데이터는 변경되지 않고 조회도 드뭅니다. 현재 진행 중인 주문은 InnoDB 테이블에 두고, 완료된 주문은 ARCHIVE 테이블로 옮길 수 있습니다.
-- 현재 주문 (InnoDB)
CREATE TABLE orders_active (
id BIGINT PRIMARY KEY,
user_id BIGINT,
status VARCHAR(20),
created_at DATETIME
) ENGINE=InnoDB;
-- 완료된 주문 (ARCHIVE)
CREATE TABLE orders_history (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT,
status VARCHAR(20),
created_at DATETIME,
completed_at DATETIME
) ENGINE=ARCHIVE;
-- 주문이 완료되면 히스토리로 이동
INSERT INTO orders_history
SELECT *, NOW() FROM orders_active WHERE id = ?;
DELETE FROM orders_active WHERE id = ?;
하지만 ARCHIVE의 제약사항을 명확히 이해해야 합니다. UPDATE와 DELETE가 안 되므로, 데이터 수정이 필요한 경우에는 사용할 수 없습니다. 인덱스가 제한적이므로, 복잡한 조회가 필요하면 부적합합니다. 트랜잭션이 없으므로, ACID가 필요한 데이터에는 사용하면 안 됩니다.
5. 스토리지 엔진 선택 전략 - 올바른 도구를 선택하기
지금까지 InnoDB, MyISAM, ARCHIVE의 특성을 살펴봤습니다. 이제 실제 프로젝트에서 어떻게 선택해야 하는지 구체적으로 정리해보겠습니다.
먼저 가장 중요한 원칙은 기본은 InnoDB를 사용하라는 것입니다. 특별한 이유가 없다면 InnoDB가 최선의 선택입니다. 트랜잭션이 필요한지 확실하지 않다면 InnoDB를 쓰는것이 좋습니다. 나중에 필요해질 수 있습니다. 외래키가 필요한지 모르겠다면 InnoDB를 쓰세요. 나중에 데이터 정합성을 보장해야 할 수도 있습니다.
InnoDB는 범용 엔진입니다. 대부분의 요구사항을 만족시키고, 성능도 충분히 좋으며, 안정성이 검증되었습니다. 고민되면 InnoDB를 선택하는 것이 안전합니다.
그렇다면 언제 다른 엔진을 고려해야 할까요? 테이블의 특성을 분석해야 합니다. 데이터의 생명주기는 어떠한가요? 읽기와 쓰기의 패턴은 무엇인가요? 데이터 정합성은 얼마나 중요한가요?
InnoDB를 사용해야 하는 경우:
핵심 비즈니스 데이터는 무조건 InnoDB입니다. 사용자, 주문, 결제, 재고 같은 데이터는 ACID가 필요하고, 외래키로 참조 무결성을 보장해야 하며, 동시성 제어가 중요합니다.
-- 핵심 비즈니스 테이블들
CREATE TABLE users (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
email VARCHAR(100) UNIQUE,
password_hash VARCHAR(255),
created_at DATETIME
) ENGINE=InnoDB;
CREATE TABLE orders (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT,
total_amount DECIMAL(10,2),
status VARCHAR(20),
created_at DATETIME,
FOREIGN KEY (user_id) REFERENCES users(id)
) ENGINE=InnoDB;
CREATE TABLE order_items (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
order_id BIGINT,
product_id BIGINT,
quantity INT,
price DECIMAL(10,2),
FOREIGN KEY (order_id) REFERENCES orders(id)
) ENGINE=InnoDB;
복잡한 쿼리가 필요한 경우도 InnoDB가 좋습니다. JOIN이 많고, 인덱스를 효율적으로 사용해야 하며, 쿼리 최적화가 중요한 경우입니다.
자주 UPDATE나 DELETE가 발생하는 테이블도 InnoDB여야 합니다. 행 수준 락으로 동시성을 유지하면서 데이터를 수정할 수 있습니다.
MyISAM을 고려할 수 있는 경우
읽기 전용이거나 거의 읽기만 하는 참조 데이터는 MyISAM을 고려할 수 있습니다. 하지만 현재는 InnoDB도 읽기 성능이 충분히 좋으므로, 꼭 필요한 경우가 아니면 InnoDB를 권장합니다.
-- 읽기 전용 참조 데이터
CREATE TABLE countries (
code CHAR(2) PRIMARY KEY,
name VARCHAR(100),
region VARCHAR(50)
) ENGINE=MyISAM;
-- 초기 로드 후 거의 변경 없음
-- 주로 조회만 함
Full-text 검색이 필요하고 InnoDB의 Full-text 성능이 부족한 경우, MyISAM을 시도해볼 수 있습니다. 하지만 이것도 요즘은 Elasticsearch 같은 전문 검색 엔진을 사용하는 것이 더 일반적입니다.
임시 테이블이나 세션 데이터처럼 손실되어도 크게 문제없는 데이터는 MyISAM을 쓸 수 있습니다. 크래시 복구가 중요하지 않고, 성능이 우선인 경우입니다.
ARCHIVE를 사용해야 하는 경우
INSERT만 하고 거의 조회하지 않는 로그 데이터는 ARCHIVE가 최적입니다. 압축률이 뛰어나서 디스크 공간을 크게 절약할 수 있습니다.
-- 웹 서버 액세스 로그
CREATE TABLE access_log (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
ip_address VARCHAR(45),
request_method VARCHAR(10),
request_uri VARCHAR(500),
response_code INT,
response_time INT,
user_agent TEXT,
referer TEXT,
created_at DATETIME
) ENGINE=ARCHIVE;
-- 하루 수백만 건씩 쌓임
-- 실시간 조회는 거의 없음
-- 배치로 분석하거나 규정 준수를 위해 보관
규정 준수를 위한 감사 로그도 ARCHIVE가 적합합니다. 법적으로 일정 기간 보관해야 하지만 거의 조회되지 않는 데이터입니다.
센서 데이터나 IoT 데이터처럼 대량으로 수집되지만 개별 조회는 드문 데이터도 ARCHIVE에 적합합니다.
히스토리 데이터를 아카이빙할 때도 ARCHIVE를 사용할 수 있습니다. 활성 데이터는 InnoDB에 두고, 오래된 데이터는 ARCHIVE로 옮겨서 디스크 공간을 절약합니다.
6. 하이브리드 전략 - 여러 엔진을 함께 사용하기
가장 효과적인 전략은 여러 스토리지 엔진을 함께 사용하는 것입니다. 각 테이블의 특성에 맞는 최적의 엔진을 선택하여, 시스템 전체의 성능과 효율성을 높일 수 있습니다.
전자상거래 시스템을 예로 들어보겠습니다. 핵심 비즈니스 테이블은 InnoDB를 사용합니다.
-- 핵심 비즈니스: InnoDB
CREATE TABLE users (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
email VARCHAR(100) UNIQUE,
name VARCHAR(50),
created_at DATETIME
) ENGINE=InnoDB;
CREATE TABLE products (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(200),
price DECIMAL(10,2),
stock INT,
category_id BIGINT
) ENGINE=InnoDB;
CREATE TABLE orders (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT,
total_amount DECIMAL(10,2),
status VARCHAR(20),
created_at DATETIME,
FOREIGN KEY (user_id) REFERENCES users(id)
) ENGINE=InnoDB;
참조 데이터는 MyISAM을 고려할 수 있습니다.
-- 참조 데이터: MyISAM (또는 InnoDB)
CREATE TABLE categories (
id BIGINT PRIMARY KEY,
name VARCHAR(100),
parent_id BIGINT
) ENGINE=MyISAM;
CREATE TABLE shipping_zones (
code VARCHAR(10) PRIMARY KEY,
region VARCHAR(50),
delivery_days INT
) ENGINE=MyISAM;
로그 데이터는 ARCHIVE를 사용합니다.
-- 로그 데이터: ARCHIVE
CREATE TABLE user_activity_log (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT,
action VARCHAR(50),
details TEXT,
created_at DATETIME
) ENGINE=ARCHIVE;
CREATE TABLE order_status_history (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
order_id BIGINT,
old_status VARCHAR(20),
new_status VARCHAR(20),
changed_at DATETIME
) ENGINE=ARCHIVE;
이렇게 구성하면 각 테이블이 자신의 특성에 맞는 엔진을 사용하여 최적의 성능을 낼 수 있습니다.
또 다른 전략은 데이터의 생명주기에 따라 엔진을 변경하는 것입니다. 예를 들어 주문 데이터는 처음에는 InnoDB 테이블에 저장하고, 완료되면 ARCHIVE 테이블로 옮깁니다.
-- 활성 주문: InnoDB (트랜잭션, 수정 필요)
CREATE TABLE orders_active (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT,
status VARCHAR(20),
total_amount DECIMAL(10,2),
created_at DATETIME,
updated_at DATETIME,
INDEX idx_status (status),
INDEX idx_user_id (user_id)
) ENGINE=InnoDB;
-- 완료된 주문: ARCHIVE (보관용, 압축)
CREATE TABLE orders_archive (
id BIGINT,
user_id BIGINT,
status VARCHAR(20),
total_amount DECIMAL(10,2),
created_at DATETIME,
completed_at DATETIME,
PRIMARY KEY (id)
) ENGINE=ARCHIVE;
-- 정기적으로 완료된 주문을 아카이브로 이동
-- 예: 30일이 지난 완료 주문
INSERT INTO orders_archive
SELECT id, user_id, status, total_amount, created_at, NOW()
FROM orders_active
WHERE status = 'COMPLETED'
AND created_at < DATE_SUB(NOW(), INTERVAL 30 DAY);
DELETE FROM orders_active
WHERE status = 'COMPLETED'
AND created_at < DATE_SUB(NOW(), INTERVAL 30 DAY);
하이브리드 전략으로 인해 활성 데이터는 InnoDB에서 빠르게 처리되고, 오래된 데이터는 ARCHIVE에서 압축되어 디스크 공간을 절약합니다. orders_active 테이블은 항상 작게 유지되어 성능이 좋고, 오래된 데이터는 필요할 때 orders_archive에서 조회할 수 있습니다.
파티셔닝과 결합하면 더욱 효과적입니다. 월별로 파티션을 나누고, 오래된 파티션은 다른 엔진으로 변경하거나 별도 테이블로 옮길 수 있습니다.
7. 성능 최적화와 모니터링
스토리지 엔진을 선택한 후에도 지속적인 모니터링과 최적화가 필요합니다. 각 엔진마다 특성이 다르므로, 튜닝 포인트도 다릅니다.
InnoDB의 경우, Buffer Pool 크기가 가장 중요합니다. InnoDB는 데이터와 인덱스를 메모리에 캐싱하는데, 이 캐시의 크기가 Buffer Pool입니다. 보통 전체 메모리의 70-80%를 Buffer Pool에 할당합니다.
-- InnoDB Buffer Pool 크기 확인
SHOW VARIABLES LIKE 'innodb_buffer_pool_size';
-- Buffer Pool 사용률 확인
SHOW STATUS LIKE 'Innodb_buffer_pool%';
Buffer Pool이 너무 작으면 디스크 I/O가 많이 발생하여 성능이 떨어집니다. 적절한 크기로 설정하면 대부분의 읽기가 메모리에서 처리되어 매우 빠릅니다.
Redo Log 크기도 중요합니다. Redo Log가 작으면 자주 flush가 발생하여 쓰기 성능이 떨어집니다. 충분히 크게 설정하되, 너무 크면 크래시 복구 시간이 길어집니다.
-- Redo Log 설정
innodb_log_file_size = 512M
innodb_log_files_in_group = 2
MyISAM의 경우, Key Buffer가 중요합니다. MyISAM은 인덱스를 Key Buffer에 캐싱하는데, 이것이 너무 작으면 인덱스 조회가 느려집니다.
-- Key Buffer 크기 설정
key_buffer_size = 256M
-- Key Buffer 사용률 확인
SHOW STATUS LIKE 'Key%';
ARCHIVE는 튜닝할 것이 많지 않습니다. 압축이 자동으로 이루어지고, 구조가 단순하기 때문입니다. 다만 대량 INSERT 시에는 배치로 처리하는 것이 효율적입니다.
-- 배치 INSERT가 더 효율적
INSERT INTO access_log VALUES
(NULL, '192.168.1.1', 'GET', '/index.html', 200, 50, '...', '...', NOW()),
(NULL, '192.168.1.2', 'GET', '/about.html', 200, 45, '...', '...', NOW()),
-- ... 수천 건
모니터링도 중요합니다. 각 테이블의 크기, 증가 속도, 쿼리 성능을 정기적으로 확인해야 합니다.
-- 테이블 크기 확인
SELECT
table_name,
engine,
ROUND(data_length / 1024 / 1024, 2) AS data_mb,
ROUND(index_length / 1024 / 1024, 2) AS index_mb,
ROUND((data_length + index_length) / 1024 / 1024, 2) AS total_mb
FROM information_schema.tables
WHERE table_schema = 'your_database'
ORDER BY (data_length + index_length) DESC;
이런 정보를 바탕으로 엔진 변경을 고려할 수 있습니다. InnoDB 테이블이 너무 커지고 있는데 대부분 로그 데이터라면, ARCHIVE로 변경을 검토할 수 있습니다.
마치며
스토리지 엔진을 정리하면서 가장 크게 느낀 점은 MySQL이라는 하나의 데이터베이스 안에서도
각 테이블이 서로 전혀 다른 성격을 가질 수 있다는 사실이었습니다.
그동안은 InnoDB를 기본값처럼 사용하면서도 왜 기본값이 InnoDB인지 깊이 생각해 본 적은 많지 않았습니다.
이번 정리를 통해 트랜잭션, 락, 장애 복구라는 요소들이 단순한 옵션이 아니라 스토리지 엔진 차원에서 결정된다는 점이 인상 깊었습니다.
이번 글을 통해 스토리지 엔진을 단순한 MySQL 설정 요소가 아니라,
데이터의 성격과 시스템 설계를 반영하는 중요한 결정 요소로 바라보게 되었습니다.
앞으로 테이블을 설계할 때는 어떤 스토리지 엔진이 가장 적절한지 한 번 더 고민해 보려고 합니다.
'데이터베이스' 카테고리의 다른 글
| MySQL SQL 파서(Parser)와 옵티마이저(Optimizer) 내부 동작 정리 (0) | 2026.02.06 |
|---|---|
| 데이터베이스 락의 모든 것 - 공유 락, 배타 락, 그리고 분산 락 (1) | 2026.02.01 |
| MYSQL DB 4가지 트랜잭션 격리레벨에 대해 (0) | 2026.01.30 |