DB를 사용하면서 발생 가능한 문제점


자바 기반 애플리케이션의 성능을 진단해 보면, 응답 속도를 지연시키는 대부분의 요인은 DB 쿼리 수행 시간과 결과를 처리하는 시간이다.

이 책의 저자는 애플리케이션에서의 DB 접속 관련 공지가 있었다고 한다.
주된 내용은 다음과 같다.

왜 위와 같은 조치를 취해야 하는지 알아보자.


DB Connection 과 Connection Pool, DataSource

일반적으로 DB에 연결하여 사용하는 방법에서 가장 느린 부분은 Connection 객체를 얻는 부분이다.
DB와 WAS 사이에는 통신을 해야 하기 때문이다.
사용자가 갑자기 증가하면 Connection 객체를 얻기 위한 시간이 엄청나게 소요될 것이며, 많은 화면이 예외를 발생시킬 것이다.

이러한 부담을 줄이기 위해 사용하는 것이 DB Connection Pool이다.

가능하면 안정되고 검증된 WAS에서 제공하는 DB Connection Pool이나 DataSource를 사용하자.

Statement와 거의 동일하게 사용할 수 있는 Statement 인터페이스의 자식 클래스로 PreparedStatement가 있다.
그리고 PL/SQL을 처리하기 위해서 사용하는 PreparedStatement의 자식 클래스로 CallableStatement가 있다.


Statement VS. PreparedStatement
이 둘의 가장 큰 차이점은 캐시(cache) 사용 여부이다.
이 둘을 사용할 때는 다음과 같은 프로세스를 거친다.

  1. 쿼리 문장 분석
  2. 컴파일
  3. 실행

Statement를 사용하면 매번 쿼리를 수행할 때마다 1~3 단계를 거치고
PreparedStatement는 처음 한 번만 세 단계를 거친 후 캐시에 담아서 재사용 한다는 것이다.

만약 같은 쿼리를 반복적으로 수행한다면 PreparedStatement가 DB에 훨씬 적은 부하를 주며, 성능도 좋다.




DB를 사용할 때 닫아야 하는 것들

일반적으로 각 객체를 얻는 순서는 Connection, Statement, ResultSet 순이며,
객체를 닫는 순서는 ResultSet, Statement, Connection 순이다.

먼저 ResultSet 객체가 닫히는 경우는 다음과 같다.


그렇다면 왜 굳이 close() 를 해야 하는가?

Connection, Statement 관련 인터페이스, ResultSet 인터페이스에서 close() 메서드를 호출하는 이유는
자동으로 호출되기 전에 관련된 DB와 JDBC 리소스를 해제하기 위함이다.

Statement 객체는 Connection 객체를 close() 한다고 자동으로 닫히지 않는다.
다음 두 경우에만 닫히므로, 반드시 close() 메서드를 호출해야 한다.

가장 문제가 되는 Connection 인터페이스의 객체에 대해 알아보자.
다음 세 가지 경우에 닫힌다.

더 이상 사용할 수 있는 연결이 없으면, 여유가 생길 때까지 대기한다.
그러다가 어느 정도 시간이 지나면 오류가 발생한다.

GC가 될 때까지 기다리면 Connection Pool이 부족해지는 것은 시간 문제다.


아래와 같은 방법으로 이를 해결할 수 있다.

try {

    . . .

} catch(Exception e) {

    . . .

} finally {
    try{rs.close();}catch(Exception rse){}
    try{ps.close();}catch(Exception pse){}
    try{con.close();}catch(Exception cone){}
}

위 예는 throws 예외 구문이 있다는 가정 하에 작성되었다.

무엇보다도 가장 좋은 방법은 DB와 관련된 처리를 담당하는 관리 클래스를 만드는 것이다.
보통 DBManager 이라는 이름의 클래스를 많이 사용한다.




JDK 7 에서 등장한 AutoClosable 인터페이스

JDK 7부터 등장한 java.lang 패키지에 AutoClosable이라는 인터페이스가 있다.

AutoClosable 인터페이스에는 리턴 타입이 void인 close() 메서드 단 한 개만 선언되어 있다.
close() 메서드의 설명은 다음과 같다.


가장 중요한 것은 try-with-resources 이다.
try 블록이 시작될 때 소괄호 안에 close() 메서드를 호출하는 객체를 생성해 주면
별도로 finally 블록에서 close() 메서드를 호출할 필요가 없어졌다는 의미다.




ResultSet.last() 메서드

이 메서드는 “ResultSet 객체가 갖고 있는 결과의 커서(Corsor)를 맨 끝으로 옮겨라” 라는 지시를 하는 메서드이다.
rs.next()가 다음 커서로 옮기는 것과 비교하면 이해하기가 쉬울 것이다.

이 메서드를 수행하는 이유가 뭘까?
대부분의 이유는 다음과 같이 사용하기 위해서이다.

rs.last();  
int totalCount = rs.getRow();  
ResultArray[] result = new ResultArray[totalCount];  

전체 데이터 개수를 확인하고 배열에 담아서 사용하기 위해서라면 양호한 편..
배열을 Vector로 변경하고 사용하면 되기 때문이다.
하지만 게시판과 같은 화면을 구성할 때 전체 수를 확인하기 위해서라면
select count(*) from ... 과 같은 쿼리를 한 번 더 던져서 확인하는 것이 훨씬 빠르다.

그럼 rs.last() 에는 문제가 있을까?
rs.last() 메서드의 수행 시간은 데이터의 건수 및 DB와의 통신 속도에 따라서 달라진다.
건수가 많으면 많을 수록 대기 시간이 증가하기 때문에 rs.next()를 수행할 때와 비교할 수 없을 정도로 속도 차이가 나기 때문에,
이 메서드의 사용은 자제해야 한다.