스프링

[ Spring ] 설정과 사용의 분리의 효과는 어마어마해! - 커넥션 풀과 DataSource

do_it_zero 2024. 10. 14. 09:26

커넥션 풀이 왜 필요할까?

커넥션을 획득할 때 복잡한 과정을 거치기 때문이다. 이는 시간도 많이 소모되며 결과적으로 응답 속도에 영향을 준다. 응답 속도가 늦어지는 것은 곧 사용자에게 부정적인 경험을 하게 만드는 것이다. 따라서 이러한 문제를 해결하기 위해 커넥션을 미리 생성해두고 관리하는 커넥션 풀이라는 방법을 만들었다.

 

커넥션 풀

이전에 DriverManager를 통해 직접 커넥션을 획득했던 코드를 봐보자.

@Slf4j
public class DBConnectionUtil {
    
    public static Connection getConnection() {
        try {
            Connection connection = DriverManager.getConnection(URL,USERNAME,PASSWORD);
            log.info("get connection={}, class={}",connection,connection.getClass());
            return connection;
        } catch (Exception e) {
            throw new IllegalStateException();
        }
    }
}


@Slf4j
public class MemberRepositoryV0 {

    public Member save(Member member) throws SQLException {
        String sql = "insert into member(member_id, money) values(?, ?)";

        Connection con = null;
        PreparedStatement pstmt = null;

        try {
            con = getConnection();
            pstmt = con.prepareStatement(sql);
            pstmt.setString(1, member.getMemberId());
            pstmt.setInt(2, member.getMoney());
            pstmt.executeUpdate();
            return member;
        } catch (SQLException e) {
            log.error("error", e);
            throw e;
        } finally {
            close(con, pstmt, null);
        }
    }

    public Member findById(String memberId) throws SQLException {
        String sql = "select * from member where member_id = ?";

        Connection con = null;
        PreparedStatement pstmt = null;
        // 데이터베이스서 쿼리 실행한 결과를 받을 객체
        ResultSet rs = null;

        try {
            con = getConnection();
            pstmt = con.prepareStatement(sql);
            pstmt.setString(1, memberId);
            
            rs = pstmt.executeQuery();

            if(rs.next()){
                Member member = new Member();
                member.setMemberId(rs.getString("member_id"));
                member.setMoney(rs.getInt("money"));
                return member;
            } else {
                throw new NoSuchElementException("member not found memberId = " + memberId);
            }
        } catch (SQLException e) {
            log.error("db error", e);
            throw e;
        } finally {
            close(con, pstmt, null);
        }
    }

    public void update(String memberId,int money) throws SQLException{
        String sql = "update member set money=? where member_id=?";

        Connection con = null;
        PreparedStatement pstmt = null;

        try {
            con = getConnection();
            pstmt = con.prepareStatement(sql);
            pstmt.setInt(1, money);
            pstmt.setString(2, memberId);
            int resultSize = pstmt.executeUpdate();
            log.info("resultSize={}",resultSize);
        } catch (SQLException e) {
            log.info("error",e);
            throw e;
        } finally {
            close(con, pstmt, null);
        }
    }

    public void delete(String memberId) throws SQLException{
        String sql = "delete from member where member_id=?";

        Connection con = null;
        PreparedStatement pstmt = null;

        try {
            con = getConnection();
            pstmt = con.prepareStatement(sql);
            pstmt.setString(1, memberId);
            pstmt.executeUpdate();

        } catch (SQLException e) {
            log.info("error",e);
            throw e;
        } finally {
            close(con, pstmt, null);
        }
    }


    
    private void close(Connection con,Statement stmt,ResultSet rs){
        if(rs != null){
            try {
                rs.close();
            } catch (SQLException e) {
                log.info("error",e);
            }
        }

        if(stmt != null){
            try {
                stmt.close();    
            } catch (SQLException e) {
                log.info("error", e);
            }
        }

        if(con != null){
            try {
                con.close();
            } catch (SQLException e) {
                log.info("error", e);    
            }
        }

    }
    
    
    private Connection getConnection(){
        return DBConnectionUtil.getConnection();
    }
}

