데이터베이스 작업에서 발생할 수 있는 동시성문제를 처리하기 위해 MySQL에서 지원하는 공유(Shared)락, 배타(Exclusive)락과 동시성문제를 처리하는 방식인 낙관적(Optimistic)락, 비관적(Pessimistic)락을 정리하려 한다. 모든 설명은 MySQL 8.0을 기반으로 한다.
동시성 문제
동시성 문제는 멀티 스레드, 프로세스 작업에서 개발자가 해결해야 하는 문제이다. 하나의 공유 자원에 두 스레드가 동시에 접근하여 데이터를 변경하는 작업을 처리하는 경우 발생할 수 있는 문제점들이다.
한 가지 예시를 들어보자. 웹 쇼핑몰에서 아이패드 1개를 판매하기 위해 상품을 등록했다. 아주 매력적인 가격이었기에 10명의 사용자가 동시에 결제를 진행한다고 가정한다. 동시성 처리를 하지 않은 경우라면 10명의 사용자들의 결제 요청이 승인되고 구매 처리가 진행되어버릴 것이다. 상품은 1개인데 10개의 주문이 승인돼는 문제가 발생한다.
이러한 문제를 해결하기 위해 하나의 사용자가 주문을 요청하면 다른 사용자는 주문이 실패하는 방식을 사용해야 할 것이다. 동시성 문제 해결을 위헤 MySQL은 shared Lock, Exclusive Lock을 지원한다. 추가적으로 문제를 해결하는 방식인 Optimistic Lock, Pessimistic Lock의 방식이 존재한다.
Shared Lock, Exclusive Lock
공유 락(읽기 락), 배타락(쓰기 락)은 테이블의 하나의 로우에서 데이터베이스 전체를 아울러 적용가능한 잠금의 종류이다. 트랜잭션이 데이터를 잠그고 다른 트랜잭션의 접근을 막는 방식으로 동시성 문제를 처리한다.
Shared Lock (Read Lock)
공유(읽기) 락은 이름에서 유추할 수 있듯이 데이터를 읽을(read, select) 때 락을 건다. shared의 의미는 정확하지는 않지만 여러 트랜잭션이 shared Lock을 동시에 취득할 수 있기에 공유라는 이름이 붙여진 것으로 생각한다.
shared Lock은 transaction이 다른 트랜잭션의 쓰기 작업을 허용하지 않기 위해 락을 건다. 여러 transaction은 동시에 shared Lock을 취득할 수 있으며, 하나의 트랜잭션이라도 shared Lock을 취득하고 있는 경우 해당 데이터에 대한 쓰기(Create, Update, Delete) 작업이 불가능하다. transaction이 쓰기 작업을 수행하기 위해서는 shared Lock이 해제되길 기다려야 하고, 모든 shared Lock이 해제되었을 때, 쓰기 작업을 수행하고 commit 한다.
이런 방식으로 어떤 동시성 문제를 방지할 수 있을까? 위의 아이패드 예시를 다시 생각해보자. 1개의 아이패드가 데이터베이스에 저장되었고, 이를 구매하기 위해서는 아이패드의 수량을 읽고(select), 수량을 0으로 변경하는 쓰기(update) 작업이 필요할 것이다. 또한 이러한 일련의 작업은 하나의 transaction으로 묶여야 할 것이다.
모든 구매자들은 아이패드의 수량을 읽고 수량을 변경하는 작업에 대해서 shared Lock을 취득한다고 하자. 자, 이제 10명의 구매자들이 동시에 아이패드 구매 transaction을 수행한다. 먼저 아주 미세한 차이로 구매를 먼저 진행한 T1(transaction 1)이 아이패드 데이터에 대한 shared Lock을 획득(select ~ for share)했다. 하지만 아이패드 수량을 0으로 쓰기 전 T2, T3가 아이패드에 대한 shared Lock을 획득했다. T1은 아이패드의 수량을 변경하지 못하고 T2, T3의 shared Lock이 반환 되기를 대기할 것이다. T2, T3도 동일하게 다른 transaction의 shared Lock의 반환을 대기할 것이다. 서로 아이패드를 구매하기위해 이를 악물고 shared Lock을 반환하지 않는 현상이 발생한다.
1개 뿐인 물건을 10명이 동시에 구매해버리는 동시성 문제는 해결이 되었다. 하지만 아무도 구매하지 못하고 스레드가 멍청하게 shared Lock 반환을 기다리는 새로운 문제가 발생했다. 이러한 문제는 transaction에 적절한 timeout을 설정하거나, 락 취득 순서에 따라 처리하는 방식을 통해 해결할 수 있다. 이번 포스팅에서는 자세하게 다루지는 않겠다.
Exclusive Lock (Write Lock)
배타(쓰기) 락 또한 이름에서 알 수 있듯이 데이터 쓰기(Create, Update, Delete)작업을 수행할 때 락을 건다. exclusive라는 이름이 붙은 이유는 오직 하나의 transaction만이 데이터에 대한 exclusice Lock을 획득할 수 있기에 독점이라는 의미가 부여된 것 같다.
exclusive Lock은 한 transaction이 작업 중인 데이터에 다른 transaction의 읽기 조차 접근하지 못하게 제어하기 위해 사용한다. exclusive Lock은 오직 하나의 트랜잭션만이 취득가능하며, exclusive Lock이 걸려있는 데이터에 대해서 다른 트랜잭션은 락의 반환을 대기해야 한다.
다시 아이패드 예시를 생각해보자. 먼저 아이패드 데이터에 접근한 트랜잭션이 select(~for update)작업 시 exclusive Lock을 획득한다. 다른 트랜잭션은 exclusive lock 반환을 대기한다.
shared Lock에서 발생했던 문제도 해결되었다. 단편적으로 봤을 때는 좋은 해결 방법인 것 같지만, 성능상의 문제가 있다. 아이패드를 구매하지 않고 구경만 하려는 사람조차 shared Lock 획득을 위해 exclusive Lock을 대기해야 하는 문제가 발생한다는 것이다. (단순 select 쿼리는 Lock 획득을 기다리지 않는다.)
뭐야 그럼 update 쿼리 발생 시점에 exclusive Lock을 획득하면 되는 거 아니냐? 라고 할 수 있겠지만 그럼 또 다른 문제가 발생한다. T1이 아이패드의 수량을 0으로 변경하기 전 T2가 아이패드의 수량을 1개라고 읽어버려서 T2의 update 쿼리가 수행된다는 것이다. 이를 해결하기 위해서는 조건부 Exclusive Lock을 설정하거나, MVCC를 통해 데이터의 접근 범위를 설정해야 한다.
Pessimistic, Optimistic Lock
낙관적(Optimistic) 락, 비관적(Pessimistic) 락은 동시성 문제를 처리하는 방식이다. 말 그대로 동시성 문제에 대해 낙관적으로 생각하거나 비관적으로 생각하여 동시성 문제를 처리한다. 각각 Application 레벨, Database 레벨에서 처리된다는 특징이 있다.
Pessimistic Lock (비관적 락)
동시성 문제를 비관적으로 바라보면 어떨까? 무조건 충돌이 발생한다고 생각할 수 있다. 따라서 Database 레벨에서 트랜잭션 시작 전 조회 혹은 변경하려는 레코드에 shared Lock 혹은 exclusive Lock을 걸고 시작하는 것이 pessimistic Lock이다.
exclusive Lock 취득을 위해서는 모든 shared Lock의 반환을 기다려야 하고, shared Lock 취득을 위해서는 exclusive Lock 반환을 기다려야 한다. Lock을 사용할 때는 항상 dead Lock의 가능성을 염두에 두어야 하고 예방하는 습관을 가져야 한다.
Optimistic Lock (낙관적 락)
낙관적 락은 데이터베이스의 락을 사용하는 것이 아닌 application 레벨에서 버전 관리를 통해 충돌을 예방하는 전략이다. 버전은 임의의 번호가 될 수도 있고, Timestamp 값, 해시 값이 될 수도 있다.
application에서 데이터를 버전 정보와 함께 읽어온 뒤, 변경 작업에서 버전 정보를 비교하여 커밋을 수행할지 말지 결정하는 방식이다.
T1 아이패드 조회 (version 1) → T2 아이패드 조회및 수정 (version 2) → T1 아이패드 수정 (version 1이면 commit 하지만 2이기에 throw Exception)
'Database' 카테고리의 다른 글
MySQL 8.0 인덱스에 대한 고찰 (1) | 2024.04.13 |
---|