DB

[DB] Connection Pool 과 DataSource

하부루 2024. 7. 23. 13:22
 

스프링 DB 1편 - 데이터 접근 핵심 원리 강의 | 김영한 - 인프런

김영한 | 백엔드 개발에 필요한 DB 데이터 접근 기술을 기초부터 이해하고, 완성할 수 있습니다. 스프링 DB 접근 기술의 원리와 구조를 이해하고, 더 깊이있는 백엔드 개발자로 성장할 수 있습니

www.inflearn.com


1. Connection Pool 이해

[데이터베이스 커넥션을 획득할 때는 다음과 같은 복잡한 과정을 거친다]

  1. 애플리케이션 로직은 DB 드라이버를 통해 커넥션을 조회.
  2. DB 드라이버는 DB와 TCP/IP 커넥션을 연결. 물론 이 과정에서 3 way handshake 같은 TCP/IP 연결을 위한 네트워크 동작이 발생.
  3. DB 드라이버는 TCP/IP 커넥션이 연결되면 ID, PW와 기타 부가정보를 DB에 전달.
  4. DB는 ID, PW를 통해 내부 인증을 완료하고, 내부에 DB 세션을 생성.
  5. DB는 커넥션 생성이 완료되었다는 응답을 보낸다.
  6. DB 드라이버는 커넥션 객체를 생성해서 클라이언트에 반환.

[문제점]

  1. 이렇게 커넥션을 새로 만드는 것은 과정도 복잡하고 시간도 많이 많이 소모되는 일이다
  2. DB는 물론이고 애플리케이션 서버에서도 TCP/IP 커넥션을 새로 생성하기 위한 리소스를 매번 사용해야 한다.
  3. 진짜 문제는 고객이 애플리케이션을 사용할 때, SQL을 실행하는 시간 뿐만 아니라 커넥션을 새로 만드는 시간이 추가되기 때문에 결과적으로 응답 속도에 영향을 준다.

[문제해결 방법]

이런 문제를 한번에 해결하는 아이디어가 바로 커넥션을 미리 생성해두고 사용하는 커넥션 풀이라는 방법이다. 커넥션 풀은 이름 그대로 커넥션을 관리하는 풀(수영장 풀을 상상하면 된다)이다.


2. Connection Pool

 

  • 애플리케이션을 시작하는 시점에 커넥션 풀은 필요한 만큼 커넥션을 미리 확보해서 풀에 보관.
  • 보통 얼마나 보관할 지는 서비스의 특징과 서버 스펙에 따라 다르지만 기본값은 보통 10개.

[커넥션 풀 OverView]

  • 애플리케이션 실행 시, 미리  TCP/IP로 DB와 커넥션이 연결되어 있는 커넥션을 만들어 두는 것.
  • 이후 애플리케이션이 커넥션이 필요할 때마다(DB 접근 sql ) 커넥션 풀에 커넥션을 요청한 뒤 받아서 사용한다.
  • 사용이 종료 된 뒤, 반환. ( 다시 사용할 수 있도록 커넥션을 close 하지않고, 살아있는 상태로 반환)

[커넥션 풀의 연결 상태]

  • 커넥션 풀에 들어 있는 커넥션은 TCP/IP로 DB와 커넥션이 연결되어 있는 상태이기 때문에 언제든지 즉시 SQL을 DB에 전달 가능

[커넥션 풀 사용1]

  • 애플리케이션 로직에서 이제는 DB 드라이버를 통해서 새로운 커넥션을 획득하는 것이 아니다.
  • 이제는 커넥션 풀을 통해 생성되어 있는 커넥션을 객체 참조로 그냥 가져다 쓰기만 하면 된다.
  • 커넥션 풀에 커넥션을 요청하면 커넥션 풀은 자신이 가지고 있는 커넥션 중에 하나를 반환한다.

