요즘 흔히 말하는 stateless한 웹 어플리케이션을 개발한다고 했을 때 가장 단순하게 DB와 웹 어플리케이션의 관계를 표현 해본 그림이다. 여러개의 웹 어플리케이션이 분산되어 로직을 처리하고 이에 대한 상태는 DB에 저장된다. 그렇다면 DBMS에선 어떻게 여러 곳에서 발생하는 요청에 대한 read나 write에 대하여 어긋남이 없이 정합성을 보장할 수 있는 것 일까?
DBMS에서 실행되는 하나의 작업단위는 트랜잭션으로 표현된다. 트랜잭션의 가장 기본적인 동작원리는 All or Nothing이다. 예를 들어 특정 테이블의 row를 제거하는 경우 이 테이블과 연관관계에 있는 다른 테이블의 row도 제거되도록 정의를 해두었을 때 이는 하나의 트랜잭션이 될 수 있다. 그러면 해당 트랜잭션은 2개의 row가 모두 제거되어 성공적으로 commiteted가 되거나 모종의 이유로 하나의 row만 제거되고 다른 하나의 row의 제거에 실패된 경우에는 해당 트랜잭션은 aborted되고 rollback이 되어 이전의 상태로 돌아가야 한다.
그렇다면 단순히 트랜잭션 만으로 정합성을 보장할 수 있을까? 물론 아니다. 트랜잭션은 쿼리를 수행할 단위 일뿐 이에 대한 상호배제가 일루어져야 할 것이다. 상호배제가 정상적으로 이루어져서 웹 어플리케이션 입장에선 혼재된 상태가 아닌 특정 커밋이 수행된 전 또는 이후의 상태에서 read나 write를 수행할 수 있어야 할 것이다.
MariaDB의 default storage engine인 innoDB를 기준으로 다음과 같이 트랜잭션간 상호배제를 위한 4가지 isolation level을 제공한다.
트랜잭션이 완료되지 않은 상태의 정보도 읽을 수 있다. 성능적으로 가장 빠르다. 예를 들어 트랜잭션1에서 특정 column에 값을 7에서 8로 변경한뒤 커밋되지 않은 상태에서 트랜잭션2에서 해당 데이터를 읽는 경우 8로 읽히게 된다. 이를 dirty-read라고 한다. 그런데 트랜잭션1이 모종의 이유로 롤백이 되는 경우 트랜잭션2에선 7이 아닌 8로 트랜잭션을 수행하게 된다.
트랜잭션이 완료되었다면 실행 도중인 다른 트랜잭션에서도 변경된 데이터를 읽을 수 있다. 예를 들어 트랜잭션1에서 특정 column의 값을 7이라고 읽은 상태에서 트랜잭션2가 해당 column을 8로 수정하고 커밋이 완료되었다. 그 이후 트랜잭션1에서 다시 column에 값을 읽으면 8로 읽게 된다. 이런 경우 동일 트랜잭션에선 같은 데이터를 몇번이고 읽어도 동일한 값을 읽는다는 Reapeatable read에 위배된다. 일반적인 웹어플리케이션에선 큰 문제가 발생하지 않아 오라클에선 default로 사용한다고 한다.
MariaDB나 MySql의 default isolation level로 특정 트랜잭션이 수행되면 해당 트랜잭션이 실행되는 시점에선 스냅샷을 만들어 이후 발생하는 다른 트랜잭션에서 값을 수정하더라도 이전 트랜잭션에 대한 결과만 영향을 주게된다. 다만 UPDATE 부정합이나 Phantom Read와 같은 현상이 발생할 수 있다.
Update 부정합은 트랜잭션1과 트랜잭션2에서 동시에 동일 데이터에 Update를 수행한다고 했을 경우 먼저 수행된 트랜잭션만 Update가 반영되고 늦게 수행된 트랜잭션은 과거의 데이터가 되버린 스냅샷을 대상으로 Update를 수행하게 되고 실제 테이블에 영향을 주지 못하게 된다.
Phantom Read는 특수한 케이스에서 발생하는데 아래의 쿼리문을 살펴보자.
START TRANSACTION;
SELECT * FROM User; /* 빈 테이블이라고 가정 0개가 조회됨*/
START TRANSACTION;
INSERT INTO User VALUES('kky',30); /*Delete의 경우에는 문제가 발생하지 않음 왜지...*/
COMMIT;
SELECT * FROM User; /* 여기서도 0개가 조회됨 */
UPDATE User SET name = 'kky2' WHERE name = 'kky'; /*한개의 row가 영향을 받음*/
SELECT * FROM User; /*여기서는 1개가 조회됨*/
COMMIT;
이유는 잘 모르겠지만 첫번째 트랜잭션 중간에 두번째 트랜잭션에서 INSERT를 수행하고 커밋이 완료된 후 Non-Reapeatable Read가 만족되려면 첫번째 트랜잭션이 끝날 때 까지 두번째 트랜잭션에서 추가된 row를 몰라야하는데 Update 이후 조회가 가능해진다.
말그대로 모든 트랜잭션에 대하여 상호배제를 한다. 특정 트랜잭션이 수행되는 동안 어떠한 수정도 할수없게 된다. 가장 성능이 나쁜 정책
처음에는 단순히 DB로 들어오는 많은 요청을 어떤식으로 처리하는지에 대해 알아보려 했는데 트랜잭션을 설계하는데 있어 생각보다 고려할 부분이 많다는 것을 알게되었다. 자신이 사용하는 DBMS의 격리수준을 알고 있는 상태에서 문제가 발생한다면 좀더 수월하게 트러블슈팅을 할 수 있지 않을까 싶다.