카테고리 : MySQL/기술노트

MySQL 스토리지 엔진 구조: handler API와 엔진별 책임 범위

MySQL 서버 계층과 스토리지 엔진이 handler API로 협력하는 방식과 InnoDB, MEMORY, MyISAM 등 엔진별 운영 책임 범위를 정리한다.

저자: MySQL 기술 노트 작성: 2026.05.22 약 16분 9,417자
다운로드

1. 왜 스토리지 엔진 구조를 이해해야 하는가

MySQL은 하나의 데이터베이스 서버처럼 보이지만 내부적으로는 크게 두 계층으로 나뉜다. SQL을 해석하고 실행 계획을 세우며 권한, 파서, 옵티마이저, 복제 SQL 계층을 담당하는 MySQL 서버 계층이 있고, 실제 레코드 저장, 인덱스 탐색, 잠금, 트랜잭션, 복구를 담당하는 스토리지 엔진 계층이 있다. 이 두 계층을 연결하는 핵심 인터페이스가 handler API다.

운영자가 이 구조를 이해하지 못하면 같은 SELECT, UPDATE, ALTER TABLE이라도 어떤 동작은 서버 계층의 책임이고 어떤 동작은 InnoDB 같은 엔진의 책임인지 구분하기 어렵다. 예를 들어 다음과 같은 질문은 모두 스토리지 엔진 경계와 연결된다.

  • EXPLAIN에는 인덱스를 사용한다고 나오는데 실제 I/O가 크게 발생하는가?
  • COUNT(*)가 어떤 엔진에서는 빠르고 InnoDB에서는 테이블 크기에 따라 달라지는가?
  • 왜 트랜잭션 격리 수준과 row lock은 InnoDB에서 중요하지만 MEMORY 엔진에서는 의미가 제한적인가?
  • ALTER TABLE이 어떤 경우에는 즉시 끝나고 어떤 경우에는 긴 rebuild와 잠금을 유발하는가?
  • 왜 Aurora MySQL은 MySQL 호환 SQL을 제공하지만 저장·복구·복제의 운영 특성이 일반 MySQL과 다른가?

MySQL 운영의 많은 오해는 “MySQL 서버가 모든 것을 직접 한다”는 단순화에서 생긴다. 실제로 MySQL 서버는 SQL 실행의 지휘자이고, 스토리지 엔진은 데이터 접근과 영속성의 현장 작업자다. handler API는 이 지휘자와 작업자가 대화하는 약속이다.

2. MySQL의 계층 구조와 handler API의 위치

MySQL의 실행 경로를 단순화하면 다음과 같다.

  1. 클라이언트가 SQL을 전송한다.
  2. MySQL 서버 계층이 인증, 권한 확인, 파싱, 전처리, 옵티마이저 실행을 수행한다.
  3. 옵티마이저가 접근 경로를 선택한다. 예를 들어 full table scan, range scan, ref lookup, index merge 등을 결정한다.
  4. 실행기가 선택된 계획에 따라 스토리지 엔진의 handler 메서드를 호출한다.
  5. 스토리지 엔진은 인덱스와 데이터 페이지를 탐색하고, 필요한 레코드를 서버 계층에 반환한다.
  6. 서버 계층은 조건 평가, 조인, 정렬, 그룹화, 결과 반환을 수행한다. 단, 일부 조건이나 집계는 엔진으로 내려갈 수 있다.

handler API는 C++ 계층에서 각 스토리지 엔진이 구현해야 하는 공통 인터페이스다. 실제 운영자가 코드를 직접 다루지는 않더라도 이 인터페이스의 성격을 알면 EXPLAIN, SHOW ENGINE INNODB STATUS, Performance Schema 지표를 해석하기 쉬워진다.

대표적인 handler 동작은 개념적으로 다음과 같다.

동작 범주 개념적 handler 호출 의미
테이블 열기 open 엔진이 테이블 핸들을 준비한다.
전체 스캔 시작 rnd_init 순차 읽기를 준비한다.
다음 행 읽기 rnd_next 테이블 스캔에서 다음 레코드를 반환한다.
인덱스 탐색 시작 index_init 특정 인덱스를 통한 접근을 준비한다.
인덱스 위치 찾기 index_read 키 조건에 맞는 첫 위치를 찾는다.
인덱스 다음 행 index_next 인덱스 순서상 다음 레코드를 반환한다.
행 삽입 write_row 새 레코드를 엔진에 저장한다.
행 수정 update_row 기존 레코드를 변경한다.
행 삭제 delete_row 기존 레코드를 삭제한다.
통계 조회 records, info 옵티마이저가 사용할 비용 추정 정보를 제공한다.

