상태를 변경시킨다는 것은 SELECT, UPDATE, INSERT, DELETE의 행동을 뜻함 !
트랜잭션은 상황에 따라 여러 개 만들어 질 수 있으며 그 하나의 트랜잭션은 Commit(저장) 되거나 Rollback(철회) 될 수 있다.
트랜잭션 특징 4가지
원자성(Atomicity)
트랜잭션이 DB에 모두 반영되거나 전혀 반영되지 않거나를 뜻함.
All or Nothing
일관성(Consistency)
트랜잭션 작업 처리 결과가 항상 일관되어야 한다. 즉 데이터 타입이 반환 후와 전이 항상 동일해야 한다.
독립성(Isolation)
하나의 트랜잭션은 다른 트랜잭션에 끼어들 수 없고 독립적인 것을 의미한다.
각각의 트랜잭션은 독립적이라 서로 간섭이 불가능함
지속성(Durability)
트랜잭션이 성공적으로 완료되면 영구적으로 결과에 반영되어야 함
보통 commit이 된다면 지속성은 만족할 수 있다.
트랜잭션 격리수준(Isolation level)이란
트랜잭션 격리수준이란?
동시에 여러 트랜잭션이 처리될 때, 특정 트랜잭션이 다른 트랜잭션에서 변경하거나 조회하는 데이터를 볼 수 있도록 허용할지? 말지?를 결정하는 것이다.
왜 필요할까? 왜 알아야할까?
트랜잭션 수준 읽기 일관성(Transacntion-Level Read Consistency)을 지키기 위해서다.
트랜잭션 수준 읽기 일관성이란?
트랜잭션이 시작된 시점으로부터 일관성 있게 데이터를 읽어 들이는 것을 뜻함.
하나의 트랜잭션이 진행되는 동안 다른 트랜잭션에의해 변경사항이 발생하더라도 이를 무시하고 계속 일관성 있는 데이터를 보여주게 된다 (트랜잭션 자신이 발생한 변경사항은 읽을 수있다.)
즉 동시성 제어 문제 해결을 위해서임 !
트랜잭션 격리수준 4가지
READ-UNCOMMITED (커밋되지 않은 읽기)
READ UNCOMMITED 격리 수준에서는 각 트랜잭션 변경 내용이 COMMIT이나 ROLLBACK 여부와 상관 없이 다른 트랜잭션에서 보여지게 된다.
발생할 수 있는 문제 - Dirty Read(더티리드)
더티리드란?
다른 트랜잭션에서 처리한 작업이 완료되지 않았음에도 불구하고 다른 트랜잭션에서 볼 수 있게 되는 현상을 뜻함.
흐름 설명
사용자 A는 team_kyu 테이블에서 id가 3인 정보를 조회한다. (그림 설명1)
사용자 A는 id가 3인 맴버의 이름을 “김자바"로 변경했다. (그림 설명2)
사용자 B는 사용자 A가 변경한 내용을 커밋하기도 전에 id = 3인 맴버를 조회하고 있다. (그림 설명3)
커밋이 되지 않았는데 “김자바"라는 이름이 조회된다.
여기서 문제는 만약 사용자 A가 작업 도중 문제가 발생하여 삽입한 내용을 롤백해도 사용자 B는 “김자바" 이름이 정상적인 팀원 이름으로 판단하고 계속 처리하게 된다.
더티 리드 현상은 데이터가 나타났다가 사라졌다 하는 현상을 초래할 수 있기 때문에 개발자와 사용자를 상당히 혼란스럽게 만든다.
따라서 READ UNCOMMITED 격리 수준은 트랜잭션의 격리 수준으로 인정하지 않을 정도로 데이터의 정합성에 악영향을 끼치므로 해당 격리수준은 피할 것을 권장한다.
READ COMMITED (커밋된 읽기)
READ COMMITED 격리 수준은 오라클에서 기본적으로 사용되고 있으며, 온라인 서비스에서 가장 많이 선택되는 격리 수준이다.
이 격리수준 레벨에서는 READ UNCOMMITED 수준에서 발생할 수 있는 더티 리드와 같은 현상은 발생하지 않는다.
어떠한 트랜잭션에서 데이터를 변경 하더라도 커밋이 완료된 데이터만 다른 트랜잭션에서 조회할 수 있기 때문 !
더티 리드 해결 흐름
사용자 A는 id=3 인 맴버의 이름을 김형욱 —> 김자바로 수정 (그림 설명1)
이 순간 새로운 값인 “김자바"는 team_kyu 테이블에 즉시 기록이 되고 이전 값 “김형욱"은 Undo 영역이란 곳에 백업이 된다. (그림 설명2)
사용자 A가 이 변경사항을 커밋하기 전에 사용자 B가 id=3인 맴버를 조회하면 결과 값은 “김형욱"이 나오게 된다. (그림 설명3)
B의 SELECT쿼리 결과는 Undo 영역의 백업된 레코드에서 가져온 결과이다. (그림 설명2)
READ COMMITED 격리 수준에서는 어떤 트랜잭션에서 변경한 내용이 커밋되기 전까지는 다른 트랜잭션에서 변경 내역을 조회할 수 없게된다.
최종적으로 A가 커밋하면 그때부터 다른 트랜잭션에서도 백업된 Undo 영역의 데이터가 아닌 변경된 데이터 값을 참조할 수 있게 된다.
언두(Undo) 로그란?
언두 영역은 UPDATE 문장이나 DELETE 같은 문장을 데이터를 변경했을 때 변경되기 전의 데이터를 보관하는 곳이다.
INSERT 문장의 경우 해당 데이터의 row id를 저장하고 이를 이용하여 물리적 메모리에 바로 접근할 수 있도록 보장한다.
언두 영역은 크게 두가지 용도로 사용된다.
트랜잭션의 롤백 대비용
트랜잭션의 격리 수준을 유지하면서 높은 동시성을 제공
발생할 수 있는 문제 - NON-PEREATABLE READ
NON-REPEATABLE READ란?
하나의 트랜잭션 내에서 동일한 SELECT 쿼리를 실행했을 때 항상 같은 결과를 보장해야 한다는 REPEATABLE READ 정합성에 어긋나는 것을 말한다.
흐름설명
사용자 B는 BEGIN 명령으로 트랜잭션을 시작하고 name = “김자바”인 사원을 조회하면 일치하는 데이터가 아직 없다. (그림 설명1)
그 이후에 사용자 A가 id = 3 인 맴버의 이름을 “김자바”로 수정하고 커밋한다. (그림 설명2)
사용자 B가 똑같은 SELECT 쿼리를 조회하면 1건의 결과가 조회된다. (그림 설명3)
사용자 B가 하나의 트랜잭션 내에서 동일한 SELECT 쿼리를 실행했을 때 항상 같은 결과를 보장해야 한다는 REPEATABLE READ 정합성에 어긋나게 된다.
이런 부정합 현상은 일반적인 웹 애플리케이션에서는 크게 문제가 되진 않지만, 하나의 트랜잭션에서 동일한 데이터를 여러번 읽고 변경하는 작업이 금전적인 처리와 연결되면 문제가 될 수 있다.
예를들어, 다른 트랜잭션에서 입금과 출금 처리를 계속 진행하고 있을 때 다른 트랜잭션에서 오늘 입금된 금액의 총합을 조회한다고 가정했을 경우
이때 READ COMMITED 격리 수준을 사용한다면 REPEATABLE READ가 보장되지 않으므로 총합을 계산하는 SELECT 쿼리를 실행할 때마다 다른 결과를 가져오는 큰 문제가 발생할 가능성이 있다.
REPEATABLE READ (반복 가능한 읽기)
REPEATABLE READ는 MySQL의 InnoDB 스토리지 엔진에서 기본적으로 사용되는 격리 수준이다.
이 격리 수준에서 READ COMMITED 격리 수준에서 발생하는 NON-REPEATEABLE READ 부정합이 발생하지 않는다.
REPEATABLE READ는 언두 영역에 백업된 이전 데이터를 통해 트랜잭션 내에서는 동일한 결과를 보여주도록 보장하여 NON-REPEATABLE READ 문제를 해결한다.
READ COMMITED 격리 수준 또한 언두 영역에 백업된 이전 데이터를 보여주지만, 두 격리 수준에는 언두 영역을 활용하는 방식이 다르다.
REPEATABLE READ 격리 수준은 언두 영역에 백업된 레코드의 여러 버전 가운데 몇 번째 버전을 보여주냐에 차이가 있어 NON-REPEATABLE READ 문제를 해결할 수 있다.
언두 영역에 백업된 모든 데이터에는 변경을 발생한 트랜잭션의 번호가 포함되어 있다.
REPEATABLE READ 격리 수준에서는 실행 중인 트랜잭션보다 작은 트랜잭션에서 변경한 데이터만 보게하여 NON-REPEATABLE READ 문제를 해결한다.
NON-REPEATABLE READ 해결흐름
사용자 A의 트랜잭션 번호는 9이고, 사용자 B의 트랜잭션 번호는 7이다.
이때 사용자 A는 맴버의 이름을 김자바로 변경하고 커밋을 수행한다. (그림 설명1)
이때 사용자 B는 id = 3인 맴버를 A 트랜잭션이 변경을 실행하기 전과 실행한 후 각각 조회를 하면 항상 동일한 “김형욱"이 나온다. (그림 설명2)
그 이유는 사용자 B가 BEGIN 명령으로 트랜잭션을 시작하면서 7번 이라는 트랜잭션 번호를 부여받았는데, 사용자 B의 7번 트랜잭션 안에서 실행되는 모든 SELECT 쿼리는 자신의 트랜잭션인 7번보다 작은 트랜잭션 번호에서 변경한 데이터만 볼 수 있기 때문이다. (그림 설명3)
따라서 사용자 A의 9번 트랜잭션에서 변경한 데이터는 열람할 수 없다.
발생할 수 있는 문제 - PHANTOM READ
PHANTOM READ란, SELECT … FOR UPDATE 쿼리와 같은 쓰기 잠금을 거는 경우 다른 트랜잭션에서 수행한 변경 작업에 의해 레코드가 보였다가 안 보였다가 하는 현상을 말한다.
흐름설명
위 그림은 사용자 A가 team_kyu 테이블에 INSERT를 실행하기 전과 후에 사용자 B가 SELECT … FOR UPDATE 쿼리로 테이블을 조회했을 때 결과를 보여주고 있다.
사용자 B는 team_kyu의 테이블에 id가 3이상인 맴버를 조회한다. (그림 설명1)
사용자 A는 team_kyu의 테이블에 id가 6인 맴버를 추가하고 있다. (그림 설명2)
사용자 B는 team_kye의 테이블에 id가 3이상인 맴버를 동일하게 조회한다. (그림 설명3)
NON-REPEATABLE READ 문제 해결에서 설명한 것처럼 동일한 트랜잭션 내에서의 동일한 쿼리는 항상 같은 결과를 출력해야 한다.
그러나 사용자 B의 두번의 SELECT … FOR UPDATE는 다른 결과를 보여주고 있다.
그 이유는 SELECT … FOR UPDATE 쿼리의 경우 SELECT하는 레코드에 쓰기 잠금을 걸어야 하는데, 언두 영역에는 잠금을 걸 수 없기 때문이다.
따라서 어쩔 수 없이 SELECT … FOR UPDATE 나 SELECT … LOCK IN SHARE MODE로 조회되는 레코드는 언두 영역의 변경 전 데이터를 가져오는 것이 아니라 현재 레코드의 값을 가져온다.
InnoDB에서는 REPEATABLE READ 격리 수준에서도 PHANTOM READ가 발생하지 않는다고 한다.
SERIALIZABLE (직렬화 가능)
가장 단순한 격리 수준이면서 동시에 가장 엄격한 격리 수준이다.
그만큼 동시 처리 성능도 다른 트랜잭션 격리 수준보다 떨어진다.
InnoDB 테이블에서 순수한 SELECT 작업은 아무런 레코드 잠금도 설정하지 않고 실행되지만, 트랜잭션 격리 수준이 SERIALIZABLE로 설정되면 읽기 작업도 공유 잠금(읽기잠금)을 획득해야 한다.
즉 한 트랜잭션에서 읽고 쓰는 레코드를 다른 트랜잭션에서는 절대 접근할 수 없게 하는 것이다.
따라서 SERIALIZABLE 격리 수준에서는 모든 부정합 문제가 발생하지 않는다.
하지만 동시 처리가 거의 불가능 하므로 사용을 권장하진 않는다.
결론 - 간단요약
격리수준 4가지
READ UNCOMMITED
한 트랜잭션의 변경된 내용을 커밋이나 롤백과 상관 없이 다른 트랜잭션에서 읽을 수 있는 격리 수준
모든 부정합 문제가 발생할 수 있음
사용을 권장하지 않음.
READ COMMITED
COMMIT이 완료된 데이터만 조회 가능한 격리 수준
더티 리드는 해결된다.
NON-REPEATABLE READ 문제가 발생할 수 있음
REPEATABLE READ
트랜잭션이 시작되기 전에 커밋된 내용에 관해서만 조회할 수 있는 격리수준
NON-REPEATABLE READ 문제는 해결된다.
PHANTOM READ 문제가 발생할 수 있음
InnoDB에서는 PHANTOM READ 문제는 해결된다.
SERIALIZABLE
한 트랜잭션을 다른 트랜잭션으로부터 완전히 분리하는 격리수준
모든 부정합 문제 해결됨
사용을 권장하지 않음.
부정합 문제 3가지
DIRTY READ - 어떤 트랜잭션에서 처리한 작업이 완료되지 않았음에도 불구하고 다른 트랜잭션에서 볼 수 있게 되는 현상
NON-REPEATABLE READ - 동일한 SELECT 쿼리를 실행했을 때 항상 같은 결과를 보장해야 한다는 “REPEATABLE READ” 정합성에 어긋나는 현상
PHANTOM READ - 한 트랜잭션내에서 동일한 쿼리를 두번 수행했는데, 첫 번째 쿼리에서 존재하지 않던 유령레코드가 두 번째 쿼리에서 나타나는 현상