development/database

[DB] 트랜잭션 격리 수준과 부정합 문제 (1)

bokshiri 2024. 12. 6. 20:01

DBMS의 데이터 변경 작업을 진행할 때 우리는 작업의 최소 단위인 트랜잭션을 기준으로 데이터의 상태를 변경한다.

트랜잭션을 이해하기 위해서는 전파 속성격리 수준을 반드시 살펴보아야 한다. 지난 글에서 전파 속성에 대해 정리하였으니, 이번 글에서는 격리 수준에 대해 정리하고자 한다.

그리고 격리 수준에 따른 부정합 문제에 대해서도 간략하게 다뤄본다.


스프링을 활용한 개발에서는 일반적으로 Configuration에서 PlatformTransactionManager를 Bean으로 등록한 후 비즈니스 Layer 혹은 application Layer에서 @Transactional 어노테이션을 활용하여 트랜잭션을 관리한다.

@Transactional 어노테이션은 전파 속성과 격리 수준을 트랜잭션 별로 제어할 수 있도록 기능을 지원하고 있다.

Isolation isolation() default Isolation.DEFAULT;

 

DEFAULT(TransactionDefinition.ISOLATION_DEFAULT),

READ_UNCOMMITTED(TransactionDefinition.ISOLATION_READ_UNCOMMITTED),

READ_COMMITTED(TransactionDefinition.ISOLATION_READ_COMMITTED),

REPEATABLE_READ(TransactionDefinition.ISOLATION_REPEATABLE_READ),

SERIALIZABLE(TransactionDefinition.ISOLATION_SERIALIZABLE);

 

별 다른 옵션을 주지 않으면 DBMS의 Default 속성을 따라가게 된다. DBMS Vendor 별 기본 값으로 설정된 격리 수준은 다음과 같다.

DBMS DEFAULT ISOLATION
ORACLE READ_COMMITTED
POSTGRESQL READ_COMMITTED
MSSQL READ_COMMITTED
MYSQL REPEATABLE READ

 

격리 수준 목록에서 SERIALIZABLE 이 가장 높은 격리 수준을 가지며, READ UNCOMMITTED가 가장 낮은 격리 수준을 가지게 된다. 격리 수준이 높은 순서대로 내림차순 하게 되면 다음과 같은 순서가 될 것이다.

SERIALIZABLE -> REPEATABLE_READ -> READ_COMMITTED -> READ_UNCOMMITTED

각 격리 수준이 어떤 의미를 가지는지 하나씩 정리해보자.

 

※ 전파 속성과 격리 수준

전파 속성과 격리 수준의 의미는 다음과 같다.

전파속성이란?
-> 하나의 트랜잭션이 존재하고, 다른 트랜잭션을 호출할 때 어떻게 동작할지를 정의하는 것.

ex) A 트랜잭션이 선언된 상태에서 B 트랜잭션을 새롭게 생성하는 REQUIREDS_NEW
    A 트랜잭션이 선언된 상태에서 A 트랜잭션에 참여하도록 유도하는 REQUIRED
격리 수준이란?
-> 두 개 이상의 트랜잭션이 존재하는 환경에서, 특정 트랜잭션이 다른 트랜잭션의 변경 작업 결과 또는 데이터에 대해 조회 가능 여부를 결정하는 것

 

1. SERIALIZABLE

SERIALIZABLE 격리 수준은 가장 높은 격리 수준 단계로, 하나의 트랜잭션에서 읽은 레코드에 대해 다른 트랜잭션에서 추가/수정/삭제 등의 작업을 할 수 없도록 제어한다. 이 격리수준 내에서는 하나의 트랜잭션이 읽은 레코드에 대해 공유락(Shared Lock)이 자동으로 걸리기 때문에, 다른 트랜잭션은 조회만 가능하며 락을 획득한 트랜잭션에서만 데이터 변경 작업(쓰기 작업)이 가능하다.