이 표는 운영자가 기억해야 할 핵심을 보여준다. 옵티마이저가 “어떤 순서와 방식으로 읽을지”를 정하면, 실제 행을 찾고 잠그고 페이지를 읽고 변경하는 일은 엔진이 담당한다.

3. 서버 계층의 책임: SQL 의미와 실행 조율

MySQL 서버 계층은 스토리지 엔진에 독립적인 공통 기능을 제공한다. 주요 책임은 다음과 같다.

3.1 파서, 전처리기, 권한 확인

SQL 문법을 해석하고, 테이블과 컬럼 이름을 resolve하며, 사용자가 해당 객체에 접근할 수 있는지 확인한다. 이 단계는 엔진과 무관하다. InnoDB 테이블이든 MEMORY 테이블이든 SELECT 권한 확인은 서버 계층에서 수행된다.

3.2 옵티마이저와 실행 계획

옵티마이저는 조인 순서, 인덱스 선택, 조건 적용 순서, 임시 테이블 사용 여부, 정렬 전략을 결정한다. 엔진은 인덱스 통계와 비용 추정 정보를 제공하지만, 최종 실행 계획을 조율하는 주체는 서버 계층이다.

예를 들어 EXPLAINtype, key, rows, filtered, Extra는 서버 계층의 계획 판단을 보여준다. 하지만 이 판단은 엔진이 제공하는 통계 품질에 의존한다. InnoDB 통계가 오래되었거나 샘플링 편차가 크면 서버 계층의 계획도 잘못될 수 있다.

3.3 SQL 레벨 조건 평가와 결과 처리

서버 계층은 스토리지 엔진에서 받은 행에 대해 SQL 조건을 평가하고, 조인 결과를 만들고, 정렬과 그룹화를 수행한다. 일부 조건은 Index Condition Pushdown처럼 엔진에 내려갈 수 있지만 모든 SQL 표현식이 엔진 내부에서 처리되는 것은 아니다.

운영 관점에서는 Using where, Using index condition, Using temporary, Using filesort 같은 실행 계획 표시를 서버 계층과 엔진 계층의 협업 결과로 읽어야 한다.

3.4 바이너리 로그와 복제 SQL 의미

일반 MySQL에서 binary log는 서버 계층 기능이다. ROW, STATEMENT, MIXED 포맷에 따라 변경 내용을 기록하고 복제 소스에서 replica로 전달한다. 반면 redo log, undo log, doublewrite buffer 같은 복구 구조는 InnoDB 엔진 책임이다. 이 구분은 장애 복구와 복제 지연을 분석할 때 중요하다.

4. 스토리지 엔진의 책임: 데이터 접근, 잠금, 복구

스토리지 엔진은 테이블의 물리적 저장 방식과 레코드 접근 방식을 구현한다. MySQL은 여러 엔진을 지원하지만 현대 운영 환경에서 일반 업무 테이블은 대부분 InnoDB를 사용한다. 그래도 엔진별 책임 범위를 이해해야 하는 이유는 임시 테이블, MEMORY 테이블, 과거 MyISAM 테이블, 시스템 테이블, Aurora 스토리지 계층 차이가 여전히 운영에 영향을 주기 때문이다.

4.1 레코드와 인덱스 저장

InnoDB는 clustered index 구조를 사용한다. primary key의 B+Tree leaf page에 실제 레코드가 저장되고, secondary index leaf에는 secondary key와 primary key 값이 저장된다. 따라서 secondary index로 행을 찾을 때 필요한 컬럼이 인덱스에 모두 없으면 primary key를 통해 clustered index를 다시 찾는 bookmark lookup이 발생한다.

MyISAM은 데이터 파일과 인덱스 파일을 분리하고, 인덱스 leaf가 데이터 파일 위치를 가리키는 구조를 사용한다. MEMORY 엔진은 메모리 기반 hash 또는 BTREE 인덱스를 사용할 수 있으나 서버 재시작 시 데이터가 사라진다.

