운영 시스템 내에서 계속해서 아래와 같은 Connection Deadlock 로그가 발생했다.
Caused by: org.hibernate.exception.JDBCConnectionException: Unable to acquire JDBC Connection
Caused by: java.sql.SQLTransientConnectionException: HikariPool-1 - Connection is not available, request timed out after 30000ms.
해당 에러가 발생하는 부분을 보니 로직이 ScheduledExecutorService를 사용하여 일정 주기마다 로직을 실행하는 부분이어서 thread와 connection pool 쪽에 대한 문제가 있다고 생각하고 유심히 로직을 봤다.
그러나 ScheduledExecutorService의 initDelay이 다 다르고 delay은 같았다. (하나만 빼고) 그래서 적어도 겹치는(동시 수행되는) thread는 두 개일텐데 왜 deadlock이 발생하는건지 로직을 좀 더 깊게 확인했다. 일단 해당 에러가 발생했을 시점에 connection pool의 connection이 부족했다는 것이니 ...
먼저 해당 로직에서 함수를 사용하는 것들 중 transaction을 얼마나 사용하는지, @Transactional의 속성은 무엇을 사용하고 있는지 전부 분석했다. 먼저 최상위 method에서 현재 상담이 가능한지 체크하는 함수와 상담사 배정을 해주는 함수 두 개를 사용했다. 각각의 함수 또한 @Transactional 처리가 안되어있었고 그 내부에 @Transactional으로 묶인 여러 함수를 사용하면서 더 많은 connection을 사용하고 있었다.
그리고 각각의 함수에 적용되어 있던 @Transactional의 전파 속성은 모두 Propagation.REQUIRED이었다.
따라서 transaction을 포괄적으로 덮기 위해 최상위 method에 @Transactional을 선언해 하나의 transaction에서 처리하게끔 처리했다.
추가적으로 hikariCP의 connection pool size 설정도 확인해봤는데 default인 10으로 되어 있어서 증가시켜주었다.
이전에도 transactional의 propagation 관련된 에러를 접했던 경험이 있는데 이번 이슈를 해결할 때 좀 헤맸던걸 보면 제대로 이해하지 못했나보다. 특히 @Transactional의 부모, 자식 간의 상관관계에 대해서 제대로 이해할 수 있었다.
관련되어서 찾아봤을 때 https://mangkyu.tistory.com/269 의 글이 많이 도움이 되었다. 트랜잭션이란 시작에서 commit or rollback이 호출될 때 까지가 하나의 트랜잭션으로 묶인다는 글을 보니 뭔가 훨씬 더 쉽게 이해가 갔다.
물리 트랜잭션과 논리 트랜잭션에 대한 개념과 논리 트랜잭션이 여러 개 있는 경우 propagation의 속성에 따라 물리 트랜잭션이 나뉜다는 것을 그림으로 보니 제대로 이해가 갔다.