MySQL 뿐만 아니라 어떤 DB를 사용하든 Deadlock 이슈는 피해갈 수 없는 이슈이다.
나 또한 회사에서 Deadlock 이슈가 종종 발생하는데 어떻게 개선할지 갈피를 못잡고 있는 상황이였다. 🥲
그래서 Deadlock에 대해서 공부하기 위해서 Deadlock이 왜 발생하고, Deadlock이 발생하지 않도록 예방하는 방법에 대해서 알아보도록 하겠다.
📌 Deadlock을 공부하던 중, 너무 너무 좋은 유튜브 채널을 알게 되어서 공유한다.
'쉬운코드'라는 유튜버이신데, 엄청 좋은 내용의 영상이 많은 것 같다. 이 글에서도 이 분의 Deadlock 관련 영상을 참고했음을 먼저 밝힌다. 🙏
Deadlock은 '교착 상태'라는 의미를 갖고 있는 영단어이다.
DB에서 하나의 자원을 여러 프로세스에서 동시에 사용하려고 하는 상황을 의미한다.
Deadlock 이슈는 실무에서 만나면, 대처하기 까다로운 이슈이지만 DB의 무결성을 지키기 위해서는 발생해야만 하는 이슈이기도 하다.
아래 예시를 통해서 좀 더 자세히 이야기해보자.
DB에는 1번 데이터와 2번 데이터가 있는데, A프로세스를 완수하려면, 1번 데이터와 2번 데이터의 Lock을 취득해야하는 상황이다.
A프로세스가 실행되고 1번 데이터의 Lock을 취득했다. 그 후 2번 데이터의 Lock을 취득하려고 하는데, 이미 다른 곳에서 Lock을 취득한 상황이다. 알고 보니 B프로세스에서 2번 데이터의 Lock을 아직 반환하지 않고 있었던 것이다.
이 때, A프로세스는 기다릴만큼 기다려보다가 2번 데이터의 Lock이 반환되지 않으면 프로세스를 완수하지 못하고 에러를 뱉게 된다. 즉, '2번 데이터의 Lock'이라는 동일한 자원을 A프로세스와 B프로세스가 동시에 사용하려고 하면서 Deadlock이슈가 발생한 것이다.
Deadlock의 발생 원인을 한 문장으로 정의하면, '동일한 자원을 여러 프로세스에서 동시에 사용하려고 하는 경우'이다.
DB에서는 이미 발생한 Deadlock 이슈는 해소될때까지 기다리는 것 또는 에러를 뱉어버리는 것 말고는 '해결'할 수 있는 방법이 없다. Deadlock 이슈가 발생하도록 구성된 시스템이나 쿼리가 근본적인 문제이기 때문에, Deadlock 이슈를 발생시키는 근본적인 이유를 찾아서 개선해야한다.
그렇다면, Deadlock을 예방 및 회피할 수 있는 방법들에 대해서 하나씩 알아보자.
INSERT INTO table1 SELECT * FROM table2 WHERE condition;
UPDATE table1
SET col=A.val
FROM (SELECT val FROM table2 WHERE condition) AS A;
위와 같은 쿼리들을 수행하게 되면, Select한 N개의 결과를 기반으로 데이터를 생성 또는 수정할 수 있게 된다. SELECT의 대상이 되는 레코드들은 Exclusive lock(쓰기 Lock)이 걸리게 되는데, 문제는 데이터의 INSERT나 UPDATE가 끝날 때까지 Lock이 걸려있는 것이다.
SELECT한 결과를 INSERT하거나 UPDATE하는 것은 단순 SELECT보다는 훨씬 오래 걸리게 될 것이고, 그만큼 Deacklock에 빠질 가능성이 올라가게 된다. 그래서, 위와 같은 쿼리는 주의해서 사용하는 것이 좋다.
특히나 SELECT문의 조건에 Index가 걸려있지 않은 컬럼이 들어가 있으면 해당 테이블의 모든 데이터에 Lock이 걸릴 수 있으니 각별히 조심하자.
1번과 이어지는 내용이라고 할 수 있는데, 하나의 기능 또는 로직에서 너무 많은 것을 수행하려고 하면 당연히 그만큼 오래 걸리게 되고, Lock을 빨리 빨리 반환하지 못하게 될 확률도 올라가게 된다.
기능이 아무리 많아도 철저하게 Lock을 오래 들고 있지 않도록 하면 문제가 없을 수 있지만, 기본적으로 딱 자기할 것만 하고 빠지는 짧은 생애주기를 갖고 있는 기능을 만드는 습관이 Deadlock이슈를 예방하는 방법이라고 할 수도 있을 것이다.
'뭐 병에 안걸리려면 손을 자주 씻는 습관을 갖자.'와 비슷한 접근인 것이다.
하나의 테이블에 여러 정보를 덕지 덕지 들고 있는 형태의 설계는 지양해야 한다. 그렇게 되면 서로가 서로의 키를 들고 있고 쿼리 안에서 서로가 서로를 참조하는 괴이한 상황이 생기면서 왜인지도 모르게 Deadlock이 걸릴 수 있기 때문이다.
사실 지금 회사에서도 이러한 종류의 비대한 테이블이 있어서 Deadlock 이슈가 종종 발생한다. 🥲
3번의 연장이라고도 볼 수 있는데, 테이블을 비대하게 만들지 않는 것이 중요한 포인트 중 하나이다. 3번은 컬럼의 수가 많음을 '비대하다'라고 표현했다면, 지금은 테이블의 레코드 수가 많아지지 않게 해야한다는 의미이다.
테이블의 성격에 따라서 다를 수 있겠지만, 오래된 데이터들이 사실상 잘 활용되지 않는 테이블이라면, 파티셔닝을 해서 테이블을 가볍게 만드는 것이 성능 향상에 도움이 될 것이고, 곧, Lock을 빨리 빨리 반환을 한다는 것을 의미하므로 Deadlock 발생 확률을 낮춰줄 수 있다.
빠릿 빠릿하게 처리해야하는 기능들이 아니라면, Queue에 쌓아 놓고 일괄적으로 안전하게 처리하는 것도 좋은 방법이 될 수 있다. Deadlock 이슈는 주로 트래픽이 급격히 늘어나는 순간에 자주 발생한다. 왜냐하면, 짧은 순간에 동일한 레코드나 같은 인덱스에 대해서 생성, 수정, 조회등등이 무자비하게 들어오기 때문이다.
그래서 이렇게 무자비하게 들어오는 요청들을 Queue에 쌓아 놓고 소화 가능한 정도의 덩어리로 잘라서 원하는 처리를 하게 되면 Deadlock 발생 확률을 낮춰 줄 수 있다.
JPA에서 제공하는 @Lock 어노테이션이나, Redis와 같은 기술을 활용하여 '분산락' 기능을 구현하면 Deadlock이 발생할 상황에서도 순차적으로 요청을 처리해서 에러를 발생시키지 않을 수 있다.