MySQL 트랜잭션 격리 수준 + Lock

이명우·2024년 2월 4일
1

쿼리 튜닝, MySQL

목록 보기
10/15

얼마 전에, 한 개발자 친구가 MySQL + JPA로 멀티 스레드 환경에서 동일한 자원에 대한 트랜잭션을 처리할 때, 데이터 일관성 테스트를 하던 중에 데드락이 발생하여 이 문제를 해결하기 위해 고민한 적이 있었다.

고민을 하면서 느낀 것은, 내가 이 문제에 대해서 아는 게 하나도 없다는 것이였다. 그래서 이번 포스팅에서는 서버 레벨에서 제어할 수 있는 수준에서 트랜잭션 격리 수준Lock에 대해 알아보려고 한다.

본 글은 MySQL + MySQL 스토리지 엔진인 InnoDB 기준으로 작성하였습니다.

InnoDB Lock

락이란?

DBMS에서 데이터 동시성 + 일관성을 유지하기 위해 사용하는 기술이다. 락을 사용하는 목적은, 여러 트랜잭션이 동일한 자원에 대해 접근할 경우 발생할 수 있는 문제를 방지하는 것이다.

InnoDB 스토리지 엔진의 잠금은 테이블, 레코드 단위에서 이루어지며, MySQL 엔진의 잠금과 별개로 이루어진다. 그 중에서 트랜잭션 격리 수준에 따라 획득 유무가 달라지는 row-level lock에 대해서 알아겠다.

row-level lock

S 락 (공유 잠금)

S 락(Shared Lock)은 여러 트랜잭션이 해당 레코드를 읽는 것을 허용하되, 쓰기 작업에 대한 권한은 주어지지 않은 Locking이다.

X 락 (배타적 잠금)

X 락(Exclusive Lock)은, 다른 트랜잭션이 동일한 레코드에 대해 락을 획득할 수 없는 락이다. X 락을 획득한 트랜잭션은 해당 레코드에 대한 쓰기 작업(수정 및 삭제) 권한을 가지게 되며, 결과적으로 한 개의 트랜잭션만이 해당 레코드에 대한 쓰기 작업을 수행할 수 있게 된다.

X락 - S락 간 차이를 이해하기 위한 예시

하나의 레코드 r에 대한 트랜잭션 T1, T2가 있다고 가정해보자.

1. T1이 r에 대한 S 락을 보유하고 있는 경우

row r에 대해 T1이 S 락을 보유하고 있는 경우, T2가 S 락을 요청하면 즉시 부여될 수 있으며, 결과적으로 T1과 T2 모두 r에 대해 S 락을 보유하게 된다.

T2가 r에 대해서 X 락을 요청하는 경우, T2는 T1이 S 락을 해제할 때 X 락을 획득할 수 있다.

2. T1이 r에 대한 X 락을 보유하고 있는 경우

T1이 r에 대한 X락을 보유하고 있는 경우, T2는 요청하는 락의 유형에 상관없이 락을 획득하기 위해 대기해야 한다.

데이터베이스의 데드락

위 예시를 조금 바꿔서 row r1, r2 두 개가 있다고 가정해보자. 트랜잭션 T1이 r1에 대한 X 락을 보유하고 있는 상태이며, T2는 r2에 대한 X 락을 보유하고 있을 때 T1이 r2에 대한 S 락을 획득하려하고, T2는 r1에 대한 S 락을 획득하려고 한다면 어떻게 될까?

서로가 보유한 락을 획득하기 위해서 무한 대기하는 상태가 되어 데드락이 발생하게 된다.

정리

S 락은 동시성을, X 락은 데이터 일관성을 높이기 위한 잠금 전략이다.

높은 수준의 동시성을 달성할 경우, 높은 데이터 일관성을 유지하기 어려워진다. 높은 동시성을 위해 더 많은 트랜잭션이 동시에 실행되도록 허용할 수 있지만, 이로 인해 데이터의 일관성이 낮아진다.

