3. Connection Pool에서 Connection을 꺼내는 코드를 작성할 수 있다.
이 글에서는 Connection Pool을 사용해야하는 이유와 Connection Pool에 대해서 배울 것
이전 글에서 만든 회원 가입 서비스를 면밀히 분석해보자
회원 가입 서비스의 동작 순서를 시각화한 그림을 보여줄텐데 모든 상황을 시각화 하면 그림이 복잡해지므로 회원 가입에 성공하는 상황만 시각화를 해보자
시각화한 그림을 흐름도(flow chart) 라고 부름
흐름도에서 회원가입 컨트롤러부터 코드와 비교해서 보자
( 클릭해서 확대해서 보세요 )
파라미터 검증 부분은 흐름도에서 생략했음
클라이언트에서 보낸 회원 정보를 임시로 저장하기 위해 DTO 를 생성하고 생성과 동시에 정보를 임시로 저장하고 있음
회원 정보를 DB에 저장 하기 위해 서비스 객체를 생성하고 회원 가입 메서드인 join 메서드를 호출하고 있음
join 메서드를 호출하면서 저장할 회원 정보를 인자로 전달했음
( 컨트롤러 내 코드를 어디까지 봤는지 잘 기억해두자 )
서비스의 join 메서드를 호출했으니 이제 서비스로 넘어가서 보자
서비스의 회원가입 기능(join 메서드)에서 DB와 통신을 해야하므로 DAO 객체를 생성하고 있음
그 후 서비스에서는 DAO를 사용해 id 중복 여부 확인(1), 연락처 중복 여부 확인(2), 회원가입(3) 처리를 하고 있음
아이디 중복 여부를 확인하기 위해서 selectOneById 메서드를 호출하고 있음
selectOneById 메서드를 호출하면서 사용자가 입력한 아이디를 인자로 전달하고 있음
( 컨트롤러 내 어디까지 봤는지 잘 기억해야하고 서비스의 어디까지 봤는지도 잘 기억하자 )
DAO의 selectOneById 메서드를 호출했으니 이제 DAO로 넘어가서 보자
쿼리를 전송하기 위해 "DB에 접속"한 후 SELECT 쿼리를 준비하고 보낸 후 실행했음
그 다음 DB로부터 결과를 받아와 rs 객체에 저장했음
! 여기서 잠깐 ! 다른 것 보다 "DB에 접속" 을 강조했음
Connection Pool을 왜 사용하는지 알기 위해서는 "쿼리를 보내고 실행한 후 결과를 받아오기 위해 DB에 접속했다" 가 굉장히 중요한 포인트임
굉장히 중요한 포인트라 흐름도에 그렸어야 했지만 그림이 너무 길어져 작게 보여 어쩔 수 없이 생략했음
다시 DAO 로 돌아가서 id 로 조회한 결과가 있다면 if문 안으로 들어가 조회한 데이터를 member 객체(DTO)에 담음
id 로 조회한 결과가 없다면 if문을 건너뛰어 member 객체(DTO)는 그대로 null 상태임
( 이미지는 생략했음 )
selectOneById 메서드에서 조회 결과를 반환하기 전에 "DB와 관련된 자원을 해제" 한 후 조회 결과를 반환함
! 여기서 잠깐 ! "DB에 접속" 을 강조했던 것처럼 여기서도 "DB와 관련된 자원을 해제" 를 강조했음
"DB와 관련된 자원을 해제" 한다는건 rs.close(), pstmt.close(), conn.close() 를 하는 것인데 conn.close() 는? DB 접속을 끊는 것
역시나 Connection Pool 을 왜 사용하는지 알기 위해서는 "쿼리를 보내고 실행한 후 결과를 받아온 다음에 DB 접속을 끊었다" 가 굉장히 중요한 포인트임
굉장히 중요한 포인트라 흐름도에 그렸어야 했지만 그림이 너무 길어져 작게 보여 어쩔 수 없이 생략했음
강조한 부분을 정리해보면 "쿼리를 보내고 실행한 후 결과를 받아오기 위해 [ DB에 접속 ] 했고 결과를 받아온 다음 [ DB 접속을 끊음 ]" 임
DAO가 결과를 반환했으니 다시 서비스의 selectOnById 메서드를 호출한 부분으로 돌아가자
DAO의 selectOneById 메서드가 반환한 정보가 있다면 if 문 안으로 들어가 서비스를 빠져나가 컨트롤러에게 결과(duplicate_id) 를 전달함 / 그러나 회원 가입에 성공한 상황만 본다고 했으므로 selectOneById 메서드가 반환한 정보가 없다고 하자
그 다음 연락처 중복 여부를 확인함
연락처 중복 여부 확인부터는 중요한 포인트만 언급하겠음
연락처 중복 여부 확인은 아이디 중복 여부 확인과 마찬가지로 DAO에서 쿼리를 보내고 실행한 후 결과를 받아오기 위해 [ DB에 접속 ] 했고 결과를 받아온 다음 [ DB 접속을 끊음 ]
서비스의 마지막으로 아이디, 연락처가 중복 되지 않았을 때는 회원가입을 진행함
회원 가입 또한 마찬가지로 DAO에서 쿼리를 보내고 실행한 후 결과를 받아오기 위해 [ DB에 접속 ] 했고 결과를 받아온 다음 [ DB 접속을 끊음 ]
회원 가입에 성공했다면 서비스는 컨트롤러에게 "OK" 를 반환하고 어떤 이유로 인해 회원 가입에 실패했다면 서비스는 컨트롤러에게 "unknown error" 을 반환함
이제 컨트롤러로 돌아가서 컨트롤러는 서비스가 반환한 결과에 따라 상태 코드를 클라이언트에게 전달함
여기까지 다소 길었지만 우리가 앞서 만든 회원 가입 서비스의 흐름을 알아봤음
중간에도 언급했지만 우리가 회원 가입 서비스의 흐름을 알아본 이유는 Connection Pool 을 왜 사용하는지 알기 위함임
지금까지가 서론이었고 이제부터 본론으로 들어가자~!
한 사람이 회원가입 하기 위해 DB에 접속하고 접속을 끊은 횟수는? 3번
1. 아이디 중복 여부를 확인하기 위해
2. 연락처 중복 여부를 확인하기 위해
3. 회원 가입을 하기 위해
열 사람이 회원 가입을 한다면 DB에 접속하고 접속을 끊는 횟수는? 30번
서비스가 대박나 천 사람, 만 사람이 회원 가입을 한다면 DB에 접속하고 접속을 끊는 횟수는? 3,000번 30,000번
이렇게 클라이언트의 요청 한번에 DB와 접속, 접속 끊기 횟수가 굉장히 많기 때문에 DB와 접속, 접속 끊기는 최소한으로 이뤄지는게 바람직함
DB와 접속, 접속 끊기를 최소화 해줄 수 있는 기술이 Connection Pool 임
Connection Pool은 미리 Connection 객체(DB에 접속)를 여러 개 만들어둠
그 후 DAO에서 Connection 객체(DB에 접속)가 필요할 때 Connection Pool에 있는 Connection 객체를 가져옴
DAO에서는 DB에 접속 하는 과정 대신 Connection Pool에서 Connection 객체를 가져오는 과정으로 바뀜
그 이후에는 지금의 DAO와 똑같이 쿼리를 보내고 결과를 가져옴
그리고 마지막으로 DB와 관련된 모든 자원들을 close 함
이때 conn.close() 를 해서 접속을 끊을텐데 Connection Pool 에서 가져온 Connection 객체를 close() 하게 되면 접속을 끊는게 아니라 가져왔던 Connection 객체를 Connection Pool에 반납하는 과정으로 바뀜
이전까지 DAO의 과정과 지금부터 사용할 Connection Pool을 사용한 DAO의 과정을 시각적으로 보자
이전까지 DAO에서는 쿼리 하나를 전송 & 실행 & 결과 받기 를 하기 위해 접속, 끊기를 해야함
이전까지 DAO에서는 쿼리를 여러 번 전송 & 실행 & 결과 받기 를 하기 위해 접속, 끊기를 여러 번 해야함
Connection Pool을 사용한 DAO에서는 [ 쿼리 하나를 전송 & 실행 & 결과 받기 ] 를 하기 위해 접속, 끊기를 하지 않음
Connection Pool을 사용한 DAO에서는 [ 쿼리를 여러 번 전송 & 실행 & 결과 받기 ] 를 하기 위해 접속, 끊기를 하지 않음
접속 대신 Connection Pool에서 Connection 객체 가져오기를 함
끊기 대신 Connection 객체를 Connection Pool에 반납 함
Connection Pool은 톰캣이 제공하는 것이라 위 흐름도에서 Connection Pool에 톰캣의 이미지를 넣어놨음
톰캣에 Connection Pool 관련된 설정을 해두면 서버가 실행될 때 설정에 따라 Connection 객체(DB에 접속)을 생성함
따라서 이제는 서버가 실행될 때 여러 번의 DB 접속이 이뤄짐
그에 따라 서버가 실행될 때는 서버와 DB에 다소 부담이 생길 수 있음
그러나 서버가 완전히 실행되고 나서는 몇 번의 쿼리를 보내든 서버와 DB에 부담이 없음
( 완전히 없지는 않고 Connection Pool을 사용하기 전 보다 상당히 완화 되었음 )
이제 DAO가 Connection Pool 을 사용하기 위해서 톰캣에 Connection Pool 관련된 설정을 하자
톰캣에 Connection Pool 관련된 설정을 하려면 우선 DBCP 라이브러리를 다운 받아야함
DBCP는 DataBase Connection Poo의 약자임
라이브러리를 다운 받기 위해 구글에서 maven repositry 검색 후 첫 번째 검색 결과(1)로 들어가자
DBCP 라이브러리 추가의 마지막으로 다운로드 받은 jar 형식의 라이브러리를 프로젝트 -> src -> main -> webapp -> WEB-INF -> lib 로 드래그 & 드롭하자
여기까지 라이브러리 추가 완료
이제 톰캣에 Connection Pool 관련 설정을 하자
톰캣에 Connection Pool 관련 설정을 할 때는 context.xml(1) 에 해야함
간혹 인터넷에서 보면 프로젝트 -> WEB-INF -> web.xml 에 Connection Pool 관련된 설정을 하는 글들이 있는데 context.xml 대신 web.xml에 Connection Pool 설정을 하기도 하지만 web.xml에 Connection Pool을 설정하면 해당 프로젝트에서만 Coonection Pool 을 사용할 수 있음
- context.xml 에 Connection Pool 설정을 한다 => 서버 단위로 Connection Pool 설정을 하므로 해당 서버 내 모든 웹 프로젝트들이 Connection Pool을 사용할 수 있음
- web.xml 에 Connection Pool 설정을 한다 => 웹 프로젝트 단위로 Connection Pool 설정을 하므로 해당 서버 내 웹 프로젝트 마다 web.xml에 Connection Pool 설정을 해줘야함
다시 Connection Pool 설정으로 돌아와서 context.xml 에 Connection Pool 설정을 하자
context.xml 의 가장 마지막인 </Context> 의 바로 위에 Resource 태그를 추가하자
Resource 태그가 Connection Pool 설정을 하는 태그
name 속성
Connection Pool의 이름
auth 속성
인증 주체 ( 정확한 용도는 잘 모르겠음 )
type 속성
Connection Pool의 데이터 타입
driverClassName 속성
사용하는 DB 마다 정해진 driver 이름 / mariadb가 아닌 DB를 사용하는 분들은 해당 DB에 맞는 driver 이름을 입력하면 됨
url 속성
DB에 접속하기 위한 URL
username 속성
DB에 접속하기 위한 사용자 아이디
password 속성
DB에 접속하기 위한 사용자 비밀번호
maxTotal 속성
동시에 사용할 수 있는 최대 Connection 개수 간단하게 서버가 시작되면 Connection Pool에서는 Connection 객체가 50개 생성된다고 생각하면 됨 톰캣 서버와 DB 서버의 사양을 고려해 적절한 값으로 설정함
취업 준비를 할 때는 마음대로 갯수를 설정해도 되지만 취업을 하고 나서 maxTotal 속성을 건드릴 일이 있다면 상사에게 물어보면 됨
maxIdle 속성
Connection Pool에 반납할 때 최대로 유지될 수 있는 Connection 개수
maxWaitMillis 속성
Connection Pool에서 Connection을 꺼내려고 하는데 모든 Connection이 사용중이여서 꺼낼 Connection이 없을 때 최대로 기다릴 시간
maxTotal 속성을 사용해서 DAO들이 사용할 Connection을 준비했는데 DAO들이 처리할 작업이 복잡해 Connection을 오래 사용하는 상황이라 Connection Pool에 있는 모든 Connection을 빌려갔는데 또 다른 DAO에서 Connection Pool에 있는 Connection을 꺼내려고 하면 Connection Pool에서는 빌려줄 Connection 이 없음
이때 DAO는 maxWaitMillis 만큼 기다리면서 Connection Pool이 Connection을 빌려주기를 기다림
maxWaitMillis 속성의 값은 밀리초 단위로 지정하며 1000 으로 하면 DAO가 Connection을 빌려주기를 1초 동안 기다린다는 것 이 속성의 기본값은 -1 로 -1은 DAO가 Connection을 빌려주기를 무한정 기다린다는 것 ( 빌려줄 때까지 계속 대기하겠다는 것 )
취업 준비를 할 때는 -1 로 설정해도 상관 없지만 취업을 하고 나서 maxWaitMillis 속성을 건드릴 일이 있다면 상사에게 물어보면 됨
Connection Pool 설정과 관련된 속성은 이외에도 더 많으니 Coonection Pool 설정을 더 세세하게 하고 싶다면 직접 찾아보자
package member;
public class MemberService {
public String join(String id, String pw, String nickname, String tel) {
// 아이디 중복 여부 확인
if(아이디가 중복되었다) {
return "duplicate_id";
}
// 연락처 중복 여부 확인
if(연락처가 중복되었다) {
return "duplicate_tel";
}
// 회원가입
}
}
! 여기서 잠깐 ! 회원가입 서비스(join 메서드)가 가입할 회원 정보를 전달 받기 위해 4개의 매개변수를 사용하고 있음이를 더 효율적으로 바꾸자자바에서 정보를 저장할 때는 DTO 또는 VO 라고 부르는 자바 코드를 사용함DTO는 Data Transfer Object, VO는 ValueObject 의 약자로 정보를 담아 전달하기 위한 용도인 클래스를 말함컨트롤러에서 회원 정보를 담아 서비스로 전달하기 위해 memberDto 를 추가하자member 패키지에 memberDto 클래스를 추가하고 아래와 같이 코드를 입력하자
지금까지 우리가 INSERT, UPDATE, DELETE, SELECT 쿼리를 보내고 결과를 받아오는 방법을 봤음
여기서 항상 Connection 을 close 해줬음
Connection을 close 해줘야하는 이유는 앞서 설명했듯이 close 를 하지 않으면 디비는 접속한 자바에서 여전히 실행할 쿼리가 남아있다고 판단해 접속을 유지함
close 를 하지 않은 상태에서 다른 자바(DAO)에서 접속을 하려고 한다면 "이미 다른 자바가 접속해있다" 와 비슷한 예외 메세지가 출력됨
그래서 반드시 close 를 해줘야함
close 를 해야하는 이유를 정확히 얘기한다면 완전히 달라지지만 지금 정확하게 얘기한다면 다른 분야의 얘기로 깊게 들어가야하므로 이렇게만 이해해도 충분함
우리가 Connection 을 close 하면 내부적으로는 다음과 같은 순서로 close가 됨
1. PreparedStatement 가 있을 경우 PreparedStatment 를 close 함
2. Connection 을 close 함
이전 글에서 사용한 select 메서드의 conn.close 메서드의 동작 순서를 통해 시각적으로 close 순서를 보자
우리는 select 메서드의 마지막에 conn.close 메서드를 호출(1) 했지만 close 메서드의 내부에서는 PreparedStatment가 있으니 먼저 PreparedStatment 를 close 함(2)
그 후 Connection을 close 함 (1)
또한 PreparedStatement 를 close 하면 내부적으로는 다음과 같은 순서로 close가 됨
1. ResultSet이 있을 경우 ResultSet을 close 함
2. PreparedStatement 를 close 함
우리는 Connection 을 close했지만(1) Connection의 close 메서드 동작 순서에 의해 PreparedStatment가 close 됨(2)
또한 PreparedStatement가 close 되지만 PreparedStatment의 close 메서드 동작 순서에 의해 먼저 ResultSet이 close 됨(3) 그 후 PreparedStatment가 close 되고(2) 마지막으로 Connection이 close 됨(1)
그림이 다소 복잡해 보이니 간단하게 소스 코드로 보면 아래와 같은 순서로 close 가 됨
내부적으로 위와 같이 동작하는 것처럼 DB를 사용하고 나면 DB와 관련된 모든 자원을 반드시 close(해제) 해줘야 DB와의 접속이 완전히 끊김
자바는 어떤 코드가 동작할 때 예외가 발생할 가능성이 있음
close메서드도 마찬가지로 어떤 이유로 예외가 발생해 close 메서드가 호출되지 못할 수 있음
그래서 DAO 에서는 Connection 객체만 close 하면 안되고 직접 ResultSet, PreparedStatement, Connection 순서로 close메서드를 호출해주는게 권장하는 close 방식임(1)
그.러.나 이 권장하는 방식에도 문제는 있음
rs.close() 메서드 위에 마우스를 올려둬 ResultSet 객체의 close 메서드 설명을 보자
ResultSet 객체의 close 메서드는 SQLException을 발생시킬 가능성이 있음
ResultSet 객체의 close 메서드를 호출했을 때 SQLException이 발생하는 경우는 무수히 많으니 언제, 왜 발생한다고 명확하게 말할 순 없지만 중요한 점은 ResultSet 객체의 close 메서드가 예외를 발생시킬 가능성이 있다는 것
만약 rs.close() 메서드를 호출(1)했는데 예외가 발생했다면?
그 밑에 있는 pstmt.close(), conn.close() 메서드가 호출되지 못하고 catch 로 이동함(2)
그렇다는건? DB와 관련된 자원을 close 하지 못한다는 것
조금 전 위에서 반드시 DB와 관련된 모든 자원은 "반드시" close 를 해줘야한다고 했는데 close가 되지 못하는 상황이 발생함
그!래!서! DB와 관련된 모든 자원을 반드시 close 하기 위해서는 일반적으로 다음과 같이 코드를 작성함
자바에서 배웠듯 finally 는 예외가 발생 여부에 상관 없이 무조건 동작하는 코드이므로 "반드시" 가 됨
finally 에서 각 객체의 null 여부를 확인하는 이유는 각 객체를 선언한 후 null로 설정을 했기 때문임
오타나 어떤 이유로 인해 class.forName 메서드에서 ClassNotFoundException 예외가 발생한다면(1) catch로 이동(2) 후 finally 가 동작(3)을 하게 되는데 이때 각 객체가 null인 상태이므로 null 여부를 체크하지 않으면 finally 에서 또 다시 NullPointerException이 발생함
일반적으로 DAO의 메서드들은 이와 같은 형태의 코드를 가지니 앞으로 자주보게 될 코드
만약 "왜 코드를 이렇게 써야하지?" 라고 생각이 든다면 지금 단계에서는 가장 마지막에 보여준 finally 까지 포함된 코드를 외우는게 편할 것
DAO 내 select 메서드 코드가 굉장히 길어졌음
그 이유는 쿼리를 보내기 위한 준비(DB 접속) 코드와 접속을 끊는 코드가 덧붙어서 굉장히 길어졌음
또 DAO의 매 메서드 마다 쿼리를 보내기 위한 준비와 접속을 끊는 코드가 매번 추가되야하므로 별도의 메서드로 빼두는게 좋음