어떤 격리 수준을 선택하면 좋을까?

uncle.ra·2023년 11월 15일
post-thumbnail

들어가기 전에

학부생 시절에 Database 수업을 들은 적이 있다.
그 때 처음 만났던 격리 수준을 Spring으로 개발하고 있는 지금까지도 따라다니는 걸 보곤
이거 무조건 잡고 넘어가자는 생각이 든다🔥

📗 우선 정의부터

격리 수준을 파보기 전에 떠오르는 생각은 동시성 제어.
하지만 와닿지 않는다. 와닿을 때까지 구글링도 해보고 책도 찾아봤는데, 역시 답은 책에 있는 걸까 ㅎㅎ

트랜잭션의 격리 수준(isolation level)이란 여러 트랜잭션이 동시에 처리될 때
특정 트랜잭션이 다른 트랜잭션에서 변경하거나 조회하는 데이터를 볼 수 있게 허용할지 말지를 결정하는 것이다.
Real MySQL 8.0 1권

이걸 나에게 와닿게 정리해보았다.

특정 트랜잭션이 변경하거나 조회하는 데이터를 다른 트랜잭션에서 볼 수 있게 허용할지 말지, 허용한다면 어느 시점의 데이터를 보여줄 것인지를 결정하는 것

📗 격리 수준은 총 4개로 나뉜다.

위의 정의를 곱씹어 생각해보고 아래를 읽어보자.

들어가기 전에
member 테이블에는 memberId가 1이고 nickname이 uncle.ra인 record가 존재한다고 가정하자.

1. READ UNCOMMITED

READ UNCOMMITED는 커밋이 되지 않은 데이터도 조회할 수 있는 격리 수준이다.

admin A가 member_id가 1인 회원의 닉네임을 UNCLE_RA로 변경하려고 하고 있다. 아직 Commit/Rollback이 되지 않은 상황에 admin B가 조회했다.
이 때 admin B는 uncle.ra가 아닌 UNCLE_RA를 보게 된다.

기능이 정상적으로 동작을 해서 Commit이 된다면 다행이지만
그러지 않고 Rollback이 되는 트랜잭션이었다면 데이터의 정합성이 깨진 데이터를 조회하게 된 상황이 벌어질 수 있다.

위와 같은 상황 처럼 특정 트랜잭션에서 commit이 완료되지 않았는데도 다른 트랜잭션에서 볼 수 있는 현상을 Dirty Read라고 한다.

READ UNCOMMITED에서는 Dirty Read 뿐만이 아니라 밑에서 설명할 Non-Repeatable Read, Phantom Read 같은 다른 부정합 문제를 가지고 있다.
따라서 READ UNCOMMITRED는 실무에서 사용하면 좋지 않다.

2. READ COMMITED

READ COMMITED는 오라클 DBMS에서 기본으로 사용되는 격리 수준이며, Commit이 된 데이터만을 조회할 수 있는 격리 수준이다.

Commit이 된 데이터만 조회할 수 있기 때문에 READ UNCOMMITED 에서 발생한 Dirty Read 부정합 문제는 발생하지 않는다.

READ UNCOMMITED에서 예를 다시 가져와보자.

admin A가 member_id가 1인 회원의 닉네임을 UNCLE_RA로 변경하려고 하고 있다. 아직 Commit/Rollback이 되지 않은 상황에 admin B가 조회했다.

READ COMMITED 격리 수준에서는 admin B가 조회했을 때 commit이 되기 이전의 닉네임인 uncle.ra를 보게 된다.

추가적인 상황을 만들어보자.

admin B의 트랜잭션 안에 동일한 조회 로직이 한 번 더 있었다면?
동일한 조회 로직 바로 직전에 admin A의 트랜잭션이 Commit이 되어서 UNCLE_RA로 닉네임이 변경되었다면?

  • 첫 번째 조회 시에는 admin A가 변경한 닉네임을 Commit하기 전이기 때문에 uncle.ra가 조회된다.

  • 두 번째 조회 시에는 admin A가 변경한 닉네임을 Commit한 이후이기 때문에 UNCLE_RA가 조회된다.

