[MySQL] 동시성 문제 해결 - 비관적 락 ( Pessimistic Lock )
🐳문제상황
시험 응시원서 시스템을 만드는 과정에서 접수번호( 시험번호 + 순차 증가하는 번호 ) 생성 로직에서 문제가 발생했다.
접수번호 생성 로직은 아래와 같다.
1. 현재 원서 중 가장 큰 접수번호를 조회한다.
2. 가장 큰 접수번호에 + 1을 하여 원서 접수 번호를 생성한다.
위의 로직에서 여러 원서를 동시에 접수할 경우 같은 접수번호를 가진 데이터 입력을 시도하게 되었고 접수번호 + 시험ID 값은 Unique 한 값이기에 에러가 발생했다.
이 문제는 여러 원서가 한 개의 원서 자원에 접근하여 발생한 동시성 문제이다.
만약 DB에 접수번호에 Unique를 설정해 주지 않았다면 같은 접수번호를 가진 원서가 발생해 큰 문제를 야기했을 것이다.
이 문제를 해결해보자 !
🐳Database 의 Lock 기능 활용
신규 원서 생성 시에 한 가지 자원( 접수번호가 가장 큰 원서 )을 놓고 경쟁할 때 이 순서를 보장하기 위해 Database의 Lock 기능을 사용할 수 있다.
락 기법에는 다양한 종류가 있는데 대표적으로 비관적 락(Pessimistic Locking) 과 낙관적 락(Optimistic Locking)이 있다.
비관적 락은 트랜잭션이 시작된 후 데이터를 읽을 때부터 변경할 때까지 데이터에 대한 락을 걸어 다른 트랜잭션이 데이터를 변경하지 못하도록 하는 것이다. 이때 단순 조회만 하는 것은 가능하다 !
낙관적 락은 데이터를 읽을 때 버전 번호를 함께 읽고, 데이터를 업데이트할 때 버전 번호를 검사해 변경여부를 확인하는 방식이다.
이 중 비관적 락을 사용하는 것이 접수번호 생성 로직을 더 안정적으로 수행할 수 있을 것으로 생각 돼 비관적 락 기능을 사용하기로 했다.
🐳MySQL Data 조회 시에 비관적 락 기능 사용하기
먼저 Lock 기능은 트랜잭션 실행부터 종료 시까지 유지된다.
MySQL에서 데이터 조회 시에 비관적 락 기능을 사용하는 방법은 간단하다. SELECT 뒤에 FOR UPDATE 를 함께 작성해 주면 된다.
아래의 예시를 보자.
( 아래 예시에서는 접수번호가 아닌 기구를 생성하는 것을 예시로 사용하겠습니다. )
START TRANSACTION;
SELECT MAX(org_no) FROM tb_org WHERE org_no LIKE '2%' FOR UPDATE;
COMMIT;
트랜잭션을 시작한 후에
기구를 생성하기 위해 기구 번호 중 가장 큰 번호를 조회하고 있다.
이때 다른 트랜잭션이 기구번호가 가장 큰 기구데이터를 수정할 수 없도록 FOR UPDATE 를 통해 데이터에 잠금을 건다.
그런데 여기서 "그러면 조회는 되는 거 아닌가 ?" 의문이 들 수 있다.
맞다 ! 조회는 가능하기 때문에 마찬가지로 조회를 한 후에 org_no += 1 한 후에 데이터를 생성하면 같은 동시성 문제를 발생시킬 수 있다.
하지만 단순 조회가 아닌 데이터에 락을 거는 조회는 락이 걸려서 사용이 불가능하다 !
위의 예시처럼 같은 데이터에 락을 걸기 위해 FOR UPDATE 문을 사용하는 케이스 말이다.
그렇기 때문에 org_no를 생성하려는 모든 로직에는 FOR UPDATE 문을 적용해야 할 것이다.
아래의 동영상 예시를 보면 더 쉽게 확인이 가능하다.
🐳비관적 락 기능 동영상 예시
동영상의 실행 순서는 아래와 같다.
1. 왼쪽 스크립트에서 트랜잭션을 실행한다.
2. 왼쪽 스크립트에서 org_no 값이 가장 큰 org 를 조회한다.
3. 트랜잭션을 마무리하지 않은 채로 오른쪽의 org_no 값이 가장 큰 org를 조회한다.
-> 이때 왼쪽 쿼리와 오른쪽 쿼리가 의미는 같지만 문법이 다른 것을 확인할 수 있다. 그렇지만 결국 같은 데이터를 조회하기 때문에 해당 데이터에 Lock 이 걸려 있어 조회하지 못하고 대기하고 있는 것을 동영상에서 확인할 수 있다.
4. 왼쪽 스크립트에서 Commit을 통해 트랜잭션을 종료한다.
5. 왼쪽 스크립트에서 트랜잭션이 종료되는 순간 오른쪽 스크립트의 쿼리가 실행되는 것을 확인할 수 있다.
위의 동영상에서 몇 가지 사실을 확인할 수 있다.
1. 조회 쿼리더라도 데이터에 Lock을 얻기 위한 FOR UPDATE문을 사용할 경우 데이터에 접근이 제한된다.
2. 쿼리의 문법이 다르더라도 같은 데이터 접근한다면 Lock이 적용된다.
🐳비관적 락 기능이 유발할 수 있는 문제들
은탄환은 없다는 말처럼 모든 완벽한 기능은 없다.
비관적 락 기능이 완벽하다면 다른 Lock 기능들은 왜 나왔겠는가 ?
어떤 기술을 사용함에 있어서 그 기술이 유발할 수 있는 문제들도 항상 고민해봐야 할 것이다.
비관적 락 기능이 유발할 수 있는 문제의 대표적 예시는 아래와 같다.
- 데드락
- 성능저하
- 리소스 고갈
- 타임아웃
데드락
데드락은 두 개 이상의 트랜잭션이 같은 리소스에 접근해 해당 리소스를 Lock 하려는 중에 발생할 수 있다.
두 개 이상의 트랜잭션이 서로 릴리즈 하기를 기다리며 무한 대기 상태에 빠질 수 있다.
성능저하, 리소스 고갈, 타임아웃
Lock이 걸려있지 않은 데이터는 자유롭게 사용이 가능한 반면 Lock이 걸려 있는 데이터는 줄을 서서 작업을 진행하므로 당연하게도 성능 저하가 일어날 수밖에 없다.
그리고 이는 트랜잭션의 처리시간 증가로 이어지고 이는 리소스 고갈로 이어지게 될 것이다. 또한 너무 길게 기다리는 트랜잭션에 대해서는 타임아웃이 발생할 수 있다.
위의 문제들은 비관적 락 기능을 사용하는 것에 대한 사이드이펙트이다.
이런 문제가 발생할 여지가 있다는 것을 인지하고 필요한 곳에만 비관적 락 기능을 사용해야겠다 !
🐳결론
이번 경험을 통해 비관적 락 기능을 공부할 수 있었다.
다양한 락 기능이 존재하지만 이번 동시성 문제의 경우 안정성이 더 중요하다고 생각해 비관적 락 기능을 사용했다.
하지만 항상 모든 상황을 해결해 주는 만능 비법 소스 같은 것은 없기에 왜 사용하고 어떤 문제가 발생할 수 있는지 고민해봐야 할 것이다 !