다음의 6가지 단계를 거쳐서 사용자의 DB 요청 처리가 이루어지게 된다.
1. 애플리케이션 로직은 DB 드라이버를 통해 커넥션을 조회한다.
2. DB 드라이버는 DB와 TCP/IP
커넥션을 연결한다.(3-way-handshake
와 같은 네트워크 동작이 발생)
3. 커넥션이 연결되면 ID/PW
와 기타 부가정보를 DB에 전달한다.
4. ID/PW
를 통해 인증이 완료되면 내부 세션을 생성한다.
5. DB는 세션 생성 완료를 통해 커넥션 생성이 끝났다는 응답을 보낸다.(실제 트랜잭션 시작 및 SQL문 실행은 세션이 처리하기 때문에)
6. DB 드라이버는 커넥션 객체를 생성해서 클라이언트에 반환한다.
하지만, 커넥션을 생성하는 것은 위와 같이 여러 단계가 필요하며 네트워크 커넥션이 사용되므로 매우 복잡하고 시간이 많이 소요된다.
즉, 사용자의 요청이 들어왔을 때 SQL 실행 시간에 커넥션 생성 시간이 더해져 응답 속도에 영향을 주게 되는 문제가 발생하는 것이다.
이를 해결하기 위한 아이디어가, 바로 커넥션 풀이다.
커넥션 풀이란 커넥션을 담은 그릇이라 생각하면 된다. 대표적인 커넥션 풀 오픈소스로는 commons-dbcp2
, tomcat-jdbc pool
, HikariCP
이 있지만 스프링 부트 2.0이상부터는 HikariCP
를 기본으로 사용한다.
애플리케이션 실행 시점에, 풀에 미리 커넥션들을 생성해두고 필요할때 커넥션 객체들의 참조를 가져와서 재사용하는 방식인 것이다.
따라서 커넥션 풀에 존재하는 커넥션들은, 모두 DB와 TCP/IP
연결이 맺어지고 DB 내부에서는 각 커넥션에 매칭되는 세션이 생성된 상태이다.
🔖 커넥션 풀 사용 과정
1. DB 드라이버가 아닌 커넥션 풀에서 커넥션을 가져온다.
2. 커넥션 사용 후 재사용할 수 있도록 커넥션 풀에 살아 있는 상태로 반환한다.(커넥션 종료 X)
커넥션 생성에 필요한 6가지 단계들이 모두 처리된 상태므로, 언제든지 즉시 SQL 문을 데이터베이스로 전달할 수 있어서 사용자 응답 속도가 빨라진다.
또한, 서버당 최대 커넥션 개수를 제한할 수 있어 DB에 무한정 연결이 생성되는 것을 막아 데이터베이스를 보호하는 효과도 존재한다.
이처럼 커넥션을 획득하는 방법은 다양하다.
JDBC DriverManager
를 직접 사용해서 매번 커넥션 생성HikariCP
와 같은 커넥션 풀 사용 하지만 만약 DriverManager
을 통해 커넥션을 획득하다가, 커넥션 풀을 사용하도록 바꾼다면? 의존 관계가 바뀌므로 어플리케이션 코드에 변경이 일어나는 문제가 발생한다.
따라서, 자바는 커넥션을 획득하는 방법에 대해 추상화를 하였는데 이것이 바로 javax.sql.DataSource
인터페이스이다.
public interface DataSource {
// 핵심 기능 : 커넥션 조회 한 가지
Connection getConnection() throws SQLException;
}
인터페이스로 추상화를 해놓았기 때문에, 구현체에 직접 의존하지 않게 된다(DataSource
인터페이스에만 의존하도록 애플리케이션 로직을 작성). 즉 커넥션 풀을 획득하는 기술을 바꾸고 싶다면 구현체만 갈아끼우면 어플리케이션 로직의 변경 없이 바로 가능해지는 것이다.
🔖 DriverMangerDataSource
스프링이DriverManager
도DataSource
를 통해서 사용할 수 있도록 제공하는 클래스이다.DriverManager
는DataSource
인터페이스를 사용하지 않기 때문이다.
DataSource
의 가장 큰 장점은, 바로 객체의 설정과 사용 시점을 분리할 수 있다는 것이다.
DataSource
를 만들고 필요한 속성들을 사용해서 URL
, USERNAME
, PASSWORD
정보 입력DataSource
의 getConnection()
만 호출해서 사용public class MemberRepository{
private final DataSource dataSource;
public MemberRepositoryV1(DataSource dataSource) {
this.dataSource = dataSource;
}
...
}
만약 위와 같이 레포지토리가 DataSource
를 의존 관계 주입으로 받고 있는 상태라면, 필요한 데이터들을 DataSource
생성 시점에 외부에서 미리 다 넣어둘 수 있기 때문에 레파지토리에서는 dataSource.getConnection()
만 호출하면 된다.
어플리케이션 개발에서는 주로 설정은 한 곳에서, 사용은 수 많은 곳에서 하므로 이는 중요한 장점이 된다.
또한, DI(Dependency Injection)
을 통해 DataSource
를 주입받는 것을 통해 OCP
원칙을 지킬 수 있다. 개방 폐쇄의 원칙(OCP
)이란 기존의 코드를 변경하지 않으면서, 기능을 추가할 수 있도록 설계가 되어야 한다는 원칙이다.
public class ConnectionTest {
@Test
void dataSourceDriverManager() throws SQLException {
// DriverManagerDataSource : 항상 새로운 커넥션 생성, 부모는 DataSource
DriverManagerDataSource dataSource = new DriverManagerDataSource(URL, USERNAME, PASSWORD);
useDataSource(dataSource);
}
private void useDataSource(DataSource dataSource) throws SQLException {
Connection connection1 = dataSource.getConnection();
Connection connection2 = dataSource.getConnection();
log.info("connection1={} {}", connection1, connection1.getClass());
log.info("connection2={} {}", connection2, connection2.getClass());
}
}
매번 다른 커넥션 객체를 생성하는 것을 볼 수 있다.
public class ConnectionTest {
@Test
void dataSourceConnectionPool() throws SQLException, InterruptedException {
// 커넥션 풀링
HikariDataSource dataSource = new HikariDataSource(); // DataSource 구현
dataSource.setJdbcUrl(URL);
dataSource.setUsername(USERNAME);
dataSource.setPassword(PASSWORD);
dataSource.setMaximumPoolSize(10);
dataSource.setPoolName("MyPool");
useDataSource(dataSource);
Thread.sleep(1000);
}
}
커넥션 풀을 매 메서드마다 사용하고 풀로 반환하고 있기 때문에 같은 커넥션인 0
번을 재사용하고 있는 것을 볼 수 있다. 만약 웹 애플리케이션에 순차적이 아닌 동시에 요청이 오면 다른 커넥션 객체들을 사용할 것이다.
아래와 같이 10개의 커넥션들이 실행 시점에 생성되는 로그를 보고 싶다면, Thread.sleep()
을 해주어 대기 시간을 추가해 주어야한다. 별도의 스레드에서 커넥션 10개를 생성하는 작업이 이루어지기 때문이다.
🔖 별도의 스레드로 커넥션을 채우는 이유
외부랑 네트워크 접속을 해야 하기 때문에, 애플리케이션 실행 시간에 영향을 주는 것을 최소화 하기 위해MyPool
이란 별도의 스레드로 처리
사실 HikariProxyConnection
는 wrapping
컬렉션으로, JDBC connection
0
, 1
번 객체가 내부에 들어있다.
맥시멈 커넥션보다 더 많은 커넥션을 요청하게 되면 이미 풀이 다 찬 상태므로, 풀이 확보될때까지 Block
되기 때문에 기다리는 시간이 생긴다. 어느정도 기다려야 예외를 터트릴지는 응답 시간에 영향을 주므로 잘 조정하도록 하자.