트랜잭션을 순차적으로 수행시킨다는 점에서 동시 처리 성능이 떨어지므로, 극단적으로 안전하게 처리해야 하는 경우를 제외하고는 사용을 하지 않는 것이 좋다. 실제로 대용량 트래픽을 다루는 환경이라면 물리적 DB Lock을 통해 동시성을 제어하는 경우 성능이 떨어질 수 있으므로, 메모리 DB인 Redis 분산 락 등을 활용하여 처리하는 것이 성능면에서 나을 것이다.

Redis 분산락 처리에 대한 참고 자료 : https://miintto.github.io/docs/distributed-lock

 

Redis 분산 락을 활용한 동시성 처리 - miintto.log

유명 가수 콘서트 티케팅 혹은 블랙 프라이데이 할인 행사 같은 이벤트는 오픈하자마자 순식간에 많은 트래픽이 몰려듭니다. 이때 제공되는 수량은 한정되어 있으므로 소수의 물량을 두고 다수

miintto.github.io

 

2. REPEATABLE_READ

REPEATABLE_READ는 InnoDB 스토리지 엔진의 MVCC(Multi Version Concurrency Control 다중 버전 동시성 제어)와 깊은 연관을 가지고 있다. MVCC는 데이터베이스의 레코드에 대해 버전을 관리해주는 기능이다. 특정 레코드의 버전을 관리함으로써 롤백을 처리하고, 서로 다른 트랜잭션 간의 데이터 접근이 발생했을 때 세밀한 제어가 가능하도록 지원한다. 또한 MVCC를 사용함으로써 하나의 트랜잭션 내에서 조회 결과가 동일함을 보장한다. MVCC에 의해 조회된 레코드에 대한 스냅샷(Undo Log)이 기록되고, 재 조회시 Undo Log의 데이터를 반환하게 되는 것이다.

 

위와 같은 성질로 인해 특정 트랜잭션에서 레코드 수정 후 Commit을 하여도, 다른 트랜잭션에서 Commit 된 결과를 조회하지 못하는 경우가 발생할 수 있다. REPEATABLE_READ가 하나의 트랜잭션 내에서 스냅샷을 통해 데이터의 일관성을 보장하기 때문이다.

상황: 서로 다른 트랜잭션에서 하나의 레코드에 대해 수정 작업을 진행한다.
데이터: 'dean' 문자열

1. A 트랜잭션을 시작한다.

2. B 트랜잭션을 시작한다.

3. B 트랜잭션에서 데이터를 조회한다 -> 결과 : dean

4. A 트랜잭션에서 데이터를 kylen으로 변경한다.

5. A 트랜잭션 Commit

6. B 트랜잭션에서 데이터를 재 조회한다.

7. READ_COMMITTED 격리 수준이었다면 kylen이 조회되었을 것이나,
   REPEATABLE_READ 격리 수준에서는 Undo Log의 데이터를 반환하므로 -> 결과 : dean

 

REPEATABLE_READ 격리수준에서 레코드 변경이 아닌 추가(Insert)가 발생하는 경우는 동작이 조금 다르다. Undo Log를 활용하여 일관성을 보장하긴 하지만, Insert와 Delete를 방지하지는 못하기 때문에 부정합 문제가 발생할 수 있다. (Phantom Read) 이와 관련된 내용은 부정합 문제 챕터에서 다시 논의하겠다.

 

3. READ_COMMITTED

READ_COMMITTED은 ORACLE, POSTGRES, MSSQL 등의 DBMS에서 기본값으로 사용하는 격리 수준이다. 이름에서 알 수 있듯이, 트랜잭션 안에서 조회할 수 있는 정보는 커밋되어 데이터베이스에 반영된 데이터이다. REPEATABLE_READ와 달리 트랜잭션 내에서 Undo-Log를 바라보지 않고, 물리적 디스크에 반영(커밋)된 데이터를 조회하므로 다른 트랜잭션에서 동일한 레코드에 작업을 하게 될 경우 결과가 달라질 수 있다. 

 

