본문 바로가기
Back-End/DB

존재하지 않은 데이터에 대한 잠금의 영향

by kong_tae 2025. 6. 15.

들어가며

이전 글에서 삽입 연산에 대해서 발생하는 동시성 문제를 해결하기 위해서 Rediss의 분산락 또는 MySQL의 네임드락을 통해 순서를 제어한다는 글을 작성하였습니다. 하지만 이후에 비관적락에 대해 학습하다 보니, 존재하지 않은 데이터에 대해서도 잠금을 걸 수 있다는 것을 알 수 있었습니다. 그렇다면 다시 한번 삽입 과정에서 락을 통해 추가적인 구현을 할 필요성에 대해 생각해봐야 할 것 같아 글을 작성하게 되었습니다.

 

 

[DB] MySQL NamedLock vs Redisson 락 관리.

들어가며이전 글(https://kongdevlog.tistory.com/20)에서 Named Lock을 통해 삽입 작업에서의 동시성을 제어하였습니다.다만 이 과정에서, Redisson을 사용하는 방법이 더 좋은 방법이라고 생각이 들어 이를

kongdevlog.tistory.com

 

존재하지 않은 데이터에 대한 잠금.

다음과 같은 테이블이 존재할 때, YEAR = 5 인 데이터를 한 개만 삽입하고 싶다고 가정하겠습니다. 따라서 저는 다음과 같은 쿼리를 작성할 것입니다.

 

SELECT * FROM EXAMPLE_TABLE WHERE YEAR = 5 FOR UPDATE;
INSERT INTO EXAMPLE_TABLE(YEAR, MONTH) VALUES(5, 5);

 

그렇다면 어떻게 잠금이 걸리는지 확인해보겠습니다.

 

YEAR 에 인덱스가 걸려있지 않은 경우.

 

 

인덱스가 걸려있지 않기 때문에 모든 데이터를 조회하며 YEAR을 확인해야 했고, 이 과정에서 해당 조회한 모든 데이터들에 대해 잠금이 걸림과 동시에, supremum pseudo-record 잠금을 통해 이후의 PK에 대해서도 잠금이 걸리게 됩니다. 따라서 다른 트랜잭션에서는 어떠한 데이터도 삽입하지 못하는 문제가 발생하게 됩니다.

 

 

YEAR 에 인덱스가 걸려있는 경우.

CREATE INDEX IDX_YEAR ON EXAMPLE_TABLE(YEAR);

 

다시 한번 동일한 쿼리를 전송해보겠습니다.

 

 

이젠 모든 행들에 잠금을 거는 것 대신, GAP LOCK이 발생하였습니다. 그렇다면 이 GAP LOCK은 어디까지를 잠그는 것일까? 에 따라서 처리의 효율성이 결정되게 될 것입니다. 해당 상황에서 다른 트랜잭션에서 삽입을 시도해보겠습니다.

 

YEAR 1 ~9 까지의 값에 대해서 모두 잠금이 걸리게 되었습니다.

이는 GAP LOCK이 현재 YEAR = 5 가 들어갈 수 있는 범위에 대해서 모두 잠금을 걸었다는 것을 의미합니다. 즉, 그림과 같이 잠금을 걸게 되었던 것입니다!

 

따라서, 데이터의 범위가 촘촘하다면 GAP LOCK의 범위가 줄어들 것입니다. 

하지만 이렇게 컬럼 한 개의 중복을 제거하기 위해서 잠금을 거는 경우는 없을 것입니다. 왜냐하면 이미 DBMS에서 Unique 제약 조건을 지원하기 때문입니다. 따라서 아래의 케이스를 고려해보겠습니다.

 

존재하지 않는 복합 인덱스에 대한 잠금.

시나리오는 다음과 같습니다. Match 테이블이 존재하며 (requester_id, responder_id)가 있어서 동일한 유저 간에는 하나의 Match 만 생성되도록 제어해야하는 상황입니다.

 

이 상황에서는 유니크 제약 조건을 걸 수 없을 것입니다. 왜냐하면 두 유저의 아이디가 각각 5, 8 이라고 가정하면 (5,8) 뿐만 아니라 (8,5) 같은 상황이 발생할 수 있기 때문입니다. 따라서 이 상황에서는 잠금이 반드시 필요하게 될 것입니다.

 

그렇다면, 중복된 매칭을 만들지 않기 위해 (requester_id, responder_id)에 대해서 복합 인덱스가 있는 경우, 아래와 같은 쿼리를 사용하게 된다면 잠금이 어떻게 걸릴까요?

SELECT * FROM MATCHES
WHERE (REQUESTER_ID = 5 AND RESPONDER_ID = 8) 
OR (REQUESTER_ID = 8 AND RESPONDER_ID = 5)
FOR UPDATE

 

 

 

GAP_LOCK이 걸릴 것이라는 예상과는 달리 `supremum pseudo-record`에 잠금이 걸리게 되었습니다. 뿐만 아니라 기존에 존재한 데이터들에도 잠금이 걸리게 되었습니다.

 

현재는 데이터가 2개뿐이므로 정상적인 상황이 아니라고 파악하고 조금 더 데이터를 추가해보겠습니다.

 

이젠 두 범위에 대해서 잠금이 걸리게 되었습니다. 하지만 해당 데이터만으로는 GAP_LOCK이 어느 정도로 걸렸는지 대해서 파악할 수 없으므로, 다른 트랜잭션에서 삽입을 시도해보겠습니다.

 

 

(5,4)에 대해서는 삽입이 이루어졌지만, (5,10)에 대해서는 대기가 발생한 것으로 보아 현재 데이터 셋에 대해서는 기존에 예상한 것과 같이 잠금이 발생한 것으로 추측이 됩니다.

 

 

즉, 이 경우에는 두 개의 GAP LOCK이 발생하고 기존 하나의 컬럼에 대해서만 잠금을 걸었을 때보다 잠금의 범위가 예측하기 어렵게 되었습니다. 물론 운이 좋아서 데이터의 간격이 좁다면 GAP_LOCK 역시 줄어들 수 있겠지만, 이는 두 개의 컬럼의 조합인 상황에서는 훨씬 드문 상황이 될 것입니다.

 

마무리

이전에는 의식하지 않고, NAMED_LOCK을 통해 삽입 작업을 제어하였으나 지금와서 생각해보니 이렇게 작업한 것이 단위 시간내에서 데이터베이스의 처리량을 늘릴 수 있는 좋은 선택이었던 것 같습니다.

댓글