이 차이는 같은 SQL이라도 I/O 패턴과 장애 복구 특성이 다르다는 뜻이다.

4.2 트랜잭션과 MVCC

InnoDB는 트랜잭션, row-level lock, MVCC, crash recovery를 제공한다. REPEATABLE READ에서 consistent read를 제공하기 위해 undo log 기반 read view를 사용하고, 변경 사항의 지속성을 위해 redo log를 사용한다.

반면 MyISAM은 트랜잭션을 제공하지 않고 table-level lock 중심으로 동작한다. MEMORY 엔진도 일반적인 영속 트랜잭션 저장소로 설계된 엔진이 아니다. 따라서 START TRANSACTION을 사용한다고 해서 모든 엔진이 동일한 원자성·격리성·복구성을 제공하는 것은 아니다.

4.3 잠금 구현

서버 계층에도 metadata lock, table lock 같은 공통 잠금이 있지만, row lock, gap lock, next-key lock 같은 세부 동시성 제어는 InnoDB 엔진의 책임이다. 그래서 lock wait, deadlock, gap lock으로 인한 insert 대기 문제를 분석할 때는 InnoDB 관점의 진단이 필요하다.

4.4 통계와 비용 정보

스토리지 엔진은 테이블·인덱스 통계를 서버 계층에 제공한다. 옵티마이저는 이 정보를 이용해 비용을 계산한다. InnoDB의 persistent statistics, histogram, ANALYZE TABLE은 모두 실행 계획 안정성과 연결된다.

잘못된 통계는 handler 호출 패턴을 바꾼다. 예를 들어 작은 range scan으로 예상했지만 실제로는 많은 레코드를 읽는 경우, Handler_read_next가 급증하거나 buffer pool read가 증가할 수 있다.

5. 주요 스토리지 엔진별 운영 해석

5.1 InnoDB: 기본 업무 엔진

InnoDB는 MySQL의 기본 스토리지 엔진이며 대부분의 운영 테이블에 적합하다. 핵심 특성은 다음과 같다.

  • ACID 트랜잭션 지원
  • row-level locking과 MVCC
  • clustered index 기반 저장
  • redo log와 undo log 기반 crash recovery
  • foreign key 지원
  • buffer pool 중심 캐싱
  • online DDL 기능 일부 지원

운영자는 InnoDB를 단순히 “데이터를 저장하는 모듈”이 아니라 MySQL 성능과 안정성의 중심으로 봐야 한다. buffer pool hit ratio, redo log pressure, purge lag, history list length, lock wait, deadlock, checkpoint age는 모두 InnoDB 내부 상태를 반영한다.

5.2 MyISAM: 과거 호환성과 제한적 용도

MyISAM은 과거 MySQL에서 널리 쓰였지만 현대 운영 업무 테이블에는 권장하기 어렵다. 트랜잭션과 crash recovery가 제한적이며 table-level lock으로 쓰기 동시성도 낮다. 다만 오래된 시스템이나 특정 읽기 중심 테이블에서 남아 있을 수 있으므로 엔진 점검 대상이다.

MyISAM 테이블이 운영 DB에 남아 있으면 장애 후 복구, 백업 일관성, 복제 장애 시 재처리 방식에서 InnoDB와 다른 절차가 필요할 수 있다.

5.3 MEMORY: 빠른 임시성의 대가

MEMORY 엔진은 데이터를 메모리에 저장한다. 서버 재시작 시 데이터가 사라지고, row format과 인덱스 특성이 InnoDB와 다르다. 사용자가 명시적으로 MEMORY 테이블을 만들 수도 있고, 내부 임시 테이블 처리에서 메모리 기반 구조가 사용될 수 있다.

운영 관점에서 MEMORY는 영속 저장소가 아니라 임시 계산 공간에 가깝다. 크기 제한, 메모리 사용량, 재시작 시 데이터 소실을 고려해야 한다.

5.4 CSV, ARCHIVE, FEDERATED 등 특수 엔진

특수 엔진은 특정 목적을 위해 존재하지만 일반 OLTP 테이블의 기본 선택지는 아니다. 예를 들어 CSV 엔진은 파일 호환성이 목적이고, ARCHIVE는 압축 저장에 초점이 있으며, FEDERATED는 원격 테이블 접근을 제공한다. 이런 엔진은 장애·성능·백업 특성이 일반적이지 않으므로 명확한 목적 없이 사용하면 운영 위험이 커진다.