[커넥션 풀 사용2]

  • 애플리케이션 로직은 커넥션 풀에서 받은 커넥션을 사용해서 SQL을 데이터베이스에 전달하고 그 결과를 받아서 처리한다.
  • 커넥션을 모두 사용하고 나면 이제는 커넥션을 종료하는 것이 아니라, 다음에 다시 사용할 수 있도록 해당 커넥션을 그대로 커넥션 풀에 반환하면 된다.
대표적인 커넥션 풀 오픈소스는 commons-dbcp2, tomcat-jdbc pool, HikariCp 등이 있다.
스프링 부트에서는 공식적으로 HikariCp를 제공한다.

 


3. DataSource 이해

[DriverManager를 통해 커넥션 획득]

[DriverManager를 통해 커넥션을 획득하다가 커넥션 풀로 변경시 문제]

[커넥션을 획득하는 방법을 추상화]

  • 자바에서는 이런 문제를 해결하기 위해 javax.sql.DataSource 라는 인터페이스를 제공한다.
  • DataSource 는 커넥션을 획득하는 방법을 추상화 하는 인터페이스이다.
  • 이 인터페이스의 핵심 기능은 커넥션 조회 하나이다.

[DriverManger로 커넥션을 생성하여 가져오기]

//단순 DriverManager만 사용
@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());
}
//dataSource 로 추상화하는 DriverManager
@Test
void dataSourceDriverManager() throws SQLException {
		 //DriverManagerDataSource - 항상 새로운 커넥션을 획득
		 DriverManagerDataSource dataSource = new DriverManagerDataSource(URL, USERNAME, PASSWORD); 
		 useDataSource(dataSource);
}
  • DriverManager 는 커넥션을 획득할 때 마다 URL , USERNAME , PASSWORD 같은 파라미터를 계속 전달해야 한다.

[DataSoruce로 커넥션을 생성하여 가져오기]

//DataSource 사용
@Test
//DriverManagerDataSource는 DriverManager의 자식
void dataSourceDriverManager() throws SQLException {
	DriverManagerDataSource dataSource = new DriverManagerDataSource(URL,USERNAME, PASSWORD);
	useDataSource(dataSource);
}
private void useDataSource(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());
}
  • 그러나 DataSource 를 사용하는 방식은 처음 객체를 생성할 때만 필요한 파리미터를 넘겨두고, 커넥션을 획득할 때는 단순히 dataSource.getConnection() 만 호출하면 된다.

[커넥션 풀 생성하기(HikariCp]

 @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");

        useDataSource(dataSource);
        Thread.sleep(1000);
    }
    private void useDataSource(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());
    }
  • HikariCP 커넥션 풀을 사용한다. HikariDataSource는 DataSource 인터페이스를 구현하고 있다.
  • 커넥션 풀 최대 사이즈를 10으로 지정하고, 풀의 이름은 MyPool이라고 지정했다.

[실행 결과]

#커넥션 풀 초기화 정보 출력, HikariCP 관련 설정 확인가능.
HikariConfig - MyPool - configuration:
HikariConfig - maximumPoolSize................................10
HikariConfig - poolName................................"MyPool"

#커넥션 풀 전용 쓰레드가 커넥션 풀에 커넥션을 10개 채움
[MyPool connection adder] MyPool - Added connection conn0: url=jdbc:h2:.. 
user=SA
[MyPool connection adder] MyPool - Added connection conn1: url=jdbc:h2:.. 
user=SA
[MyPool connection adder] MyPool - Added connection conn2: url=jdbc:h2:.. 
user=SA
[MyPool connection adder] MyPool - Added connection conn3: url=jdbc:h2:.. 
user=SA
[MyPool connection adder] MyPool - Added connection conn4: url=jdbc:h2:.. 
user=SA
[MyPool connection adder] MyPool - Added connection conn9: url=jdbc:h2:.. 
user=SA 

