-
[Spring Data JPA] 트랜잭션 끝나도 DB 커넥션 릴리즈 안됨 (feat. OSIV)🩸 삽질의 추억 2025. 6. 27. 02:27728x90
개발 환경:
- Spring 3.4.4, Spring Data JPA(Hibernate 6.6.11)
- 한 메서드 내에 DB 접근와 SSE send를 하고 있다.
- SSE가 tomcat thread로 동기 처리된다. (원래는 비동기 처리해야함. 커넥션 점유 테스트를 위해 이렇게 둠.)
- 커넥션 점유를 5초 이상하면 로그를 출력하도록 설정했다.
문제 상황
- 해당 메서드(~=스레드) 에서 커넥션을 계속 점유하고 있다는 로그가 출력된다.
- `maximum-pool-size` 를 초과하면 에러가 발생한다.
사건의 발단이 된 코드는 다음과 같다.
코드가 길어서 축약했다.
public SseEmitter subscribe(AuthUser authUser) { Long userId = userRepository.findByOauthId(authUser.getOauthId()) .orElseThrow(() -> new BusinessExceptionHandler("유저가 존재하지 않습니다.", ErrorCode.NOT_FOUND_ERROR)) .getId(); SseEmitter emitter = new SseEmitter(TIMEOUT); emitters.put(userId, emitter); ... try { emitter.send(SseEmitter.event().name("connect").data("SSE 연결 완료")); } catch (IOException e) { emitter.complete(); emitters.remove(userId); } return emitter; }spring: hikari: leak-detection-threshold: 5000
기존에 알고 있던 사실:
userRepository.findByOauthId 를 실행하기 위해 엔티티 매니저가 DB 커넥션을 획득한다.
userRepository.findByOauthId 트랜잭션이 종료됨에 따라 엔티티 매니저가 DB 커넥션을 릴리즈한다.
의문점은 다음과 같다.
userRepository.findByOauthId 트랜잭션이 종료됨에 따라
엔티티 매니저가 DB 커넥션을 릴리즈해야하는데..
왜 계속 점유를 하고 있는거지?
의문점 해결:
먼저, OSIV 에 대해서 이해를 해야한다.
참고
더보기0. 검색 키워드
- open-in-view
- OSIV
1.
Spring Boot 에는 open-in-view 설정의 기본 값이 true이다.
나는 아무 설정을 하지 않았으므로 당연히 `open-in-view=true` 로 설정되어 있었다.
이 값이 true이면,
- JPA의 EntityManager는 클라이언트에게 HTTP 응답을 완료할 때까지 열려있다.(= 응답이 끝나고 나서 em.close()를 한다.)
- Hibernate x.x 에서 DB의 conn.close()는 EntityManger.close() 가 동작하는 과정에서 내부적으로 conn.close()가 실행된다.
- 위의 두 내용을 종합하면 => DB connection은 HTTP 응답 반환 이후에 close 된다.
- SSE 를 동기 처리한 코드에서는 위의 이유+`SSE 는 close 되기 전까지 thread를 물고 있는다.` 라는 복합 이유로 인해 DB 커넥션이 계속 점유되고 있는 것이다.
그래서 트랜잭션이 끝나도 DB 커넥션이 반납되지 않았던 것이다!
추가 분석:
왜, 어떻게 세션이 끝날 때까지 커넥션이 붙잡혀 있는지 궁금했다.
hibernate를 비롯한 jpa 라이브러리 코드를 추척했다.
커넥션의 획득과 해제 시점 설정 관련된 코드가 hibernate.connection.handling_mode 임을 알게 되었다.
출처: 챗지피티, https://jaimemin.tistory.com/2704* 위의 블로그는 시퀀스 다이어그램도 있어서 읽어보면 좋다.
+ hibernate의 handling_mode 에 관한 자료가 많이 없다...
Spring이 Hibernate를 부트스트래핑하는 과정을 배제하고
일단 Hibernate 자체만을 먼저 분석한다.
JdbcSettings.java에서 connection_handling의 기본 값이
DELAYED_ACQUISITION_AND_RELEASE_AFTER_TRANSACTION임을 알 수 있다. 이는 hibernate 5.2+ 부터 적용된다.

CONNECTION_HANDLING 의 종류가 무엇이 있는지는
PhysicalConnectionHandlingMode.java에서 확인할 수 있다.