6. handler API 관점으로 읽는 실행 지표

MySQL 상태 변수에는 handler 호출 패턴을 간접적으로 보여주는 값들이 있다. 이 값들은 SQL 전체를 완벽히 설명하지는 않지만 실행 경향을 파악하는 데 유용하다.

SHOW GLOBAL STATUS LIKE 'Handler_read%';
SHOW GLOBAL STATUS LIKE 'Handler_write';
SHOW GLOBAL STATUS LIKE 'Handler_update';
SHOW GLOBAL STATUS LIKE 'Handler_delete';

실행 결과(MySQL 8.0.46):

+-----------------------+-------+
| Variable_name         | Value |
+-----------------------+-------+
| Handler_read_first    | 41    |
| Handler_read_key      | 1726  |
| Handler_read_last     | 0     |
| Handler_read_next     | 4040  |
| Handler_read_prev     | 0     |
| Handler_read_rnd      | 0     |
| Handler_read_rnd_next | 191   |
+-----------------------+-------+
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| Handler_write | 15    |
+---------------+-------+
+----------------+-------+
| Variable_name  | Value |
+----------------+-------+
| Handler_update | 331   |
+----------------+-------+
+----------------+-------+
| Variable_name  | Value |
+----------------+-------+
| Handler_delete | 8     |
+----------------+-------+

대표 지표의 해석은 다음과 같다.

상태 변수 운영 해석
Handler_read_key 인덱스 키 기반으로 행을 찾은 횟수다. 높다고 항상 좋다는 뜻은 아니지만 인덱스 접근이 많음을 의미한다.
Handler_read_next 인덱스 순서로 다음 행을 읽은 횟수다. range scan이나 index scan에서 증가한다.
Handler_read_rnd_next 테이블 스캔 형태의 다음 행 읽기에서 증가한다. 불필요하게 높으면 full scan 증가를 의심한다.
Handler_write 행 삽입 작업이 발생한 횟수다. 내부 임시 테이블 쓰기도 영향을 줄 수 있다.
Handler_update 행 수정 작업이 발생한 횟수다.
Handler_delete 행 삭제 작업이 발생한 횟수다.

특정 세션에서 쿼리 전후 차이를 보고 싶다면 다음처럼 확인할 수 있다.

DROP TABLE IF EXISTS orders;

CREATE TABLE orders (
    order_id BIGINT PRIMARY KEY AUTO_INCREMENT,
    customer_id BIGINT NOT NULL,
    order_date DATE NOT NULL,
    KEY ix_order_date_customer (order_date, customer_id)
) ENGINE=InnoDB;

INSERT INTO orders (customer_id, order_date) VALUES
(1001, '2026-01-02'), (1001, '2026-01-03'), (1002, '2025-12-31');

SHOW SESSION STATUS LIKE 'Handler_read%';

SELECT /* 점검 대상 쿼리 */
       customer_id, COUNT(*)
FROM orders
WHERE order_date >= '2026-01-01'
GROUP BY customer_id;

SHOW SESSION STATUS LIKE 'Handler_read%';

실행 결과(MySQL 8.0.46):

Note (Code 1051): Unknown table 'mysql_tech_note.orders'
+-----------------------+-------+
| Variable_name         | Value |
+-----------------------+-------+
| Handler_read_first    | 0     |
| Handler_read_key      | 43    |
| Handler_read_last     | 0     |
| Handler_read_next     | 15    |
| Handler_read_prev     | 0     |
| Handler_read_rnd      | 0     |
| Handler_read_rnd_next | 0     |
+-----------------------+-------+
+-------------+----------+
| customer_id | COUNT(*) |
+-------------+----------+
|        1001 |        2 |
+-------------+----------+
+-----------------------+-------+
| Variable_name         | Value |
+-----------------------+-------+
| Handler_read_first    | 0     |
| Handler_read_key      | 46    |
| Handler_read_last     | 0     |
| Handler_read_next     | 17    |
| Handler_read_prev     | 0     |
| Handler_read_rnd      | 0     |
| Handler_read_rnd_next | 2     |
+-----------------------+-------+