#커넥션 풀에서 커넥션 획득1
ConnectionTest - connection=HikariProxyConnection@446445803 wrapping conn0: 
url=jdbc:h2:tcp://localhost/~/test user=SA, class=class 
com.zaxxer.hikari.pool.HikariProxyConnection

#커넥션 풀에서 커넥션 획득2
ConnectionTest - connection=HikariProxyConnection@832292933 wrapping conn1: 
url=jdbc:h2:tcp://localhost/~/test user=SA, class=class 
com.zaxxer.hikari.pool.HikariProxyConnection

//사용중인 커넥션 active=2, 풀에서 대기 상태인 idle=8  확인 가능
MyPool - After adding stats (total=10, active=2, idle=8, waiting=0)

4. DataSoruce 적용

[MemberRepositoryV1.class]

/**
 * JDBC - DataSource 사용, JdbcUtils 사용
 */
 @Slf4j
 public class MemberRepositoryV1 {
 
	 private final DataSource dataSource;
	 
	 public MemberRepositoryV1(DataSource dataSource) {
			 this.dataSource = dataSource;
	 }
	 //save()...
	 //findById()...
	 //update()....
	 //delete()....
	 private void close(Connection con, Statement stmt, ResultSet rs) {
			 JdbcUtils.closeResultSet(rs);
			 JdbcUtils.closeStatement(stmt);
			 JdbcUtils.closeConnection(con);
	 }
	 private Connection getConnection() throws SQLException {
			 Connection con = dataSource.getConnection();
	     log.info("get connection={}, class={}", con, con.getClass());
			 return con;
   }
}

[MemberRepositoryV1Test.class]

@Slf4j
class MemberRepositoryV1Test {

    MemberRepositoryV1 repository;

    @BeforeEach
    void beforeEach() {
        //기본 DriverManger - 항상 새로운 커넥션을 획득
        //DriverManagerDataSource dataSource = new DriverManagerDataSource(URL, USERNAME, PASSWORD);

        //HikariCP 커넥션 풀
        HikariDataSource dataSource = new HikariDataSource();
        dataSource.setJdbcUrl(URL);
        dataSource.setUsername(USERNAME);
        dataSource.setPassword(PASSWORD);

        repository = new MemberRepositoryV1(dataSource);
    }
    @Test
    void crud() throws SQLException {
        //save
        Member member = new Member("memberV100", 10000);
        repository.save(member);

        //findById
        Member findMember = repository.findById(member.getMemberId());
        assertThat(findMember).isEqualTo(member);

        //update: moneoy : 10000 -> 20000
        repository.update(member.getMemberId(), 20000);
        Member updatedMember = repository.findById(member.getMemberId());
        assertThat(updatedMember.getMoney()).isEqualTo(20000);

        //delete
        repository.delete(member.getMemberId());
        assertThatThrownBy(() -> repository.findById(member.getMemberId()))
                .isInstanceOf(NoSuchElementException.class);

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }

[실행 결과]

get connection=HikariProxyConnection@xxxxxxxx1 wrapping conn0: url=jdbc:h2:... 
user=SA

get connection=HikariProxyConnection@xxxxxxxx2 wrapping conn0: url=jdbc:h2:... 
user=SA

get connection=HikariProxyConnection@xxxxxxxx3 wrapping conn0: url=jdbc:h2:... 
user=SA

get connection=HikariProxyConnection@xxxxxxxx4 wrapping conn0: url=jdbc:h2:... 
user=SA

get connection=HikariProxyConnection@xxxxxxxx5 wrapping conn0: url=jdbc:h2:... 
user=SA

get connection=HikariProxyConnection@xxxxxxxx6 wrapping conn0: url=jdbc:h2:... 
user=SA


커넥션 풀 사용시 conn0 커넥션이 재사용 된 것을 확인할 수 있다
웹 애플리케이션에 동시에 여러 요청이 들어오면 여러 쓰레드에서 커넥션 풀의커넥션을 가져가는 상황
을 확인할 수 있다.