백엔드 서버를 운영하다 보면 다음과 같은 에러를 종종 볼 수 있습니다.
Could not open JPA EntityManager for transaction; nested exception is org.hibernate.exception.JDBCConnectionException: Unable to acquire JDBC Connection
이런 에러는 왜 나는거고, 어떻게 해결할 수 있을까요?
에러를 이해하기 위해서는 먼저 데이터베이스 커넥션 풀에 대해서 알아보아야 합니다.
어플리케이션(App.)에서 데이터베이스(DB)로부터 데이터를 조회하기 위해서는 App. 이 DB 와 커넥션을 맺고 데이터를 조회하게 됩니다. 커넥션을 맺는다 라는 의미는 App. 에서 DB 로 접근한다는 의미이며 서로 연결된다 라는 의미가 있습니다.
DB 는 이런 커넥션이 많아지면 많아질 수록 부하가 심해집니다. 트래픽이 많은 운영 환경에서는 조회가 많이 일어나기 때문에 당연히 커넥션도 많이 필요하게 됩니다. 그럼 DB 는 부하가 심해지기 때문에 이를 방지하기 위해 일정 수준의 커넥션이 연결되지 않도록 정책이 설정되어 있습니다. 따라서 효율적인 커넥션 관리는 대규모 어플리케이션의 성능과 안정성을 위해서는 필수적입니다.
이런 부하를 최소화하기 위해 커넥션을 App. 과 DB 가 서로 연결하는 시점이 아닌 서버가 뜰 때 일정 개수를 미리 만들어 둡니다. 커넥션을 미리 생성해두고 유동적으로 사용하며, 이런 커넥션을 미리 만들어 둔 공간을 커넥션 풀 이라고 합니다.
조회를 할 때, 커넥션 풀에서 커넥션을 하나 빌려오고 DB 와 연결을 하고 DB 처리가 완료되면 커넥션을 반납합니다.
다시 에러 메시지를 확인해보면 트랜잭션을 만들 수 없었고, JDBC Connection 을 가져올 수 없었다.
라고 합니다. 이 뜻은, DB 처리를 위한 커넥션이 모자라다는 뜻입니다.
그럼 둘 중에 하나입니다.
1) Connection Pool
에 충분한 Connection 이 없거나, 2) 빌려간 Connection 이 반납되지 않고 계속 사용 중이거나 입니다.
따라서 이 에러는 아래의 해결 방법으로 해결해볼 수 있습니다.
커넥션 풀 사이즈가 너무 작게 설정되어 있다면 늘려줄 수 있습니다. 서비스의 크기에 따라 다르겠지만 너무 크게 설정하는 것도 리소스 낭비이기 때문에 적절한 사이즈를 정해야 합니다.
커넥션을 맺기 위해 기다리는 시간을 늘려서 에러 메시지를 해결할 수도 있습니다.
spring:
datasource:
hikari:
...
connection-timeout: 10000 // 10초로 늘리기
...
일반적으로는 해결방법 1 과 2는 임시 방편으로 사용해볼 수 있겠으나 근본적인 해결방법이 아닐 수 있습니다.
코드를 작성하다 보면 트랜잭션 범위를 애매하게 잡아두거나, 너무 넓게 잡아둔 경우를 볼 수 있습니다.
아래 코드는 실제로 운영을 하다가 발견했던 코드입니다.
@Transactional
public void sampleMethod() {
// 1. get from db
// 2. call an API from other service
// 3. merge 1 and 2
}
위와 같은 코드 때문에 지속적으로 Could not open JPA EntityManager for transaction
에러가 발생했었습니다. 이 이유는 2. 단계인 외부 API 호출에서 지연이 발생했기 때문입니다.
구글에서 트랜잭션과 관련된 글을 찾아보면 DB I/O 와 기타 I/O 를 하나의 트랜잭션으로 묶어두는 것은 좋지 않다고 합니다.
Mixing the database I/O with other types of I/O in a transactional context is a bad smell. So, the first solution for these sorts of problems is to separate these types of I/O altogether . If for whatever reason we can’t separate them, we can still use Spring APIs to manage transactions manually.
이런 경우에는 DB 조회 부분만 @Transactional
으로 트랜잭션 범위로 설정해두고 나머지는 트랜잭션 밖으로 분리해야합니다.
생각보다 흔하게 발생하는 에러 메시지여서 따로 정리해두고 나중에도 참고하려고 합니다.