DATABASE

DB를 사용하면서 발생 할 수 있는 문제점

예나부기 2021. 8. 26.

웹 이나 윈도우 어플리케이션에 상관없이, 항상 공통적으로 사용되는 부분이 바로 데이터베이스다.
데이터베이스를 사용하겠다는 것은 한마디로 요약하면 "데이터를 저장하겠다"라는 뜻이고,
빈번하게 데이터베이스를 통해 필요한 정보를 갱신하게 되므로, 이에 따른 자원관리는 각별히 신경써야 할 부분이다.
얼마전 구입한 "자바 성능을 결정짓는 코딩 습관과 튜닝이야기(이상민 지음)" 책의 한 챕터를 통해서
데이터베이스 사용과 연관된 문제점들을 살펴 보도록 하겠다.

1. DBConnection과 Connection Pool, DataSource

    1) JDBC관련 API는 클래스가 아니라 인터페이스다.
    2) JDK의 API에 있는 java.sql 인터페이스를 각 DB벤더에서 상황에 맞게 구현하도록 되어있다.

일반적인 방법으로 오라클 DB에 연결하여 사용하는 방법은 다음과 같다.
try{
        Class.forName("oracle.jdbc.driver.OracleDriver");
        Connection con = DriverManager.getConnection("jdbc:oracle:thin:@ServerIP:152:SID","ID","Password");
        PreparedStatement ps = con.prepareStatement("SEELECT .... where id=?");
        ps.setString(1,id);
        ResultSet rs = ps.executeQuery();
        //중간 데이터 처리 부분 생략
} catch(ClassNotFoundException e){
        System.out.println("드라이버 load fail");
        throw e;
} catch(SQLException e){
        System.out.println("Connection fail");
        throw e;
} finally{
    rs.close();
    ps.close();
    con.close();
}

물음 1) 위 소스처럼 JDBC를 사용할때 어느 부분에서 가장 느릴까? 

답: 가장 느린 부분은 Connection 객체를 얻는 부분이다. 왜냐하면 같은 장비에 DB가 구성되어 있다고 하더라도, DB와 WAS 사이에는 통신을 해야하기 때문이다. DB가 다른 장비에 있다면 이 통신 시간은 더 소용된다.


물음 2) DB Connection Pool은 왜 사용해야 하는가?? 

답: Connection 객체를 생성하는 부분에서 발생하는 대기 시간을 줄이고, 네트워크의 부담을 줄이기 위해 사용한다.


물음 3) DataSource와 DB Connection Pool의 차이는 뭘까?

답: DataSourcer는 JDK 1.4 버전부터 생긴 표준이다. Connection Pool로 연결을 관리해야 하고, 트랜잭션 관리도 가능하도록 만들어야 한다.  그러므로 DataSource가 DB Connection Pool을 포함한다고 생각해 두면 된다. 여기서 유의할 점은 DB Connection Pool은 자바 표준으로 지정되어 있는 것이 없다는 점이다. 따라서 WAS 벤더에 따라서 사용법이 많이 상이할 수 있다. 그러나 DataSource는 자바 표준이므로 WAS에 상관없이 사용법이 동일하다.

물음 4) Statement와 PreparedStatement의 차이점은 무엇인가? 성능면에서 무엇이 더 합리적인가?

답:  *참고) CallableStatement는 PL/SQL을 처리하기 때문에 성능상 비교 대상이 없다.
                Statement와 PreparedStatement는 처음 사용할 때에는 다음과 같은 동일한 프로세스를 거친다.
                 1) 쿼리 문장 분석
                 2) 컴파일
                 3) 실행
중요) Statement를 사용하면 매번 쿼리를 수행할 때마다 위의 1~3단게를 거치게 되고, PreparedStatement는 처음 한 번만 이 단계를 거친 후 캐시에 담아서 재사용 한다는 것이다. 만약 동일한 쿼리를 반복적으로 수행한다면, PreparedStatement가 DB에 훨씬 적은 부하를 주며, 성능도 좋다. 또한 쿼리에서의 변수를 ''로 묶어서 처리하지 않고 ? 처리하기 때문에 가독성도 좋아진다.

물음 4) Statement관련 인터페이스를 사용하여 만든 쿼리를 수행할때 여러가지 메소드가 있는데, 그것들간의 차이는 무엇인가?

답: executeQuery(), executeUpdate(), execute() 메소드가 있으며 각각의 사용법 및 특성 및 차이는 다음과 같다.