반면 높은 수준의 데이터 일관성을 달성할 경우, 동시성은 낮아지게 된다. 높은 일관성 달성을 위해서는, 쓰기 작업에 대한 트랜잭션 간 충돌 방지를 위해 엄격한 락킹 전략을 사용하여 동시에 실행되는 트랜잭션의 수를 제한해야 한다. 이로 인해 동시성은 낮아질 수 밖에 없다.

따라서 트랜잭션에 대한 락 전략을 설계할 때, 상황에 맞는 락 설계를 할 수 있는 개발자의 역량이 중요하다.

트랜잭션 격리 수준(Transaction Isolation Level) With MySQL + InnoDB

트랜잭션

격리 수준을 알아보기 전에, 트랜잭션이 정확히 무엇인지, 무엇을 달성하기 위한 것인지 알아보자.

MySQL 공식문서에서 확인할 수 있는 트랜잭션의 정의는 다음과 같다.

  • 트랜잭션 : 데이터베이스에서 실행되는 작업의 원자적 단위로, 커밋(commit)되거나 롤백(rollback)될 수 있다.

  • 커밋(rollback) : Commit은 트랜잭션을 종료하고, 트랜잭션 동안 이루어진 모든 변경사항을 데이터베이스에 영구적으로 반영하는 SQL이다.

  • 롤백(commit) : Rollback은 진행 중인 트랜잭션을 종료하고, 트랜잭션 동안 수행된 모든 변경사항을 취소하는 SQL이다.

또한 데이터베이스에서 구현하여 추상화된 트랜잭션은 ACID로 대표되는 네 가지 특성을 보장하고 있다.

ACID

ACID는 트랜잭션이 안전하게 수행된다는 것을 보장하기 위한 성질 네가지의 약어이며, 각각 다음과 같다.

  1. 원자성(Atomicity) : 트랜잭션 내의 모든 작업이 완전히 수행되거나 수행되지 않아야 함을 의미한다. 하나의 작업이 실패할 경우, 전체 트랜잭션을 롤백함

  2. 일관성(Consistency) : 트랜잭션은 유효한 상태에서 시작하여 유효한 상태에서 끝나야 함

  3. 격리성(Consistency) : 동시에 실행되는 트랜잭션들이 서로에게 영향을 주지 않도록 격리되어야 함

  4. 지속성(Durability) : 트랜잭션이 성공적으로 완료될 경우, 그 결과는 영구적으로 데이트베이스에 저장되어야 함

이 중 트랜잭션 격리 수준격리성을 보장하기 위한 수단이다.

트랜잭션 격리 수준

트랜잭션 격리 수준은 다른 트랜잭션에서 변경한 데이터를 어느 시점에서 볼 수 있는지 정의하는 것이다. 이를 통해 동시성데이터 일관성을 관리하는 방법을 결정한다.

격리 수준이 낮을수록 동시성이 높아지고, 격리 수준이 높아질수록 데이터 일관성이 높아진다. 트랜잭션의 격리 수준은 네 가지 단계로 이루어진다. 이 네가지 단계에 따라서 허용하는 데이터 일관성 문제의 범위가 달라지는데 이를 먼저 알아보겠다.

격리 수준에 따라서 발생하는 데이터 일관성 문제

격리 수준에 따라서 크게 세 가지 현상이 발생하고, 이와 별개로 Lost Update까지 발생할 수 있다.

1.Dirty Read : 한 트랜잭션이 다른 트랜잭션에 의해 아직 커밋되지 않은 데이터를 읽는 현상

예시

위 예시에서는, Alice 트랜잭션에서 한 레코드를 UPDATE하는 동안, Bob 트랜잭션에서 아직 커밋되지않은 레코드를 SELECT 하고 있는데, Alice 트랜잭션이 롤백될 경우 데이터 일관성을 해칠 수 있다. 이런 현상을 Dirty Read라고 한다.


2.Non-Repeatable Read : 한 트랜잭션이 같은 데이터를 두 번 읽을 때, 두 번째 읽기에서 다른 트랜잭션에 의해 변경된 데이터를 읽는 현상

예시

