커넥션 풀을 왜 알아야할까?

손재민·2023년 11월 22일
1

Database

목록 보기
1/1
post-thumbnail

안녕하세요. 저는 백엔드 공부를 하고 있는 학생입니다. 스프링과 MySQL을 이용해 서버를 구축하고 개발공부를 하고 있습니다.
MySQL을 이용해 개발을 하던 중 너무나도 자연스럽게 JPA와 같이 ORM, 추상화된 인테페이스만을 사용해 아무 생각없이 개발하고 있는 저의 모습을 확인할 수 있었습니다. 또한 깊이 있는 공부가 부족하다는 것을 깨닫고 이 글을 작성하게 되었습니다.
여기에서부터는 문장의 간결함을 위해서 높임말은 생략하겠습니다.

1. DB Connection이란?

DB를 사용하기 위해 DB와 애플리케이션 간 통신을 할 수 있는 수단이다.

DB Connection은 DB Driver와 DB 연결 정보를 담는 URL이 필요하다.

DriverURL설명
oracle.jdbc.driver.OracleDriverjdbc:oracle:thin:@ipaddress:1521:ORA7oracle-DB/thin-Driver
oracle.jdbc.driver.OracleDriverjdbc:oracle:oci7:@ipaddress:1521:ORA7oracle-DB/OCI-Driver
com.mysql.jdbc.Driverjdbc:mysql://ip_address:3306/database_naemmysql
sun.jdbc.odbc.JdbcOdbcDriverjdbc:odbc:database_nameJDBC-ODBC Bridge
com.informix.jdbc.IfxDriverjdbc:informix-sqli://ip:port/dbName:informixserver=sidinformix-DB

2. DB Connection 구조

2-Tier

  • 클라이언트로서의 자바 프로그램(JSP)이 직접 DB 서버에 접근해 데이터를 액세스하는 구조이다.

3-Tier

  • 자바 프로그램과 DB 서버 중간에 미들웨어 층을 둔다.
  • 미들웨어 층한테 비즈니스 로직, 트랜잭션 처리, 리소스 관리를 전부 맡기는 구조이다.

3. JDBC(Java Database Connectivity)

자바를 이용해 다양한 종류의 RDBMS와 접속하고 SQL문을 수행하여 처리하고자 할 때 사용되는 표준 SQL 인터페이스 API이다.

JDBC가 없었다면 DB마다 연결방식과 통신규약이 따로 있기 때문에 프로그램을 DB에 연결시키기 위해 해당 DB에 관련된 기술적 내용을 습득해야한다.

또한 DB 변경시 많은 변경 사항이 생긴다.

JDBC API를 사용하는 애플케이션의 구조는 자바 애플리케이션JDBC APIJDBC DriverDB 로 구성된다.


4. Connection Pool이 생겨난 배경

Connection conn = null;
PreparedStatement  pstmt = null;
ResultSet rs = null;

try {
    sql = "SELECT * FROM T_BOARD"

    // 1. 드라이버 연결 DB 커넥션 객체를 얻음
    connection = DriverManager.getConnection(DBURL, DBUSER, DBPASSWORD);

    // 2. 쿼리 수행을 위한 PreparedStatement 객체 생성
    pstmt = conn.createStatement();

    // 3. executeQuery: 쿼리 실행 후
    // ResultSet: DB 레코드 ResultSet에 객체에 담김
    rs = pstmt.executeQuery(sql);
    } catch (Exception e) {
    } finally {
        conn.close();
        pstmt.close();
        rs.close();
    }
}

자바에서 DB에 직접 연결해서 처리하는 경우 JDBC Driver를 로드하고 커넥션 객체를 받아와야한다.

매번 사용자가 요청을 할 때마다 드라이버를 로드하고 커넥션 객체를 생성해 연결하고 종료하기 때문에 비효율적이다.

→ 그래서 커넥션 풀을 사용한다.


5. Connection Pool 개념

웹 컨테이너(WAS)가 실행되면서 일정량의 Connection 객체를 미리 만들어서 Pool에 저장한다.

클라이언트 요청이 오면 Connection 객체를 빌려주고 해당 객체 임무가 완료되면 다시 Connection 객체를 반납해 Pool에 저장한다.

Container 구동 시 일정 수의 Connection 객체를 생성한다.

애플리케이션이 DBMS 작업을 수행해야 하면, Connection Pool에서 Connection 객체를 받아와 작업을 진행한다. 그리고 Connection Pool에 Connection 객체를 반납한다.

Connection Pool의 종류로는 “common-dbcp2”, “tomcat-jdbc Pool”, “DrvierManager DataSource”, “HikariCP” 등이 있다.

스프링 부트 2.0 이후부터는 커넥션 풀을 관리하기 위해 HikariCP를 채택하여 사용하고 있다.


6. Connection Pool 동작 원리

Hikari CP가 동작하는 방식

  1. Thread가 Connection을 요청하면 Connection Pool의 각자의 방식에 따라 유휴 Connection을 찾아서 반환한다. Hikari CP의 경우, 이전에 사용했던 Connection이 존재하는지 확인하고, 이를 우선적으로 반환하는 특징이 있다.
  2. 가능한 Connection이 존재하지 않으면, HandOffQueue를 Polling하면서 다른 Thread가 Connection을 반납하기를 기다린다. (지정한 TimeOut 시간까지 대기하다가 시간이 만료되면 예외를 던진다.)
  3. 최종적으로 사용한 Connection을 반납하면 Connection Pool이 Connection 사용 내역을 기록하고, HandOffQueue에 반납된 Connection을 삽입한다.
  4. 이를 통해 HandOffQueue를 Polling하던 Thread는 Connection을 획득하고 작업을 이어나간다.