실무에서는 이 값을 단독 판단 기준으로 쓰기보다 EXPLAIN ANALYZE, Performance Schema, slow query log와 함께 본다.

EXPLAIN ANALYZE
SELECT customer_id, COUNT(*)
FROM orders
WHERE order_date >= '2026-01-01'
GROUP BY customer_id;

EXPLAIN ANALYZE는 옵티마이저의 예상 행 수와 실제 실행 행 수 차이를 보여주므로, 엔진 통계와 handler 호출 패턴이 계획과 얼마나 맞는지 확인하는 데 도움이 된다.

7. 엔진 확인과 운영 진단 SQL

운영 환경에서는 먼저 어떤 테이블이 어떤 엔진을 사용하는지 확인해야 한다.

SELECT table_schema,
       table_name,
       engine,
       table_rows,
       data_length,
       index_length
FROM information_schema.tables
WHERE table_schema NOT IN ('mysql', 'performance_schema', 'information_schema', 'sys')
ORDER BY table_schema, table_name;

실행 결과(MySQL 8.0.46):

+-----------------+------------+--------+------------+-------------+--------------+
| TABLE_SCHEMA    | TABLE_NAME | ENGINE | TABLE_ROWS | DATA_LENGTH | INDEX_LENGTH |
+-----------------+------------+--------+------------+-------------+--------------+
| mysql_tech_note | orders     | InnoDB |          3 |       16384 |        16384 |
+-----------------+------------+--------+------------+-------------+--------------+

InnoDB가 아닌 테이블만 찾으려면 다음 쿼리를 사용할 수 있다.

SELECT table_schema,
       table_name,
       engine
FROM information_schema.tables
WHERE table_schema NOT IN ('mysql', 'performance_schema', 'information_schema', 'sys')
  AND table_type = 'BASE TABLE'
  AND engine <> 'InnoDB'
ORDER BY table_schema, table_name;

테이블별 인덱스 구성은 다음처럼 확인한다.

SELECT table_schema,
       table_name,
       index_name,
       non_unique,
       seq_in_index,
       column_name,
       cardinality
FROM information_schema.statistics
WHERE table_schema = 'app'
ORDER BY table_name, index_name, seq_in_index;

InnoDB 내부 상태는 다음 명령으로 볼 수 있다.

SELECT NAME, SUBSYSTEM, COUNT, COMMENT
FROM information_schema.innodb_metrics
WHERE NAME IN ('trx_rw_commits', 'trx_ro_commits', 'buffer_pool_reads')
ORDER BY NAME;

실행 결과(MySQL 8.0.46):

+-------------------+-------------+-------+---------------------------------------------------------------+
| NAME              | SUBSYSTEM   | COUNT | COMMENT                                                       |
+-------------------+-------------+-------+---------------------------------------------------------------+
| buffer_pool_reads | buffer      |  1011 | Number of reads directly from disk (innodb_buffer_pool_reads) |
| trx_ro_commits    | transaction |     0 | Number of read-only transactions committed                    |
| trx_rw_commits    | transaction |     0 | Number of read-write transactions committed                   |
+-------------------+-------------+-------+---------------------------------------------------------------+

MySQL 8.0에서는 Performance Schema와 sys schema를 함께 활용한다.

SELECT *
FROM sys.schema_table_statistics
WHERE table_schema = 'app'
ORDER BY rows_fetched DESC
LIMIT 20;

SELECT *
FROM sys.schema_index_statistics
WHERE table_schema = 'app'
ORDER BY rows_selected DESC
LIMIT 20;

이 결과는 어떤 테이블과 인덱스가 많이 읽히는지, 인덱스 접근이 실제로 유효한지 파악하는 출발점이다.

8. 서버 계층과 엔진 계층을 혼동할 때 생기는 오해

8.1 COUNT(*)는 항상 메타데이터만 읽는다는 오해

InnoDB에서 조건 없는 COUNT(*)는 일반적으로 테이블 또는 인덱스를 스캔해야 한다. MVCC 때문에 각 트랜잭션 read view에서 보이는 행 수가 다를 수 있으므로 단순한 전역 카운터만으로 정확한 결과를 반환하기 어렵다. 반면 MyISAM은 테이블 메타데이터에 row count를 유지하므로 특정 조건에서 빠르게 반환할 수 있다.

