개요
Hikari 라는 의미와... Hibernate 라는 단어를 잘 모르고 Test 를 진행하려니까 뭐하는건지 모르겠어서 .... 대충이라도 정리
HikariCP (hikari Connection Pool) : 커넥션을 매번 새로 만ㄷ르면 비용이 크니까 여러개 만들어두고 빌려줬다가 반납받는 형식의 pool 관리자
이때 커넥션이라는건 실제 DB 와의 연결 1개를 의미함 HikariCP 가 connection 을 관리한다고 보면 됨
(이 때 속성으로 SQL 이 실행하자마자 즉시 커밋할건지, 명시적 commit rollback 이 있어야 반영할건지 결정하는게 autoCommit)
Hibernate 는 JPA interface 의 실제 구현체 == SQL 생성, CP 에서 커넥션 빌려서 실제로 쿼리를 보내는 역할
그래서 query 실행시키면 hibernate.xxx.xxx 이런식으로 뜸
즉 동작순서가 : Hibernate 가 connection 을 DataSource (얘도 인터페이스) 에게 요청하면 HikariCP (DataSource 구현체, 풀 관리) 가 꺼내주는 형태
SpringBoot 는 Hibernate, HikariCP 등을 자동으로 설정해주는 역할
문제 상황
기존에 생각했던 DB connection 동작은 Lazy connection acquisition
SpringBoot 의 기본 값은 아래와 같기 때문에 @Transacion 진입 시점에는 connection 을 잡지 않고 첫 DB 문장 실행 시점에 Connection 을 점유하는 것으로 알고 있었음
| hibernate.connection.handling_mode = DELAYED_ACQUISITION_AND_RELEASE_AFTER_TRANSACTION |
-> 이 가정을 토대로... DB 접근 전에 Lock 이 실패하면 Connection 낭비가 없다는 테스트를 작성
@SpringBootTest
@ActiveProfiles("test")
class TransactionConnectionWasteTest {
@Autowired private ConnectionWasteProbeService probe;
@Autowired private HikariDataSource hikariDataSource;
@Test
@DisplayName("[A] @Transactional 진입만으론 커넥션 미획득")
void connection_not_acquired_when_lock_fails_before_db_access() {
HikariPoolMXBean pool = hikariDataSource.getHikariPoolMXBean();
int baseline = pool.getActiveConnections();
AtomicInteger activeAtFailure = new AtomicInteger(-1);
assertThatThrownBy(() -> probe.failLockWithoutDbAccess(activeAtFailure))
.isInstanceOf(SimulatedLockFailure.class);
// 기대: baseline 그대로 (DB 접근 0회이므로 커넥션 미점유)
assertThat(activeAtFailure.get()).isEqualTo(baseline);
}
static class ConnectionWasteProbeService {
@Transactional
public void failLockWithoutDbAccess(AtomicInteger activeRecorder) {
// DB 접근 없이 측정 후 throw
activeRecorder.set(dataSource.getHikariPoolMXBean().getActiveConnections());
throw new SimulatedLockFailure();
}
}
}

하지만 실패
activeAtFailure == baseline 이라고 가정했는데 +1 이 됨
== DB 문장을 하나도 실행 안 했는데 커넥션이 점유되어있다
해결 과정
왜... 이렇게 되었는지 검색해봤다
Hikari 는 기본적으로 auto-commit = true 로 커넥션을 발급
Hibernate 는 JDBC 트랜잭션 시작하려면 autocommit = false 로 토글해야하고 토글 == 커넥션을 일단 잡아야함
| autoCommit=true | SQL 한 줄 실행하면 그 즉시 DB 에 자동 commit |
| autoCommit=false | SQL 실행해도 commit 안 됨. 누군가가 명시적으로 connection.commit() 호출해야 반영 이걸 해주는게 @Transaction Spring 의 JpaTransactionManager 가 메서드 정상 종료 시 connection.commit() 을 호출하고 예외 발생 시 connection.rollback() 을 호출해줌 |
-> hibernate.connection.handling_mode 가 lazy 여도 트랜잭션 진입 타이밍에 autocommit 토글용 커넥션이 점유됨
-> 그럼 connection acquisition 은 언제 진짜 lazy 인가? => autocommit 토글이 필요 없을때만
이 문제를 고쳐주지 않으면 DB 를 건드리지 않고 예외가 나도 (ex Lock 실패) 커넥션을 잡았다가 놓음
-> 부하를 줬을 때 pool 이 빠르게 없어짐
-> Lock 은 무조건 .... 트랜잭션 바깥으로 빼기
application.yml 에서 이렇게 수정해줘야 진짜 lazy 가 됨
spring:
datasource:
hikari:
auto-commit: false # 풀이 발급하는 커넥션을 autocommit=false 로 고정
jpa:
properties:
hibernate:
connection:
provider_disables_autocommit: true # Hibernate 가 pool 의 autocommit 관리를 신뢰
test.yml 을 고쳐도 되지만 일단 여기선 아래처럼 추가
@TestPropertySource(properties = {
"spring.datasource.hikari.auto-commit=false",
"spring.jpa.properties.hibernate.connection.provider_disables_autocommit=true"
})
-> pool 이 autocommit = false 인 connection 을 발급하면 Hibernate 는 토글할 필요 없이 첫 SQL 까지 커넥션 획득 보류 (lazy)

회고
SpringBoot 기본값이 lazy 지만... autocommit 가정이 깨지면 안 써짐
이거 꺼두면 트랜잭션 내부에서 Lock 실패만 해도 Connection 1개를 무의미하게 점유했다는 의미
HikariCP 와 Hibernate 양쪽 공식 문서에서 Spring 환경이라면 hikari.auto-commit = false 를 켜라고 권장함
auto-commit = false 가 좀 무섭긴 한데
@Transactional 은 autoCOmmit 플래그랑 무관하게 명시적 commit, rollback 하므로 서비스 코드는 그대로 둬도 됨