트랙잭션은 ACID를 지킨다.
- Atomicity : 트랜잭션이 중간에 실패하면 전체 결과가 반영되지 않음
- Consistency : 트랜잭션이 성공적으로 반영된다면 일관성있는 데이터베이스의 상태가 유지된다.
- Isolation : 하나의 트랙잭션이 수행될 때 다른 트랜잭션이 적용되지 못한다.
- Durability : 반영된 트랜잭션의 결과가 지속적으로 유지된다.
이 중에서 오늘 다뤄 볼 내용은 바로 트랜잭션의 특성 중 Isolation에 대한 내용이다. 우리가 일반적으로 격리성이 지켜진다고 하면 하나의 트랙잭션이 진행될 때 절대로 다른 트랜잭션의 작업이 진행되지 못하는 것처럼 알고 있을 수도 있다. 하지만 이런식으로 동작하게 되면 하나의 필드에는 하나의 작업만이 진행할 수 있고 결과적으로는 성능저하가 발생하게 된다. 이러한 성능 문제를 어느정도 완화하기 위해 트랜잭션은 격리되는 정도(격리 수준)에 따라 동시에 다른 작업을 진행 할 수 있도록 한다.
격리 수준은 아래와 같다. 아래로 갈 수록 격리 수준이 높아진다.
- READ UNCOMMITTED
- READ COMMITTED
- REATABLE READ
- SERIALIZBLE
먼저 가장 격리 수준이 낮은 즉 다른 트랜잭션이 실행 될 수 있는 여지가 많은 READ UNCOMMITED부터 살펴보자.
1. READ UNCOMMITED
해석해보면 uncommited된 것을 읽는다는 뜻이다. 즉 트랜잭션의 과정에서 커밋하지 않은 트랜잭션의 변경 내용을 조회할 수 있다는 의미이다.
예를 들어 아래와 같은 트랜잭션이 있다고해보자
트랜잭션 A : 철수의 상태를 탈퇴로 바꾸는 트랜잭션
트랜잭션 B : 철수의 상태를 읽어 오는 트랜잭션
이런 상태에서 트랜잭션 A가 커밋되기 전에 트랜잭션 B가 수행된다고 해보자. 이렇게 되면 아직 트랜잭션A(철수를 탈퇴시키는 트랜잭션)이 커밋되지 않은 상태임에도 불구하고 트랜잭션 B가 수행되면 철수가 실제로 현재는 탈퇴가 아님에도 탈퇴로 값을 읽어온다. 이런식으로 커밋되지 않은 값을 읽어오는 것을 Dirty Read라고 하며 이런 식으로 읽어오면 데이터의 정합성의 문제가 발생하게 된다.
2. READ COMMITED
이것 역시 해석해보면 commit된 것을 읽는다는 뜻으로 온라인 서비스에서 가장 많이 사용되는 격리 수준이다. 이는 커밋된 트랜잭션만을 읽을 수 있는 정도의 격리 수준으로 위의 예에서 트랙잭션 A를 아직 커밋하지 않았기 때문에 트랜잭션 B에서 철수의 상태를 여전히 회원으로 읽어온다. 하지만 READ COMMITED 역시 정합성에 문제를 일으킬 수 있는데 아래와 같은 경우가 그렇다.
- 철수의 상태를 탈퇴로 바꿈(커밋 전)
- 철수의 상태를 읽어옴 ( 결과 값 : 회원)
- 1번 트랜잭션을 커밋
- 철수 상태를 읽어옴 (결과값 : 탈퇴)
즉 만약 하나의 트랜잭션에서 같은 값을 SELECT문을 이용해 읽었을 때 다른 값을 읽어온다. 이러한 문제는 위의 예를 봤을 떄는 큰 문제가 아닌 것 처럼보이지만 데이터를 UPDATE하는 트랜잭션이 많은 비즈니스에서는 데이터를 읽어올 때마다 다른 값을 읽게 되는 문제가 발생한다.
3. REPEATABLE READ
해석해보면 "반복적으로 읽을 수 있다." 정도가 될텐데 위의 READ COMMITTED에서 발생하는 문제를 해결할 수 있다. REPEATABLE READ는 해당 트랜잭션이 시작하기 전 내용에 대해서 조회할 수 있는 격리수준으로서 하나의 트랜잭션안에서는 반복적으로 데이터를 읽어와도 동일한 값을 읽을 수 있다. 즉 현재 진행하고 있는 트랜잭션보다 이전에 실행되었던 트랜잭션만을 반영한 데이터를 갖고 오게된다.
한 트랜잭션이 너무 길어지게 되면 트랜잭션이 시작된 시점의 데이터를 일관되게 보여줘야하기 때문에 멀티버전을 관리해야한다고 한다.
하지만 REPEATABLE READ역시 데이터의 부정합이 발생할 수 있는데 UPDATE와 Phatom READ가 있다.
1. UPDATE 부정합의 경우
-- transaction A
START TRANSACTION;
SELECT * FROM Monster WHERE name='파이리';
-- transaction B
START TRANSACTION;
SELECT * FROM Monster WHERE name = 'junha';
UPDATE Monster SET name = '리자드' WHERE name = '파이리';
COMMIT;
-- transaction B commit
UPDATE Monster SET name = '리자몽' WHERE name = '파이리'; -- 데이터가 존재하지 않음
COMMIT;
-- transaction A commit
트랜잭션 A가 커밋 되기전에 데이터를 업데이트 시키는 트랜잭션 B(파이리 -> 리자드)를 커밋하고 다시 업데이트한 후 트랜잭션 A를 커밋하는 쿼리문이다.
위에서 배웠듯이 REPEATABLE READ는 자신보다 이전에 실행된 트랜잭션은 반영하지 않기 때문에 트랜잭션 B에서 값을 변경(파이리 -> 리자드)하더라도 이어지는 트랜잭션 A에서의 변경(파이리 -> 리자몽)에서 그대로 변경되어야 한다고 생각하기 쉽다. 하지만 파이리는 바로 리자몽이 될 수 없다. 그 이유를 알기 위해선 쿼리가 진행되면서 어떤 일이 일어나는지 봐야한다.
- 트랜잭션 B(UPDATE 파이리 -> 리자드)를 커밋하면 REPEATABLE READ를 보장하기 위해 파이리를 undo-log에 넣어둔다.
- 트랜잭션 A(UPDATE 파이리 -> 리자몽)를 실행시키면 아래와 같은 과정이 진행된다.
- 먼저 업데이트할 ROW를 LOCK하기 위해 해당 테이블에서 해당 ROW를 찾는다.
- 하지만 파이리는 테이블에 있는 데이터가 아니라 undo-log에 있는 데이터이기 때문에 찾을 수 없다.
- 데이터가 없음을 출력하게 된다.
즉 파이리는 리자몽이 되지 못한다..
2. Phantom READ
팬텀 리드는 한 트랜잭션에서 동일한 쿼리를 두번 실행시켰을 때 없던 레코드가 나타나는 현상이다.
-- transaction A
START TRANSACTION;
SELECT * FROM HUMAN; -- 창세전 : 인간이 아무도 없음
-- transaction B
START TRANSACTION;
INSERT INTO HUMAN VALUES("아담");
INSERT INTO HUMAN VALUES("하와");
INSERT INTO HUMAN VALUES("뱀");
COMMIT;
-- transaction B commit
SELECT * FROM HUMAN; -- 아직 Repeatable read때문에 인간이 반영이 안됨
UPDATE HUMAN SET name = "사탄" WHERE name="뱀";
SELECT * FROM HUMAN; -- 사탄이 나온다.
COMMIT;
-- transaction A commit
먼저 구조는 아래와 같다.
- 트랜잭션 A에서 세상에 존재하는 인간 목록을 갖고 오라고 함 => 아무도 없음
- 트랜잭션 B를 이용해 인간을 3명 집어넣고 커밋
- 다시 인간 목록을 불러 옴 => 아무도 없음
- 인간 목록 중에 뱀을 사탄으로 업그레이드함
- 다시 인간 목록을 불러옴 => 1명(사탄)이 불러와짐
- 트랜잭션 A 커밋
여기서는 5번이 Phantom Read의 결과로 원래 현재 진행되는 트랜재션보다 뒤쪽의 트랜잭션이 반영되는 결과를 갖고 오면 안되지만 이런식으로 업데이트 한 후에는 갖고오게 된다.
4. SERIALIZABLE
가장 엄격한 격리 수준이으로 읽기 작업에도 공유 잠금을 설정하게 되고 읽기를 하는 동안에도 다른 트랜잭션에서 데이터를 변경할 수 없게된다. 이로 인해 동시성이 저하되고 결과적으로는 성능저하가 발생한다.
아래 링크를 참고해 작성하였습니다.
'Technology > DB' 카테고리의 다른 글
[MongoDB] aggregate explain (0) | 2022.09.16 |
---|---|
[MongoDB] 스키마 디자인 패턴 1: 버킷 패턴(Bucket Pattern) (0) | 2022.02.24 |