데드락이 걸리는 상황

토닉·2022년 10월 9일
1

데드락이 걸리는 상황

팀 프로젝트를 하다가 특정 트랜잭션에서 데드락이 걸리는 상황을 마주했습니다. 팀의 비즈니스 로직은 이해하기 어려울 것 같아 간단한 예시를 통해 저희 상황과 해결 과정을 설명하겠습니다.

상황

테이블에는 Team , Member 가 있습니다. TeamMember 는 일대다 관계로 맺어져있습니다.

이 때 제가 하고 싶은 비즈니스 로직은 다음과 같습니다.

  1. ‘A’ 팀을 조회한다.
  2. ‘A’에 user3 멤버를 등록한다.
  3. A 팀에 기존 인원수(count)에 1을 더한다.

위 1~3번의 과정을 하나의 트랜잭션으로 보겠습니다. 그렇다면 위 트랜잭션이 동시에 실행됐을 때 어떤 상황이 발생될까요?

  • r1(T) : 1번 트랜잭션에서 Team을 select한다.
  • r2(T) : 2번 트랜잭션에서 Team을 select한다.
  • w1(M) : 1번 트랜잭션에서 Member를 insert한다.
  • w2(M) : 2번 트랜잭션에서 Member를 insert한다.
  • u1(T) : 1번 트랜잭션에서 Team을 update한다.
  • u2(T) : 2번 트랜잭션에서 Team을 update한다.

결론적으로 데드락이 발생합니다.

데드락이란 교착 상태를 의미하며 무한히 다음 자원을 기다리게 되는 상태를 말합니다.

이 상황이 발생하는 이유를 알려면 우선 락에 대해서 알아야 합니다.

락이란?

잠금(Lock)이라고도 불리며 동시성을 제어하기 위한 기능입니다. 여러 커넥션에서 동시에 동일한 자원을 요청할 경우 순서대로 한 시점에는 하나의 커넥션만 변경할 수 있게 해주는 역할을 합니다. 만약 잠금이 없다면 하나의 데이터를 여러 커넥션에서 동시에 변경해버릴 수 있게 됩니다.

MySQL 서버에서 InnoDB 스토리지 엔진에서는 쿼리문에 따라 락을 걸게 됩니다. 해당 DML 쿼리문일 경우 레코드 락으로 레코드에 걸게됩니다.

  • select : 락x, 읽기 락, 쓰기 락
  • update, delete, insert: 쓰기락

이 곳에서 읽기 락이란 shared lock 또는 s lock이라고도 불립니다. 조회를 하기 위해 필요한 락이며 읽기 락이 걸린 레코드는 다른 트랜잭션에서 update, delete를 할 수 없지만 select은 할 수 있습니다.

쓰기 락은 exclusive lock, x lock이라고 불립니다. 쓰기 락이 걸린 레코드에는 다른 트랜잭션이 select, update, delete가 불가능합니다.

그렇다면 위 과정에선 락이 어떻게 동작할까요?

r1(T), r2(T) 는 단순 조회이기 때문에 어떤 락도 걸리지 않을 것 입니다.

w1(M), W2(M) 에서는 Member 테이블에 해당하는 레코드에 쓰기 락이 걸리게 됩니다. 쓰기 락이 걸렸기 때문에 다른 트랜잭션에서 접근을 하지 못하게됩니다.

u1(T), u1(T) 를 하면 Team 테이블에 해당하는 레코드에 쓰기 락이 걸리게 됩니다.

어? 그럼 서로 데드락이 걸리지 않을 것 같은데?

앞서 말한대로 락이 걸린다면 서로간의 트랜잭션은 정상 동작할 것 입니다. 여기서 데드락이 걸리는 이유는 외래키 제약조건에 따른 락 때문입니다. 현재 Member를 Team에 추가하는 과정에서 Insert 문을 사용합니다.

  • insert into menber (name, team_id) values('user3', 1);

team_id 필드도 같이 선언해주는 것을 볼 수 있습니다. MySQL의 InnoDB에서는 insert를 할 때 외래키 제약조건이 걸린 필드가 있다면 해당 필드에 해당하는 다른 테이블의 레코드에 읽기 락을 걸게 됩니다. 위 예시로는 Member 를 insert하게 되면 Member 에 쓰기 락을 Team 에 읽기 락 2가지를 걸게 됩니다.

그럼 위 현상이 설명이 됩니다. 2개의 트랜잭션이 w1(M), w2(M) 을 할 때 연관된 Team 레코드에 읽기 락을 걸게 됩니다. 읽기 락은 앞서 말한대로 2개의 트랜잭션 모두 가질 수 있습니다. 이 후에 2개의 트랜잭션이 Team 레코드에 update를 하기 위해 쓰기 락을 가지려고 하니 이미 읽기 락이 걸려있기 때문에 교착상태에 빠지게 됩니다.

해결 방법

update와 insert 순서를 바꾸면 됩니다.

1번 트랜잭션이 먼저 Team update 쿼리를 했을 때 쓰기 락을 얻게 됩니다. 그럼 2번 트랜잭션은 1번 트랜잭션이 커밋이 된 후에 실행하게 됩니다. 데드락 상황은 발생하지 않고 각 트랜잭션을 직렬화하여 실행할 수 있게 됩니다.

정합성

데드락 문제는 해결할 수 있지만 데이터의 정합성도 고려해주어야 합니다. 2개의 트랜잭션이 정상적으로 동작하면 어떻게 될까요?

6명의 팀원이 들어왔는데 실제 팀에는 count가 5인 것을 볼 수 있습니다. 이런 문제는 왜 발생할까요?

2개의 트랜잭션이 `Team` 을 조회할 때 count를 4로 읽을 때 문제가 발생합니다. 2번째로 읽은 트랜잭션 입장에서는 1번째로 읽은 트랜잭션이 해당 count를 바꿔도 이 전에 읽었기 때문에 커밋되기 전의 값을 기준으로 update를 하게 됩니다. 이런 상황은 어떻게 해야할까요?

select … for update

조회할 때 for update 문장을 추가해주면 읽기를 할 때 쓰기락을 걸어줄 수 있습니다. 쓰기락을 걸게 되면 해당 레코드에 조회하는 것도 막을 수 있기 때문에 위 정합성을 해결할 수 있습니다.

select * from team where id=1 for update;

for update를 걸어주면 2개의 트랜잭션 또는 다수의 트랜잭션이 실행되더라도 위 처럼 직렬로 실행하기 때문에 데이터의 정합성을 맞출 수 있습니다. 위 같이 트랜잭션이 실행되는 것을 serial schedule 이라고도 부릅니다.

profile
우아한테크코스 4기 교육생

0개의 댓글