트랜잭션에 락을 사용하는 경우 (격리 레벨 Isolation Level)
트랜잭션(Transaction)의 이론적 개념과 특성,
먼저 트랜잭션의 이론적 개념은 데이터베이스 관리 시스템(DBMS)에서 '데이터를 조작하는 최소한의 작업(unit of work)'을 이야기합니다. 그리고 트랜잭션은 'ACID'라는 특성을 보장해야 하는데요.
- 원자성(Atomicity)
- 원자 단위로서 더 이상 쪼갤 수 없는 논리적 최소 단위임을 말합니다.
- 한 트랜잭션 내에서 실행한 작업들은 하나로 간주하는 것으로 모두 성공하거나 모두 실패해야 합니다. (All or Nothing) - 일관성(Consistency)
- 트랜잭션이 성공했다면 데이터베이스는 항상 일관성 있는 상태로 유지되어야 한다는 것을 말합니다. - 격리성(Isolation)
- 동시에 실행되는 트랜잭션들이 서로 영향을 미치지 않도록 격리해야 합니다.
(이러한 트랜잭션 격리 수준 Isolation Level은 4단계로 나뉩니다.) - 지속성(Durability)
- 트랜잭션을 성공적으로 마치면 그 결과가 항상 저장되어야 한다는 것입니다. (영구적으로 적용)
하지만 이 ACID 특성은 상황에 따라 지켜지지 않는 경우도 있습니다. 이유는 ACID 특성을 엄격하게 지키다 보면 동시성(Concurrency)이 매우 떨어지기 때문입니다. 그렇기 때문에 DB 엔진은 ACID 특성을 희생하여 동시성을 얻을 수 있는 방법을 제공하는데 그것이 바로 Transaction의 격리 레벨인 Isolation Level입니다.
격리성(Isolation)을 덜 지키는 Level을 사용할수록 문제가 발생할 가능성은 커지지만 동시에 더 높은 동시성을 얻을 수 있습니다.
(필요성에 따라 적절한 Isolation Level을 적용하는 것이 중요합니다.)
일관성(Consistency)과 항상 함께 따라다니는 동시성(Concurrency)
트랜잭션의 특징 중 일관성이 완전히 보장될 경우에 여러 클라이언트의 요청을 받는 데이터베이스의 특성상 응답의 지연이 발생하는(동시성이 저해되는) 현상이 발생할 수 있습니다.
만약 일관성이 너무 높다면 한 테이블에 접근하여 작업해야 하는 수많은 트랜잭션들이 줄을 서서 하나씩 차례대로 접근하여 작업하게 되는(나머지는 순서를 기다려야 하는) 상황이 발생하게 됩니다. 그렇다고 동시성을 높여버리면 동시에 접근한 트랜잭션들에 의해서 데이터가 꼬이는 상황이 발생할 수 있기 때문에 해당되는 로직의 특성에 따라 적절한 일관성과 동시성의 균형을 설정하는 것이 중요합니다.
'동시성 제어'란 동시에 실행되는 트랜잭션의 수를 최대화 하는 것과 데이터 CRUD 시 데이터의 무결성을 유지하는 것을 이야기합니다.
앞에서 본 것처럼 동시성 제어가 어려운 이유는 동시성(Concurrency)과 일관성(Consistency)이 하나가 증가하면 다른 하나는 감소하는 트레이드오프의 관계이기 때문입니다.
또한 동시성은 '낙관적 동시성 제어'와 '비관적 동시성 제어'로 나뉘는데요.
- 낙관적 동시성 제어
같은 데이터를 동시에 수정하지 않을 것으로 가정합니다. 데이터를 읽는 시점에 락을 걸진 않지만 수정하는 시점에서 기존에 읽어온 데이터가 다른 사용자에 의해 변경되었는지 재검사가 필요합니다. - 비관적 동시성 제어
같은 데이터를 동시에 수정할 것으로 가정합니다. 데이터를 읽는 시점에서 락을 걸고 조회, 갱신 완료 시까지 락을 유지합니다.
락(Lock)
락(Lock)은 트랜잭션 처리의 순차성을 보장하기 위한 방법입니다.
중요한 것은 DBMS마다 락을 구현하는 방식과 세부적인 방법이 다르기 때문에 DBMS를 효과적으로 다루기 위해서는 해당 DB의 Lock에 대한 이해를 하고 사용하는 것이 좋습니다.
(오라클의 경우 가장 낮은 격리 레벨을 사용하는 방법을 아예 제공하지 않습니다.)
트랜잭션에 걸린 Lock은 트랜잭션이 'commit' 되거나 'rollback' 될 때 함께 Unlock 됩니다.
- Shared Lock (Read Lock), 공유락
데이터를 읽을 때 사용되는 Lock으로 공유락은 공유락끼리 동시에 접근이 가능합니다. Write는 불허하고, Read는 해당 Critical Section에 접근이 허용된다는 것입니다.
(Read Lock은 Read에만 열려있는 것) - Exclusive Lock (Write Lock), 배타락
데이터를 변경할 때 사용되는 Lock으로 트랜잭션이 완료될 때까지 유지됩니다. Lock이 해제될 때까지 조회를 포함한 다른 트랜잭션은 해당 리소스에 접근할 수 없습니다.
격리 레벨과 발생할 수 있는 문제들
격리 수준과 그 수준이 어느 현상까지 허용하는지 또 발생할 수 있는 문제점은 어떤 것들이 있는지 알아보겠습니다.
격리 레벨(Isolation Level)은 4단계로 되어있으며 아래와 같습니다.
- Read Uncommitted
가장 낮은 격리 수준으로 커밋되지 않은 데이터를 읽을 수 있습니다. 아래에서 볼 세 가지 문제가 모두 발생할 수 있습니다.
- Read Committed
커밋된 데이터만 읽을 수 있으며, Dirty Read가 발생하지 않습니다.
- Repetable Read
한 번 조회한 데이터를 반복해서 조회해도 같은 데이터가 조회됩니다. Dirty Read, Non Repeatable Read가 발생하지 않습니다.
- Serializable
가장 엄격한 격리 수준으로 Drity Read, Non Repeatable Read, Phantom Read 모두 발생하지 않습니다.
// 별도로 정의하지 않으면 DB의 Isolation Level을 따름
@Transactional(isolation = Isolation.DEFAULT)
@Transactional(isolation = Isolation.READ_UNCOMMITTED)
@Transactional(isolation = Isolation.READ_COMMITTED)
@Transactional(isolation = Isolation.REPEATABLE_READ)
@Transactional(isolation = Isolation.SERIALIZABLE)
(각 레벨 별 @Transaction 적용 방법)
이어서 동시성으로 인해 발생할 수 있는 문제점들과 예시입니다.
- Dirty Read
커밋이 되지 않은 데이터를 다른 트랜잭션이 읽을 수 있습니다. 때문에 트랜잭션이 롤백되었을 경우 최종 결괏값이 비 일관적으로 적용될 가능성이 있습니다.
오라클의 경우에는 다중 버전 읽기 일관성 모델을 채택하였기 때문에 락을 사용하지 않고도 Dirty Read를 피할 수 있습니다.
(가장 낮은 레벨로 고립화 수준을 낮추는 방법을 아예 제공하지 않습니다.)
- A 트랜잭션에서 회원 a의 구매 목록을 추가하여 결제 금액이 4만 원에서 -> 5만 원으로 변경
- 아직 Commit 하지 않은 상태
- B 트랜잭션에서 회원 a의 결제 금액을 조회
- 결제 금액이 5만 원으로 조회됨 (Dirty Read)
- A 트랜잭션에 문제가 발생하여 Rollback 처리, 결제 금액도 5만 원에서 -> 다시 4만 원으로 변경
- B 트랜잭션은 조회한 5만 원을 가지고 결제를 진행
=> 데이터 적합성의 문제가 생깁니다.
(적합성은 데이터가 서로 모순 없이 일관되게 '일치'해야 한다는 의미)
- Non Repeatable Read
반복해서 같은 데이터를 읽을 수 없게 되는 문제입니다. 한 트랜잭션 내 같은 쿼리를 두 번 수행할 때 그 사이에 다른 트랜잭션이 값을 수정/삭제하므로 두 쿼리의 결과가 상이하게 나타나는 비 일관성의 문제가 발생합니다.
- B 트랜잭션에서 회원 a의 결제 금액을 조회
- 4만 원이 조회됨
- A 트랜잭션에서 회원 a의 결제 금액을 5만 원으로 변경하고 Commit
- B 트랜잭션에서 회원 a의 결제 금액을 다시 조회
- 5만 원이 조회됨
=> 하나의 트랜잭션 내에서 똑같은 Select를 수행했을 경우 같은 결과를 반환해야 한다는 Repeatable Read 정합성에 어긋나는 결과를 가져옵니다.
- Phantom Read
반복 조회 시 결과 집합이 달라지는 문제입니다. 한 트랜잭션 안에서 일정 범위의 레코드를 두 번 읽을 때, 처음 결과에 없던 레코드가 두 번째에서는 나타나는 문제입니다.
- A 트랜잭션에서 회원 a의 구매 목록을 조회
- 회원 a의 구매 목록에는 x, y가 존재
- B 트랜잭션에서 회원 a의 구매 목록에 z를 추가합니다
- A 트랜잭션에서 회원 a의 구매 목록을 다시 조회
- 구매 목록에서 이전 조회 시 없었던 z가 추가되어 x, y, z가 존재