마찬가지로, Alice 트랜잭션에서 UPDATE하기 이전 레코드와 UPDATE 한 이후의 레코드를 Bob 트랜잭션에서 읽고 있다. 처음 SELECT한 결과와 다른 결과를 읽고 있기 때문에 Bob 트랜잭션에서 데이터 일관성 유지에 문제가 발생하며 이런 현상을 Non-Repeatable Read라고 한다.


3.Phantom Read : 한 트랜잭션이 범위를 기준으로 데이터를 읽을 때, 다른 트랜잭션이 그 범위 내에 새로운 데이터를 삽입하거나 삭제하여 처음 읽을 때와 다른 결과를 반환하는 현상. 주로 SELECT * FROM USER WHERE AGE > 18; 과 같은 범위 쿼리를 여러번 실행하는 트랜잭션에서 발생

예시

Bob 트랜잭션에서 post_id 컬럼 값이 1인 레코드들을 조회하는 사이에, Alice 트랜잭션에서 post 테이블에 새 레코드를 INSERT하여 처음 결과와 두 번째 쿼리의 결과가 다르게 된다. 이 또한 Bob 트랜잭션에서 데이터 일관성에 문제가 발생하며, 이를 Phanthom Read라고 한다.


4.Lost Update : 두 트랜잭션이 동시에 같은 데이터를 업데이트할 때, 한 트랜잭션의 변경사항이 다른 트랜잭션에 의해 덮어쓰여지는 현상.

예시

위 예시에서 Alice가 WAS에 제품의 정보를 요청하고, WAS는 DB로부터 제품의 개수를 조회한 후, Alice에게 5의 값을 반환하고 있다. 이를 통해 Alice는 제품을 구매하기 위한 PUT 요청을 WAS에 전송하지만, 배치 작업으로 인해 해당 상품의 수량은 데이터베이스에 0개로 UPDATE 된 상태가 되어있다. 따라서 Alice의 요청에 의해 상품 개수는 -1개로 다시 UPDATE가 되는 상황이 발생한다. 이런 상황을 Lost Update라고 한다.

정리

격리 수준을 조정함으로써, 어느 정도의 동시성을 허용하면서 동시에 데이터 일관성을 어느 정도까지 보장할지를 결정할 수 있으며. 다시 말해, 격리 수준을 설정하는 것은 데이터의 일관성 문제와 동시성 사이의 균형을 찾는 과정인 것이다. 내가 개발하는 애플리케이션의 요구사항이 어떤지, 얼마나 많은 트랜잭션을 처리하는지, 어떤 트랜잭션을 주로 처리하느냐에 따라서, 격리 수준을 적절하게 선택하고, 목표하는 애플리케이션 성능을 달성할 수 있어야 한다.

InnoDB 언두 로그

InnoDB는 DML(UPDATE, INSERT, DELETE)에 대해 변경 이전의 데이터를 스냅샷으로 백업해두는데, 이 데이터들을 언두 로그라고 한다. InnoDB 언두 로그는 다음 두 가지 목적을 위해서 사용된다.

InnoDB는 언두 로그를 활용한 데이터 백업으로 각 격리 수준에서 동시성과 일관성의 트레이드 오프를 줄일 수 있다.

  • 트랜잭션 보장 : 앞서 트랜잭션은 커밋 혹은 롤백될 수 있다고 설명했는데, 언두 로그는 이 중 롤백변경 이전 데이터를 복구하기 위해 사용된다.
  • 격리 수준 보장 : 특정 트랜잭션에 대해 격리 수준에 맞는 데이터를 언두 로그를 이용해서 반환한다.

InnoDB는 격리 수준을 보장하기 위해서 MVCC라는 기능을 제공하고 있다. 언두 로그는 이 MVCC를 구현하는데 핵심적인 역할을 한다.

MVCC?

MVCC는 다중 버전 동시성 제어(Multi Version Concurrency Control)의 약자로 레코드에 대해 여러 버전을 관리하여, Non-Locking Consistent Read를 지원하는 기능이다. UPDATEDELETE가 발생한 레코드에 대해 MVCC를 위해서 언두 로그가 활용되며, INSERT의 경우 버전 관리가 필요 없기 때문에 MVCC를 지원하지 않는다.

Non-Locking Consistent Read

