[자바/스프링] HikariCP

자바 애플리케이션에서 JDBC(java database connectivity) API를 사용하여 데이터베이스에 연결하기 위해서는 DataSource 인터페이스를 통한 Connection 인스턴스 생성이 필요하다. DataSourceConnection 인스턴스를 생성하기 위한 표준 인터페이스로, JDBC 명세에 규정되어 있다. DataSource 구현체들은 모두 JDBC 명세의 공통 인터페이스를 구현하기 때문에 언제든지 서로 교체 가능하다. 이러한 DataSource 구현체들은 커넥션 풀링(connection pooling), 분산 트랜잭션(distributed transaction) 등의 기능 지원과 데이터베이스 연결 관련 세부 속성 설정에 있어 차이가 있다.

스프링 프레임워크는 자체적으로 DriverManagerDataSourceSingleConnectionDataSource와 같은 DataSource 구현체를 제공하지만 이들은 커넥션 풀링 기능을 제공하지 않으므로 주로 테스트 용도로 사용된다. DriverManagerDataSource는 클라이언트가 매번 요청할 때마다 커넥션을 새로 열기 때문에 리소스 효율이 좋지 않으며, DriverManagerDataSource의 하위 클래스인 SingleConnectionDataSource는 하나의 커넥션만 재사용하고 닫지 않으므로 멀티 스레드 환경에서 사용하기는 어렵다.

HikariCP와 아파치 커먼즈(Apache Commons) DBCP는 DataSource 인터페이스의 오픈소스 구현체이다. 스프링 2.0 버전부터 DBCP 대신 HicariCP 구현체가 기본으로 내장된다.


HikariCP의 속성

유휴 시간초과 (idleTimeout)

idleTimeout 속성은 커넥션 풀에서 커넥션이 유휴 상태로 유지될 수 있는 최대 시간을 제어한다. 이 설정은 최소 유휴 커넥션 수(minimumIdle)가 최대 풀 크기(maximumPoolSize) 보다 작도록 정의된 경우에만 적용된다. 커넥션 풀 개수가 최소 유휴 커넥션 수에 도달하면 유휴 커넥션은 삭제되지 않는다. 커넥션이 유휴 상태로 종료되는지 아닌지는 최대 +30초, 평균 +15초의 변동이 있을 수 있다. 유휴 시간초과 전에는 커넥션이 유휴 상태로 종료되지 않는다. 값이 0인 경우 유휴 커넥션은 풀에서 제거되지 않는다. 허용되는 최소값은 10000(10초)이며 기본값은 600000(10분)이다.


최대 수명 시간 (maxLifeTime)

maxLifeTime 속성은 커넥션 풀에 있는 커넥션의 최대 수명을 제어한다. 사용 중인 커넥션은 절대로 제거되지 않으며, 커넥션이 닫힌 상태일 때만 제거된다. maxLifeTime 속성을 설정하더라도 해당 시간에 커넥션 풀의 모든 커넥션이 한번에 제거되지는 않으며, 의도적으로 커넥션 풀에서 커넥션의 대량 소멸을 방지하기 위해 약간의 음감쇠(negative attenuation)가 적용(커넥션 별 수명 시간을 maxLifeTime 속성값에서 최대 2.5%까지 감소시키는 변동을 적용)된다.

maxLifeTime 속성값을 설정하는 것이 권장되며, 네트워크 지연 시간을 고려하여 데이터베이스에 설정된 커넥션 시간 제한보다 몇 초 짧게 설정하는 것이 좋다. 0 값은 최대 수명 시간이 없음을 나타내며(무한 수명) 이 경우 커넥션이 닫힌 상태이더라도 제거되지 않지만 idleTimeout 설정은 적용된다. 허용되는 최소값은 30000(30초)이며, 기본값은 1800000(30분)이다.

데이터베이스의 커넥션 시간 제한(예로 MySQL의 WAIT_TIMEOUT 값, 오라클의 IDLE_TIME 값)이 지나면 데이터베이스는 애플리케이션 커넥션 관련 설정에 관계 없이 해당 커넥션을 종료한다. maxLifeTime 속성값이 커넥션 시간 제한 보다 긴 경우 데이터베이스에 의해 해당 커넥션이 종료된 후에 애플리케이션에서 커넥션에 대한 상태 확인(유효성 검사)이 이루어질 수 있으며 이 경우 다음과 같은 에러가 발생 가능하다.

MyDataSource - Failed to validate connection ConnectionID:384 ClientConnectionId: eae60768-5342-4d5f-a134-c1309b87a17a (The connection is closed.). Possibly consider using a shorter maxLifetime value.


