커넥션 풀을 사용하는 이유

DB에 쿼리를 하나 날리기 위해서는 아래와 같은 과정을 거쳐야 한다.

  1. TCP/IP 연결 수립 (3-way handshake): DB 서버와 네트워크 연결을 맺는다. 물리적으로 시간이 가장 많이 걸린다.
  2. ID/PW 인증: 전달받은 계정 정보를 DB 내부적으로 확인한다.
  3. DB 세션 생성: DB가 해당 클라이언트를 위한 메모리와 리소스를 할당한다.
  4. SQL 실행: 실제 원하는 작업을 수행한다.
  5. 연결 종료: 리소스 해제 및 TCP 연결 종료

실제 중요한 4번보다, 1~3번 과정의 준비 과정(연결 수립) 시간이 더 오래 걸리는 배보다 배꼽이 더 큰 상황이 발생하므로 커넥션 풀을 사용한다.

 

커넥션 풀

  • TCP/IP 연결을 수립하는 과정에서 시간이 소요되므로, 미리 생성해두고 사용하는 것. 보통 10개이다.
  • hikariCP가 기본적으로 커넥션 풀로 사용된다.
@Slf4j
public class ConnectionTest {

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

    /**
     * DB 접속 정보를 한 번만 넘겨서 DataSource를 생성하면, 그 이후부터는 커넥션을 가져올 때 DB 접속 정보가 필요 없다.
     * getConnection()만 호출하면 된다. -> 설정과 사용을 분리한 것 
     */
    @Test
    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는 커넥션을 얻는 방법을 추상화하는 인터페이스이다.

 

@Slf4j
class MemberRepositoryV1Test {

    MemberRepositoryV1 repository;

    @BeforeEach
    void beforeEach() {
        // 기본 DriverManger - 항상 새로운 커넥션 획득하기 -> 성능 낮음
//        DriverManagerDataSource dataSource = new DriverManagerDataSource(URL, USERNAME, PASSWORD);
//        repository = new MemberRepositoryV1(dataSource);

        // 커넥션 풀링 - 커넥션 풀에 만들어진 커넥션 획득하기 -> TCP/IP 연결 수립 과정 생략 및 반환된 커넥션을 재사용
        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());
        log.info("findMember = {}", findMember);
        Assertions.assertThat(member).isEqualTo(findMember);

        // update
        repository.update(member.getMemberId(), 20000);
        Member updatedMember = repository.findById(member.getMemberId());
        Assertions.assertThat(updatedMember.getMoney()).isEqualTo(20000);

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

 

 

DB 연결 구조

데이터베이스 연결 구조

 

  • 커넥션에 연결된 세션을 통해 요청이 실행된다.
  • 사용자가 커넥션을 닫거나 관리자가 세션을 강제 종료하면 세션이 종료된다.

 

 

트랜잭션

  • 수동 커밋 모드를 시작하는 것을 관례적으로 트랜잭션을 시작한다 라고 한다. (기본이 자동 커밋이기 때문)
  • 해당 모드는 세션 내에서 한 번 설정되고 나면, 해당 세션이 종료될 때까지 변경될 수 없다.
  • 커밋이 되지 않으면, 변경된 데이터는 해당 세션에서만 볼 수 있고 다른 세션에서는 볼 수 없다. -> 다른 세션에서 트랜잭션 시작하기 전 변경되기 전 데이터로 조회. (READ UNCOMMITED 제외)
  • 또한, 트랜잭션을 시작하려면 커넥션이 필요하며, 트랜잭션 내에서는 같은 커넥션을 유지해야 한다.

 

 

DB 락

  • 세션이 트랜잭션을 시작하고 데이터를 변경하는 동안, 다른 세션에서 해당 데이터를 변경하지 못하도록 막는 것.
  • 변경하고자 하는 데이터 (row)에 대해 락을 획득해야 update sql을 수행할 수 있다. 즉, 락은 update 시점에만 걸린다.
  • select for update 구문을 사용하면 조회 시점에 락을 획득할 수 있다. 그러면 조회만 했지만, 다른 세션에서 해당 row에 대해 update sql을 락을 획득할 때 까지 수행하지 못한다.
  • 락을 획득하지 못한 상태라면, 락을 획득할 때가지 기다려야 한다. (대기 시간이 설정을 넘어가면 타임아웃 오류)