본문 바로가기

backend

DB Lock

만약 인터넷 강의 수강 신청 정원이 10명인데 9명일 때 동시에 두명에 신청을 하면 어떻게 될까요? 이러한 상황에서 DBMS는 트랜잭션의 동시성을 제어하고 데이터의 정합성, 무결성 등을 지키기 위해 락을 사용합니다. 또한 트랜잭션의 격리성 단계로 제어하기 위해서도 사용됩니다.

Lock의 범위

락은 한 row에 대해서 걸 수도 있지만, 상황에 따라 상위 개념인 테이블이나 페이지, 블럭 뿐만 아니라 데이터베이스 전체, 스키마에 대해서도 걸 수 있습니다.

Lock의 종류

공유 락(Shared Lock)

공유 락은 읽기 작업을 할 때 거는 락입니다. 공유 락은 같은 읽기 작업에 대해 허용합니다. 여러 명의 클라이언트가 동일한 데이터에 대한 읽기 작업을 한다고 해도 데이터가 변하지 않기 때문입니다. 하지만 쓰기 작업에 대해서는 금지합니다. 읽기 작업 도중 데이터가 변경되면 안되기 때문입니다. S Lock이라고도 불립니다.

 배타 락(Exclusive Lock)

배타 락은 쓰기 작업을 할 때 거는 락입니다. 쓰기 작업은 데이터를 변경하는 작업이기 때문에 베타 락은 획득하면 해당 데이터에 접근하는 것을 막습니다. X Lock이라고도 불립니다.

업데이트 락(Update Lock)

업데이트 락은 데이터 수정을 하기 전에 거는 락입니다. 수정을 준비하고, 데드락을 방지하기 위해 사용되는 락입니다. 배타 락으로 변환이 가능합니다.

내재 락(Intent Lock)

내재 락은 위에서 짧게 언급한 락의 범위와 연관되어 있습니다. 내재 락은 상위 개념의 락을 걸기 전에 거는 락입니다. 내재 락이 왜 필요할까요? 한 row에 대해 쓰기 작업을 위해 배타 락이 걸렸습니다. 이때 다른 트랜잭션이 상위 개념(테이블, 페이지)에 대한 락을 걸기 위해 시도하려면 모든 row를 돌아보며 일일이 확인해야 합니다. 이 작업은 매우 비효율적인 일입니다. 따라서 DB에선 사용자가 배타 락을 거는 시점에 해당 row의 테이블에 내재 락(IX Lock)을 걸어 상위 개념에 락을 거는 것에 대한 여부를 빠르게 확인할 수 있게 돕습니다. 내재 락의 종류는 IS(내재 공유 락), IX(내재 배타 락), SIX(공유 내재 배타 락) 등이 있습니다.

더보기

SIX(공유 내재 배타 락)은 공유 락(S Lock)과 내재 배타 락(IX Lock)이 결합된 형태입니다. 이 락은 상위 레벨(ex: 테이블)에 대해 공유 락을 걸고 읽기 작업을 할 때 하위 레벨에 대해 쓰기 작업을 할 때 사용됩니다.

Blocking

블로킹은 한 트랜잭션이 특정 데이터에 대해 락을 걸었을 때, 다른 트랜잭션이 락을 걸기 위해 대기하는 상태를 말합니다. 예를 들어 어떤 데이터에 쓰기 작업을 하기 위해 배타 락을 걸었을 때, 다른 트랜잭션이 읽기나 쓰기 작업을 하려고 락을 걸려고 시도한다면 블로킹 상태가 되는 것입니다. 이 경우 먼저 락을 걸었던 트랜잭션이 끝나면 정상적으로 다른 트랜잭션이 실행됩니다.

이런 느낌

DeadLock

데드 락은 서로가 서로의 락을 대기하며 무한 교착 상태에 빠지는 것을 말합니다. 서로 다른 데이터를 점유하고 있는 두 개의 트랜잭션이 서로의 데이터에 접근하려고 하면 블로킹 상태가 되며 데드락이 발생하게 되는 것이죠. 

