DB

커넥션 풀 및 데이터 소스

chanyoun 2023. 10. 25. 21:32

커넥션 풀

image-20230607165640859
  1. 앱은 데이터베이스에 접근하기 위해 DB 드라이버를 사용해 연결을 시도한다.
  2. DB 드라이버는 데이터베이스와 특별한 네트워크 연결인 TCP/IP를 사용하여 소통한다. 여기서 3-way-hand-shake 과정이 일어나면서 안전하게 연결이 이루어 진다.
  3. 안전한 연결이 만들어지면, 드라이버는 우리의 사용자 이름과 비밀번호 같은 정보를 데이터베이스에 전달한다.
  4. 데이터베이스는 받은 정보로 사용자를 확인하고, 확인이 완료되면 데이터베이스 내부에서 사용자를 위한 공간인 '세션'을 하나 만들어 준다.
  5. 이 모든 과정이 잘 진행되었다면, 데이터베이스는 "연결 완료!"라는 메시지를 드라이버에게 보냅니다.
  6. 드라이버는 마지막으로 앱에게 만들어진 커넥션을 반환한다.

데이터 베이스 커넥션을 획득하는 과정은 위와 같다. 따라서 매번 커넥션을 만드는 과정은 매우 복잡하고 시간이 많이 소모된다. 결과적으로 응답속도에 영향을 미치며, 이것은 사용자에게도 영향을 미친다.

 

위와같이 매번 커넥션을 만드는 문제를 해결하기 위해 커넥션 풀이라는 것을 사용한다.

image-20230607165919774

커넥션 풀에 커넥션을 미리 10개 정도 생성해주고, 추후 DB에 접근하기위해 커넥션 풀에있는 커넥션을 하나 사용해 DB에 연결을 하면 된다. 그후 사용한 커넥션은 다시 커넥션 풀에 반환한다.

 

❗️이때 커넥션 을 종료하는게 아닌 커넥션이 살아있는 상태로 커넥션 풀에 반환해야 한다.

✔️Spring 에서는 HikariCP 라는 커넥션 풀을 사용한다.

 

 

DataSource

image-20230607170406926

DataSource란 커넥션을 획득하는 방법을 추상화하는 것이다.

만약 우리가 DriverManager 를 사용해 커넥션을 얻어 사용하다 HikariCP 커넥션 풀을 사용하여 커넥션을 획득 하는 방법을 변경했다 가정한다.

이때 애플리케이션 코드 변경을 피할수 없는데 이를 해결하기위해 DataSource 라는 표준 인터페이스를 사용한다.

 

image-20230607170522661

javax.sql.DataSource 라는 인터페이스를 사용하여 커넥션을 획득 하는 방법을 추상화 한다.

따라서 우린 DataSource에 의존하도록 애플리케이션 로직을 작성하면 이후 커넥션을 획득 하는 과정을 변경하더라도 애플리케이션의 코드를 변경하지 않아도 된다.

 

 

DriverManger 와 DriverManagerDataSource 의 차이

@Test
void driverManager() throws SQLException {
  Connection con1 = DriverManager.getConnection(URL, USERNAME, PASSWORD);
  Connection con2 = DriverManager.getConnection(URL, USERNAME, PASSWORD);
  log.info("connection={}, class={}", con1, con1.getClass());
  log.info("connection={}, class={}", con2, con2.getClass());
}
@Test
void dataSourceDriverManager() throws SQLException {
  //DriverManagerDataSource - 항상 새로운 커넥션을 획득
  DriverManagerDataSource dataSource = new DriverManagerDataSource(URL, USERNAME, PASSWORD);
  userDataSource(dataSource);
}

private void userDataSource(DataSource dataSource) throws SQLException {
  Connection con1 = dataSource.getConnection();
  Connection con2 = dataSource.getConnection();
  log.info("connection={}, class={}", con1, con1.getClass());
  log.info("connection={}, class={}", con2, con2.getClass());
}

DriverManagerdataSource 인터페이스를 사용하지 않는다. 반면 DataSourceDriverManager는 dataSource 인터페이스를 구현한 DriverManagerDataSource 를 사용하여 커넥션을 획득한다.

 

두 방법의 가장큰 차이는 DriverManagerDataSource는 설정정보를 초기 한번만 설정을 해주며, 그후엔 아무런 정보에 의존하지 않은채로 connection을 획득한다.

이는 설정과 사용을 분리한 것이며 이런 코드가 추후 유지보수를 할때 훨씬 편리하다.

 

굳이 DriverManagerDataSource 를 만들어둔 이유는, 만약 사용자가 dataSource 인터페이스를 구현하지 않는 DriverManger 를 통해 db 커넥션을 사용하고 있다가, 커넥션 풀을 사용하기 위해dataSource 를 상속받는 구현체로 바꾸고 싶다할때, 애플리케이션 로직의 변동이 필요하다.