위 에러 발생을 막기 위해서는 maxLifeTime 속성값을 데이터베이스의 커넥션 시간 제한보다 몇 초 짧게 설정한다.


커넥션 테스트 쿼리 (connectionTestQuery)

커넥션 테스트 쿼리란 커넥션 풀에서 커넥션이 제공되기 직전에 실행되어 데이터베이스에 대한 커넥션이 여전히 유효한지 확인하는 쿼리를 말한다. JDBC4는 커넥션 테스트 쿼리를 실행하는 대신 Connection.isValid()를 통해 커넥션이 닫혔는지 또는 유효한지 확인하며 구현체마다 그 메커니즘은 다를 수 있다. connectionTestQuery 속성은 JDBC4의 Connection.isValid() API를 지원하지 않는 레거시 드라이버를 위한 것이며 JDBC 드라이버가 JDBC4를 지원하는 경우 이 속성을 설정하지 않는 것이 좋다. 드라이버가 JDBC4와 호환되지 않는 경우 이 속성을 사용하지 않고 커넥션 풀을 실행하면 HikariCP는 에러를 발생시킨다. 기본값은 none이다.

JDBC 드라이버가 JDBC4를 지원하고, 커넥션 테스트 쿼리 속성을 설정하지 않는 경우 PoolBaseisUseJdbc4Validation 속성이 true가 되며 isConnectionDead() 메서드는 JDBC4의 Connection.isValid()를 사용하여 커넥션이 유효한지 검사한다.

JDBC4의 Connection.isValid()를 이용한 커넥션 유효성 검사가 커넥션 테스트 쿼리에 비해 성능면에서 항상 유리하다고 볼 수는 없다. 구현체는 단순 더미 쿼리를 실행하기도 하기도 하며, 단순 더미 쿼리 실행 대신 저수준의 내부적인 핑 확인 방법을 사용하기도 하므로 해당 구현체가 사용하는 테스트 쿼리의 정확한 메커니즘을 확인해보는 것이 좋다.


최대 풀 크기 (maximumPoolSize)

maximumPoolSize 속성은 유휴 커넥션과 사용 중인 커넥션을 모두 포함하여 커넥션 풀이 도달할 수 있는 최대 크기를 제어한다. 기본적으로 이 값에 따라 애플리케이션의 데이터베이스에 대한 실제 최대 커넥션 수가 결정된다. 실행 환경에 따라 적절한 값을 결정하는 것이 좋다. 커넥션 풀이 이 크기에 도달하였고 사용 가능한 유휴 커넥션이 없는 경우, getConnection() 호출은 시간 초과가 발생하기 전에 최대 connectionTimeout 밀리초 동안 차단된다. 기본값은 10이다.


스프링 부트의 DataSource 빈 구성 및 HikariCP 속성 설정

스프링 부트는 기본적으로 애플리케이션 프로퍼티 설정을 감지하여 DataSource 빈 생성 및 속성 설정을 수행한다. 공통 프로퍼티인 spring.datasource에 데이터베이스 연결 정보를 설정하면 관련 값으로 DataSource 빈이 생성되고 애플리테이션 컨텍스트에 등록된다.

spring:
  datasource:
    url:
    username:
    password:
    driverClassName:


스프링 버전 2부터 HikariCP가 기본 DataSource 구현체로서 사용되기 때문에 위와 같이 애플리케이션 프로퍼티를 설정하고 애플리케이션을 실행하면 해당 프로퍼티 정보를 기반으로 DataSource의 HikariCP 구현체인 HikariDataSource 빈이 구성된다.

HikariCP의 장점 중 하나는 다른 구현체에 비해 많은 구성 속성을 제공하여 데이터베이스 커넥션 풀링 기능을 세부적으로 사용자화 할 수 있다는 것이다. 다음과 같이 spring.datasource.hikari 애플리케이션 프로퍼티 설정을 통해 추가적인 HikariDataSource 빈 속성 설정이 가능하다.

spring:
  datasource: 
    url:
    username:
    password:
    driverClassName:
    hikari:
      connetionTimeout:
      idleTimeout:
      maxLifetime:
      schema:


DataSource 빈을 구성하는 클래스인 DatasourceProperties 클래스를 사용하여 DataSource 인스턴스를 직접 빌드할 수도 있다. 애플리케이션 프로퍼티 파일에 데이터베이스 연결 정보를 설정한 후 @ConfigurationProperties 어노테이션을 사용하여 프로퍼티 값을 DatasourceProperties 인스턴스의 필드에 주입하고 DataSourceBuilder를 반환하는, DatasourcePropertiesinitializeDataSourceBuilder() 메서드를 사용하여 DataSource 빈을 생성한다.