MemberRepositoryV0 에서 save나 findById 등의 로직을 실행할 때 DBConnectionUtil.getConnection()으로 매번 새로운 커넥션을 연결하고 커넥션을 획득했다.DBConnectionUtil.getConnection() 과정 자체가 서두에 말한 복잡한 과정을 포함하고 있다.

 

그렇다면 커넥션 풀은?

커넥션 풀은 필요한 만큼 커넥션을 미리 확보해서 풀에 보관한다. 서버스의 특징과 서버 스펙에 따라 다르지만 보통 기본 값인 10개이다. 커넥션 풀에 들어 있는 커넥션은 TCP/IP로 DB와 커넥션이 연결되어 있는 상태이기 때문에 언제든지 즉시 SQL을 db에 전달할 수 있다. 더 이상 애플리케이션 로직에서는 DB드리어버를 통해 새로운 커넥션을 획득하지 않아도 된다. 커넥션 풀을 사용하면 커넥션 풀에 이미 생성되어 있는 커넥션을 객체 참조로 쓰면 된다.
애플리케이션 로직은 커넥션 풀에서 받은 커넥션을 사용해서 SQL을 데이터베이스에 전달하고 그 결과를 받아서 처리한다. 이후 커넥션을 사용하고 나면 커넥션을 종료하는 것이 아니라 커넥션이 살아있는 상태로 커넥션 풀에 반환하면 된다.

커넥션 풀은 개념적으로 단순해서 직접 구현할 수도 있지만, 사용도 편리하고 성능도 뛰어난 오픈소스 커넥션 풀이 많기 때문에 오픈소스를 사용하는 것이 좋다. 대표적으로 commons-dbcp2,tomcat-jdbc,HikariCP등이 있다. 스프링부트 2.0부터는 기본 커넥션 풀로 hikariCp를 제공한다.

 

공통적으로 쓰이는 기능이라면? 추상화! DataSource

커넥션 풀을 사용하기 전 한번 생각해볼 것이 있다. DriverManager를 통해서 획득하든지, 커넥션 풀을 써서 커넥션을 획득하든 애플리케이션 입장에서는 '커넥션을 획득한다'라는 기능으로는 동일하다. 하지만 실제 구현 코드는 각 방법마다 다를 수 있다. 이럴 때 필요한 것은 바로 추상화이다. 커넥션을 획득하는 방법을 DataSoure라는 인터페이스가 담당한다. 애플리케이션 실제 구현체가 의존하는 것이 아닌 DataSource를 의존하도록 로직을 작성하면 된다. 그리고 구현 기술을 변경하고 싶다면 해당 구현체로 갈아끼우면 된다.
정리하자면 자바는 DataSource를 통해 커넥션을 획득하는 방법을 추상화했다. 애플리케이션 로직은 Datasource 인터페이스만 의존하면 되는 것이다.

 

DataSource를 통한 커넥션을 획득 - 설정과 사용의 분리

 

DriverManagerDataSource

driverManager 통해서 커넥션을 얻는 방법과 dataSource가 적용된 DriverManagerDataSource 커넥션을 얻는 테스트 코드는 다음과 같다.

@Slf4j
public class ConnectionTest {
   
    @Test
    public 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
    public void dataSourceDriverManager() throws SQLException{
        
        DriverManagerDataSource dataSource = new DriverManagerDataSource(URL,USERNAME,PASSWORD);

        Connection con1 = dataSource.getConnection();
        Connection con2 = dataSource.getConnection();
        log.info("connection = {}, class = {}",con1,con1.getClass());
        log.info("connection = {}, class = {}",con2,con2.getClass());
    }
}

