Spring

커넥션 풀

gogi masidda 2024. 2. 1. 13:48

DB 드라이버를 사용하면 매번 데이터베이스를 사용할 때마다 TCP/IP 커넥션을 맺어야 한다.

그래서 커넥션을 미리 생성해두고 사용하는 커넥션 풀이라는 방법을 사용한다.

 

애플리케이션이 시작할 때 커넥션 풀은 필요한 만큼 커넥션을 미리 확보해서 보관한다.

커넥션 풀에 들어가 있는 커넥션은 TCP/IP로 데이터베이스와 연결되어 있는 상태라서 언제든지 즉시 SQL을 데이터베이스에 전달할 수 있다.

어플리케이션 로직은 이미 생성되어있는 커넥션을 객체 참조로 가져다쓰기만 하면 된다.

커넥션을 사용하고 나면 커넥션을 종료하는 것이 아니라 살아있는 상태로 커넥션 풀에 반환한다. 


DataSource

커넥션을 DB 드라이버로 얻다가 커넥션 풀 중 하나인 hikariCP로 바꾸려 하면, 커넥션을 획득하는 어플리케이션 코드도 함께 변경해야한다. 또는 DBCP2에서 hikariCP로 바꾸려 할때도 동일한 문제가 있다.

자바는 이 문제를 해결하기 위해 DataSource라는 인터페이스를 제공하여 커넥션을 획득하는 방법을 추상화한다.

따라서 DBCP2 커넥션 풀, hikariCP 커넥션 풀의 코드를 직접 의존하는 것이 아니라, DataSource 인터페이스에만 의존하도록하면 된다. DriverManager는 DriverManagerDataSource라는 DataSource를 구현한 클래스를 제공한다.

DataSource 인터페이스에만 의존함으로써 어플리케이션 로직을 변경하지 않을 수 있다.

 

@Slf4j
public class ConnectionTest {

    @Test
    void dataSourceDriverManager() throws SQLException {
        //DriverManagerDataSource - 항상 새로운 커넥션을 획득
        DriverManagerDataSource dataSource = new DriverManagerDataSource(URL, USERNAME, PASSWORD);//생성할 때만 파라미터 입력. 향후 변경 시에 간편함.
        useDataSource(dataSource);
    }

    @Test
    void dataSourceConnectionPool() throws SQLException, InterruptedException {
        //커넥션 풀링
        HikariDataSource dataSource = new HikariDataSource();
        dataSource.setJdbcUrl(URL);
        dataSource.setUsername(USERNAME);
        dataSource.setPassword(PASSWORD);
        dataSource.setMaximumPoolSize(10); //디폴트가 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());
    }
}

별도의 쓰레드를 이용해서 최대 Pool size만큼의 데이터 소스를 채운다.

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

 

최대 풀 사이즈가 10인데 그 이상을 꺼내서 쓰려하면 대기가 걸린다. 최대 대기 시간은 maxLifeTime으로 설정한다.

DriverManager와는 달리 hikariCP는 사용 전 설정할 때만 파라미터를 넣고, 사용할 때는 넣지 않는다. 이로써 설정과 사용이 분리되면서 넣을 파라미터가 바뀌더라도 설정에서만 변경해주면 되기 때문에 간편하다.


DataSource 적용

/**
 * JDBC - DataSource 사용, JdbcUtils 사용
 */
@Slf4j
public class MemberRepositoryV1 {

    private final DataSource dataSource;

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

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

        Connection con = null;
        PreparedStatement pstmt = null; //PreparedStatement는 파라미터 할당 기능이 추가된 것.

        try {
            con = getConnection();
            pstmt = con.prepareStatement(sql);
            pstmt.setString(1, member.getMemberId());
            pstmt.setInt(2, member.getMoney());
            pstmt.executeUpdate(); //데이터를 변경할 때. 위에서 설정한게 실행됨. 영향받은 row 수만큼 반환
            return member;
        } catch (SQLException e) {
            log.error("DB error", e);
            throw e;
        } finally {
            close(con, pstmt, null); //finally에서 close해야 항상 close
        }

    }

    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 = DBConnectionUtil.getConnection();
            pstmt = con.prepareStatement(sql);
            pstmt.setString(1, memberId);

            rs = pstmt.executeQuery(); //select할 때는 executeQuery. 결과를 ResultSet에 담아서 반환
            if(rs.next()) { //내부에 커서가 있는데 한번은 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, rs);
        }
    }

    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.error("DB 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.error("DB 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;
    }
}

맨위에서 DataSource를 선언하고, 생성자를 만들고, 맨 아래에서 getConnection() 함수를 수정한 것 말고는 달라진게 없다. 

hikariCP를 사용하더라도 어플리케이션 로직에서는 DataSource 인터페이스에만 의존하기 때문이다. 

 

@Slf4j
class MemberRepositoryV1Test {

    MemberRepositoryV1 repository;

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

        //커넥션 풀링
        HikariDataSource dataSource = new HikariDataSource();
        dataSource.setJdbcUrl(URL);
        dataSource.setUsername(USERNAME);
        dataSource.setPassword(PASSWORD);
        repository = new MemberRepositoryV1(dataSource);
    }

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

        //findById
        Member findMember = repository.findById(member.getMemberId());
        log.info("findMember = {}", findMember);
        log.info("member == findMember = {}", member == findMember); //동일성(identity) 두 객체의 주소값이 같은지
        log.info("member equals findMember = {}", member.equals(findMember)); //동등성(equality) 두 객체의 내용이 같은지
        assertThat(findMember).isEqualTo(member);

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

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

        Thread.sleep(1000);
    }
}

테스트에서 hikariCP를 사용해도 잘 동작한다.

DriverManager에서는 항상 새로운 커넥션이 생성되지만, hikariCP에서는 커넥션이 재사용된다. 사용하고 돌려주는 것을 반복하는 형태이다.

728x90

'Spring' 카테고리의 다른 글

스프링으로 트랜잭션 문제 해결  (0) 2024.02.13
트랜잭션  (3) 2024.02.08
JDBC  (0) 2024.01.31
Spring Toy Project-1  (3) 2024.01.31
스프링 타입 컨버터  (1) 2024.01.15