Non-Locking Consistent Read는 앞서 설명한 Lock을 걸지 않고 읽기 작업을 수행하는 데이터 읽기 방식이다.

이제 InnoDB가 지원하는 트랜잭션 관련 기능들을 바탕으로, 각 격리 수준에 대해서 알아보자.

READ UNCOMMITTED

가장 낮은 격리 수준으로, 다른 트랜잭션이 커밋하지 않은 변경 내용을 읽을 수 있다. Dirty Read, Non-Repeatable Read, Phantom Read 모두 발생할 수 있다. 동시성은 매우 향상되지만, 데이터 일관성에 대한 모든 현상을 허용하는 격리 수준이라고 할 수 있다.

READ COMMITTED

READ COMMITTED는 커밋된 레코드만 읽을 수 있는 격리 수준이다. 그렇기 때문에 Dirty Read는 발생하지 않지만, Non-Repeatable ReadPhantom Read는 발생한다.

InnoDB의 경우, MVCC를 지원하기 때문에 READ COMMITTED 수준에서 Non-Repeatable Read가 발생하지 않는다. 트랜잭션 내 쿼리 실행 시점마다 커밋된 데이터의 스냅샷을 제공하기 때문이다. 다만, 트랜잭션 중간에 삽입되거나 삭제된 레코드로 인해 Phantom Read가 발생할 수 있다.

REPEATABLE READ

InnoDB 스토리지 엔진에서 디폴트로 설정되어있는 격리 수준이다.

REPEATABLE READ 격리 수준에서는 한 트랜잭션 내에서 같은 쿼리를 여러 번 실행할 경우, 첫 번째 쿼리 시점의 데이터 스냅샷을 기준으로 결과를 제공한다. 즉, 한 트랜잭션 내에서는 동일한 데이터를 반복해서 조회할 때 마다 동일한 결과를 보게 되는 것이다. 이를 통해서 Non-repeatable Read를 허용하지 않지만, Phantom read는 발생한다.

InnoDB의 경우 앞서 설명한 MVCC를 지원하기 때문에 REPEATABLE READ 수준에서는 트랜잭션 시작 이전 시점의 스냅샷을 통해 읽기 작업을 수행하게 되고, 이를 통해 Phantom Read를 효과적으로 방지한다.

다만, 쿼리에서 SELECT ... FOR UPDATESELECT ... FOR SHARE 처럼 명시적인 락킹을 거는 경우 Phantom read가 발생할 수 있다.

SERIALIZABLE

가장 엄격한 격리 수준으로 "SERIALIZABLAE" 에서는 각 트랜잭션이 마치 순차적으로 실행되는 것처럼 격리되어 처리된다.

즉, 하나의 트랜잭션이 완료될 때까지 다른 트랜잭션은 해당 트랜잭션에 의해 접근되거나 변경된 데이터에 대해 접근하거나 변경할 수 없다. 이로 인해 Dirty Read, Non-Repeatable Read, Phantom Read 모두 발생하지 않지만, 동시성이 떨어지게 된다.

InnoDB에서 이전 단계의 격리 수준은 MVCC에 의해 잠금 없이 읽기 작업을 수행했지만, SERIALIZABLE 격리 수준에서는 단순한 읽기 작업에도 S 락을 획득해야 한다.

마무리

설정한 격리 수준에 따라서, 어떤 현상을 허용할지를 결정할 수 있다. 이를 통해 동시성과 데이터 일관성의 트레이드 오프를 조절할 수 있다. 일반적으로 MySQL 환경의 OLTP에서 REPEATABLE READREAD COMMITTED가 사용된다고 하는데, 이 또한 서비스 도메인(금융 서비스와 같은 경우, 데이터 일관성에 대해 더 높은 수준을 요구할 것이다)에 따라서 달라질 수도 있을 것 같다.

이번 포스팅에서는 트랜잭션과 S락 X락, 격리 수준들에 대해서 알아보았다. 다음 포스팅에서는 JPA의 @Transactional 어노테이션과 @Lock 어노테이션을 활용해서 트랜잭션을 제어하는 방법을 알아보겠다.


참고

profile
백엔드 개발자

0개의 댓글