따라서 큰 InnoDB 테이블의 SELECT COUNT(*) FROM big_table;은 운영 중 부하를 만들 수 있다. 대시보드나 배치에서 반복 호출하는 경우 근사치, 별도 집계 테이블, 파티션 단위 집계 등을 검토해야 한다.

8.2 WHERE 조건은 모두 엔진에서 처리된다는 오해

조건 중 일부는 인덱스 탐색 조건으로 사용되고, 일부는 엔진의 index condition으로 내려가며, 일부는 서버 계층에서 행을 받은 뒤 평가된다. 함수 적용, 형 변환, collation, 복잡한 표현식은 인덱스 사용성을 떨어뜨릴 수 있다.

DROP TABLE IF EXISTS users;

CREATE TABLE users (
    user_id BIGINT PRIMARY KEY AUTO_INCREMENT,
    created_at DATETIME NOT NULL,
    KEY ix_created_at (created_at)
) ENGINE=InnoDB;

INSERT INTO users (created_at) VALUES ('2026-05-22 09:00:00'), ('2026-05-23 09:00:00');

-- 인덱스 사용성이 낮아질 수 있는 예
SELECT *
FROM users
WHERE DATE(created_at) = '2026-05-22';

-- 범위 조건으로 바꾸면 인덱스 접근 가능성이 높아진다
SELECT *
FROM users
WHERE created_at >= '2026-05-22 00:00:00'
  AND created_at <  '2026-05-23 00:00:00';

DROP TABLE users;

이 차이는 옵티마이저가 handler API를 어떤 방식으로 호출할 수 있는지를 바꾼다. 첫 번째 쿼리는 인덱스 컬럼에 함수가 적용되어 효율적 range scan이 어려울 수 있고, 두 번째 쿼리는 created_at 인덱스를 통한 범위 탐색이 가능하다.

8.3 트랜잭션은 SQL 계층만의 기능이라는 오해

COMMIT, ROLLBACK 문법은 서버 계층에서 처리되지만 실제 변경의 원자성, undo, redo, row lock은 엔진이 구현한다. InnoDB가 아닌 엔진을 섞어 쓰면 하나의 트랜잭션처럼 보이는 작업도 엔진별로 일관성이 달라질 수 있다. 운영 DB에서 업무 테이블 엔진을 통일해야 하는 이유다.

8.4 ALTER TABLE은 모두 같은 비용이라는 오해

DDL은 서버 계층에서 SQL로 시작되지만 실제 알고리즘은 엔진 기능에 크게 의존한다. MySQL 8.0 InnoDB는 instant DDL, inplace DDL, copy 방식 등 다양한 경로를 제공한다. 어떤 변경은 메타데이터만 바꾸고 끝나지만, 어떤 변경은 전체 테이블 rebuild와 긴 redo/undo·I/O 부하를 만든다.

ALTER TABLE orders ADD COLUMN memo varchar(255), ALGORITHM=INSTANT;

실행 결과(MySQL 8.0.46):

mysql> ALTER TABLE orders ADD COLUMN memo varchar(255), ALGORITHM=INSTANT;
Query OK, 0 rows affected (0.02 sec)
Records: 0  Duplicates: 0  Warnings: 0

가능한 경우 ALGORITHMLOCK 옵션을 명시해 의도하지 않은 copy DDL을 피한다. 단, 모든 변경이 instant를 지원하지 않으므로 사전 검증이 필요하다.

9. Aurora MySQL에서의 차이점

Aurora MySQL은 MySQL 호환 SQL 계층을 제공하지만 스토리지 아키텍처는 일반 MySQL InnoDB와 다르다. 운영자가 특히 주의할 차이는 다음과 같다.

9.1 분산 스토리지와 redo 중심 전파

일반 MySQL은 로컬 디스크의 InnoDB tablespace와 redo log를 중심으로 동작한다. Aurora는 compute와 storage를 분리하고, 다중 AZ에 분산된 스토리지 계층에 로그 레코드 중심으로 변경을 전파하는 구조를 사용한다. SQL 계층과 InnoDB 호환 동작은 유지되지만, 장애 복구와 replica 지연, 스토리지 확장 특성은 일반 MySQL과 다르게 나타난다.