따라서 Non_Repeatable Read (반복 읽기 불가능), Phantom Read (유령 읽기) 부정합 문제가 발생할 수 있다. 다음의 상황을 가정해보자.

 

* Phantom Read (유령 읽기)

1. B 트랜잭션에서 A 테이블의 데이터를 조회한다. -> 결과 1건

2. A 트랜잭션에서 A 테이블에 데이터를 1개 추가한 후 커밋한다.

3. B 트랜잭션은 종료되지 않은 상태에서, 1의 작업을 재 수행 한다. -> 결과 2건 (부정합 문제 발생)

---

* Non-Repeatable Read (반복 읽기 불가능)

1. B 트랜잭션에서 데이터를 조회한다. -> 결과값: dean

2. A 트랜잭션에서 해당 데이터를 조회한 후 kylen으로 변경한다.

3. A 트랜잭션이 커밋된다.

4. B 트랜잭션에서 같은 데이터를 재 조회한다. -> 결과값 kylen (최초 결과 값과 A 트랜잭션 종료 이후 값이 달라짐) (부정합 문제 발생)

 

4. READ_UNCOMMITTED

READ_UNCOMMITTED는 Dirty Read 부정합 문제를 발생시킬 수 있는 격리수준이다. 특정 트랜잭션에서 데이터를 추가 또는 변경한 후 트랜잭션이 종료되지 않은 상태에서, 다른 트랜잭션이 추가 또는 변경된 데이터를 조회할 수 있다. (Dirty Read) 데이터의 일관성과 정합성이 중요하지 않고 성능과 속도가 중요한 경우에 사용할 수 있는 격리수준이다. 실무에서 사용해 본 경험은 없다. 보통은 DBMS의 Default 격리수준을 유지하고, 특별한 경우를 제외하고는 트랜잭션 단위로도 격리수준을 제어하는 일은 극히 드물기 때문이다. 운영 업무 데이터의 정합성이 중요하지 않은 경우는 흔하지 않다고 생각한다.

 

1. A 트랜잭션에서 데이터를 추가한다. -> 결과 1건

2. A 트랜잭션이 종료(커밋)되지 않은 상태에서 B 트랜잭션에서 해당 데이터를 조회한다 -> 결과 1건 (Dirty Read 부정합 문제 발생)

 


※ 마치며

지금까지 데이터베이스의 격리 수준에 대해 정리하였다. 격리 수준에 따라 발생하는 부정합 문제들은 다음 글에서 사례와 함께 살펴보자.

 

 References

https://ssdragon.tistory.com/138

 

트랜잭션을 사용할 때 각 DB들의 기본 격리 수준은 무엇일까?

스프링 프레임워크를 사용한다면 @Transactional 을 Service단에 붙여서 자주 사용할 것이다. 이 경우 아래와 같이 isolation (격리수준)이 default(기본값)으로 지정된다. @Transactional의 옵션인 isolation의 기

ssdragon.tistory.com

https://mangkyu.tistory.com/299

 

[MySQL] 트랜잭션의 격리 수준(Isolation Level)에 대해 쉽고 완벽하게 이해하기

이번에는 트랜잭션 격리 수준(Isolation Level)에 대해 알아보도록 하겠습니다. 아래의 내용은 RealMySQL과 MySQL 공식 문서 등을 참고하여 작성하였으며, 모든 내용은 InnoDB를 기준으로 설명합니다. 해

mangkyu.tistory.com

https://cl8d.tistory.com/101

 

[Real MySQL 8.0] InnoDB 스토리지 엔진 알아보기 - 1편 (클러스터링 / 외래키 / MVCC)

🌱 InnoDB 스토리지 엔진 아키텍처MySQL에서 가장 많이 사용하는 스토리지 엔진인 'InnoDB'에 대한 아키텍처를 알아보자.그림이 복잡하기는 하지만, 왼쪽은 이전에 봤던 MySQL 엔진이고, 오른쪽이 InnoD

cl8d.tistory.com