위와 같이 한 트랜잭션 내에서 동일하게 조회했음에도 다른 데이터가 조회되는 현상을 Non-Repeatable Read라고 한다.

READ COMMITED 수준에서는

Dirty Read 부정합 문제는 발생하지 않지만, Non-Repeatable Read와 아래에서 설명할 Phantom Read 문제를 가지고 있다.

3. REPEATABLE READ

MySQL의 스토리지 엔진에서 기본으로 사용되는 격리 수준으로,
동일 트랜잭션 내에서는 동일한 결과를 보장해주는 격리 수준이다.

어떻게 Non-Repeatable Read 부정합 문제를 해결할 수 있는 걸까?🤔

MySQL 8.0을 기준으로 얘기해보면. InnoDB 스토리지 엔진은 트랜잭션이 Rollback될 수 있는 가능성을 염두해서 변경되기 전 record를 언두(Undo) 영역에 백업해두고 이후에 실제 Record의 값을 변경한다.

REPEATABLE READ 수준에서는 언두 영역에 백업한 데이터를 이용해 동일한 트랜잭션 내에서는 동일한 결과를 보여줄 수 있는 것이다.

하지만 REPEATABLE READ 격리 수준이라고 하더라도 Phantom Read가 발생할 수 있다. 변경 사항에 대해서는 트랜잭션 내에서 동일한 데이터를 보장해주지만 추가된 데이터의 경우에는 마치 유령처럼 추가된 데이터도 조회가 되면서 데이터의 정합성이 깨지는 것이다.

하지만 InnoDB 스토리지 엔진에서는 갭 락과 넥스트 키 락 덕분에 REPEATABLE READ 격리 수준에서도 Phantom Read가 발생하지 않는다.

여기서 Phantom Read는 동일한 트랜잭션 내에서 동일한 쿼리를 실행했을 때 먼저 수행된 쿼리에서 나오지 않았던 추가된 Record가 나타나는 현상을 의미한다.

4. SERIALIZABLE

가장 단순한 격리 수준이면서 동시에 가장 엄격한 격리 수준이다. 그만큼 동시 처리 성능도 다른 트랜잭션 격리 수준보다 떨어진다.

일반적으로 SERIALIZABLE 보다 낮은 격리 수준에서는 잠금 없는 일관된 읽기(Non-locking consistent read)가 가능하다. 따로 Lock을 획득하는 과정 없이 실행된다.

하지만 SERIALIZABLE 격리 수준을 설정하면 읽기 작업도 Shared-Lock을 획득해야만한다. 동시에 다른 트랜잭션에서는 Shared-Lock을 획득한 Record에 대해서 변경을 하지 못하고 대기해야만 한다.

위에서 설명한 데이터의 부정합 문제는 전부 발생시키지 않지만 그만큼 처리 속도가 현저히 떨어지게 된다.

📗 어떤 격리 수준을 선택하면 좋을까?

개발을 하면서 항상 느끼는 거지만, 모든 것은 Trade Off인 것 같다.
격리 수준 역시 데이터의 정합성과 성능 사이의 Trade Off이다.

격리 수준이 낮을 수록 데이터의 정합성은 보장하기 어려워지지만 성능적 이점을 가져갈 수 있고
격리 수준이 높을 수록 데이터의 정합성은 보장하기 용이해지지만 성능적으로 떨어지게 된다.

프로젝트의 성격에 따라 달라질 수 있지만

SNS 프로젝트이며 MySQL을 DBMS로 생각했을 때, 필자는 REPEATABLE READ로 기본으로 가져가고자 한다.

REPEATABLE READ는 (다른 부정합 문제들도 존재하지만) 위에서 언급한 Dirty Read, Non-Repeatable Read, Phantom Read에 대한 부정합 문제가 발생하지 않는다.
또한 Lock을 획득하는 것도 비용이다. SERIALIZABLE 격리 수준을 설정할 만큼의 동시성 제어가 필요한 프로젝트가 아니기도 하고 InnoDB 스토리지 엔진에서는 갭 락과 넥스트 키 락 덕분에 REPEATABLE READ 격리 수준에서도 Phantom Read 부정합 문제가 발생하지 않기 때문에 불필요한 선택으로 여겨진다.

📗 참고

0개의 댓글