DB를 사용하면서 발생 가능한 문제점들
JDBC 관련 API는 클래스가 아닌 인터페이스. JDK의 API에 있는 java.sql 인터페이스를 각 DB 벤더에서 상황에 맞게 구현했다. (벤더에 따라 처리능력 다를 수 있다)
기본적으로 DB와 연결이 될 땐 Connection 객체를 만들고 쿼리를 수행하여 데이터를 받은 뒤 다 사용한 후 Connection 객체를 닫는다. 세션 공부할 때와 비슷.
이 중에서 가장 시간을 많이 소모하는 부분은 Connetion 객체를 얻는 부분이다. DB와 WAS 사이에 통신이 필요하기 때문
이 때 Connection 객체를 생성할 때 대기시간을 줄이고 네트워크의 부담을 줄이기 위해 사용하는 것이 DB Connection pool이다.
DataSource JDK 1.4부터생긴 표준: Connection Pool로 연결을 관리하며 트랜재션 관리도 가능하게 만들어야 한다. DataSource가 Connection pool을 포함한다 봐도 무관하다. pool은 표준으로 지정된게 없지만 DataSource는 자바 표준이라 사용법이 동일하다.
(Spring JPA 쓸 때 보이던 HikariPool이 이런건가 공부해봐야지)
Statement 인터페이스
Connection이 생성되면 Statement 객체를 받는다. 이는 인터페이스며 자식 클래스로 PreparedStatement가 있다.
PL/SQL을 처리하기 위해서 사용하는 PreparedStatement의 자식 클래스로 CallableStatement가 있다.
Statement와 PreparedStatement의 차이는 캐시 사용 여부이다.
Statement를 사용하면 매번 쿼리를 수행할 때
1) 쿼리 문장 분석
2) 컴파일
3) 실행
세 단계를 거친다.
PreparedStatement는 처음 한 번만 세 단계를 거친 후 캐시에 담아 재사용을 한다.동일한 쿼리를 반복적으로 수행한다면 PreparedStatement가 DB에 훨씬 적은 부하를 주며, 성능도 좋다. 또 쿼리에서 변수를 ""로 묶어 처리하지 않고 ?로 처리하기에 가독성도 좋다.
쿼리 수행
Statement를 통해 만든 쿼리를 수행한다. 쿼리 수행 메서드에는 여러 가지가 있는데, 그 중 많이 사용하는 것은
executeQuery(), executeUpdate(), execute() 메서드이다.
- executeQuery() 메서드는 select 관련 쿼리를 수행한다. 수행 결과로 요청한 데이터 값이 ResultSet 객체의 형태로 전달된다.
- executeUpdate() 메서드는 select 관련 쿼리를 제외한 DML(INSERT, UPDATE, DELETE 등) 및 DDL(CREATE TABLE, CREATE VIEW 등) 쿼리를 수행한다. 결과는 int 형태로 리턴된다.
- execute() 메서드는 쿼리의 종류와 상관없이 쿼리를 수행한다. 결과는 boolean 형태의 데이터로 리턴한다. 데이터가 있다면 true로 리턴하여 getResultSet() 메서드를 사용하여 값을 받을 수 있다. 데이터가 없다면 false를 리턴하고 변경된 행의 개수를 확인하기 위해선 getUpdateCount() 메서드를 사용한다.
쿼리를 수행한 결과는 ResultSet 인터페이스에 담긴다. 여러 건의 데이터가 넘어오기 때문에 next() 메서드를 사용해 데이터의 커서를 다음으로 옮기면서 처리할 수 있도록 되어있다. ... 이거 관련해선 JDBC, JPA 같은 부분을 공부해보는게 좋을 것 같다.
DB를 사용할 때 닫아야 하는 것들
Connection, Statement 관련 인터페이스, ResultSet 인터페이스를 close() 메서드를 사용해 닫아야 한다고 했다.
호출한 순서의 반대로 close()가 이루어진다.
먼저 ResultSet이 닫히는 경우는 다음과 같다.
- close() 메서드를 호출하는 경우
- GC의 대상이 되어 GC되는 경우
- 관련된 Statement 객체의 close() 메서드가 호출되는 경우
GC가 되면 자동으로 닫히고, Statement 객체가 닫혀도 알아서 닫히는데 굳이 close를 해야하나.
해야한다. 자동으로 호출되기 전에 관련 DB와 JDBC 리소스를 해제하기 위함.
0.000001초라도 빨리 닫으면, 그만큼 DB 서버의 부담이 적어지기 때문
Statement 객체는 다음 두 상항에 close한다
- close() 메서드를 호출하는 경우
- GC의 대상이 되어 GC가 되는 경우
Connection 인터페이스의 경우는 다음과 같다.
- close() 메서드를 호출하는 경우
- GC의 대상이 되어 GC되는 경우
- 치명적인 에러가 발생하는 경우
Connection의 대부분은 Pool을 사용하여 관리한다. 시스템 기동 시 지정 수만큼 연결하고, 필요에 따라 증가시키도록 되어있다. 사용자가 증가해 더 이상 사용할 수 있는 연결이 없으면, 여유가 생길 때까지 대기한다. 여기서 시간이 지나면 오류가 발생한다. 그러므로 close() 메서드를 호출하여 연결을 닫아야 한다. GC가 될 때까지 기다리면 Pool이 부족해지는 것은 분명하다.
close할 땐 try- catch- final의 final에 추가하는 것이 좋다.
가장 좋은 방법은 DB 관련 처리 담당 클래스를 만든느 것이다. 보통 DBManager라는 이름의 클래스를 만들어 사용. Connection 객체도 JNDI를 찾아(Lookup) 사용하는 DataSource를 이용하여 얻는다. 여기에 ServiceLocator 패턴까지 적용하면 , DB 연결 시의 시간을 최소한으로 단축 할 수 있다.
JDK 7의 AutoClosable 인터페이스
AutoClosable 인터페이스에는 리턴 타입이 void인 close() 메서드 단 한개만 선언되어 있다. close() 메서드의 설명은 다음과 같다.
- try-with-resources 문장으로 관리되는 객체에 대해서 자동으로 close() 처리한다.
- InterruptedException을 던지지 않도록 하는 것을 권장한다.
- 이 close() 메서드를 두 번 이상 호출할 경우 뭔가 눈에 보이는 부작용이 나타나도록 해야한다.
여기서 가장 중요한건 try-with-resources이다.
try 블록이 시작될 때 소괄호 안에 close() 메서드를 호출하는 객체를 생성해주면 처리할 수 있다.
try(BufferedReader br = new BufferedReader(reader)) {}
즉 별도로 finally 블록 close() 메서드를 호출할 필요가 없다. 호출 대상이 여러 개라면 세미콜론으로 구분하여 추가.
만약 close() 메서드를 호출해야하는 대상이 AutoCloseable 인터페이스를 구현한 것인지 잘 확인해보자.
ResultSet.last() 메서드
ResultSet의 인터페이스 스펙에는 단순히 데이터가 아닌 데이터의 커서를 가지고 있는다.
ResultSet 객체가 가진 결과의 커서를 맨끝으로 옮기게 하는 메서드이다. 이 메서드를 수행하는 이유는 전체 데이터의 개수를 확인하고 배열에 담아서 사용하기 위해서다. 배열을 Vector로 변경하고사용하면 되기 때문. 하지만 게시판 같은 화면을 구성할 때 전체 건수를 확인하기 위해 이렇게 사용하는 경우도 있다. 차라리 쿼리를 하나 던져서 해결하자.
last()에 문제가 있을가? 이의 수행시간은 데이터의 건수, DB와의 통신 속도에 따라 달라진다. 건수가 많아질 수록 대기시간이 증가한다.
next()를 수행할 때와는 비교도 안될 정도의 속도차이가 난다. 그러니 이 메서드의 사용은 자제하자. DBMS 중엔 이 성능을 개선한 모델도 있다.
## JDBC 유의 팁
setAutoCommit() 메서드는 필요할 때만 사용하자.
setAutoCommit() 메서드를 사용하여 자동 커밋 여부를 지정하는 작업은 반드시 필요할 때만 하자. 단순한 select 작업만을 수행할 때에도 커밋 여부를 지정하여 사용하는 경우가 많은데, 여러 개의 쿼리를 동시에 작업할 때 성능에 영향을 줄 수 있다.배치성 작업은 executeBatch() 메서드를 사용하자.
배치성 작업을 할 때는 Statement 인터페이스에 정의되어 있는 addBatch() 메서드를 사용하여 쿼리를 지정하고, executeBatch() 메서드를 사용하여 쿼리를 수행하자. 여러 개의 쿼리를 한 번에 수행할 수 있기 때문에 JDBC 호출 횟수가 감소되어 성능이 좋아진다.setFetchSize() 메서드를 사용하여 데이터를 더 빠르게 가져오자.
한 번에 가져오는 열의 개수는 JDDBC의 종류에 따라 다를 것이다. 하지만 가져오는 데이터의 수가 정해져 있을 경우 Statement와 ResultSet 인터페이스에 있는 setFetchSize() 메서드를 사용하여 원하는 개수를 정의하자 하지만 너무 많은 건수를 지정하면 서버에 부하가 올 수 있으니, 적절하게 사용한 건만 필요할 때는 한 건만 가져오자
실제 쿼리에서는 100건 정도를 갖고 오는데, ResultSet.next()를 while 블록을 사용해서 수행하지 않고, 단 한 번만 메서드를 수행해 결과를 처리하는 경우가 있다. 이 경우에 단 한 건만을 가져오도록 쿼리를 수정해야한다.
(이게 무슨 말이야!!! JDBC 공부가 부족하구나!!! 공부해야겠다!!!)
작가의 정리글
DB에서 일어나는 성능 문제가 상당히 크다! 잘 공부해야해~~