@Transactional 전파로 생긴 데드락의 원인은 무엇일까?

Alex·2024년 10월 16일

Binder프로젝트

목록 보기
18/18

전체 단위테스트를 돌리다가
특정한 테스트까 깨지는 문제가 있었다.

댓글에 욕설이 있는지를 확인하는 API에 대한 테스트다.

DB에 지정해둔 욕설이 댓글에 있는지 확인하고,
욕설이 맞는데도 DB에 없으면 이 욕설을 DB에 저장한다.

GPT를 사용해서 댓글에 욕설이 있는지를 확인한다.

트랜잭션의 전파

원인은 이 부분이었다.

propagation = Propagation.REQUIRES_NEW는 새로운 트랜잭션을 시작한다.참고자료

Propagation.REQUIRES_NEW가 있으면, 스프링은 데이터베이스에 대한 커넥션을 하나 추가로 생성한다고 한다.

여러 스레드가 동시에 커넥션을 요청하면 서로 기다리게 돼어서 시스템이 멈출 수 있다.

두 개의 트랜잭션이 서로가 끝나길 기다리면서 발생하는 데드락

지금 데드락은 기존 트랜잭션과 새로 생긴 트랜잭션이 서로가 끝나길 기다리게 되면서 발생했다.

다른 상황으로 예시를 들어보겠다.

유저 a를 DB에 넣는 상황을 생각해보자. 이건 트랜잭션 1에서 작동한다.
Propagation.REQUIRES_NEW를 통해서 새로운 커넥션(트랜잭션 2)을 만들고, 유저 로그와 관련된 테이블에 로그를 넣는다.

이때 커넥션에서 만든 유저의 외래키를 사용한다.

db는 첫번째 커넥션이 끝날 때까지(커밋) 기다리고 두번째 커넥션이 커밋하게 한다. 하지만, 자바에서는 두번째 커넥션이 끝나야 첫번째 커낵션이 끝나는 로직이다. 그래서 데드락이 발생하는 것.

DB의 관점에서는 트랜잭션 1이 커밋된 상태에서만 트랜잭션 2가 끝날 수 있따. 그래서, 트랜잭션 1이 끝나길 기다린다.

하지만, 스프링 서버에서는 다른 방식으로 끝난다. 트랜잭션 2가 끝나고 나서 트랜잭션 1이 끝날 수 있다.

우리 코드로 돌아가보자.

여기서 일단 curse를 저장한다.

여기서 테이블을 뒤져서 내용이 있는지 확인한다.

이 부분이 데드락이 발생한 원인으로 보인다.
트랜잭션1이 curse를 저장하고서 쓰기락을 갖고 있는 상황에서
트랜잭션2이 조회를 위한 읽기락을 획득하려고 해서 그런 것 같다.

MySQL 워크벤치에서 SHOW FULL PROCESSLIST를 입력하면 쿼리들의 실행 목록이 뜬다.

여기서 저 update 쿼리가 time만 늘어나고 종료되지는 않았다.

SELECT * FROM performance_schema.events_transactions_current;
이걸 통해서 DB의 트랜잭션 상태를 확인해보자.

두개의 트랜잭션이 계속 활성화상태고 커밋이 되지 않았다.

그렇다면, 커밋을 해버리면 되지 않을까?

아예 트랜잭션을 끝내버리고 새로 만드니 그 다음 작업에서 데드락이 발생하지 않았다.

단계별로 작동 과정을 살펴보자.


이 단계에서는 트랜잭션 하나가 ACTIVE 상태다.

트랜잭션을 커밋하고 종료하면, 930번에 스레드가 사용하던 트래잭션이 커밋된 걸 볼 수 있다.

여기로 넘어왔을 때 새로운 트랙잭션이 생겼다.

Propagation.REQUIRES_NEW 어노테이션이 달려 있던 메서드 하나가 끝나고 테스트 케이스로 돌아오면, ACTIVE 상태였던 스레드 931번의 트랜잭션이 커밋된 걸 볼 수 있다.

물론, DB에 커밋을 했기 때문에 테스트가 끝나고 데이터를 삭제하는 작업을 해주어야 한다.

profile
답을 찾기 위해서 노력하는 사람

0개의 댓글