executeQuery()메소드는 select 관련 쿼리를 수행한다. 수행 결과로 요청한 데이터 값이 ResultSet 객체의 형태로 전달된다. executeUpdate()메소드는 select 관련 쿼리를 제외한 DML(INSERT,UPDATE,DELETE 등 ) 및 DDL(CREATE TABLE,CREATE VIEW)등 쿼리를 수행한다. 결과는 int 형태로 리턴된다. execute() 메소드는 쿼리의 종류와 상관없이 쿼리를 수행한다. eecute() 메소드의 수행결과는 ResultSet이 아닌 boolean 형태의 데이터를 리턴하는데, 만약 데이터가 있을 경우에는 true를 리턴하여 getResultSet() 메소드를 사용하여 결과값을 받을 수 있다. 그렇지 않은 경우에는 false를 리턴하므로 변경된 행의 개수를 확인하기 위해서는 getUpdateCount() 메소드를 사용하여 값을 확인하면 된다.

여기서 한가지 퀴즈. ResultSet 인터페이스를 통해서 1,000건이 되는 데이터를 다음과 같이 받았다.
-------------------------------------------------------------------------
//상단 내용 생략
PreparedStatement ps = con.prepareStatement("SELECT ... where id=?") ; 
ps.setString(1,id);
ResultSet rs = ps.executeQuery();
//하단 내용 생략
그러면 이 ResultSet의 객체 rs에는 몇 건의 데이터가 들어가 있을까?
---------------------------------------------------------------------------
(1) 1건
(2) 10건
(3) 100건
(4) 1,000건
(5) DB마다 다르므로 알 수 없다.
(6) 한 건도 없다.

답은.. 다음 절 끝에서 밝히겠다.

물음 5) JDBC객체를 쓸때 닫아야 하는 것들
일반적으로 JDBC 객체를 얻는 순서는 Connection -> Statement -> ResultSet 순이며, 객체를 닫는 순서는 ResultSet -> Statement -> Connection 순이다. 즉, 먼저 얻은 객체를 가장 마지막에 닫는다.
가장 이상적인 방법은 다음과 같다.(책의 내용을 일부 수정하였고 메소드 단위의 throws 구문이 있다고 가정)

---------------------------------------------------------------------
Connection con = null;
PreparedStatement ps = null;
ResultSet rs = null;
try{
    //상단 부분 생략
    con =..;
    ps =..;
    rs =..;
    //중간 생략
} catch(Exception e){
    ...
} finally {
    if(rs != null){
        rs.close();
    }
    if(ps != null){    
        ps.close();
    }
    if( con != null){
        con.close();
    }

}

퀴즈 정답 : "한 건도 없다" 하지만 "DB마다 다르므로 알 수 없다"라고 할 수도 있다. JDK API의 ResultSet 클래스에 대한 설명 중 두 번째 줄에는 다음과 같이 쓰여 있다.

A ResultSet object maintains a cursor pointing to its current row of data.


번역하면 "ResultSet 객체는 현재 열의 데이터를 가리키는 커서를 관리한다"는 말이다. 즉 데이터는 갖고 있지 않고 커서만을 관리하는 객체라는 뜻이다. 하지만 API에 있는 설명은 인터페이스에 대한 설명이므로, 그 인터페이스를 구현한 jdbc를 제공하는 벤더에 따라서 다를 수 있다는 의미가 된다. 일반적으로 10건에서 100건 정도를 미리 갖고 온다. 만약 모든 데이터를 한꺼번에 ResultSet에 담는다고 하자. 10만건의 데이터를 가져오는 쿼리를 수행하면 해당 WAS는 OutOfMemoryError를 발생시키면서 다운 될 것이다.

끝으로 JDBC를 사용하면서 유의할 만한 몇 가지 팁을 이야기 하겠다.

1. setAutoCommit() 메소드는 필요할 때만 사용하자: setAutoCommit() 메소드를 사용하여 자동 커밋 여부를 지정하는 작업은 반드시 필요할 때만 하자. 단순한 select 작업만을 수행할 때에도 커밋 여부를 지정하여 사용하는 경우가 많은데, 여러 개의 쿼리를 동시에 작업할 때 성능에 영향을 주게 되므로 되도록 자제하다.

2. 배치성 작업은 executeBatch() 메소드를 사용하자: 배치성 작업을 할 때에는 Statement 인터페이스에 정의되어 있는 addBatch()메소드를 사용하여 쿼리를 지정하고, executeBatch() 메소드를 사용하여 쿼리를 수행하자. 여러 개의 쿼리를 한번에 수행 할 수 있기 때문에 JDBC 호출 횟수가 감소되어 성능이 좋아진다.

3. setFetchSize() 메소드를 사용하여 데이터를 더 빠르게 가져오자: 한번에 가져오는 열의 개수는 JDBC의 종류에 따라서 다를 것이다. 하지만 가져오는 데이터의 수가 정해져 있을 경우에는 Statement와 ResultSet 인터페이스에 있는 setFetchSize()메소드를 사용하여 원하는 개수를 정의하자. 하지만 너무 많은 건수를 지정하면 서버에 많은 부하가 올 수 있으니, 적절하게 사용해야 한다.

 

 

출처 : http://hongsgo.egloos.com/v/2062326

댓글