요런 느낌

DeadLock 발생 시나리오

데드락의 발생 원인은 4가지로 볼 수 있습니다.

상호 배제(Mutual Exclusion)

상호 배제는 각각의 트랜잭션이 서로 특정 데이터를 독점적으로 사용하려고 하는 상황입니다.

점유 대기(Hold and Wait)

점유 대기는 한 데이터에 Lock을 걸어둔 상태로 다른 트랜잭션이 건 락이 풀릴 때까지 대기하는 상황입니다.

비선점(No Preemption)

비선점은 이미 점유하고 있는 락을 강제로 뺏을 수 없는 상황입니다.

순환 대기(Circular Wait)

순환 대기는 트랜잭션 1이 트랜잭션 2가 가진 락을 대기하고, 트랜잭션 2가 트랜잭션 1이 가진 락을 대기하는 상황입니다.

 

이 4가지의 조건이 모두 충족될 때 데드락이 발생합니다.

낙관적 락과 비관적 락

낙관적 락과 비관적 락은 공유 락과 배타 락과 같은 기술보단 락 적용 전략에 가깝습니다. 두 전략을 상황에 맞게 적절히 사용하면 높은 성능 향상을 기대할 수 있습니다.

낙관적 락(Optimistic Lock)

낙관적 락은 동시성 문제가 많이 발생하지 않는다고 가정합니다. 낙관적 락을 사용하면 락을 사용하지 않습니다. 그럼 어떻게 트랜잭션의 충돌 상황을 감지하고 해결할까요? 

 

낙관적 락은 데이터의 버전이나 수정 시점으로 충돌을 감지합니다. 데이터를 수정하기 위해 데이터를 읽는 작업을 할 때, 버전이나 수정 시점을 함께 읽어옵니다. 그리고 데이터를 변경할 때 처음에 읽어온 버전이나 수정 시점과 다르면 오류를 반환합니다. 이후 어플리케이션 단에서 문제를 해결합니다.

 

이러한 낙관적 락은 락을 사용하지 않기 때문에 DeadLock이 발생하지 않고, 충돌 가능성이 낮다면 높은 성능을 보장합니다. 하지만 충돌 가능성이 높다면 충돌 감지와 재시도가 빈번해져 성능이 저하될 수 있습니다.

비관적 락(Pessimistic Lock)

비관적 락은 동시성 문제가 많이 발생한다고 가정합니다. 비관적 락을 사용하면 트랜잭션이 시작되는 시점에서 공유 락 또는 배타 락을 걸고 시작합니다. 

 

비관적 락은 트랜잭션의 격리 레벨을 Repeatable Read 레벨이나 Serializable 레벨을 사용합니다.

 

비관적 락은 락을 사용하기 때문에 성능 저하가 발생할 수 있습니다. 하지만 동시성 문제가 많이 발생할 수 있다면 데이터의 정합성을 보장하기 위해서 필요합니다.

적용하기

우리의 프로젝트에 낙관적 락과 비관적 락을 어떻게 적용할 수 있을까요? 

 

낙관적 락의 경우 Entity에 version 필드를 만들어 @Version 어노테이션으로 관리할 수 있습니다.

비관적 락은 Repository 파일에 @Lock 어노테이션에 LockModeType 속성을 지정해서 비관적 락을 사용할 수 있습니다. 

 

동시성 문제는 가벼운 문제가 아닙니다. 만약 금융 앱에서 동시성 문제가 발생할 경우 심각한 손해를 볼 수도 있죠. 따라서 락의 사용법을 알고 적절하게 성능을 고려하여 적용하는 것이 좋습니다. 이 글은 락을 가볍게 설명하는 글이니 직접 적용해서 사용해보시는 것을 추천드립니다.