Spring

[Spring] JDBC & DriverManager & DataSource & HikariCP

모모_모 2024. 3. 28. 22:53

[JDBC 소개]

애플리케이션 서버에서 DB의 데이터를 수정하거나 조회하기 위해서는, 크게 3가지 과정이 필요하다.

 

 

1. 커넥션 연결 : tcp/ip를 사용해 커넥션을 연결한다

2. SQL 전달 : 애플리케이션 서버는 DB가 이해할 수 있는 SQL을 연결된 커넥션을 통해 DB에 전달

3. 결과 응답 : DB는 SQL을 수행하고 그 결과를 응답한다.

 

그러나 문제는, 각 DB마다 커넥션 연결방법, SQL 전달방법, 결과응답 방법이 모두 다르다.

 

JDBC의 등장 배경은 여기서 나타난다.

 

java.sql.Connection - 연결

java.sql.Statement - SQL 담은 내용

java.sql.ResultSet - SQL 요청 응답

 

위 3가지는 자바의 JDBC 표준 인터페이스이다. 개발자 입장에선 표준 인터페이스를 따라 개발한다.

이 JDBC 표준 인터페이스를, 각 DB 회사에서 자신의 DB에 맞도록 구현해 라이브러리로 제공한다. 이것을 JDBC 드라이버라 한다. (ex. MySQL JDBC 드라이버, Oracle JDBC 드라이버)

 

즉 모종의 이유로 서로 다른 DB에 접근할 경우, 애플리케이션 로직에서는 JDBC 표준에 맞는 일관성을 유지할 수 있다.

접근할 DB에 맞게 JDBC 드라이버만 교체하면, DB에 문제없이 접근할 수 있다.


 

[DriverManager]

JDBC는 java.sql.Connection의 표준 커넥션 인터페이스를 정의하고, DB 드라이버들은 이 표준 커넥션 인터페이스를 구현한 구현체를 제공한다.

 

JDBC가 제공하는 DriverManager는 라이브러리에 등록된 DB 드라이버들을 관리, 커넥션을 획득하는 기능을 제공한다.

 

 

커넥션을 얻는 과정

 

1. App logic에서 커넥션이 필요하면, DriverManager.getConnection(url, id, pw...)을 호출한다

2. DriverManager는 라이브러리에 등록된 드라이버 목록들을 자동으로 인식한다.

3. 순서대로 등록된 드라이버 목록에서 url, id, pw등을 넘겨 커넥션 획득 가능 여부를 판단한다.

4. 찾은 커넥션 구현체를 클라이언트에 반환한다.

 

쿼리를 실행하는 과정

 

반환된 커넥션 구현체(1)에 sql(2)문을 집어넣어 Statement 구현체(3)를 할당한다. 

 

(1) : Connection con = DriverManager.getConnection()

(2) : String sql = "insert into member(member_id, money) values(?,?)"

(3) : PreparedStatement pstmt = con.prepareStatement(sql)

 

할당된 Statement 구현체를 실행한다.(4)

 

(4) pstmt.executeUpdate()

 

응답을 얻는 과정

 

만약 insert가 아닌 조회 혹은 다른 query라면, Statement 구현체를 실행한 리턴값으로 ResultSet 구현체(5)를 할당한다.

 

(5) ResultSet rs = pstmt.executeQuery()

 

 

 

전체 코드는 아래와 같다

