5. 트랜잭션과 잠금
트랜잭션은 작업의 완전성을 보장해준다.
즉, 논리적인 작업 셋을 모두 완벽하게 처리하거나, 처리하지 못할 경우에는 원 상태로 복구해 이상현상이 발생하지 않게 만들어주는 기능이다.
잠금과 트랜잭션은 비슷한 개념 같지만 잠금은 동시성을 제어하기 위한, 트랜잭션은 데이터의 정합성을 보장하기 위한 기능이다.
잠금은 여러 커넥션에서 동시에 동일한 자원을 요청할 경우 순서대로 한 시점에는 하나의 커넥션만 변경할 수 있게 해주는 역할을 한다.
5.1 트랜잭션
5.1.1 MySQL에서의 트랜잭션
트랜잭션은 하나의 논리적인 작업 셋에 하나의 쿼리가 있든 두 개의 쿼리가 있든 논리적인 작업 셋 자체가 100% 적용되거나 아무것도 적용되지 않아야 함을 보장해주는 것이다.
간단한 예제로 트랜잭션 관점에서 InnoDB 테이블과 MyISAM 테이블의 차이를 살펴보자.
// 3 삽입
mysql> INSERT INTO tab_myisam (fdpk) VALUES (3);
mysql> INSERT INTO tab_innodb (fdpk) VALUES (3);
// 1,2,3 삽입
mysql> INSERT INTO tab_myisam (fdpk) VALUES (1),(2),(3);
ERROR 1062 (2300): Duplicate entry '3' for key 'PRIMARY'
mysql> INSERT INTO tab_innodb (fdpk) VALUES (1),(2),(3);
ERROR 1062 (2300): Duplicate entry '3' for key 'PRIMARY'
mysql> SELECT * FROM tab_myisam;
// 1,2,3 조회됨
mysql> SELECT * FROM tab_innodb;
// 3 조회됨
두 INSERT 문장 모두 프라이머리 키 중복 오류로 쿼리가 실패한다.
그런데 MyISAM 테이블에는 오류가 발생했음에도 '1'과 '2'는 INSERT된 상태로 남아있다. 즉, '1'과 '2'를 저장하고, '3'을 저장하려고 하는 순간 중복 키 오류가 발생한 것이다. 하지만 MyISAM 테이블에서는 INSERT된 '1'과 '2'를 그대로 두고 쿼리 실행을 종료해 버린다.
하지만 InnoDB는 쿼리 중 일부라도 오류가 발생하면 전체를 원 상태로 만든다는 트랜잭션의 원칙대로 INSERT 문장을 실행하기 전 상태로 복구했다. MyISAM 테이블에서 발생하는 이런 현상을 부분 업데이트(Partial Update)라고 표현하며, 이런 현상은 데이터의 정합성을 맞추는데 상당히 어려운 문제를 만들어 낸다.
부분 업데이트 현상이 발생하면 실패한 쿼리로 인해 남은 레코드를 다시 삭제하는 재처리 작업이 필요할 수 있는데 트랜잭션이 지원되는 InnoDB 테이블에서는 이런 현상을 쉽게 방지할 수 있으니 굳! 👍
5.2 MySQL 엔진의 잠금
MySQL에서 사용되는 잠금은 크게 스토리지 엔진 레벨과 MySQL 엔진 레벨로 나눌 수 있다. MySQL 엔진은 스토리지 엔진을 제외한 나머지 부분으로 이해하면 되는데, MySQL 엔진 레벨의 잠금은 모든 스토리지 엔진에 영향을 미치지만, 스토리지 엔진 레벨의 잠금은 스토리지 엔진 간 상호 영향을 미치지는 않는다.
MySQL 엔진에서는 테이블 데이터 동기화를 위한 테이블 락 이외에도 테이블의 구조를 잠그는 메타데이터 락(Metadata Lock) 그리고 사용자의 필요에 맞게 사용할 수 있는 네임드 락(Named Lock)이라는 잠금 기능도 제공한다. 이러한 잠금의 특징과 어떤 경우에 사용되는지 살펴보자!
5.2.1 글로벌 락
글로벌 락(Global Lock)은 "FLUSH TABLES WITH READ LOCK" 명령으로 획득할 수 있으며, MySQL에서 제공하는 잠금 가운데 가장 범위가 크다.
한 세션에서 글로벌 락을 획득하면 다른 세션에서 SELECT를 제외한 대부분의 DDL 문장이나 DML 문장을 실행하는 경우 글로벌 락이 해제될 때까지 해당 문장이 대기 상태로 남는다. 글로벌 락이 영향을 미치는 범위는 MySQL 서버 전체이며, 작업 대상 테이블이나 데이터베이스가 다르더라도 동일하게 영향을 미친다.
"FLUSH TABLES WITH READ LOCK" 명령을 이용한 글로벌 락은 MySQL 서버의 모든 변경 작업을 멈춘다. 하지만 MySQL 서버가 업그레드되면서 MyISAM이나 MEMORY 스토리지 엔진보다는 InnoDB 스토리지 엔진의 사용이 일반화됐다.
InnoDB 스토리지 엔진은 트랜잭션을 지원하기때문에 데이터 상태를 위해 모든 데이터 변경 작업을 멈출 필요는 없다. MySQL 8.0부터는 InnoDB가 기본 스토리지 엔진으로 채택되면서 조금 더 가벼운 글로벌 락의 필요성이 생기며 백업 락이 도입됐다.
5.2.2 테이블 락
테이블 락(Table Lock)은 개별 테이블 단위로 설정되는 잠금이며, 명시적 또는 묵시적으로 특정 테이블의 락을 획득할 수 있다. 명시적으로는 "Lock TABLES table_name [ READ | Write]" 명령으로 특정 테이블의 락을 획득할 수 있다.
명시적인 테이블 락도 특별한 상황이 아니면 애플리케이션에서 사용할 필요가 거의 없다. 명시적으로 테이블을 잠그는 작업은 글로벌 락과 동일하게 온라인 작업에 상당한 영향을 미치기 때문이다.
묵시적인 테이블 락은 MyISAM이나 MEMORY 테이블에 데이터를 변경하는 쿼리르 실행하면 발생한다. MySQL 서버가 데이터가 변경되는 테이블에 잠금을 설정하고 데이터를 변경한 후, 즉시 잠금을 해제하는 형태로 사용된다. 즉, 묵시적인 테이블 락은 쿼리가 실행되는 동안 자동으로 획득됐다가 쿼리가 완료된 후 자동해제된다.
하지만 InnoDB 테이블의 경우 스토리지 엔진 차원에서 레코드 기반의 잠금을 제공하기 때문에 단순 데이터 변경 쿼리로 인해 묵시적인 테이블 락이 설정되지는 않는다. 더 정확히는 InnoDB 테이블에도 테이블 락이 설정되지만 대부분의 데이터 변경(DML) 쿼리에서는 무시되고 스키마를 변경하는 쿼리(DDL)의 경우에만 영향을 미친다.
5.2.3 네임드 락
네임드 락(Named Lock)은 "GET_LOCK()" 함수를 이용해 임의의 문자열에 대해 잠금을 설정할 수 있다.
이 잠금의 특징은 대상이 테이블이나 레코드 또는 AUTO_INCREMENT와 같은 데이터베이스 객체가 아니라는 것이다. 네임드 락은 단순히 사용자가 지정한 문자열에 대해 획득하고 반납하는 잠금이다. 네임드 락은 자주 사용되지는 않는다. 예를 들어, 데이터베이스 서버 1대에 5대의 웹 서버가 접속해서 서비스하는 상황에서 여러 클라이언트가 상호 동기화를 처리해야 할 때 네임드 락을 이용하면 쉽게 해결할 수 있다.
또한 많은 레코드에 대해서 복잡한 요건으로 레코드를 변경하는 트랜잭션에 유용하게 사용할 수 있다. 배치 프로그램처럼 한 번에 많은 레코드를 변경하는 쿼리는 데드락의 원인이 되곤 한다. 각 프로그램의 실행 시간을 분산하거나 프로그램의 코드를 수정해서 데드락을 최소화할 수 있지만, 이는 간단한 방법도 아니고 완전한 해결책도 될 수 없다. 이러한 경우에 동일 데이터를 변경하거나 참조하는 프로그램끼리 분류해서 네임드 락을 걸고 쿼리를 실행하면 아주 간단히 해결할 수 있다.
5.2.4 메타데이터 락
메타데이터 락(Metadata Lock)은 데이터베이스 객체의 이름이나 구조를 변경하는 경우에 획득하는 잠금이다. 명시적으로 획득하거나 해제할 수 있는 것이 아니고 테이블의 이름을 변경하는 경우 자동으로 획득하는 잠금이다.
"RENAME TABLE" 명령의 경우 원본 이름과 변경될 이름 두 개 모두 한꺼번에 잠금을 설정한다.
그런데 MySQL 서버의 DDL은 단일 스레드로 작동하기에 테이블 구조를 변경할 때, 작업 과정이 오래 걸릴 수 있다. 만약 테이블의 구조를 변경할 시 새로운 구조의 테이블을 만들고 범위별로 INSERT 하여 여러개의 스레드로 빠르게 복사하는 것이 좋다.