기존의 DBManger 클래스를 통한 DB 커넥션은 sql요청마다 DB와 TCP/IP통신으로 커넥션을 획득해야하는 과정이 있어 SQL 실행 시간에 매 요청시 마다 커넥션을 생성하는 시간이 추가되어 응답 속도에 영향을 미치는 문제가 있습니다. 대표적인 해결법은 Database Connection Poll을 사용하는 방법이 존재합니다.
DBCP에 대한 설명으로는 아래 링크를 참고하시면 됩니다.
JDBC와 DBCP
DBCPUtils
public class DBCPUtils {
private static HikariConfig config = new HikariConfig();
private static HikariDataSource hikariDs;
static {
config.setDriverClassName(DRIVER);
config.setJdbcUrl(URL);
config.setUsername(USER);
config.setPassword(PASSWORD);
config.setPoolName("MY-Pool");
config.setMinimumIdle(4);
config.setMaximumPoolSize(10);
config.setConnectionTimeout(3000);
System.out.println("hikari 커넥션 풀 생성");
hikariDs = new HikariDataSource(config);
}
private DBCPUtils() {
}
// select를 수행한후 리소스 해제
public static void close(Connection conn, Statement stmt, ResultSet rs) {
try {
if (rs != null) {
rs.close();
}
if (stmt != null) {
stmt.close();
}
if (conn != null) {
System.out.println("커넥션 반환");
conn.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
// DML(insert, update, delete) 수행후 리소스 해제
public static void close(Connection conn, Statement stmt) {
try {
if (stmt != null) {
stmt.close();
}
if (conn != null) {
System.out.println("커넥션 반환");
conn.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
public static Connection getConnection() throws SQLException {
System.out.println("hikariCP conn = " + hikariDs.getConnection().getClass());
System.out.println("Total Connections: " + hikariDs.getHikariPoolMXBean().getTotalConnections()); // 총 커넥션
System.out.println("Active Connections: " + hikariDs.getHikariPoolMXBean().getActiveConnections()); // 활동 커넥션
System.out.println("Idle Connections: " + hikariDs.getHikariPoolMXBean().getIdleConnections()); // 유휴 커넥션
return hikariDs.getConnection();
}
}
DBCP를 구현한 클래스입니다. 커넥션 풀에는 대표적으로 commons-jdbc2, tomcat-jdbc pool, HikariCP이 존재합니다.
해당 프로젝트는 스프링에 대해 더 이해하기 위함이 바탕으로 되어 있기에 스프링부트 사용시 기본적으로 제공되는 HikariCP를 사용하기로 했습니다. 이 외에도 HikariCP가 위에서 말한 3개의 커넥션 풀에서 가장 성능이 좋기때문입니다.
커넥션 풀을 설정하고 기존의 DAO 클래스의 기능을 반복적으로 테스트시 sql 실행이 끝나고 close 메서드가 호출되어도 유휴 커넥션이 생기지 않는 문제가 발생되었으며, 결국 아래와 같은 예외가 발생했습니다.
예외를 읽어보면 사용가능한 커넥션이 없어, sql을 실행하지 못해 커넥션을 기다리다가 결국 타임 아웃이 발생했다는것을 알게되었습니다.
일단 확실한것은 커넥션 반환 메서드가 호출은 되지만 반환이 제대로 이뤄지지 않는다는 부분이였습니다.이후 전혀 갈피를 잡지 못해 멘토님께서 싱글톤 패턴을 확인했는지 말씀해주셨을때 한가지의 상황이 생각났습니다.
싱글톤 패턴을 간단히 설명하면 단 하나의 인스턴스만 생성해 사용하며 자원을 효율적으로 사용하고자할 때 적용하며, private 제어자로 생성자를 작성하며 public static 메서드로 인스턴스에 접근하는 방법을 사용합니다.
HikariCP 코드 작성시 생성자는 private으로 했으나 DBCPUtils 클래스 자체의 인스턴스를 생성하는 메서드가 없으며 해당 클래스에 접근하는 메서드 또한 없이 정적 메서드로만 BoardDAO에서 getConnection()을 호출하는 상황이였습니다.
위 상황을 알고나서의 해결방법은 간단했습니다.
public class DBCPUtils {
private static final HikariConfig config = new HikariConfig();
private static final HikariDataSource hikariDs;
private static final DBCPUtils instance = new DBCPUtils();
private DBCPUtils() {
}
public static DBCPUtils getConnectionPool() {
return instance;
}
--- 아래 코드는 생략 -----
public class BoardDAO {
private static final BoardDAO instance = new BoardDAO(); // 싱글톤
private final DBCPUtils pool;
private BoardDAO() {
this.pool = getConnectionPool();
}
public static BoardDAO getInstance() {
return instance;
}
// 전체 게시글 조회
public List<BoardVO> selectAllBoards() {
String sql = "select * from board order by num desc";
List<BoardVO> list = new ArrayList<BoardVO>();
Connection conn = null;
Statement stmt = null;
ResultSet rs = null;
try {
conn = pool.getConnection();
System.out.println("전체 게시글 조회 커넥션: " + conn);
stmt = conn.createStatement();
rs = stmt.executeQuery(sql);
--- 생략 ---
} catch (Exception e) {
e.printStackTrace();
} finally {
System.out.println("전체 게시글 반환 시작");
if (conn != null) {
System.out.println("전체 게시글 반환 진행 " + conn);
}
pool.close(conn, stmt, rs);
}
return list;
}
우아한 기술 블로그 HikariCP Dead lock에서 벗어나기 (이론편)
Baeldung Introduction to HikariCP