트러블 슈팅

Transaction Isolation 격리 단계

수달하나 2024. 5. 30. 23:41

 

트랜잭션

트랜잭션은 데이터베이스의 상태를 변경하기 위해서 실행되는 단위를 말한다.

애매하다.

최근 이 트랜잭션에 대한 혼동이 있어서 버그가 발생했고 그 이슈를 해결하면서 다시 한 번 트랜잭션에 대한 개념을 돌이킬 수 있었다.

 

데이터베이스의 상태를 변화시킨다는 것은 한 문장의 질의어를 수행하는 것을 의미 하지 않는다.

상태를 변경하기 위해 실행되는 단위는 하나의 질의어로 수행가능할 수 있지만 두개 이상의 질의어 혹은 여러개의 질의어를 통해서 수행 될 수 있다는 것을 혼동하면 안된다.

 

버그 발견

어떠한 상태를 확인하는 로직이 있었다.

1초에 한번씩 스케줄을 돌면서 상태를 확인하고 비 정상상태가 판단 되었을 경우 소켓을 통해 그 결과를 웹에 반영하는 형태의 구조였다. 한 번 특정 상태를 유지하면 더 이상 웹소켓에 반영하지 않아야 하는것이 정상 동작이지만 기존의 상태가 유지됨에도 불구하고 계속적으로 소켓을 쏘는 것을 확인했고 무엇인가 로직이 잘못되어 있다는 것을 알 수 있었다.

 

상태 판단의 로직의 아래와 같았다.

작업A 와 작업B 는 독립적으로 수행되는 트랜잭션이고 같은 DB의 같은 테이블에 결과를 반영한다.

작업A 와 작업 B 는 순차적으로 진행되어야 하고 그 이후에 DB에 접근하여 특정 상태를 확인하는 방식으로 진행된다.

문제는 상태의 변경과 상태의 확인 두 로직의 순서에 있었다.

특정 상태를 확인하는 작업 C 는 스케쥴러를 통해서 반복적으로 수행된다. 

DB에 접근해서 상태를 확인하고 해당 조건이 부합했을 때 소켓 통신을 통해 Web에 반영시키는 구조였다.

 

문제 파악

문제는 서로다른 트랜잭션으로 구성이 되어있는 각각의 작업A 와 작업B 그리고 작업C 의 순서를 보장할 수 없다는 것이었다.

코드상 얼핏보면 작업A이후 바로 작업B가 진행되고 작업C는 독립적으로 수행되는 것 처럼 보여졌으나 실제로 동작하는 과정은 예측할 수 없었다.

 

작업A와 작업B는 특정 조건에 따라서 수행되는 로직이었는데 평상시 그 빈도수가 낮다보니 작업C 와의 동시성 문제에 있어서 이슈를 발생시키지 않았다. 

하지만 우연하게도 작업A 가 수행 된 직후 작업B가 수행되기 전 스케쥴러에 등록된 작업C가 수행된다면 정상적으로 DB의 상태를 읽어오지 못하는 버그가 발생하는 것 이었다.

 

문제 해결

우선 작업A와 작업B는 같은 트랜잭션으로 묶어줄 필요성이 있었다.

하나의 질의어를 수행하는 것이 아닌 DB의 상태를 변경하는 것이 트랜잭션이라면 궁극적으로 작업A와 작업B는 하나의 트랜잭션으로 통합하는 과정이 옳다고 생각했다.

따라서 기존의 분류된 작업을 하나로 통합했다.

하지만 하나의 트랜잭션을 처리 하더라도 실질적으로 DB의 상태를 변경하기 위해서는 쿼리를 2번 날려야 했다.

이것은 Read 작업을 수행하는 작업C의 접근을 막을 수는 없었다.

따라서 트랜잭션이 처리 될 때 해당 테이블의 Read 작업을 수행할 수 없도록 접근을 막어야 했다.

 

어떠한 방식으로 데이터베이스의 접근을 막을 수 있을까를 고민하다가 현재 개발 환경에 쉽게 적용시킬 수 있는 Isolation 기능을 적용시키기로 했다.

 

Transactional Isolation

스프링에서 트랜잭션을 관리하기 위해서 크게 두 가지 방법을 제공하고 있다.

하나는 Propagation 전파 속성, 나머지 하나는 Isolation 격리 수준을 통해 관리되어 지고 있다.

그 중 Isolation 기능을 통해서 격리 수준을 조절하여 트랜잭션이 진행되는 동안 외부의 접근데 대한 가능성을 다양하게 조절 할 수 있다.

 

격리 수준은 총 4 단계로 구분 된다.

 

1. READ UNCOMMITED

가장 낮은 수준의 격리 수준으로 커밋되지 않은 데이터를 읽을 수 있다. 모든 면에서 문제가 발생할 수 있기 때문에 기본 격리 수준을 READ UNCOMMITED 로 지정하는 DBMS 는 없다. 트랜잭션이 수행되는 동안 읽기와 쓰기가 가능하다.

 

2. READ COMMITED

커밋된 데이터만 읽을 수 있도록 하며 트랜잭션이 수행되는 동안 읽기와 쓰기가 가능하다. 

Dirty Read 현상을 방지 할 수 있다.

 

3. REPEATABLE READ

트랜잭션이 시작된 이후 읽기 데이터에 관하여 일관성을 유지 할 수 있다. 트랜잭션이 수행되는 동안 읽기와 쓰기가 가능하다.

Non-repeatable Read 현상을 방지 할 수 있다.

 

4. SERIALIZABLE

가장 높은 격리 수준으로 트랜잭션이 순차적으로 실행될 수 있도록 한다. 

Phantom Read 를 방지하며 트랜잭션이 수행되는 동안 기본적으로 읽기와 쓰기가 불가능하다.(조건에 따라서 가능)

 

* Dirty Read : 트랜잭션A 가 아직 커밋되지 않은 트랜잭션B의 변경 내용을 읽는 것을 의미한다.

* Non-repeatable Read : 같은 트랜잭션 내에서 같은 쿼리를 실행하지만 다른 결과를 읽는것을 의미한다.

* Phantom Read :  같은 트랜잭션 내에서 동일한 쿼리를 실행 할 때 다른 결과를 나타내는 것을 의미한다.

 

 

결과

내가 겪은 문제는 Non-repeatable Read 현상이기 때문에 Transaction의 고립 수준을 REPEATABLE READ 로 설정함으로써 해결 할 수 있었다.

사실 매우 복잡하고 어려운 기능은 아니었다.

하지만 코드에만 집중하다 보니 실제로 발생할 수 있는 이런 문제들을 쉽게 놓치는 경우가 발생했다.

좀 더 넓은 시야로 볼 수 있는 연습이 필요할 것 이다.