한국어로 번역해서 정리하면 다음과 같다.PhysicalConnectionHandlingMode 주석 번역 IMMEDIATE_ACQUISITION_AND_HOLD 세션이 열리는 즉시 획득되며
세션이 닫힐 때까지 유지된다.
이는 유일하게 유효한 조합이며 커넥션을 즉시 획득하는 것을 포함한다.DELAYED_ACQUISITION_AND_HOLD 필요한 즉시 획득되며
세션이 닫힐 때까지 유지된다.
이는 원래 Hibernate 동작이다.DELAYED_ACQUISITION_AND_RELEASE_AFTER_STATEMENT 필요한 즉시 획득되며
각 명령문이 실행된 후 해제된다.DELAYED_ACQUISITION_AND_RELEASE_BEFORE_TRANSACTION_COMPLETION 필요한 즉시 획득되며,
커밋 또는 롤백 전에 해제된다.DELAYED_ACQUISITION_AND_RELEASE_AFTER_TRANSACTION 필요한 즉시 획득되며,
각 트랜잭션이 완료된 후 해제된다.의문점 하나, (애매하게 해결됨)
DELAYED_ACQUISITION_AND_HOLD 이 "This is the original Hibernate behavior" 라며 기본 동작이라는 주석이 적혀 있는데,
JdbcSettings.java 에는 DELAYED_ACQUISITION_AND_RELEASE_AFTER_TRANSACTION가 default 라고 나와 있다.
Q. the original behavior != default behavior 인건가?
A.
https://hibernate.atlassian.net/browse/HHH-5184?focusedCommentId=74692
이 링크에서 DELAYED_ACQUISITION_AND_HOLD 를 "original legacy behavior" 라고 표현한다.
DELAYED_ACQUISITION_AND_HOLD를 the original behavior라고 표현하는 것에 대해
default behavior 보단 legacy behavior 로 이해하면 될 것 같다.
아무튼 JdbcSettings.java의 CONNECTION_HANDLING 은 JdbcEnvironmentInitator.java에서 다음과 같이 사용된다.
JdbcEnvironmentInitator.java의 TemporaryJdbcSessionOwner() 메서드 내부 로직의 일부를 캡쳐했다.

hibernate.connection.handling_mode 를 application.yml에서 직접 설정하지 않았으므로
hibernate.connection.handling_mode 를 설정하는 방법은 다음과 같다.
spring: jpa: properties: hibernate.connection.handling_mode=specifiedHandlingMode 는 null 이고 이에 따라 getDefaultConnectionHandlingMode() 가 실행된다.
JdbcResourceLocalTransactionCoordinatorBuilderImpl.java의 getDefaultConnectionHandlingMode() 는 다음과 같다.

이에 따라
JdbcSettings.java에서 connection_handling의 기본 값이 DELAYED_ACQUISITION_AND_RELEASE_AFTER_TRANSACTION 라는 증명을 종료한다.
여기까지 왔으면 HIbernate 는 DELAYED_ACQUISITION_AND_RELEASE_AFTER_TRANSACTION 설정
"DB 커넥션은 필요한 즉시 획득되며, 각 트랜잭션이 완료된 후 해제된다." 정책을 따름을 알 수 있다.
하지만 이 아티클 초반의 '문제 정의'에 따르면
DB 커넥션은 트랜잭션이 완료된 이후에도 점유되고 있었다.
즉, 실제로는 DELAYED_ACQUISITION_AND_HOLD 정책을 따르고 있는 것이다.
이번에는 Spring이 Hibernate를 부트스트래핑하는 과정에서 CONNECTION_HANDING 설정이 어떻게 바뀌는지 분석한다.
Hibernate가 부트스트래핑 될때 중점적으로 봐야하는 파일은 HibernateJpaVendorAdapter.java 이다.
이름대로 HibernateJpaVendorAdapter는 Hibernate를 JPA 구현체로 설정하기 위한 어댑터 클래스이다.
HibernateJpaVendorAdapter를 사용하면 Hibenate의 설정을 JPA에 자동 적용해준다.
JPA는 추상화된 데이터 접근을 제공하기 때문에 특정 벤더에 종속적이지 않다.
HIbernate는 JPA의 구현체 중 하나이다.
출처: https://dev-coco.tistory.com/74HibernateJpaVendorAdapter 클래스에서 중점적으로 봐야하는 메서드는
setPrepareConnection(boolean prepareConnection) 이다.

