[WEB] JSP와 서블릿을 이용한 게시판-2-

개나뇽·2024년 8월 11일
0

목적

기존의 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개의 커넥션 풀에서 가장 성능이 좋기때문입니다.

  • 바이트 코드단위의 최적화
    - 바이트코드 레벨까지의 최적화가 이루어져 있습니다.
  • 효과적인 Collection Framework 사용
    - ArrayList 가 범위 검사를 제거하고 헤드에서 테일까지 제거 스캔을 수행하는 사용자 정의 클래스인 FastList 로 대체되었습니다.

트러블 슈팅

상황


커넥션 풀을 설정하고 기존의 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;
    }  
  1. DBCPUtils에 인스턴스를 생성하는 부분을 작성하며 getConnectionPool()을 통해 인스턴스에 접근합니다.
  2. BoardDAO에서 인스턴스를 저장할 변수를 선언하며 해당 변수에 getConnectionPool() 를 사용해 값을 할당합니다.

    코드 수정후 동일하게 테스트한 결과 유휴 커넥션이 문제 없이 생성되며 또한 동일한 커넥션이 사용되는것을 확인했습니다.

ref) 문제 해결을 위해 참고한 글입니다.

우아한 기술 블로그 HikariCP Dead lock에서 벗어나기 (이론편)
Baeldung Introduction to HikariCP

profile
정신차려 이 각박한 세상속에서!!!

0개의 댓글