9.2 백업과 복구의 운영 의미

Aurora의 continuous backup과 point-in-time restore는 스토리지 서비스와 깊게 통합되어 있다. 따라서 백업 파일을 직접 다루는 전통적인 mysqldump 또는 physical backup 운영과는 절차가 다르다. 논리 백업은 여전히 필요할 수 있지만, 장애 복구 기본 전략은 클러스터 스냅샷, PITR, 복제 클러스터, 글로벌 데이터베이스 등 Aurora 기능과 함께 설계한다.

9.3 Performance Insights와 엔진 지표 해석

Aurora에서도 SQL 실행, lock wait, buffer cache, redo 관련 지표를 봐야 하지만 CloudWatch, Performance Insights, Enhanced Monitoring이 중요한 관측 도구가 된다. 일반 MySQL의 OS 파일 I/O 지표와 Aurora 스토리지 지표는 1:1로 대응하지 않는다. 예를 들어 로컬 디스크 병목처럼 보이는 현상을 Aurora에서는 스토리지 네트워크, commit latency, replica lag, wait event 관점으로 다시 해석해야 한다.

9.4 엔진 선택 제약

Aurora MySQL에서는 일반 MySQL과 달리 지원 엔진과 운영 권장사항이 제한될 수 있다. 업무 테이블은 InnoDB 기반으로 설계하는 것이 사실상 표준이며, 특수 엔진 의존 설계는 이식성과 운영 안정성을 떨어뜨린다.

10. 장애와 성능 문제에서의 해석 방법

스토리지 엔진 경계를 이해하면 장애 분석의 질문이 더 정확해진다.

10.1 full scan 증가

증상은 CPU 증가, buffer pool read 증가, Handler_read_rnd_next 증가, slow query 증가로 나타날 수 있다. 이때 원인은 서버 계층의 잘못된 실행 계획일 수도 있고, 엔진 통계 부정확성이나 인덱스 설계 문제일 수도 있다.

점검 순서는 다음과 같다.

EXPLAIN ANALYZE
SELECT customer_id, COUNT(*)
FROM orders
WHERE order_date >= '2026-01-01'
GROUP BY customer_id;

SHOW SESSION STATUS LIKE 'Handler_read%';

ANALYZE TABLE orders;
DROP TABLE orders;

ANALYZE TABLE은 통계 갱신에 도움이 될 수 있지만, 근본적으로 인덱스 설계나 SQL 조건이 잘못된 경우에는 해결책이 아니다.

10.2 lock wait와 deadlock

lock wait는 단순히 “MySQL이 느리다”가 아니라 엔진의 동시성 제어에서 대기가 발생한 것이다. InnoDB에서는 다음 정보를 함께 본다.

SELECT *
FROM performance_schema.data_locks\G

SELECT *
FROM performance_schema.data_lock_waits\G

SELECT NAME, COUNT
FROM information_schema.innodb_metrics
WHERE NAME IN ('lock_deadlocks', 'lock_timeouts', 'lock_row_lock_waits')
ORDER BY NAME;

실행 결과(MySQL 8.0.46):

+---------------------+-------+
| NAME                | COUNT |
+---------------------+-------+
| lock_deadlocks      |     0 |
| lock_row_lock_waits |     0 |
| lock_timeouts       |     0 |
+---------------------+-------+

운영자는 대기 중인 SQL, 보유 중인 lock, 인덱스 조건, 트랜잭션 지속 시간을 함께 봐야 한다. 인덱스가 부적절하면 필요 이상으로 많은 레코드나 gap을 잠글 수 있다.

10.3 redo log pressure와 checkpoint 지연

쓰기 부하가 높은 시스템에서는 InnoDB redo log와 checkpoint가 성능 병목이 될 수 있다. 일반 MySQL에서는 redo log 용량, flush 정책, 디스크 지연을 본다. Aurora에서는 commit latency, storage write latency, wait event와 클러스터 지표를 함께 봐야 한다.

10.4 임시 테이블과 디스크 spill

서버 계층의 정렬·그룹화·조인 처리에서 내부 임시 테이블이 만들어질 수 있다. 메모리 내 처리로 끝나지 않으면 디스크 임시 테이블이 발생한다. 이때 스토리지 엔진 자체의 row 접근뿐 아니라 서버 계층의 실행 처리 비용이 커진다.

