문제 발견
최근 업무 중 웹 서버에서 오랜 시간동안 처리되지 않고 있는 스레드를 발견했다. 해당 요청은 작업이 오래 걸릴만한 프로세스가 없었기 때문에 직감적으로 데드락 이슈가 발생했음을 느끼고 바로 확인에 들어갔다.
원인 파악
다음 2가지 상황에서 데드락이 발생했을 가능성을 염두에 두고 원인을 파악했다.
- 관리자가 Update 작업 이후 커밋 혹은 롤백을 하지 않음
- 소스에 교착 상태를 유발하는 코드가 존재
문제 발견 후 해당 프로세스에서 변경되는 테이블들의 Lock 정보를 조회했고 전부 서버의 커넥션에서 발생한 것을 확인했다. 따라서 소스 내 데드락을 유발하는 코드가 있다고 판단했고 코드를 살펴본 후 원인을 알 수 있었다.
원인
문제의 코드에는 어떤 row를 수정한 뒤 특정 조건에서 동일한 row의 값을 다시 수정하는 후처리 프로세스가 존재했다. 후처리 프로세스는 다른 클래스를 호출해 처리하고 있었는데 여기서 호출하는 메서드에 전파 속성이 REQUIRED_NEW로 변경되어 있었다. 히스토리를 확인해보니 해당 메서드의 트랜잭션을 분리시켜야 하는 이슈가 있어 적용된 사항임을 확인했고 결국 상위 트랜잭션과 하위 트랜잭션에서 각각 동일한 row에 배타적 잠금을 시도해 데드락이 발생한 상황이었다.
해결
근본적인 문제는 트랜잭션을 완벽히 분리하기 위한 목적으로 전파 속성인 TRANSACTION_NEW를 이용한 것인데 REQUIRED_NEW는 완벽히 독립적인 트랜잭션을 생성해주는 것이 아니므로 트랜잭션이 완벽히 분리되도록 처리할 필요가 있었다. 해당 프로젝트는 서비스 패키지에 있는 구현체들에 대해 선언적 트랜잭션이 설정되어 있었기 때문에 컨트롤러 계층에서 각 서비스를 호출하도록 변경하여 각각의 트랜잭션으로 수행되도록 했다.
REQUIRED_NEW는 새로운 트랜잭션을 생성하는게 아닌가?
해결 파트에서 TRANSACION_NEW 속성으로는 트랜잭션이 완벽히 분리되지 않는다고 했는데 정확하게는 상위 트랜잭션과 확실히 분리된 트랜잭션이 생성된다. 그런데 왜 완벽히 독립적이지 않다고 했을까?
REQUIRED_NEW를 이용하면 새로운 트랜잭션을 생성할 수 있지만 이 트랜잭션에서 예외가 발생했을 때 외부로 전파되기 때문에 예외 처리를 해주지 않으면 사실상 상위 트랜잭션과 함께 롤백되게 된다.
또한, 별도의 쓰레드로 병렬 처리를 하지 않는 이상 REQUIRED_NEW로 생성된 하위 트랜잭션이 종료되어야 상위 트랜잭션이 다시 수행된다. 이런 관점에서 본다면 트랜잭션이 완벽하게 독립되었다고 말하기 어렵다.
그럼 위 이슈에서는 예외 처리를 해주면 되지 않나? 라고 생각할 수도 있는데 이 프로젝트는 외부로 던져진 예외를 캐치해 로깅하는 공통 기능이 있었기 때문에 후처리 메서드에 별도의 예외 처리를 하기보다는 서비스를 호출할 때부터 별개의 트랜잭션으로 분리되도록 하는 것이 더 적합하다고 판단했다.