public class MemberRepositoryV0 {
 public Member save(Member member) throws SQLException {
 	String sql = "insert into member(member_id, money) values(?, ?)"; //(2)
 	Connection con = null;
 	PreparedStatement pstmt = null;
 	try {
            con = getConnection(); //(1)
            pstmt = con.prepareStatement(sql); //(3)
            pstmt.setString(1, member.getMemberId());
            pstmt.setInt(2, member.getMoney());
            pstmt.executeUpdate(); //(4)
 			return member;
        } catch (SQLException e) {
            log.error("db 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();
    }
 }

 

  • DriverManager.getConnection() 으로 얻은 Connection은, 획득한 자원을 모두 close 해주어야 한다. (1. Connection, 2. Statement, 3. ResultSet)
  • 위의 코드에선, close시 자원을 반환할 때 모두 각각 예외처리를 해주기 위해서 별개의 method를 만들었다.
  • Prepared Statement는, Statement의 자식 타입으로 "?"를 통한 파라미터 바인딩을 가능하게 해준다.
  • SQL Injection 공격을 예방하기 위해 파라미터 바인딩은 필수이다.

 

[Connection Pool]

 

커넥션 풀이란?

애플리케이션을 시작하는 시점에 필요한만큼 커넥션을 미리 확보에 풀에 보관하는 것.

 

 

DriverManager를 이용한 connection을 얻는 방법은, 매번 DB에 접근할때마다 connection 객체를새로 생성해서 받아오고 닫는 일련의 과정을 반복해야만 한다.

그러나 DB 커넥션을 획득할 때, 다음과 같은 복잡한 과정을 거친다.

 

 

  1. 애플리케이션 로직은 DB 드라이버를 통해 커넥션을 조회한다.
  2. DB 드라이버는 DB와 TCP/IP 커넥션을 연결한다. 물론 이 과정에서 3 way handshake 같은 TCP/IP 연결 을 위한 네트워크 동작이 발생한다.
  3. DB 드라이버는 TCP/IP 커넥션이 연결되면 ID, PW와 기타 부가정보를 DB에 전달한다.
  4. DB는 ID, PW를 통해 내부 인증을 완료하고, 내부에 DB 세션을 생성한다.
  5. DB는 커넥션 생성이 완료되었다는 응답을 보낸다.
  6. DB 드라이버는 커넥션 객체를 생성해서 클라이언트에 반환한다.

 

일련의 과정을 매 DB 접근시마다 반복하는 것은, 불필요하고 번거롭고 자원을 필요이상으로 낭비하는 행위이다.

 

DataSource는 이 Connection 객체를 얻기 위해, Connection Pool(커넥션 풀)을 사용하는 JDBC의 인터페이스이다.

 

 

커넥션 풀을 사용하는 과정은 이러하다.

애플리케이션 서버에서, 커넥션 풀을 만들어 TCP/IP로 DB와 연결된 커넥션을 필요한 만큼 생성해놓는다.

 

 

커넥션 풀의 이미 생성된 커낵션을 객체 참조로 가져다 쓴다.

 

 

 

사용 완료 이후에는 커넥션을 종료(close)하지 않고, 커넥션을 커넥션 풀에 반환한다.

 

이러한 커넥션 풀 오픈소스는 commons-dpcp2, tomcat-jdbc pool, HikariCP가 존재한다.


[DataSource]

자바에서는 커넥션 풀을 이용하는 여러 방법을 추상화하여, DataSource 인터페이스를 제공한다.

 

개발자 입장에선 DriverManager와 마찬가지로, DataSource 인터페이스에만 의존하도록 애플리케이션 로직을 작성하면 된다.

 

아래의 코드는 기존 DriverManager.getConnection()을 이용해 connection을 얻는 방법과,

DriverManagerDataSource의 커넥션 풀을 사용하여 connection을 얻는 test code이다.

package hello.jdbc.connection;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import static hello.jdbc.connection.ConnectionConst.*;
@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());
 }
 @Test
 void dataSourceDriverManager() throws SQLException {
 //DriverManagerDataSource - 항상 새로운 커넥션 획득
 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());
 }
}

 

 

  • DriverManager.getConnection에서는 항상 DB와 접속을 위해 URL,username,password를 입력.
  • dataSource.getConnection() 에선 호출하기만 하면 된다. (처음 객체 생성시에만 파라미터가 필요하다.)
  • 커넥션 풀을 이용함으로써, 설정과 사용의 분리가 일어난다.

 