SHOW GLOBAL STATUS LIKE 'Created_tmp%';

SELECT event_name,
       count_star,
       sum_timer_wait
FROM performance_schema.events_waits_summary_global_by_event_name
WHERE event_name LIKE 'wait/io/file/%tmp%'
ORDER BY sum_timer_wait DESC;

11. 운영 체크리스트

스토리지 엔진과 handler API 관점에서 운영 DB를 점검할 때는 다음 항목을 확인한다.

  • 주요 쿼리에 대해 EXPLAIN ANALYZE
  • Handler_read_rnd_next, Handler_read_next, Handler_read_key
  • 큰 테이블의 COUNT(*)
  • DDL 실행 전에 ALGORITHM, LOCK

12. 실무 의사결정 기준

12.1 새 테이블의 엔진 선택

일반 업무 테이블은 InnoDB를 기본값으로 삼는다. 트랜잭션, 복구, 동시성, 외래 키, 운영 도구 호환성을 고려하면 다른 엔진을 선택할 이유는 제한적이다. 특수 엔진은 명확한 근거와 장애 시나리오가 있을 때만 사용한다.

DROP TABLE IF EXISTS payment_events;

CREATE TABLE payment_events (
    id bigint unsigned NOT NULL AUTO_INCREMENT,
    order_id bigint unsigned NOT NULL,
    event_type varchar(50) NOT NULL,
    created_at datetime(6) NOT NULL,
    payload json NULL,
    PRIMARY KEY (id),
    KEY idx_order_created (order_id, created_at),
    KEY idx_created (created_at)
) ENGINE=InnoDB;

DROP TABLE payment_events;

12.2 쿼리 튜닝의 출발점

쿼리 튜닝은 SQL 문장만 보는 일이 아니다. 서버 계층의 실행 계획과 엔진 계층의 실제 읽기 방식을 함께 봐야 한다.

  1. EXPLAIN ANALYZE로 계획과 실제 실행량을 확인한다.
  2. 인덱스가 조건, 조인, 정렬을 얼마나 지원하는지 본다.
  3. handler 상태 변수나 Performance Schema로 실제 읽기 패턴을 확인한다.
  4. InnoDB buffer pool, I/O, lock, redo 상태를 함께 본다.
  5. 필요하면 통계 갱신, 인덱스 변경, SQL 재작성, 배치 시간 조정을 수행한다.

12.3 엔진 혼합 사용의 위험 평가

하나의 애플리케이션 스키마 안에서 InnoDB와 비트랜잭션 엔진이 섞이면 장애 시 일관성 해석이 어려워진다. 특히 결제, 주문, 재고, 계정 같은 핵심 업무 데이터는 엔진 혼합을 피해야 한다. 운영 중 발견된 비표준 엔진 테이블은 즉시 삭제할 대상이 아니라, 용도와 의존성을 확인한 뒤 전환 계획을 세워야 한다.

13. 결론

MySQL의 스토리지 엔진 구조는 단순한 내부 구현 세부사항이 아니라 운영 판단의 기본 프레임이다. 서버 계층은 SQL 의미를 해석하고 실행 계획을 조율하며, 스토리지 엔진은 실제 레코드 접근, 인덱스 탐색, 잠금, 트랜잭션, 복구를 담당한다. handler API는 이 둘을 연결하는 계약이며, 실행 계획과 상태 변수는 이 계약이 실제로 어떻게 사용되었는지를 보여주는 단서다.

현대 MySQL 운영의 중심은 InnoDB이지만, MySQL이 여러 엔진을 지원한다는 사실은 여전히 중요하다. 엔진마다 영속성, 잠금, 통계, 복구, DDL 특성이 다르기 때문이다. Aurora MySQL처럼 SQL 호환성을 유지하면서 스토리지 계층을 다르게 구현한 환경에서는 이 구분이 더 중요해진다.

다음 글들에서는 InnoDB 내부 구조, buffer pool, redo/undo, MVCC, lock과 같은 엔진 내부 주제를 더 구체적으로 다룰 수 있다. 스토리지 엔진 경계를 먼저 이해하면 이후의 성능 튜닝과 장애 분석이 “증상 대응”이 아니라 “동작 원리에 기반한 운영 판단”으로 바뀐다.