-
JDBC 살펴보기Backend/학습내용 정리 2024. 9. 12. 15:30
매번 단순하게 Spring properties 들을 이용해 데이터베이스 연결 설정을 하다 실제 어떻게 작동하는지 확인해보고자 간단하게 DriverManager 와 DataSource 에 대해 학습했습니다. 이번 글에서는 간단하게 DriverManager, PrepareStatement, DataSource 데 관해 정리하겠습니다.
글 목차
2. Statement vs PreparedStatement
DriverManager
Static Method 만 제공한다. 단순하게 Connection 을 얻을 수 있는데 DriverManager.getConnection() 메서드를 이용해 Connection 인터페이스 객체를 얻을 수 있습니다.
각 DB 밴더사에서는 java.sql.Connection 인터페이스와 java.sql.Driver 인터페이스를 구현한 구현체를 제공하며 Driver 를 통해 Connection 즉 데이터베이스와 연결이 가능하며 Connection 인터페이스를 통해 데이터베이스와 실제 쿼리를 주고 받을 수 있습니다.public static void main(String[] args) { DriverManager.drivers().forEach(driver -> System.out.println("driver = " + driver.getClass())); try (Connection mysqlConnection = DriverManager.getConnection("jdbc:mysql://localhost/jdbc?user=root&password=password"); Connection postgresConnection = DriverManager.getConnection("jdbc:postgresql://localhost/jdbc?user=root&password=password"); Statement mysqlConnectionStatement = mysqlConnection.createStatement(); Statement postgresConnectionStatement = postgresConnection.createStatement() ) { System.out.println("mysql connection = " + mysqlConnection.getClass().getName()); System.out.println("postgresql connection = " + postgresConnection.getClass().getName()); mysqlConnection.setAutoCommit(false); mysqlConnectionStatement.execute("insert into connection(name) values ('test1')"); mysqlConnection.commit(); postgresConnection.setAutoCommit(false); postgresConnectionStatement.execute("insert into connection(name) values ('test1')"); postgresConnection.rollback(); } catch (SQLException e) { throw new RuntimeException(e); } }
참고로 각 Driver 구현체들은 static 생성자로 인해 어플리케이션 구동 시점에 모두 DriverManager 에 등록됩니다.
DriverManager.getConnection() 메서드를 따라가다 보면 아래 반복문을 통해 알맞는 Driver 를 가져와 해당 Driver 를 통해 데이터베이스와 연결하여 Connection 을 반환합니다.for (DriverInfo aDriver : registeredDrivers) { // If the caller does not have permission to load the driver then // skip it. if (isDriverAllowed(aDriver.driver, callerCL)) { try { println(" trying " + aDriver.driver.getClass().getName()); Connection con = aDriver.driver.connect(url, info); if (con != null) { // Success! println("getConnection returning " + aDriver.driver.getClass().getName()); return (con); } } catch (SQLException ex) { if (reason == null) { reason = ex; } } } else { println(" skipping: " + aDriver.driver.getClass().getName()); } }
DriverManager 는 간단한게 Connection 을 얻을 수 있는 util 클래스라고 생각할 수 있습니다. 따라서 ConnectionPool 과 같은 기능은 제공하지 않습니다.
Statement vs PreparedStatement
Statement 와 PreparedStatement 의 가장 큰 차이점이라면 파라미터를 ? 기호로 받고 이후에 실제 파라미터를 setXxx 메서드를 통해 바인딩할 수 있다는 점입니다. 이를 통해 SQL Injection 을 방지할 수 있게 됩니다.
추가로 PreparedStatement 는 Application Layer 에서 Caching 을 진행합니다. 즉, Statement 보다는 PreparedStatement 를 사용하는것이 보안과 성능에 좋습니다.
MySQL 기준 PreparedStatement 의 구현체는 크게 2개가 있다 ClientPreparedStatement 와 ServerPreparedStatement 가 존재하는데 ClientPreparedStatement 를 사용하는 경우 Application Layer 에서 따로 Caching 을 진행하지 않습니다.
아래 블로그를 확인해보면 설정에 대해 더 자세히 알 수 있습니다.
https://2ssue.github.io/programming/HikariCP-MySQL/#cacheprepstmts-cachepreparedstatements
https://github.com/brettwooldridge/HikariCP/wiki/MySQL-Configuration
간단하게 요약하면 MySQL 4.1 버전 이하에서는 PreparedStatement 를 완전히 지원하지 않고 이후 버전에서 제대로 지원을 하기 시작했습니다. 이때 이전 버전에 대한 호환성 때문에 ClientPreparedStatement 를 기본 설정으로 진행하고 추가적인 설정을 적용해야 ServerPreparedStatement 를 사용하여 실제 캐시를 통한 성능 향상을 기대할 수 있습니다.
아래는 테스트한 코드입니다.public static void main(String[] args) { PreparedStatement test1 = null; PreparedStatement test2 = null; try (Connection mysqlConnection = DriverManager.getConnection("jdbc:mysql://localhost/jdbc?user=root&password=password&useServerPrepStmts=true&cachePrepStmts=true") ) { String sql = "select * from connection where id = ?"; for (int i = 0; i < 2; i++) { PreparedStatement preparedStatement = mysqlConnection.prepareStatement(sql); preparedStatement.setInt(1, i); if (i == 0) { test1 = preparedStatement; } else { test2 = preparedStatement; } ResultSet resultSet = preparedStatement.executeQuery(); System.out.println("resultSet = " + resultSet); // close 메서드를 호출해야 cache 를 진행하는것 같음 preparedStatement.close(); } System.out.println(test1 == test2); } catch (SQLException e) { throw new RuntimeException(e); } }
위 코드에서 driver url 을 작성할 때 useServerPrepStmts, cachePrepStmts 설정을 추가한 것을 확인할 수 있습니다.
참고로 ServerPreparedStatement 클래스의 close() 메서드를 호출해야 실제 cache 를 진행하는 것 같습니다.
HikariCP에서 제공하는 MySQL 성능 관련 추가 설정은 아래와 같습니다.jdbcUrl=jdbc:mysql://localhost:3306/simpsons
username=test
password=test
dataSource.cachePrepStmts=true
dataSource.prepStmtCacheSize=250
dataSource.prepStmtCacheSqlLimit=2048
dataSource.useServerPrepStmts=true
dataSource.useLocalSessionState=true
dataSource.rewriteBatchedStatements=true
dataSource.cacheResultSetMetadata=true
dataSource.cacheServerConfiguration=true
dataSource.elideSetAutoCommits=true
dataSource.maintainTimeStats=false
DataSource위에서 살펴본 DriverManager 의 경우는 java.sql 패키지에 존재하는 클래스입니다. 해당 클래스를 이용해 직접 Connection 을 생성할 수 있습니다. 하지만 이는 인터페이스가 아닌 실제 클래스이기 때문에 DIP, OCP도 만족하지 않는 그저 유틸 클래스입니다. 따라서 커넥션을 얻어오는 방법이 변경된다면 클라이언트 코드 수정이 불가피할 것입니다.
이를 방지하기 위해 DataSource는 커넥션을 얻어오는 방법을 추상화한 인터페이스입니다. 하위에 여러 구현체를 가지기 때문에 여러 구현체를 바꿔가면서 클라이언트 코드를 변경할 필요가 없다는 장점이 있습니다.
MysqlDataSource, PGSimpleDataSource 등의 각 DB 벤더사에 맞는 구현체가 존재합니다. 이때 주의할 부분은 DataSource 를 구현한 구현체가 직접 Connection Pooling 을 지원하는 것은 아닙니다.
따라서 Connection Pooling 을 이용하기 위해서는 HikariCP 혹은 Apache Commons DBCP 등의 외부 커넥션 풀 관련 라이브러리를 사용해야 합니다.public static void main(String[] args) { MysqlDataSource dataSource = new MysqlDataSource(); dataSource.setUrl("jdbc:mysql://localhost/jdbc?user=root&password=password"); try { Connection connection = dataSource.getConnection(); Statement statement = connection.createStatement(); connection.setAutoCommit(false); statement.execute("insert into connection(name) values ('test3')"); connection.commit(); } catch (SQLException e) { throw new RuntimeException(e); } }
아래 블로그를 참고하면 DataSource 와 Connection 인터페이스, 그리고 이를 구현한 구현체들간 관계에 대해 잘 그려주었습니다.
https://velog.io/@hubcreator/JDBC-Connection%EA%B3%BC-DataSource'Backend > 학습내용 정리' 카테고리의 다른 글
Spring @EventListener 사용하기 (2) (0) 2024.07.19 트랜잭션과 JPA 낙관적 락 (0) 2024.06.17 Spring @EventListener 사용하기 (0) 2024.05.29 Server Sent Event 정리 (0) 2024.05.16