spring:
  datasource:
    db:
      url:
      username:
      password:
      driverClassName:
public class DataSourceConfig {
  @ConfigurationProperties("spring.datasource.db")
  @Bean
  public DatasourceProperties dbDatasourceProperties() {
    return new DatasourceProperties();
  }
  
  @Bean
  public DataSource dbDatasource() {
    return dbDatasourceProperties()
      .initializeDataSourceBuilder()
      .build();
  }
}


DatasourcePropertiesDataSource의 추상화된 프로퍼티 클래스이므로 구현체에 대한 모든 프로퍼티 처리를 하지는 못한다. 애플리케이션 프로퍼티 파일을 통해 HikariDataSource 빈을 구성하고 모든 속성을 설정하기 위해서는 수동 빈 정의 없이 스프링 부트가 애플리케이션 프로퍼티를 기반으로 자동으로 생성하는 HikariDataSource을 주입 받아 사용하거나, @ConfigurationProperties 어노테이션과 HikariConfig를 사용하여 HikariDataSource 빈을 수동으로 구성하는 방법을 사용해야 한다.

spring:
  datasource:
    db:
      jdbcUrl:
      username:
      password:
      driverClassName:
      schema:
@ConfigurationProperties("spring.datasource.db")
public class DbDataSourceConfiguration extends HikariConfig {
  @Bean
  public HikariDataSource dbDatasource() {
    return new HikariDataSource(this);
  }
}


데이터베이스의 JDBC URL에 대한 필드명은 DataSourceProperties의 경우 url이지만 HikariConfigjdbcUrl이므로 애플리케이션 프로퍼티도 동일하게 지정해야 올바른 구성이 가능하다.

애플리케이션이 여러 데이터베이스를 사용하는 경우 DataSource 빈을 여러 개 구성하여 멀티 데이터소스 구성이 필요하다. 다음과 같이 애플리케이션 프로퍼티 기반으로 여러 데이터베이스에 대한 DataSource 빈 구성이 가능하다.

spring:
  datasource:
    db1:
      url:
      username:
      password:
      driverClassName:
    db2:
      url:
      username:
      password:
      driverClassName:
public class DataSourceConfig {
  @ConfigurationProperties("spring.datasource.db1")
  @Bean
  public DatasourceProperties db1DatasourceProperties() {
    return new DatasourceProperties();
  }
  
  @Bean
  public DataSource db1Datasource() {
    return db1DatasourceProperties()
      .initializeDataSourceBuilder()
      .build();
  }
  
  @ConfigurationProperties("spring.datasource.db2")
  @Bean
  public DatasourceProperties db2DatasourceProperties() {
    return new DatasourceProperties();
  } 
  
  @Bean
  public DataSource db2Datasource() {
    return db2DatasourceProperties()
      .initializeDataSourceBuilder()
      .build();
  }
}


스프링 부트의 HikariDataSource 빈 자동 구성

스프링 부트의 HikariDataSource 빈 자동 구성은 DataSourceConfigurationHikari 정적 클래스 빈 정의를 기반으로 수행된다. Hikari 정적 클래스에 설정된 어노테이션의 파라미터를 보면 알 수 있듯이 HikariDataSource 빈 자동 구성은 DataSource 빈이 존재하지 않을 때 수행된다. 따라서 멀티 데이터소스 구성을 위해서는 수동 구성이 필요하다.

HikariDataSource 빈 자동 구성은 빈 정의 시 spring.datasource.hikari 애플리케이션 프로퍼티를 인자로 전달하는 @ConfigurationProperties 어노테이션을 사용하고 DataSourceProperties 빈을 주입 받아 사용하는데, 이 DataSourceProperties 빈의 속성은 spring.datasource 애플리케이션 프로퍼티를 기반으로 구성된다. 또한 자동 구성 시 호출되는 DataSourcePropertiesinitializeDataSourceBuilder() 메서드는 driverClassName, url, username, password 필드값만 설정하므로 위 속성 외에 HikariCP의 다른 속성을 설정하려면 spring.datasource.hikari 애플리케이션 프로퍼티를 사용해야 한다.

spring:
  datasource: 
    url:
    username:
    password:
    driverClassName:
    hikari:
      connetionTimeout:
      idleTimeout:
      maxLifetime:
      schema:


참고

Comments