→ DB 드라이버를 통해 커넥션을 조회, 연결, 인증, SQL을 실행하는 시간 등 커넥션 객체를 생성하기 위한 과정을 생략할 수 있게 된다.


7. Connection Pool 장점

  1. DB 접속 설정 객체를 미리 만들어 연결해 메모리 상에 등록하기 때문에 불필요한 작업이 사라진다.(커넥션 생성, 삭제)

→ 클라이언트가 DB에 빠르게 접근 가능하다.

  1. DB Connection 수를 제한할 수 있어서 과도한 접속으로 인한 서버 자원 고갈을 예방할 수 있다.
  2. DB 서버 환경이 바뀔 경우 쉬운 유지 보수가 가능하다.
  3. 연결이 끝난 Connection을 재사용함으로써 새로 객체를 생성하는 비용을 절약할 수 있다.

8. Connection Pool 유의사항

커넥션 풀을 크게 설정하면 메모리 소모가 크다. 하지만 많은 사용자가 대기 시간이 줄어든다.

커넥션 풀을 작게 설정하면 그 만큼 대기시간이 늘어난다. 하지만 적은 메모리를 사용한다.

Connection의 주체는 Thread이기 때문에 Thread와 함께 고려해야한다.

  • Thread Pool 크기 < Connection Pool 크기
    • Thread Pool에서 트랜잭션을 처리하는 Thread가 사용하는 Connection 외에 남는 Connection은 실질적으로 메모리 공간만 차지한다.
  • Thread Pool 크기와 Connection Pool 모두 크기 증가
    • Thread 증가로 인해 더 많은 Context Switching이 발생한다.

9. 간단한 DBCP를 구현하여 성능 비교해보기

public class Application {

    private static final String URL = "jdbc:mysql://localhost:3306/dbcp-test";
    private static final String ID = "id";
    private static final String PASSWORD = "password";

    public static void main(String[] args) {
        Instant start = Instant.now();

        for (int i = 0; i < 100; i++) {
            Connection connection = generateConnection();
            doInsert(connection);
        }

        Instant end = Instant.now();
        System.out.println("수행시간: " + Duration.between(start, end).toMillis() + " 밀리초");

    }

    private static void doInsert(final Connection connection) {
        try (connection) {
            String sql = "INSERT INTO members(name, password) VALUES(?, ?)";
            PreparedStatement preparedStatement = connection.prepareStatement(sql);
            preparedStatement.setString(1, "test");
            preparedStatement.setString(2, "1234");
            preparedStatement.executeUpdate();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private static Connection generateConnection() {
        Connection connection = null;
        try {
            Class.forName("com.mysql.cj.jdbc.Driver");
            connection = DriverManager.getConnection(URL, ID, PASSWORD);
        } catch (Exception e) {
            e.printStackTrace();
        }

        return connection;
    }
}

100번의 Insert 구문을 실행한다.

→ 한번의 for 루프마다 Connection을 생성하는 것을 볼 수 있다.

위 코드의 동작 시간은 약 3610ms 이다.

public class ConnectionPool {

    private static final String URL = "jdbc:mysql://localhost:3306/dbcp-test";
    private static final String ID = "id";
    private static final String PASSWORD = "password";

    private final List<Connection> pool;
    private int pointer = 0;

    public ConnectionPool() {
        this.pool = initializeConnectionPool();
    }

    private List<Connection> initializeConnectionPool() {
        return IntStream.range(0, 10)
                .mapToObj(ignored -> generateConnection())
                .collect(Collectors.toList());
    }

    private Connection generateConnection() {
        Connection connection = null;
        try {
            Class.forName("com.mysql.cj.jdbc.Driver");
            connection = DriverManager.getConnection(URL, ID, PASSWORD);
        } catch (Exception e) {
            e.printStackTrace();
        }

        return connection;
    }

    public Connection getConnection() {
        Connection connection = pool.get(pointer % 10);
        pointer++;
        return connection;
    }
}

총 10개의 커넥션을 생성해둔다.

public class ApplicationCp {

    public static void main(String[] args) {
        ConnectionPool connectionPool = new ConnectionPool();

        Instant start = Instant.now();

        for (int i = 0; i < 100; i++) {
            Connection connection = connectionPool.getConnection();
            doInsert(connection);
        }

        Instant end = Instant.now();
        System.out.println("수행시간: " + Duration.between(start, end).toMillis() + " 밀리초");
    }

    private static void doInsert(final Connection connection) {
        try {
            String sql = "INSERT INTO members(name, password) VALUES(?, ?)";
            PreparedStatement preparedStatement = connection.prepareStatement(sql);
            preparedStatement.setString(1, "test");
            preparedStatement.setString(2, "1234");

            preparedStatement.executeUpdate();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

커넥션 풀을 사용하여 똑같이 100번의 Insert 구문을 실행하였다.

약 669ms 가 나왔다. 5.39배 성능이 향상되었다.

데이터베이스 커넥션을 생성하고, 해제하는 과정이 굉장히 큰 오버헤드임을 알게되었다.


참고자료

[데이터베이스] Connection Pool이란?

커넥션 풀이란 (Connection Pool)

데이터베이스 커넥션 풀 (Connection Pool)과 HikariCP

profile
대덕SW마이스터고 백엔드 개발자

0개의 댓글