본문 바로가기
에러 핸들링

Could not open JPA EntityManager for transaction

by kdohyeon (김대니) 2023. 2. 3.
반응형

에러 메시지

백엔드 서버를 운영하다가 아래와 같은 애러가 빈번하게 발생했다. 발생하는 원인과 해결책에 대해서 정리해보고자 한다.

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 는 일정 수준의 커넥션이 연결되지 않도록 정책이 설정되어 있다. 따라서 효율적인 커넥션 관리는 대규모 어플리케이션의 성능과 안정성을 위해서는 필수적이다.

커넥션 풀 (Connection Pool)

이런 부하를 최소화하기 위해 커넥션을 App. 과 DB 가 서로 연결하는 시점이 아닌 서버가 뜰 때 일정 개수를 미리 만들어 둔다. 커넥션을 미리 생성해두고 유동적으로 사용하며, 이런 커넥션을 미리 만들어 둔 공간을 커넥션 풀 이라고 한다.

조회를 할 때, 커넥션 풀에서 커넥션을 하나 빌려오고 DB 와 연결을 하고 DB 처리가 완료되면 커넥션을 반납한다.

에러의 원인

다시 에러 메시지를 확인해보면 "트랜잭션을 만들 수 없었고, JDBC Connection 을 가져올 수 없었다." 라고 한다. 이 뜻은, DB 처리를 위한 커넥션이 모자라다는 뜻이다.

그럼 둘 중에 하나일텐데,

  1. Connection Pool 에 충분한 Connection 이 없거나,
  2. 빌려간 Connection 이 반납되지 않고 계속 사용 중이거나...

에러 해결

따라서, 이 에러는 아래의 해결 방법으로 해결할 수 있다.

1. Connection Pool 사이즈 늘리기

커넥션 풀 사이즈가 너무 작게 설정되어 있다면 늘려줄 수 있다. 서비스의 크기에 따라 다르겠지만 너무 크게 설정하는 것도 리소스 낭비이기 때문에 적절한 사이즈를 정해야 한다.

2. Connection Timeout 시간 늘리기

커넥션을 맺기 위해 기다리는 시간을 늘려서 에러 메시지를 해결할 수도 있다.

spring:
    datasource:
        hikari:
            ...
            connection-timeout: 10000 // 10초로 늘리기
            ...

일반적으로는 해결방법 1 과 2는 임시 방편으로 사용해볼 수 있겠으나 근본적인 해결방법이 아닐 수 있다.

3. 트랜잭션 범위를 작게 설정

코드를 작성하다 보면 트랜잭션 범위를 애매하게 잡아두거나, 너무 넓게 잡아둔 경우가 존재할 수 있다.

아래 코드는 실제로 운영을 하다가 발견했던 코드였는데,

@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 에러가 발생했다. 이 이유는 두 번째 단계인 외부 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 으로 트랜잭션 범위로 설정해두고 나머지는 트랜잭션 밖으로 분리해야 한다.

트랜잭션에 대해 알아보기

반응형

댓글