=== 실행 결과 ===
[main] INFO hello.jdbc.connection.ConnectionTest -- connection = conn0: url=jdbc:h2:tcp://localhost/~/test user=SA, class = class org.h2.jdbc.JdbcConnection
[main] INFO hello.jdbc.connection.ConnectionTest -- connection = conn1: url=jdbc:h2:tcp://localhost/~/test user=SA, class = class org.h2.jdbc.JdbcConnection
[main] INFO hello.jdbc.connection.ConnectionTest -- connection = conn2: url=jdbc:h2:tcp://localhost/~/test user=SA, class = class org.h2.jdbc.JdbcConnection
[main] INFO hello.jdbc.connection.ConnectionTest -- connection = conn3: url=jdbc:h2:tcp://localhost/~/test user=SA, class = class org.h2.jdbc.JdbcConnection

코드를 보면 별반 차이가 없어 보인다. 오히려 DriverManagerDataSource를 이용하는 쪽이 코드가 한 줄 더 늘어난 것처럼 보인다. 하지만 설정의 분리라는 점에서 큰 차이가 생긴다. 예를 들어서 매 커넥션 마다DriverManager.getConnection(URL, USERNAME, PASSWORD); 이렇게 해야한다면 getConnection(UERL, USERNAME, PASSWORLD); 등의 오타가 생길 수 도 있다. 뿐만 아니라 향후 USERNAME이 userName으로 바뀐다고 상상해보자. DriverManager.getConnection(URL, USERNAME, PASSWORD)의 경우 모든 코드를 직접 USERNAME->userName으로 수정해야 한다.

반면 DriverManagerDataSource의 경우 new DriverManagerDataSource(URL,USERNAME,PASSWORD) 의 단 한번의 설정만 하고 쓰면 된다. 추후 USERNAME->userName로 변경 될 경우 new DriverManagerDataSource(URL,USERNAME,PASSWORD) 여기서 USERNAME->userName으로 한번만 바꾸면 된다. 또한 사용하는 입장에서도 설정에 관한 것은 신경쓰지 않고 dataSource.getConnection()으로만 쓰면 되는 것이다.

 

커넥션 풀을 DataSource로 이용해보기

@Slf4j
public class ConnectionTest {
   
    @Test
    public 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
    public void dataSourceDriverManager() throws SQLException{

        DriverManagerDataSource dataSource = new DriverManagerDataSource(URL,USERNAME,PASSWORD);

        Connection con1 = dataSource.getConnection();
        Connection con2 = dataSource.getConnection();
        log.info("connection = {}, class = {}",con1,con1.getClass());
        log.info("connection = {}, class = {}",con2,con2.getClass());
    }

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

        Connection con1 = dataSource.getConnection();
        Connection con2 = dataSource.getConnection();
        log.info("connection = {}, class = {}",con1,con1.getClass());
        log.info("connection = {}, class = {}",con2,con2.getClass());
        Thread.sleep(2000);
    }
}

=== 테스트 실행 결과 ====
[main] INFO hello.jdbc.connection.ConnectionTest --  driverManager connection = conn0: url=jdbc:h2:tcp://localhost/~/test user=SA, class = class org.h2.jdbc.JdbcConnection
[main] INFO hello.jdbc.connection.ConnectionTest -- driverManager connection = conn1: url=jdbc:h2:tcp://localhost/~/test user=SA, class = class org.h2.jdbc.JdbcConnection
[main] INFO com.zaxxer.hikari.HikariDataSource -- MyPool - Starting...
[main] INFO com.zaxxer.hikari.pool.HikariPool -- MyPool - Added connection conn2: url=jdbc:h2:tcp://localhost/~/test user=SA
[main] INFO com.zaxxer.hikari.HikariDataSource -- MyPool - Start completed.
[main] INFO hello.jdbc.connection.ConnectionTest --  HikariDataSource connection = HikariProxyConnection@1306834002 wrapping conn2: url=jdbc:h2:tcp://localhost/~/test user=SA, class = class com.zaxxer.hikari.pool.HikariProxyConnection
[main] INFO hello.jdbc.connection.ConnectionTest --  HikariDataSource connection = HikariProxyConnection@542365801 wrapping conn3: url=jdbc:h2:tcp://localhost/~/test user=SA, class = class com.zaxxer.hikari.pool.HikariProxyConnection
[main] INFO hello.jdbc.connection.ConnectionTest --  dataSourceDriverManager connection = conn12: url=jdbc:h2:tcp://localhost/~/test user=SA, class = class org.h2.jdbc.JdbcConnection
[main] INFO hello.jdbc.connection.ConnectionTest --  dataSourceDriverManager connection = conn13: url=jdbc:h2:tcp://localhost/~/test user=SA, class = class org.h2.jdbc.JdbcConnection

