minghxx.blog
  • [Spring] 스프링 DB 1편 2) 커넥션 풀과 데이터소스 이해
    2023년 11월 12일 09시 21분 59초에 업로드 된 글입니다.
    작성자: 민발자
    728x90

    스프링 DB 1편 데이터 접근 핵심 원리

    Session 2 커넥션 풀과 데이터소스 이해

    1. 커넥션 풀 이해

    1) 데이터베이스 커넥션 획득 과정

     

    1. 애플리케이션 로직은 DB 드라이버를 통해 커넥션을 조회

    2. DB 드라이버는 DB와 TCP/IP 커넥션을 연결, 이 과정에서 3-way-handshake 같은 TCP/IP 연결을 위한 네트워크 동작 발생

    3. DB 드라이버는 커넥션이 연결되면 ID, PW와 기타 부가 정보를 DB에 전달

    4. DB는 ID, PW로 내부 인증을 완료하고 내부에 DB 세션 생성

    5. DB는 커넥션 생성이 완료되었다고 응답을 보냄

    6. DB 드라이버는 커넥션 객체를 생성해 클라이언트에 반환

    ▶ 커넥션을 획득하는 과정이 복잡하고 시간이 많이 소모, DB는 물론이고 애플리케이션 서버에서도 TCP/IP 커넥션을 새로 생성하기 위한 리소스를 매번 사용해야 함

    ▶ 커넥션을 미리 생성해두고 사용하는 커넥션 풀 방법을 사용

     

    2) 커넥션 풀 초기화

    애플리케이션을 시작하는 시점에 커넥션 풀은 필요한 만큼 커넥션을 미리 확보해 풀에 보관, 기본값은 보통 10개

     

    3) 커넥션 풀 연결 상태

    커넥션 풀에 들어 있는 커넥션은 TCP/IP로 DB와 커넥션이 연결되어 있는 상태

    언제든 즉시 SQL을 DB에 전달할 수 있다.

     

    4) 커넥션 풀 사용

    요청이 들어오면 커넥션 풀을 조회해 이미 생성되어 있는 커넥션 객체 참조를 가져다 쓰기만 하면 된다.

    애플리케이션 로직은 커넥션 풀에서 받은 커넥션을 사용해 SQL을 데이터베이스에 전달하고 그 결과를 받아서 처리

    커넥션을 모두 사용하고 나면 종료하는 것이 아니라 그대로 커넥션 풀에 반환

     

    5) 커넥션 풀 정리

    성능 테스트를 통해 커넥션 풀의 숫자를 정함

    커넥션 풀은 서버당 최대 커넥션 수를 제한할 수 있어 DB에 무한정 연결이 생성되는 것을 막아줘 DB 보호 효과도 있음

    커넥션 오프소스를 통해 성능도 뛰어나고 편리하게 사용 가능 commons-dbcp2, tomcat-jdbc pool, HikariCP 등

    최근에는 성능, 사용의 편리함, 안전성 측면에서 검증된 HikariCP 주로 사용 스프링 부트 2.0 이상부터 기본으로 제공


    2. 데이터소스 이해

    1) 커넥션 획득 방법 변경 시 문제점

    DriverManager를 통해 커넥션을 획득하다가 커넥션 풀로 변경하면 애플리케이션 코드도 함께 변경해야 한다.

    의존관계가 DriverManager에서 HikariCP로 변경되기 때문

     

    2) 커넥션 획득 방법의 추상화

    커넥션 획득 방법을 추상화한 javax.sql.DataSource 인터페이스를 통해 위의 문제점을 해결할 수 있다.

    대부분의 커넥션 풀은 DataSource 인터페이스를 이미 구현

    개발자는 DBCP2, HikariCP 커넥션 풀의 코드를 직접 의존하는 것이 아니라 DataSource 인터페이스에만 의존하도록 애플리케이션 로직을 작성, 커넥션 풀 구현 기술을 변경하고 싶으면 해당 구현체로 변경만 하면 된다.

    DriverManager은 DataSource인터페이스를 사용하지 않기 때문에 커넥션 풀을 변경하려면 관련 코드 모두 수정 필요, 스프링은 DriverManager도 DataSource를 통해 사용할 수 있도록 DriverManagerDataSource라는 DataSource 구현 클래스 제공


    3. 데이터소스 예제 1 - DriverManager

    1) DriverManager를 통해 커넥션 획득

    Connection con1 = DriverManager.getConnection(URL, USERNAME, PASSWORD);

     

    2) DriverManagerDataSource를 통해 커넥션 획득

    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());
    }

     

    3) DriverManager와 DriverManagerDataSource 차이

    DriverManager는 커넥션을 획득할 때마다 파라미터를 계속 전달해야 한다.

    DataSource를 사용하는 방식은 처음 객체를 생성할 때만 파라미터를 넘겨두고 커넥션을 획득할 때는 getConnection() 메서드만 호출

     

    4) 설정과 사용 분리

    - 설정

    DataSource를 만들고 필요한 속성들을 사용해 URL, USERNAME, PASSWORD 같은 부분을 입력하는 것을 말함

    설정과 관련된 속성들은 한 곳에 있는 것이 향후 변경에 더 유연하게 대체 가능

    - 사용

    설정을 신경 쓰지 않고 DataSource의 getConnection()만 호출해서 사용

    - 설정과 사용 분리 설명

    필요한 데이터를 DataSource가 만들어지는 시점에 미리 넣어두면 DataSource를 사용하는 곳에서는 메서드만 호출하게 되므로 URL, USERNAME, PASSWORD 같은 속성들에 의존하지 않아도 된다. 그냥 DataSource만 주입받아서 getConnection()만 호출하면 된다.

    리포지토리는 DataSource만 의존하고 속성을 몰라도 된다. 


    4. 데이터소스 예제 2 - 커넥션 풀

    1) 데이터소스 커넥션 풀 추가

    void dataSourceConnectionPool() throws SQLException, InterruptedException {
    	//커넥션 풀링:HikariProxyConnection(Proxy)->JdbcConnection(Target) 
    	HikariDataSource dataSource = new HikariDataSource(); 
        
    	dataSource.setJdbcUrl(URL);
    	dataSource.setUsername(USERNAME); 
    	dataSource.setPassword(PASSWORD); 
    	dataSource.setMaximumPoolSize(10); 
    	dataSource.setPoolName("MyPool");
    	useDataSource(dataSource); 
        
    	Thread.sleep(1000);//커넥션 풀에서 커넥션 생성 시간 대기
    }

     

    HikariCP 커넥션 풀 사용

    HikariDataSource는 DataSource 인터페이스 구현하고 있음

    커넥션 풀에서 커넥션을 생성하는 작업은 애플리케이션 실행 속도에 영향을 주지 않기 위해 별도의 스레드에서 작동

    별도의 쓰레드에서 동작하기 때문에 테스트가 먼저 종료되기 때문에 대기시간을 주어야 쓰레드 풀에 커넥션이 생성되는 로그를 확인할 수 있다.

     

    2) 실행 결과 로그

    #커넥션 풀 초기화 정보 출력
    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 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

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

    - HikariConfig : HikariCP 관련 설정 확인, 풀 이름과, 최대 풀 수 확인

    - MyPool connection adder

    별도의 쓰레드 사용해서 커넥션 풀에 커넥션을 채우고 있음 최대 풀 수 10까지 채운다.

    커넥션 풀에 커넥션을 채우는 것은 상대적으로 오래 걸리기 때문에 애플리케이션을 실행할 때 커넥션 풀을 채울 때까지 대기할 수 없다.

    별도의 쓰레드를 사용해서 커넥션 풀을 채워야 애플리케이션 실행 시간에 영향을 주지 않는다.

    - 커넥션 풀에서 커넥션 획득

    현재 2개를 획득하고 반환하지 않았음

    active=2 2개 사용 중이고 idle=8 풀에서 대기 상태인 커넥션이 8개임을 확인할 수 있다.


    5. 데이터소스 적용

    1) 데이터소스 적용

    @Slf4j
    public class MemberRepositoryV1 {
      private final DataSource dataSource;
      public MemberRepositoryV1(DataSource dataSource) {
          this.dataSource = dataSource;
      }
      ...
      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;
      }
    }

    - DataSource 의존관계 주입

    직접 만든 DBConnectionUtil 대신 외부에서 DataSource를 주입받아 사용

    DataSource는 표준 인터페이스, 구현체가 변경되어도 해당 코드를 변경하지 않아도 된다.

    - JdbcUtils 편의 메서드

    커넥션을 좀 더 편리하게 닫을 수 있음

    2) 테스트

    @Slf4j
    class MemberRepositoryV1Test {
        MemberRepositoryV1 repository;
        
        @BeforeEach
        void beforeEach() throws Exception {
        
            //기본 DriverManager - 항상 새로운 커넥션 획득 
            DriverManagerDataSource dataSource = new DriverManagerDataSource(URL, USERNAME, PASSWORD);
        	
            //커넥션 풀링:HikariProxyConnection->JdbcConnection 
            HikariDataSource dataSource = new HikariDataSource(); 
            dataSource.setJdbcUrl(URL); 
            dataSource.setUsername(USERNAME); 
            dataSource.setPassword(PASSWORD);
            repository = new MemberRepositoryV1(dataSource);
        }
        ...
    }

    MemberRepositoryV1은 의존관계 주입이 필요 beforeEach()에서 주입

     

    - DriverManagerDataSource 사용

    get connection=conn0: url=jdbc:h2:.. user=SA class=class org.h2.jdbc.JdbcConnection
    get connection=conn1: url=jdbc:h2:.. user=SA class=class org.h2.jdbc.JdbcConnection
    get connection=conn2: url=jdbc:h2:.. user=SA class=class org.h2.jdbc.JdbcConnection
    get connection=conn3: url=jdbc:h2:.. user=SA class=class org.h2.jdbc.JdbcConnection
    get connection=conn4: url=jdbc:h2:.. user=SA class=class org.h2.jdbc.JdbcConnection
    get connection=conn5: url=jdbc:h2:.. user=SA class=class org.h2.jdbc.JdbcConnection

    conn0~5까지 항상 새로운 커넥션이 생성된다.

     

    - 커넥션 풀 사용

    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이 계속 재사용된다.

    테스트가 순서대로 진행되면서 사용 후 반환하는 것을 반복

    웹 애플리케이션이 동시에 여러 요청이 들어오면 여러 쓰레드에서 커넥션 풀의 커넥션을 가져가 사용한다.

    728x90
    댓글