커넥션을 새로 만드는 것은 과정도 복잡하고 시간도 많이 소모되는 일이다. DB는 물론이고 애플리케이션 서버에서도 TCP/IP 커넥션을 새로 생성하기 위한 리소스를 매번 사용해야 한다. 이는 SQL을 실행하는 시간에 더해 커넥션을 새로 만드는 시간까지 추가되기 때문에 애플리케이션의 응답 속도에 영향을 준다.
미리 DB Connection을 맺어둔다. 연결된 커넥션들을 마치 풀처럼 두고 connection을 재사용한다.
애플리케이션 로직은 커넥션 풀에서 받은 커넥션을 사용해서 SQL을 DB에 전달하고 그 결과를 받아 처리한다. 커넥션을 모두 사용하고 나면 커넥션을 종료하는 것이 아닌, 다음에 다시 사용할 수 있도록 해당 커넥션을 그대로 커넥션 풀에 반환한다.
commons-dbcp2
, tomcat-jdbc-pool
, HikariCP
등이 있다.hikariCP
를 제공한다.MySQL과 HikariCP를 기준으로 중요한 것 위주로 정리해보자.
DB 서버의 설정은 다음과 같다.
max_connections
: client와 맺을 수 있는 최대 connection 수wait_timeout
: connection이 inactive 할 때 다시 요청이 오기까지 얼마의 시간을 기다린 뒤에 close 할 것인지를 결정. 시간 내에 요청이 도착하면 0으로 초기화.wait_timeout
은 비정상적인 connection 종료, connection을 다 쓰고 반환이 안되는 경우, 네트워크 단절의 경우와 같은 문제 시 connection을 반환할 수 있도록 한다.
Backend Application에서의 설정은 다음과 같다.
minimumIdle
: pool에서 유지하는 최소한의 idle connection 수maximunPoolSize
: pool이 가질 수 있는 최대 connection 수. idle과 active(in-use) connection 합쳐서 최대 수.maxLifetime
: pool에서 connection의 최대 수명. maxLifetime을 넘기면 idle일 경우 pool에서 바로 제거, active인 경우 pool로 반환된 후 제거.wait_timeout
설정으로 DB상의 커넥션을 끊게 되면 backend application 상에 남아있는 같은 connection을 가지고 요청이 또 들어왔을 경우 DB에 요청이 전달되지 않고 exception이 발생하게 된다.connectionTimeout
: pool에서 connection을 받기 위한 대기 시간.HikariCP에서의 minimumIdle의 기본 값은 maximumPoolSize와 동일 (= pool size 고정)하게 사용하도록 권장한다.
minimumIdle < maximumPoolSize일 경우, 트래픽이 몰려올 때마다 connection을 추가로 만들어야 하는데 이 connection을 생성하는 속도보다 트래픽이 몰려오는 속도가 더 빠르다면, 백엔드 서버의 응답이 느려질 수 있기 때문에 처음부터 적절한 갯수로 pool size를 고정하도록 가이드를 제시하는 것이다.
만약, Spring Boot의 HikariCP 대신 Commons DBCP2로 변경하고자 한다면 다음과 같이 pom.xml에 의존성을 설정하면 된다.
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-dbcp2</artifactId>
<version>2.9.0</version>
</dependency>
커넥션풀 설정값 (DBCP2에서 이름) | 설명 | |
---|---|---|
initialSize | 클래스 생성 후 최초로 getConnection() 메서드를 호출할 때 커넥션 풀에 채워 넣을 커넥션 개수 (기본값 0) | |
minIdle | 최소한으로 유지될 Connection 객체의 수 (기본값 0) | |
maxIdle | 반납된 유휴 Connection 객체를 유지할 수 있는 최대 값 (기본값 8) | |
maxActive (maxTotal) | 동시에 사용할 수 있는 최대 커넥션 갯수 (기본값 8) | 톰캣의 스레드는 200개인데그거에 맞춰서 커넥션도 200개 만드는 것이 좋음 |
maxWait (maxWaitMillis) | 할당받을 Connection 객체가 없을 때 스레드를 블록시킬 시간 (1/1000초 단위) |
네트워크가 죽고 다시 살아났을때, 사용하는 옵션들
커넥션풀 설정값 | 설명 | |
---|---|---|
validationQuery | 풀에 커넥션을 반환하기 전이나, 풀을 획득하기 전에 커넥션이 valid한지를 검사, mysql 기준으로 보통 select 1 설정 | 커넥션의 연결(건강)을 확인하기 위해 |
testOnBorrow | 커넥션 풀에서 커넥션을 얻어올 때 테스트 실행 (default = true) | |
testOnReturn | 커넥션 풀로 커넥션을 반환할 때 테스트 실행 (default = false) | |
testWhileIdle | Evictor 가 실행될 때 커넥션 풀 안에 있는 유휴 상태의 커넥션을 대상으로 테스트 실행 (default = false) | |
maxConnLifetimeMillis | 커넥션의 최대 라이프타임을 지정 (default = -1) | |
logExpiredConnections | 로그로 maxConnLifetimeMillis를 초과한 경우에 커넥션이 닫혔음을 남김 (default = true) | |
lifo | true 로 설정하면 최근에 반환한 커넥션을 가장 우선 대여해 줍니다. (기본값) | Last in first out |
JDBC 커넥션의 유효성은 validationQuery 옵션에 설정된 쿼리를 실행해 확인할 수 있다. Commons DBCP 1.x에서는 다음과 같은 세 가지 테스트 옵션으로 유효성을 검사한다. 유효성을 검사할 때는 validationQuery 옵션에 하나 이상의 결과를 반환하는 쿼리를 설정해야 한다. Commons DBCP 2.x에서는 validationQuery 옵션이 없을 때 Connection.isValid() 메서드를 호출해 유효성을 검사한다.validationQuery는 밴더사 마다 다른 쿼리문을 사용하길 권장한다 그중 MySQL은 "select 1"로 사용한다.
검증에 지나치게 자원을 소모하지 않게 testOnBorrow 옵션과 testOnReturn 옵션은 false로 설정하고, 오랫동안 대기 상태였던 커넥션이 끊어지는 현상을 막게 testWhileIdle 옵션은 true로 설정하는 것을 추천한다.
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.dbcp2.BasicDataSource;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.sql.DataSource;
@Slf4j
@Getter
@Setter
@Configuration
@ConfigurationProperties(prefix = "mysql")
public class DataSourceConfig {
private String driverClassName;
private String url;
private String username;
private String password;
private Integer maxIdle;
private Integer maxTotal;
private Integer initialSize;
private Integer minIdle;
@Bean
public DataSource dataSource() {
BasicDataSource basicDataSource = new BasicDataSource();
basicDataSource.setDriverClassName(driverClassName);
basicDataSource.setUrl(url);
basicDataSource.setUsername(username);
basicDataSource.setPassword(password);
basicDataSource.setMaxIdle(maxIdle);
basicDataSource.setMaxTotal(maxTotal);
basicDataSource.setInitialSize(initialSize);
basicDataSource.setMinIdle(minIdle);
basicDataSource.setValidationQuery("SELECT 1");
basicDataSource.setTestOnReturn(true);
basicDataSource.setTestOnBorrow(true);
basicDataSource.setTestWhileIdle(true);
return basicDataSource;
}
}