이런 불필요한 애플리케이션 로직의 변동을 막기위해 dataSource 를 구현하면서 DriverManger 기능을 사용하기위한 DriverMangaerDataSource 를 만들어 놓은 것이다.

 

❗️하지만 DriverManagerDataSource 또한 DataSource의 구현체일뿐 내부적으로는 DriverManger 를 사용하기 때문에 커넥션 풀을 사용하지는 않는다.

 

 

HikariDataSourcePool (커넥션 풀)사용

@Test
void dataSourceConnectionPool() throws SQLException, InterruptedException {
  HikariDataSource dataSource = new HikariDataSource();
  dataSource.setJdbcUrl(URL);
  dataSource.setUsername(USERNAME);
  dataSource.setPassword(PASSWORD);
  dataSource.setMaximumPoolSize(10);
  dataSource.setPoolName("MyPool");

  userDataSource(dataSource);
  Thread.sleep(1000);
}

private void userDataSource(DataSource dataSource) throws SQLException {
  Connection con1 = dataSource.getConnection();
  Connection con2 = dataSource.getConnection();
  log.info("connection={}, class={}", con1, con1.getClass());
  log.info("connection={}, class={}", con2, con2.getClass());
}

위처럼 HikariDataSource를 사용하여 커넥션 풀에서 해당 커넥션을 가져올수있다.

해당 Test를 실행해보면 10개의 connection을 main 스레드가 아닌 adder 쓰레드 에서 생성하는 것을 볼수있다.

17:27:57.161 [MyPool connection adder] DEBUG com.zaxxer.hikari.pool.HikariPool - MyPool - Added connection conn2: url=jdbc:h2:tcp://localhost/~/test user=SA

이때 main 스레드가 아닌 별도의 쓰레드에서 커넥션 풀에 커넥션을 채우는 이유는 커넥션 풀에 커넥션을 채우는 것은 상대적으로 오래 걸린다 따라서 커넥션풀에 커넥션을 채울때까지 애플리케이션이 대기할수 없기 때문에 별도의 쓰레드를 사용하여 커넥션풀을 채우는 것이다.

 

또한 우리가 2개의 connection을 가져오고 그후 반환하지 않았으므로

After adding stats (total=10, active=2, idle=8, waiting=0)

위처럼 active는 2개, 대기상태인 커넥션은 8개로 나온다.

 

❗️만약 지금 생성한 커넥션의 크기(10개)보다 더많은 커넥션을 얻으려 한다면 이땐 block이 걸린다. 즉 사용중인 커넥션이 반환될때 까지 대기한다. 이때 해당 커넥션을 기다리는 시간또한 설정해줄수 있다.

 

	@Test
	void dataSourceConnectionPool() throws SQLException, InterruptedException {
		HikariDataSource dataSource = new HikariDataSource();
		dataSource.setJdbcUrl(URL);
		dataSource.setUsername(USERNAME);
		dataSource.setPassword(PASSWORD);
		dataSource.setMaximumPoolSize(10);
		dataSource.setPoolName("MyPool");

		useAndCloseDataSource(dataSource);
		Thread.sleep(1000);
		reuseDataSource(dataSource);
	}

	private void useAndCloseDataSource(DataSource dataSource) throws SQLException {
		try (Connection con1 = dataSource.getConnection()) {
			log.info("First connection={}, class={}", con1, con1.getClass());
		}
	}

	private void reuseDataSource(DataSource dataSource) throws SQLException {
		try (Connection con2 = dataSource.getConnection()) {
			log.info("Second connection={}, class={}", con2, con2.getClass());
		}
	}
//실행결과
21:17:38.837 ... First connection=HikariProxyConnection@640808588 wrapping conn0: url=jdbc:h2:mem:testdb user=SA ...
21:17:39.844 ... Second connection=HikariProxyConnection@1866229258 wrapping conn0: url=jdbc:h2:mem:testdb user=SA ...

위 코드는 HikariDataSource 를 사용하는 예시이다. DriverManagerDataSource 를 사용하면 항상 새로운 커넥션을 만든것과 다르게 HikariDataSource 를 사용하면 커넥션 풀에 있는 커넥션을 사용한다.

위코드에서 각 HikariProxyConnection 객체가 가지고 있는 커넥션이 conn0 으로 같다. 이렇게 커넥션 풀 안에 있는 코드를 사용하면 추후 커넥션을 재사용을 할수 있다.

 

 

3줄 요약

  • DB 와 연결하려면 커넥션을 사용해야 하는데 이 커넥션을 매번 만드려면 시간이 오래걸린다.
  • DB 커넥션 풀을 사용하여 여러 커넥션을 미리 만들어두고 DB와 연결할수 있는 커넥션을 가져온다.
  • 커넥션 풀마다 커넥션을 가져오는 방법이 모두다르기 때문에, 데이터소스 인터페이스를 구현한 구현체에 의해 가져와 다른 커넥션을 사용하더라도 애플리케이션 코드의 변경을 최소화 한다.

 

Reference