Could not open JPA EntityManager 에러는 어떤 상황에서 발생할까?
Java에서 Spring Boot와 JPA를 사용할 때 Could not open JPA EntityManager 에러는 보통 Repository나 Service에서 데이터베이스 접근을 시도하는 순간 발생합니다. 이름만 보면 EntityManager 생성 자체가 실패한 것처럼 보이지만, 실제 원인은 그보다 아래 계층에 있는 경우가 많습니다.
JPA의 EntityManager는 엔티티를 조회하고 저장하기 위해 데이터베이스 연결과 트랜잭션 컨텍스트를 필요로 합니다. 그런데 이 과정에서 DB 연결을 얻지 못하거나, 트랜잭션 설정이 맞지 않거나, 이미 커넥션 풀이 고갈된 상태라면 EntityManager를 열 수 없다는 형태의 예외가 발생할 수 있습니다.
실무에서는 에러 메시지의 첫 줄만 보고 판단하면 원인을 놓치기 쉽습니다. 이 에러는 단독 원인이라기보다, 내부에 감싸진 실제 예외를 찾아야 해결 방향이 보이는 유형입니다.
Java Could not open JPA EntityManager 에러의 대표 로그
Java 애플리케이션에서 Could not open JPA EntityManager 에러가 발생하면 보통 아래와 비슷한 로그가 출력됩니다.
org.springframework.transaction.CannotCreateTransactionException:
Could not open JPA EntityManager for transaction
Caused by: org.hibernate.exception.JDBCConnectionException:
Unable to acquire JDBC Connection
여기서 중요한 부분은 Could not open JPA EntityManager for transaction 자체가 아니라 그 아래의 Caused by입니다. Spring은 트랜잭션을 시작하기 위해 EntityManager를 열려고 했고, Hibernate는 JDBC Connection을 얻으려 했지만 실패한 상황입니다.
즉, 이 에러를 보면 먼저 “JPA 설정이 문제인가?”라고 접근하기보다 “왜 JDBC 커넥션을 얻지 못했는가?”를 확인하는 편이 더 빠릅니다.
Could not open JPA EntityManager 원인 1: DB 연결 정보 오류
Could not open JPA EntityManager 에러에서 가장 먼저 확인할 부분은 데이터베이스 연결 정보입니다. 개발 환경, 테스트 환경, 운영 환경을 나눠 쓰는 프로젝트에서는 profile 설정이 잘못 적용되어 엉뚱한 DB 주소를 바라보는 경우가 있습니다.
spring:
datasource:
url: jdbc:mysql://localhost:3306/sample_db
username: sample_user
password: sample_password
driver-class-name: com.mysql.cj.jdbc.Driver
이 설정에서 확인할 부분은 단순합니다. DB 호스트, 포트, 데이터베이스명, 계정, 비밀번호가 실제 환경과 맞는지 봐야 합니다. 특히 Docker나 Kubernetes 환경에서는 localhost가 내 PC가 아니라 컨테이너 자기 자신을 의미할 수 있습니다.
로컬에서는 정상인데 서버에서만 실패한다면 DB 접근 권한, 보안 그룹, 방화벽, 네트워크 경로도 함께 확인해야 합니다. 반대로 서버에서는 정상인데 로컬에서만 실패한다면 profile 설정이나 로컬 DB 실행 여부를 먼저 보는 것이 좋습니다.
확인할 명령어 예시
애플리케이션을 보기 전에 DB에 직접 접속이 가능한지 먼저 확인하면 원인 범위를 빠르게 줄일 수 있습니다.
mysql -h localhost -P 3306 -u sample_user -p sample_db
이 명령어로도 접속이 안 된다면 Spring Boot나 JPA 문제가 아닐 가능성이 큽니다. DB 서버 상태, 계정 권한, 네트워크 접근부터 확인하는 편이 맞습니다.
Could not open JPA EntityManager 원인 2: 커넥션 풀 고갈
Java에서 Could not open JPA EntityManager 에러가 간헐적으로 발생한다면 커넥션 풀 상태를 확인해야 합니다. Spring Boot는 기본적으로 HikariCP를 사용하는 경우가 많고, 요청이 들어올 때마다 DB 커넥션을 풀에서 빌려 사용합니다.
그런데 커넥션이 반환되지 않거나, 오래 걸리는 쿼리가 누적되거나, 동시에 처리할 요청보다 커넥션 풀 크기가 너무 작으면 새 트랜잭션을 시작할 때 커넥션을 얻지 못합니다. 이때도 EntityManager를 열 수 없다는 에러로 나타날 수 있습니다.
spring:
datasource:
hikari:
maximum-pool-size: 10
minimum-idle: 5
connection-timeout: 30000
여기서 maximum-pool-size는 동시에 사용할 수 있는 최대 DB 커넥션 수입니다. 값을 무조건 크게 잡는다고 좋은 것은 아닙니다. 애플리케이션 서버의 동시 처리량뿐 아니라 DB 서버가 감당할 수 있는 연결 수까지 함께 봐야 합니다.
커넥션 풀 고갈이 의심될 때는 단순히 풀 크기를 늘리기보다, 반환되지 않는 커넥션이 있는지, 오래 걸리는 쿼리가 있는지, 트랜잭션 범위가 과하게 넓은지 함께 확인하는 것이 좋습니다.
커넥션 누수 확인 설정
HikariCP에서는 커넥션을 너무 오래 점유하는 코드를 찾기 위해 leak detection 설정을 사용할 수 있습니다.
spring:
datasource:
hikari:
leak-detection-threshold: 20000
위 설정은 커넥션이 일정 시간 이상 반환되지 않을 때 로그를 남기게 합니다. 다만 이 설정은 원인을 찾기 위한 보조 도구로 보는 것이 좋습니다. 운영 환경에 상시로 켜기보다는 문제 분석이 필요한 상황에서 신중하게 적용하는 편이 낫습니다.
Could not open JPA EntityManager 원인 3: 트랜잭션 설정 문제
Could not open JPA EntityManager 에러는 트랜잭션 설정과도 관련이 있습니다. JPA는 보통 트랜잭션 안에서 엔티티 변경을 감지하고 flush를 수행합니다. 그래서 Service 계층에서 @Transactional을 적절히 사용하는 것이 중요합니다.
@Service
public class OrderService {
private final OrderRepository orderRepository;
public OrderService(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
@Transactional
public void updateOrder(Long orderId) {
Order order = orderRepository.findById(orderId)
.orElseThrow();
order.changeStatus(OrderStatus.PAID);
}
}
위 코드처럼 변경 작업은 Service 메서드에 트랜잭션을 걸어주는 방식이 일반적입니다. Repository 메서드 자체는 Spring Data JPA가 어느 정도 트랜잭션을 처리해주지만, 여러 작업을 하나의 업무 단위로 묶어야 한다면 Service 계층에서 경계를 잡는 편이 명확합니다.
주의할 점은 @Transactional을 붙였는데도 동작하지 않는 경우입니다. 대표적으로 같은 클래스 내부에서 자기 자신의 메서드를 호출하면 Spring 프록시를 거치지 않기 때문에 트랜잭션이 적용되지 않을 수 있습니다.
트랜잭션이 적용되지 않는 예시
@Service
public class OrderService {
public void process() {
updateOrder();
}
@Transactional
public void updateOrder() {
// DB 변경 로직
}
}
이 코드는 겉으로는 @Transactional이 붙어 있어서 정상처럼 보입니다. 하지만 process()에서 같은 객체의 updateOrder()를 직접 호출하면 Spring AOP 프록시를 통하지 않습니다.
이런 경우에는 트랜잭션이 필요한 메서드를 별도 Service로 분리하거나, 외부에서 프록시를 통해 호출되도록 구조를 바꾸는 편이 좋습니다. 단순히 어노테이션을 붙이는 것보다 호출 경로를 함께 보는 것이 중요합니다.
Could not open JPA EntityManager 원인 4: Lazy Loading과 영속성 컨텍스트 문제
Could not open JPA EntityManager와 함께 LazyInitializationException이 같이 보이는 경우도 있습니다. 이 경우에는 DB 연결 자체보다 영속성 컨텍스트가 닫힌 뒤에 지연 로딩을 시도한 것이 원인일 수 있습니다.
Order order = orderRepository.findById(orderId).orElseThrow();
return order.getMember().getName();
만약 member가 지연 로딩 관계이고, 트랜잭션이 이미 끝난 뒤 접근한다면 Hibernate는 추가 조회를 수행할 수 없습니다. 이때 EntityManager가 닫혀 있다는 식의 예외가 함께 나타날 수 있습니다.
해결 방법은 상황에 따라 다릅니다. 화면 응답에 필요한 데이터라면 fetch join이나 DTO 조회를 사용하는 방식이 읽기 쉽습니다. 단순히 Open Session In View 설정에 기대는 방식은 컨트롤러나 뷰 계층까지 영속성 컨텍스트 범위가 넓어져 의도하지 않은 쿼리를 만들 수 있습니다.
fetch join 예시
@Query("select o from Order o join fetch o.member where o.id = :orderId")
Optional<Order> findWithMemberById(@Param("orderId") Long orderId);
이 방식은 주문을 조회할 때 회원 정보까지 함께 가져옵니다. 응답에 필요한 연관 데이터가 명확할 때는 fetch join이 문제를 드러내기 쉽고, 쿼리 의도도 비교적 분명합니다.
Could not open JPA EntityManager 원인 5: 테스트 환경 설정 누락
테스트 코드에서만 Java Could not open JPA EntityManager 에러가 발생한다면 테스트용 설정을 확인해야 합니다. 특히 @SpringBootTest, @DataJpaTest, profile, test datasource 설정이 서로 맞지 않으면 애플리케이션 실행 때와 다른 문제가 생깁니다.
@SpringBootTest
@ActiveProfiles("test")
class OrderServiceTest {
@Test
void 주문_상태를_변경한다() {
// test code
}
}
테스트에서 test profile을 사용한다면 application-test.yml에 datasource 설정이 있어야 합니다. H2를 사용할지, 로컬 MySQL을 사용할지, Testcontainers를 사용할지 팀 기준을 먼저 정하는 것이 좋습니다.
spring:
datasource:
url: jdbc:h2:mem:testdb
driver-class-name: org.h2.Driver
username: sa
password:
jpa:
hibernate:
ddl-auto: create-drop
다만 H2는 MySQL과 SQL 문법이나 타입 처리에서 차이가 날 수 있습니다. 간단한 Repository 테스트에는 편하지만, 실제 MySQL 문법을 많이 쓰는 프로젝트라면 Testcontainers로 MySQL을 띄우는 방식이 더 안정적인 선택일 수 있습니다.
Could not open JPA EntityManager 해결 순서
Could not open JPA EntityManager 에러는 원인이 여러 갈래라서 순서 없이 보면 시간이 오래 걸립니다. 아래 순서대로 확인하면 불필요한 추측을 줄일 수 있습니다.
1단계: Caused by 로그 확인
가장 먼저 전체 스택트레이스에서 Caused by를 확인합니다. Access denied, Communications link failure, Connection is not available, LazyInitializationException 중 무엇이 보이는지에 따라 해결 방향이 달라집니다.
Caused by: java.sql.SQLNonTransientConnectionException:
Could not create connection to database server
이런 로그라면 JPA 코드보다 DB 접속 경로를 먼저 봐야 합니다.
2단계: datasource 설정 확인
profile별 설정 파일을 확인합니다. 로컬에서는 application-local.yml, 개발 서버에서는 application-dev.yml, 운영 환경에서는 application-prod.yml처럼 나누는 경우가 많습니다.
java -jar app.jar --spring.profiles.active=dev
실행 profile이 예상한 값인지 확인해야 합니다. 설정 파일은 맞는데 실제로 다른 profile로 실행되고 있으면, 코드는 정상이어도 잘못된 DB를 바라보게 됩니다.
3단계: DB 직접 접속 테스트
애플리케이션 밖에서 DB 접속이 되는지 확인합니다. 이 단계에서 실패하면 Repository, Entity, Transaction 설정을 볼 필요가 없습니다.
mysql -h db.example.com -P 3306 -u app_user -p
Docker Compose를 사용한다면 컨테이너 내부에서 접속 테스트를 해보는 것이 더 정확합니다. 호스트 PC에서는 접속되지만 애플리케이션 컨테이너에서는 안 되는 경우가 있기 때문입니다.
4단계: 트랜잭션 경계 확인
Service 계층에 @Transactional이 적절히 붙어 있는지 확인합니다. 단순 조회라면 readOnly 트랜잭션을 사용할 수 있고, 변경 작업이라면 일반 트랜잭션으로 묶어주는 것이 안전합니다.
@Transactional(readOnly = true)
public OrderResponse getOrder(Long orderId) {
Order order = orderRepository.findById(orderId)
.orElseThrow();
return OrderResponse.from(order);
}
읽기 작업에는 readOnly = true를 명시하면 의도를 드러내기 좋습니다. 팀에서 코드를 볼 때 이 메서드가 데이터를 변경하지 않는다는 힌트가 되기 때문입니다.
5단계: Lazy Loading 접근 위치 확인
트랜잭션 밖에서 연관 엔티티에 접근하는 코드가 있는지 확인합니다. 컨트롤러나 JSON 직렬화 과정에서 지연 로딩이 발생하는 경우도 있으므로 응답 DTO 변환 위치를 함께 봐야 합니다.
@Transactional(readOnly = true)
public OrderResponse getOrder(Long orderId) {
Order order = orderRepository.findWithMemberById(orderId)
.orElseThrow();
return new OrderResponse(
order.getId(),
order.getMember().getName()
);
}
DTO 변환을 트랜잭션 안에서 끝내면 지연 로딩 문제를 피하기 쉽습니다. 다만 필요한 데이터가 명확하다면 처음부터 fetch join이나 전용 조회 쿼리로 가져오는 쪽이 더 읽기 좋은 경우가 많습니다.
실무에서 자주 놓치는 확인 포인트
Could not open JPA EntityManager 에러를 볼 때 자주 놓치는 부분은 “에러가 발생한 코드 위치”와 “실제 원인 위치”가 다를 수 있다는 점입니다. Controller에서 터진 것처럼 보여도 원인은 datasource 설정일 수 있고, Repository 호출에서 터져도 원인은 커넥션 풀 고갈일 수 있습니다.
또 하나는 환경 차이입니다. 로컬, 개발 서버, 운영 서버가 서로 다른 DB 설정과 네트워크 조건을 가지고 있으면 같은 코드라도 결과가 달라집니다. 그래서 이 에러는 코드만 보기보다 실행 환경과 설정을 함께 보는 편이 정확합니다.
체크리스트
- 전체 스택트레이스에서 가장 아래쪽
Caused by를 확인했는가? - 현재 실행 중인 Spring profile이 의도한 값인가?
- datasource url, username, password가 맞는가?
- 애플리케이션 실행 환경에서 DB에 직접 접속이 가능한가?
- HikariCP 커넥션 풀이 고갈되고 있지는 않은가?
@Transactional이 외부 프록시 호출을 통해 적용되고 있는가?- 트랜잭션 종료 후 Lazy Loading이 발생하고 있지는 않은가?
- 테스트 환경에서 별도 datasource 설정이 누락되지 않았는가?
Java Could not open JPA EntityManager 해결 예시
예를 들어 아래와 같은 상황이라고 가정해보겠습니다.
CannotCreateTransactionException:
Could not open JPA EntityManager for transaction
Caused by: java.sql.SQLTransientConnectionException:
HikariPool-1 - Connection is not available, request timed out after 30000ms.
이 경우에는 EntityManager 설정을 먼저 수정할 문제가 아닙니다. HikariCP에서 커넥션을 빌리지 못하고 있습니다. 따라서 쿼리 지연, 트랜잭션 범위, 커넥션 반환 여부, pool size 설정을 확인해야 합니다.
만약 특정 API에서만 발생한다면 해당 API의 Service 메서드가 너무 많은 DB 작업을 하나의 트랜잭션 안에서 처리하고 있는지 확인합니다. 외부 API 호출이나 파일 처리 같은 시간이 걸리는 작업을 트랜잭션 안에 넣어두면 커넥션을 오래 점유하게 됩니다.
@Transactional
public void processOrder(Long orderId) {
Order order = orderRepository.findById(orderId).orElseThrow();
externalPaymentClient.confirm(order.getPaymentKey());
order.markAsPaid();
}
위 코드는 결제 승인 같은 외부 호출이 트랜잭션 안에 들어가 있습니다. 이러면 외부 응답을 기다리는 동안 DB 커넥션을 계속 잡고 있을 수 있습니다. 이런 구조는 아래처럼 외부 호출과 DB 변경 트랜잭션을 분리하는 편이 더 안전합니다.
public void processOrder(Long orderId) {
PaymentResult result = externalPaymentClient.confirm(orderId);
orderTransactionService.markAsPaid(orderId, result);
}
@Service
public class OrderTransactionService {
@Transactional
public void markAsPaid(Long orderId, PaymentResult result) {
Order order = orderRepository.findById(orderId).orElseThrow();
order.markAsPaid(result);
}
}
이렇게 분리하면 DB 커넥션을 점유하는 시간이 줄어듭니다. 또한 트랜잭션이 필요한 코드와 외부 시스템을 호출하는 코드가 분리되어 유지보수할 때 흐름을 파악하기 쉽습니다.
정리: Could not open JPA EntityManager는 원인 로그부터 봐야 한다
Java에서 Could not open JPA EntityManager 에러가 발생하면 EntityManager라는 단어에만 집중하기 쉽습니다. 하지만 대부분은 DB 연결 실패, 커넥션 풀 고갈, 트랜잭션 경계 문제, Lazy Loading 접근 위치, 테스트 설정 누락 중 하나로 이어집니다.
해결 순서는 명확하게 잡는 것이 좋습니다. 먼저 전체 스택트레이스의 Caused by를 보고, datasource 설정과 DB 접속 가능 여부를 확인합니다. 그다음 커넥션 풀 상태, 트랜잭션 적용 여부, 지연 로딩 접근 위치를 차례로 보면 됩니다.
이 에러는 메시지 하나만 외워서 해결하기 어렵습니다. JPA가 EntityManager를 열기 위해 어떤 조건을 필요로 하는지 이해하고, 그 조건 중 무엇이 깨졌는지 확인하는 방식으로 접근해야 합니다. 그렇게 보면 같은 에러 메시지라도 원인을 훨씬 빠르게 좁힐 수 있습니다.
'개발 > JAVA' 카테고리의 다른 글
| [JAVA] Java Deadlock 발생했을 때 처리 전략: 원인 분석부터 재발 방지까지 (0) | 2026.05.19 |
|---|---|
| [JAVA] Java에서 Too many connections 발생했을 때 대응 방법 (0) | 2026.05.18 |
| [JAVA] Java Spring Transaction silently rolled back 문제 원인 분석 (1) | 2026.05.16 |
| [JAVA] LazyInitializationException 실무에서 해결한 방법 (0) | 2026.05.15 |
| [JAVA] Embedded Tomcat 기동 실패 원인 분석 (0) | 2026.05.14 |