HikariCP 커넥션 풀을 사용했다. 일단 테스트 결과를 봐보자.
커넥션 풀을 사용하기 전에는 커넥션 클래스가 class = class org.h2.jdbc.JdbcConnection 였지만, 커넥션 풀에서 커넥션을 가져온 경우 커넥션 클래스가 class = class com.zaxxer.hikari.pool.HikariProxyConnection 이다.

logback.xml을 작성하여 커넥션 풀을 사용할 경우의 로그를 좀 더 자세하게 봐보자.

resources/logbakc.xml 경로로 xml 파일을 만든 후 아래 코드를 작성하면 된다. 

<configuration>
 <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
 <encoder>
 <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} -%kvp- %msg%n</pattern>
 </encoder>
 </appender>
 <root level="DEBUG">
 <appender-ref ref="STDOUT" />
 </root>
</configuration>

=== 커넥션 풀만 테스트 실행 결과 ===
[main] INFO  h.jdbc.connection.ConnectionTest --  HikariDataSource connection = HikariProxyConnection@2024240125 wrapping conn0: url=jdbc:h2:tcp://localhost/~/test user=SA, class = class com.zaxxer.hikari.pool.HikariProxyConnection
[main] INFO  h.jdbc.connection.ConnectionTest --  HikariDataSource connection = HikariProxyConnection@1097324923 wrapping conn1: url=jdbc:h2:tcp://localhost/~/test user=SA, class = class com.zaxxer.hikari.pool.HikariProxyConnection
[MyPool connection adder] DEBUG com.zaxxer.hikari.pool.HikariPool -- MyPool - Added connection conn2: url=jdbc:h2:tcp://localhost/~/test user=SA
[MyPool connection adder] DEBUG com.zaxxer.hikari.pool.HikariPool -- MyPool - Added connection conn3: url=jdbc:h2:tcp://localhost/~/test user=SA
[MyPool connection adder] DEBUG com.zaxxer.hikari.pool.HikariPool -- MyPool - Added connection conn4: url=jdbc:h2:tcp://localhost/~/test user=SA
[MyPool connection adder] DEBUG com.zaxxer.hikari.pool.HikariPool -- MyPool - Added connection conn5: url=jdbc:h2:tcp://localhost/~/test user=SA
[MyPool connection adder] DEBUG com.zaxxer.hikari.pool.HikariPool -- MyPool - Added connection conn6: url=jdbc:h2:tcp://localhost/~/test user=SA
[MyPool connection adder] DEBUG com.zaxxer.hikari.pool.HikariPool -- MyPool - Added connection conn7: url=jdbc:h2:tcp://localhost/~/test user=SA
[MyPool housekeeper] DEBUG com.zaxxer.hikari.pool.HikariPool -- MyPool - Pool stats (total=8, active=2, idle=6, waiting=0)
[MyPool connection adder] DEBUG com.zaxxer.hikari.pool.HikariPool -- MyPool - Added connection conn8: url=jdbc:h2:tcp://localhost/~/test user=SA
[MyPool connection adder] DEBUG com.zaxxer.hikari.pool.HikariPool -- MyPool - Added connection conn9: url=jdbc:h2:tcp://localhost/~/test user=SA

여기서 중요한 점은 MyPool Connection adder 스레드가 생성되어 커넥션을 만든다는 것이다. main 스레드가 커넥션 풀에 두개의 conn0과 conn1을 만든 후 main 스레드는 로직 코드를 실행하여 종료가 된다. 하지만 커넥션 풀에 커넥션이 10개가 되어야하므로 이것을 만들어줄 스레드가 필요한데 그 역할을 하는 것이 바로 MyPool connection adder인 것이다.
그렇다면 왜 별도의 스레드를 사용해서 커넥션 풀에 커넥션을 채울까?
커넥션 풀에 커넥션을 채우는 것은 상대적으로 오래 걸리는 일이다. 애플리케이션을 실행할 때 커넥션 풀을 채울 때 까지 마냥 대기하고 있다면 애플리케이션 실행 시간이 늦어질 것이다. 따라서 별도의 스레드를 사용함으로 커넥션 풀을 채워서 애플리케이션 실행 시간에 영향을 주지 않게 되는 것이다.