[HikariCP]

커넥션 풀 오픈소스중 가장 많이 사용되는 오픈소스이다.

JDBC와 HikariCP간 벤치마크를 수행한 결과

 

  • Connection Cycle ops/ms는 DataSource.getConnection(), Connection.close() 에 대한 DB 연결과 연결해제를 비교 측정한 내용
  • Statement Cycle ops/ms는 Conection.prepareStatement(), Statement.execute(), Statement.close() 데이터 베이스의 상태로 준비 > 수행 > 종료 단계를 비교 측정한 내용

 

 

HikariCP가 Connection Pool을 운영하는 원리를 알아보자면..

 

HikariCP에서는 내부적으로 ConcurrentBag이라는 구조체를 이용해 Connection을 관리한다.

  1. 외부에서 HikariPool.getConnection() 호출
  2. 내부적으로 ConcurrentBag.borrow() 호출, 커넥션을 요청한 쓰레드가 이전에 사용한 동일한 커넥션을 사용할 수 있도록 방문 내역을 살펴본다.
  3. 다른 쓰레드가 사용중이라면, 현재 idle 상태의 커넥션을 찾아 준다.
  4. 전체 커넥션이 모두 사용중이라면, 쓰레드를handoffQuere로 보내 대기하도록 한다
  5. HikariCP의 default Connection timeout인 30초를 기다리고, 그동안 Connection을 얻지 못한다면 Timeout으로 exception이 발생한다.

 

HikariCP의 Connection 반환 과정

 

JDBC의 connection.close()는 아래의 과정을 거쳐 현재 열려있는 데이터베이스 연결을 닫는다.

  1. 현재 진행 중인 트랜잭션이 커밋되지 않았다면, 자동으로 롤백.
  2. 데이터베이스 연결에 대한 리소스(네트워크 연결, 메모리 등)가 해제.
  3. 연결에 대한 모든 작업(쿼리 실행, 트랜잭션 관리 등)이 중단.
  4. 해당 연결로부터 가져온 모든 Statement, PreparedStatement, ResultSet 등의 자원도 닫힌다.

하지만 HikariCP의 경우, 프록시 객체를 사용해 커넥션을 완전히 종료하지 않고 커넥션 풀로 반환하도록 구현한다.

ProxyConnection은 실제 Connection 객체와 동일한 인터페이스에서 close() 메서드를 오버라이드해 커넥션 풀로 반환하도록 하였다.


 

글이 길어졌는데, JDBC로 시작해 connection pool의 사용배경을 거쳐 HikariCP까지 전부 흐름으로 이해하기 좋은 맥락이라고 생각해 한번에 포스팅 하였습니다.

 

 

 

 

 

출처

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

 

스프링 DB 1편 - 데이터 접근 핵심 원리 | 김영한 - 인프런

김영한 | 백엔드 개발에 필요한 DB 데이터 접근 기술을 기초부터 이해하고, 완성할 수 있습니다. 스프링 DB 접근 기술의 원리와 구조를 이해하고, 더 깊이있는 백엔드 개발자로 성장할 수 있습니

www.inflearn.com

https://techblog.woowahan.com/2664/

 

HikariCP Dead lock에서 벗어나기 (이론편) | 우아한형제들 기술블로그

{{item.name}} 안녕하세요! 공통시스템개발팀에서 메세지 플랫폼 개발을 하고 있는 이재훈입니다. 메세지 플랫폼 운영 장애를 바탕으로 HikariCP에서 Dead lock이 발생할 수 있는 case와 Dead lock을 회피할

techblog.woowahan.com

https://github.com/brettwooldridge/HikariCP

 

GitHub - brettwooldridge/HikariCP: 光 HikariCP・A solid, high-performance, JDBC connection pool at last.

光 HikariCP・A solid, high-performance, JDBC connection pool at last. - brettwooldridge/HikariCP

github.com