[Real MySQL] 05. 트랜잭션 격리 수준에 따라 데이터 조회 결과 알아보기

김학성·2023년 5월 25일
0

Real MySQL

목록 보기
5/6
post-thumbnail

이 포스팅은 Real MySQL의 05장을 읽고 개인적으로 학습하고 이해한 내용입니다. 틀린 부분이 있다면 언제든 지적 부탁드립니다.

하나 이상의 명령어들이 모여 하나의 논리적 단위를 나타내는 트랜잭션은 얼마나 엄격하게 격리되느냐에 따라 조회되는 데이터가 달라질 수 있는데요.

아직 디스크에 저장되지 않는 변경사항도 조회할 수 있는지, 디스크에 저장된 변경사항만 보여짐과 동시에 트랜잭션 내에서는 무조건 같은 결과만 보여져야한다는지를 수준에 따라 결정하여 데이터의 정합성을 지키는 것이 그 목표가 될 수 있습니다.

아래에서는 트랜잭션 격리 수준에 따라 기대하는 결과가 어떻게 나올지 살펴보도록 하겠습니다.

실습은 autocommit을 비활성화한 상태로 진행했습니다. ( set autocommit = false; )

READ UNCOMMITTED

MySQL에서는 데이터 변경 시 버퍼 풀의 변경된 사항을 먼저 기록하게 됩니다.
커밋되지 않은 경우 아직 디스크에 기록되지 않는 아래와 같은 상태입니다.

이렇게 아직 디스크에 기록되지 않고 버퍼 풀에만 기록되어진 데이터 페이지를 더티 페이지라고 하는데요. READ UNCOMMITTED 수준에서는 이렇게 커밋되지 않는 데이터 페이지를 다른 트랜잭션이 참조할 수 있는 있게 됩니다.

이런 현상을 더티 리드(Dirty read)라고 부르는데, 데이터 부정합을 발생시키는 좋지 않은 현상 중 하나로 트랜잭션마다 다른 데이터를 참조할 수 있는 문제가 생길 수 있습니다.

그래서 해당 격리 수준은 권고하지 않기 때문에 가급적 사용하지 않는 것을 권장하고 있습니다.

READ COMMITTED

READ UNCOMMITTED와는 다르게 커밋이 된 데이터만 읽을 수 있는 격리 수준입니다.
아래와 같이 사용자 A가 트랜잭션을 시작하고 특정 레코드의 데이터를 변경하고나서 아직 커밋을 수정하지 않은 상태입니다.

커밋하지 않았기 때문에 변경사항은 버퍼 풀에만 기록되고 변경되기 이전의 데이터는 언두 로그에 저장됩니다.
이때 사용자 B는 해당 레코드를 조회하면 언두 로그의 이전 데이터를 조회하게 됩니다.

즉, 해당 격리 수준에서는 커밋된 데이터만 조회할 수 있습니다.

그런데 이 격리 수준에서도 데이터 부정합이 발생될 수 있는데요. 트랜잭션에서는 어떤 데이터를 조회하면 항상 같은 결과를 가져와야하는 정합성인 REPEATABLE READ 정합성 규칙을 가지고 있습니다.

아래 예시는 사용자 B가 특정 레코드를 조회하고, 사용자 A가 레코드를 수정한 뒤 커밋한 상태입니다. 이어서 사용자 B가 같은 레코드를 조회했을 때 변경된 데이터가 조회되고 있습니다.

얼핏보면 문제될건 없어보이지만 위에서 얘기했던 트랜잭션 내에서 조회한 데이터가 달라졌기 때문에 REPEATABLE READ 정합성 규칙을 어긋나게 됩니다.

결국 이 수준에서는 Dirty read 부정합은 해결했지만 NON-REPEATABLE READ 부정합의 문제가 발생함을 알 수 있습니다.

만약 트랜잭션 내에서 같은 데이터를 여러 번 참조하여 변경하는 작업의 환경이라면 문제가 될 수 있기 때문에 주의해야합니다.

REPEATABLE READ

MySQL InnoDB엔진에서 기본으로 사용되는 격리 수준이며, NON-REPEATABLE READ 부정합이 발생하지 않아 트랜잭션 내에서는 REPEATABLE READ 정합성을 보장하게 됩니다.

아래 예제에서는 사용자 B가 조회한 레코드의 데이터가 사용자 A가 변경하고 커밋한 데이터와 상관 없이 같은 데이터를 보여주는 것을 알 수 있습니다.

이는 이전 데이터를 저장하고 있는 언두 로그를 참조하고 있기 때문입니다.
이 격리 수준에서는 Dirty read와 NON-REPEATABLE READ 부정합이 발생되지 않아 권장되는 격리 수준입니다.

그러나 여기에서도 부정합이 발생할 수 있는 상황이 존재하는데요.
아래 예시는 사용자 B가 FOR UPDATE문과 함께 특정 범위의 레코드들을 조회하면서 레코드 잠금을 걸게 됩니다.

사용자 A가 레코드를 추가하고나서 사용자 B가 다시 동일한 범위로 레코드들을 조회하는데 사용자 A가 추가한 레코드가 함께 결과에 포함되어 있습니다.
이런 상황은 사용자 B가 조회하려는 데이터가 언두 로그에 존재하는데, 언두 로그는 FOR UPDATE로 인해 레코드 잠금을 할 수 없기 때문에 변경 전 데이터가 아닌 현재 데이터를 가져오게 되어 의도치 않은 레코드가 포함되는 PHANTOM ROW 현상이 발생하게 됩니다.

이 현상은 트랜잭션 내에서 레코드가 갑자기 포함되거나 포함되지 않는 현상인데, 다행이 MySQL InnoDB에서는 index-row 잠금과 gap 잠금을 활용하여 팬텀 로우 부정합이 발생하지 않습니다.

SERIALIZABLE

이번 격리 수준은 읽기 조회에서 공유 잠금을 걸어 다른 트랜잭션이 데이터를 수정하지 못하게 하는데요.
MySQL에서는 MVCC로 다중 버전의 레코드를 가지고 있어서 데이터를 변경하는 중에서도 일관된 데이터 읽기가 가능합니다. 그래서 언두 로그를 사용하여 데이터가 변경 중이여도 변경 전 데이터를 참조하거나 트랜잭션에서 롤백해도 이전 데이터의 레코드로 돌아갈 수 있는데, 해당 격리 수준에서는 읽기 작업에서도 데이터 변경이 되지 않기 때문에 다른 트랜잭션에서 접근이 되지 않아 가장 고립된 격리 수준을 제공하지만 그 만큼 성능 저하가 발생하게 됩니다.

REPEATABLE READ 격리 수준에서 설명했듯이 MySQL InnoDB엔진에서는 팬텀 로우 부정합이 발생하지 않기 때문에 굳이 이 격리 수준을 적용할 필요는 없어보입니다.

profile
경험과 성장을 기록하는 개발자입니다

0개의 댓글