DataSource를 적용해보기

@Slf4j
public class MemberRepositoryV1 {

    private DataSource dataSource;

    public MemberRepositoryV1(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    public Member save(Member member) throws SQLException {
        String sql = "insert into member(member_id, money) values(?, ?)";

        Connection con = null;
        PreparedStatement pstmt = null;

        try {
            con = getConnection();
            pstmt = con.prepareStatement(sql);
            pstmt.setString(1, member.getMemberId());
            pstmt.setInt(2, member.getMoney());
            pstmt.executeUpdate();
            return member;
        } catch (SQLException e) {
            log.error("error", e);
            throw e;
        } finally {
            close(con, pstmt, null);
        }
    }

    public Member findById(String memberId) throws SQLException {
        String sql = "select * from member where member_id = ?";

        Connection con = null;
        PreparedStatement pstmt = null;
        // 데이터베이스서 쿼리 실행한 결과를 받을 객체
        ResultSet rs = null;

        try {
            con = getConnection();
            pstmt = con.prepareStatement(sql);
            pstmt.setString(1, memberId);
            
            rs = pstmt.executeQuery();

            if(rs.next()){
                Member member = new Member();
                member.setMemberId(rs.getString("member_id"));
                member.setMoney(rs.getInt("money"));
                return member;
            } else {
                throw new NoSuchElementException("member not found memberId = " + memberId);
            }
        } catch (SQLException e) {
            log.error("db error", e);
            throw e;
        } finally {
            close(con, pstmt, null);
        }
    }

    public void update(String memberId,int money) throws SQLException{
        String sql = "update member set money=? where member_id=?";

        Connection con = null;
        PreparedStatement pstmt = null;

        try {
            con = getConnection();
            pstmt = con.prepareStatement(sql);
            pstmt.setInt(1, money);
            pstmt.setString(2, memberId);
            int resultSize = pstmt.executeUpdate();
            log.info("resultSize={}",resultSize);
        } catch (SQLException e) {
            log.info("error",e);
            throw e;
        } finally {
            close(con, pstmt, null);
        }
    }

    public void delete(String memberId) throws SQLException{
        String sql = "delete from member where member_id=?";

        Connection con = null;
        PreparedStatement pstmt = null;

        try {
            con = getConnection();
            pstmt = con.prepareStatement(sql);
            pstmt.setString(1, memberId);
            pstmt.executeUpdate();

        } catch (SQLException e) {
            log.info("error",e);
            throw e;
        } finally {
            close(con, pstmt, null);
        }
    }

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

외부에서 DataSoure를 생성자 주입으로 받는다. dataSource에 대한 설정은 외부에서 하면 되고 외부에서 설정된 dataSource를 로직에서 사용하면 된다. JdbcUtils를 이용해서 커넥션을 편리하게 닫았다.

위의 MemberRepositoryV1 테스트를 해보자.

@Slf4j
public class MemberRepositoryV1Test {

    MemberRepositoryV1 repository;

    // DataSource 설정
    @BeforeEach
    public void BeforeEach() {
        HikariDataSource dataSource = new HikariDataSource();
        dataSource.setJdbcUrl(URL);
        dataSource.setUsername(USERNAME);
        dataSource.setPassword(PASSWORD);

        repository = new MemberRepositoryV1(dataSource);
    }

    @Test
    public void crud() throws SQLException {
        // save
        Member member = new Member("test", 10000);
        repository.save(member);

        // findById
        Member findMember = repository.findById(member.getMemberId());
        log.info("findMember={}", findMember);
        Assertions.assertThat(findMember).isEqualTo(member);

        // update : money 1000->2000
        repository.update(member.getMemberId(), 20000);
        Member updatedMember = repository.findById(member.getMemberId());
        Assertions.assertThat(updatedMember.getMoney()).isEqualTo(20000);

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


=== 테스트 실행 결과 ===

[main] INFO hello.jdbc.repository.MemberRepositoryV1 -- get connection = HikariProxyConnection@509891820 wrapping conn0: url=jdbc:h2:tcp://localhost/~/test user=SA ,class = class com.zaxxer.hikari.pool.HikariProxyConnection
[main] INFO hello.jdbc.repository.MemberRepositoryV1 -- get connection = HikariProxyConnection@1138410383 wrapping conn0: url=jdbc:h2:tcp://localhost/~/test user=SA ,class = class com.zaxxer.hikari.pool.HikariProxyConnection
[main] INFO hello.jdbc.repository.MemberRepositoryV1Test -- findMember=Member(memberId=test, money=10000)
[main] INFO hello.jdbc.repository.MemberRepositoryV1 -- get connection = HikariProxyConnection@609656250 wrapping conn0: url=jdbc:h2:tcp://localhost/~/test user=SA ,class = class com.zaxxer.hikari.pool.HikariProxyConnection
[main] INFO hello.jdbc.repository.MemberRepositoryV1 -- resultSize=1
[main] INFO hello.jdbc.repository.MemberRepositoryV1 -- get connection = HikariProxyConnection@1574877131 wrapping conn0: url=jdbc:h2:tcp://localhost/~/test user=SA ,class = class com.zaxxer.hikari.pool.HikariProxyConnection
[main] INFO hello.jdbc.repository.MemberRepositoryV1 -- get connection = HikariProxyConnection@1835073088 wrapping conn0: url=jdbc:h2:tcp://localhost/~/test user=SA ,class = class com.zaxxer.hikari.pool.HikariProxyConnection
[main] INFO hello.jdbc.repository.MemberRepositoryV1 -- get connection = HikariProxyConnection@808228639 wrapping conn0: url=jdbc:h2:tcp://localhost/~/test user=SA ,class = class com.zaxxer.hikari.pool.HikariProxyConnection

비교를 위해 이전 MemberRepositoryV0Test 결과를 봐보자.

[main] INFO hello.jdbc.connection.DBConnectionUtil -- get connection=conn0: url=jdbc:h2:tcp://localhost/~/test user=SA, class=class org.h2.jdbc.JdbcConnection
[main] INFO hello.jdbc.connection.DBConnectionUtil -- get connection=conn1: url=jdbc:h2:tcp://localhost/~/test user=SA, class=class org.h2.jdbc.JdbcConnection
[main] INFO hello.jdbc.repository.MemberRepositoryV0Test -- findMember=Member(memberId=test, money=10000)
[main] INFO hello.jdbc.connection.DBConnectionUtil -- get connection=conn2: url=jdbc:h2:tcp://localhost/~/test user=SA, class=class org.h2.jdbc.JdbcConnection
[main] INFO hello.jdbc.repository.MemberRepositoryV0 -- resultSize=1
[main] INFO hello.jdbc.connection.DBConnectionUtil -- get connection=conn3: url=jdbc:h2:tcp://localhost/~/test user=SA, class=class org.h2.jdbc.JdbcConnection
[main] INFO hello.jdbc.connection.DBConnectionUtil -- get connection=conn4: url=jdbc:h2:tcp://localhost/~/test user=SA, class=class org.h2.jdbc.JdbcConnection
[main] INFO hello.jdbc.connection.DBConnectionUtil -- get connection=conn5: url=jdbc:h2:tcp://localhost/~/test user=SA, class=class org.h2.jdbc.JdbcConnection

커넥션 풀을 사용할 경우 conn0 커넥션이 재사용 된 것을 확인할 수 있다. 테스트는 순서대로 실행되기 때문에 커넥션을 사용하고 다시 돌려주는 것을 반복한다. 따라서 conn0 만 사용된다.

그렇다면 실제 실행시간은 얼마나 차이가 날까?

실제 실행 시간이 얼마나 날지 궁금해졌다. 테스트 코드는 다음과 같다.

    @Test
    public void result_TimeV0() throws SQLException{
        long startTime = System.currentTimeMillis();
        // 100번 1000번 10000번 반복할 예정
        for(int i = 0; i< 100;i++){
            crud();
        }
        long endTime = System.currentTimeMillis();
        log.info("실행 시간 : {}ms",(endTime - startTime));
    }

    @Test
    public void result_TimeV1() throws SQLException{
        long startTime = System.currentTimeMillis();
        // 100번 1000번 10000번 반복할 예정
        for(int i = 0; i< 100;i++){
            crud();
        }
        long endTime = System.currentTimeMillis();
        log.info("실행 시간 : {}ms",(endTime - startTime));
    }

=== 100번 ===

  • 커넥션 풀을 사용하지 않음
    [main] INFO hello.jdbc.repository.MemberRepositoryV0Test -- 실행 시간 : 1878ms
  • 커넥션 풀을 사용
    [main] INFO hello.jdbc.repository.MemberRepositoryV1Test -- 실행 시간 : 449ms

-> 커넥션 풀을 사용한 경우가 약 4배 정도 빠르다.

=== 1000번 ====

  • 커넥션 풀을 사용하지 않음
    [main] INFO hello.jdbc.repository.MemberRepositoryV0Test -- 실행 시간 : 14838ms
  • 커넥션 풀을 사용
    [main] INFO hello.jdbc.repository.MemberRepositoryV1Test -- 실행 시간 : 1693ms

-> 9배 정도 빠르다.

=== 10000번 ===

  • 커넥션 풀 사용하지 않음
    10000건의 어느 정도 진행 후 다음과 같은 예외가 발생했다.java.lang.IllegalStateException
    at hello.jdbc.connection.DBConnectionUtil.getConnection(DBConnectionUtil.java:17)
    at hello.jdbc.repository.MemberRepositoryV0.getConnection(MemberRepositoryV0.java:144)
    at hello.jdbc.repository.MemberRepositoryV0.findById(MemberRepositoryV0.java:50)
    at hello.jdbc.repository.MemberRepositoryV0Test.crud(MemberRepositoryV0Test.java:40)
    at hello.jdbc.repository.MemberRepositoryV0Test.result_TimeV0(MemberRepositoryV0Test.java:20)
    at java.base/java.lang.reflect.Method.invoke(Method.java:568)
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1511
  • 위의 예외는 DBConnectionUtil 발생한 것으로 확인된다.
@Slf4j
public class DBConnectionUtil {
    
    public static Connection getConnection() {
        try {
            Connection connection = DriverManager.getConnection(URL,USERNAME,PASSWORD);
            log.info("get connection={}, class={}",connection,connection.getClass());
            return connection;
        } catch (Exception e) {
            throw new IllegalStateException();
        }
    }
}

즉 데이터베이스로부터 커넥션을 얻는 과정에서 문제가 생긴 것이다.

  • 커넥션 풀 사용
    [main] INFO hello.jdbc.repository.MemberRepositoryV1Test -- 실행 시간 : 11496ms

커넥션 풀을 사용하지 않는 경우 1000번 반복한 경우보다도 빠르다.

 

커넥션 풀의 이점

테스트를 진행해보니 커넥션 풀을 사용하는게 얼마나 큰 이점이 생기는지 확인할 수 있었다.

 

첫번째로 안정적이다. 미리 데이터베이스와 연결이 되어 있는 커넥션을 쓰기 때문이다. 데이터베이스 연결시 혹시라도 발생할 수 있는 연결 문제에서 자유롭다.

두번째는 속도이다. 당연히 속도가 차이날 수 밖에 없다. 데이터베이스와 연결하는 과정이 끝나 있는 커넥션들을 쓰는 것이기 때문이다. 테스트를 통해 건수가 많아질 수 록 커넥션 풀을 사용하는 것이 속도도 빠르고 안정적임을 확인할 수 있었다.

참고 자료
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-db-1/