[Error] Could not open JPA EntityManager for transaction

김대니·2023년 1월 17일
3

에러 핸들링

목록 보기
1/2
post-thumbnail

에러 메시지

백엔드 서버를 운영하다 보면 다음과 같은 에러를 종종 볼 수 있습니다.

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 에러가 발생했었습니다. 이 이유는 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 으로 트랜잭션 범위로 설정해두고 나머지는 트랜잭션 밖으로 분리해야합니다.

마무리하며

생각보다 흔하게 발생하는 에러 메시지여서 따로 정리해두고 나중에도 참고하려고 합니다.

profile
?=!, 물음표를 느낌표로

0개의 댓글