prepareConnection 값은 true 가 default 값이며
이는 Spring 4.3.1 부터 적용되고 있다.
메서드 주석에 의하면 해당 메서드는
- 격리 수준과 읽기 전용 여부 같은 트랜잭션 특성 정보를 JDBC 커넥션에 직접 적용할지 결정한다.
- prepareConnection=true 인 경우 CONNECTION_HANDLING 값은 DELAYED_ACQUISITION_AND_HOLD로 적용된다.
그럼 Spring이 setPrepareConnection() 메서드를 Hibernate에 hibernate.connection.handling_mode=DELAYED_ACQUISITION_AND_HOLD를 어떻게, 어디서 전달하느냐를 따라가본다.
코드가 길어서 캡쳐 대신 복붙으로 가져왔다.
동일한 HibernateJpaVendorAdapter 클래스 내부의 메서드이다.
@Override public Map<String, Object> getJpaPropertyMap(PersistenceUnitInfo pui) { return buildJpaPropertyMap(this.jpaDialect.prepareConnection && // 1번 ✅ pui.getTransactionType() != PersistenceUnitTransactionType.JTA); } @Override public Map<String, Object> getJpaPropertyMap() { return buildJpaPropertyMap(this.jpaDialect.prepareConnection); // 1번 ✅ } private Map<String, Object> buildJpaPropertyMap(boolean connectionReleaseOnClose) { Map<String, Object> jpaProperties = new HashMap<>(); // ... 다른 설정들 jpaProperties.put() if (connectionReleaseOnClose) { jpaProperties.put(AvailableSettings.CONNECTION_HANDLING, PhysicalConnectionHandlingMode.DELAYED_ACQUISITION_AND_HOLD); // 2번 ✅ } // For SpringBeanContainer to be called on Hibernate 6.2 jpaProperties.put("hibernate.cdi.extensions", "true"); return jpaProperties; }1. 1번 ✅ 에서 prepareConnection = true 로 인자가 넘어간다.
2. buildJpaPropertyMap의 파라미터 connectionReleaseOnClose 는 true를 받는다.
3. if(connectionReleaseOnClose) 조건문은 true로, 조건문 내부가 실행된다.
4. 1번 ✅ 에서 handling mode 값이 DELAYED_ACQUISITION_AND_HOLD 로 설정되는 것을 확인할 수 있다.
아까 언급했던 JdbcEnvironmentInitator.java#TemporaryJdbcSessionOwner() 내부 로직을 확인한다.

JpaPropertiesMap 에 CONNECTION_HANDLING 값이 DELAYED_ACQUISITION_AND_HOLD 으로 명시되어 설정되었으므로Hibernate가 부트스트래핑되는 과정에서 specifiedHandlingMode는 더이상 null 값이 아니게 되고, specifiedHandlingMode(DELAYED_ACQUISITION_AND_HOLD) 으로 설정된다.
이에 따라
open-in-view 가 true이면 DB connection은 EntityManager 가 종료되기 전까지 유지된다는 증명을 완료한다.
추가 참고자료
[Spring] JPA 와 Mybatis 동시 사용 시 Connection Deadlock 벗어난 이슈 정리
계기PHP 에서 Java 로 작업을 진행하면서 기존에 작성했던 Query 문을 사용해야 할 필요가 생었겼다. 근데 한번에 효율적으로 데이터들을 불러오려고 작성된 Query 를 사용하려다보니 우선은 Query 문
karsei.pe.kr
728x90'🩸 삽질의 추억' 카테고리의 다른 글
[Fixture Monkey] giveMeBuilder().set()의 값이 적용되지 않음 (0) 2025.10.20 [Github Actions] attempted methods [none password] (0) 2025.05.27 [Java, Docker] gradlew ... did not complete successfully (1) 2024.09.29 [PWA Builder, Android] 앱 상단 URL바 삭제하기 | display : standalone/fullscreen 적용안됨 (0) 2024.08.03 [signing.keystore] 비밀번호 올바르게 입력했는데 앱 패키징 안됨: Error generating app package (0) 2024.08.03