서블릿은 많은 경우, 데이터의 생성과 삭제 및 수정 등을 하며 클라이언트의 요청에 응답을 한다. 이 과정에서 더욱 상세히 데이터를 다루기 위해서는 데이터베이스가 필수적이다. 자바의 어플리케이션이 MySQL과 같은 외부 DB에 접속하기 위해서는 별도의 데이터 접근 API가 필요한데 이것이 바로 JDBC이다.
서블릿과 JDBC
DriverManager를 통해 Connection 생성하기
서블릿에서 DB의 데이터를 가져온다고 생각해보자. 어떤 과정을 거쳐야 할까? 먼저 자바의 언어로 DB와는 소통할 수 없으므로 JDBC 드라이버를 등록해야 한다. 그후 JDBC 드라이버를 통해 DB와 연결을 하기 위한 Connection 인터페이스의 구현 객체를 생성한다. 다음으로는 원하는 질의문(SQL)을 DB로 보내 그에 따른 결과물을 받는다. 이 과정을 자바에서는 각각 하나의 객체들이 도맡아 하는데 과정은 다음과 같다.
- DriverManager를 이용해 jdbc.sql.Driver(JDBC 드라이버) 를 등록한다.
- DriverManager의 getConnection() 메서드를 통해 DB와 연결하고 Connection 인터페이스의 구현 객체를 생성한다.
- 질의문을 작성할 Statment 객체를 Connection 객체의 createStatement() 메서드를 통해 생성한다.
- Statement 객체의 executeQuery() 메서드를 통해 질의문을 실행하고 그 결과는 ResultSet 객체로 받는다.
1. DriverManager를 통해 JDBC 드라이버 등록
DriverManager.registerDriver(new com.mysql.cj.jdbc.Driver());
참고) mysql.jdbc.Driver 의 경우 현재 버전에 depricated 돼, mysql.cj.jdbc.Driver()를 사용함.
2. DriverManager의 getConnection() 메서드를 통해 DB와 연결하고 Connection 인터페이스의 구현 객체를 생성
Connection conn = DriverManager.getConnection(
"jdbc:mysql://localhost/dbname?useUnicode=True&serverTimezone=UTC",
"username",
"password"
);
getConnection() 메서드의 매게변수로는 JDBC URL, 유저의 이름, 비밀번호가 입력된다. URL의 형식은
"JDBC:(사용하는DB)/(DB의 URL)/(DB명)"이 된다. 이때, mysql-connector-java 버전 5.1 이후의 경우 KST를 인식하지 못하는 경우가 있어, serverTimezone을 UTC로 설정해주어야 한다.
3. Connection 객체의 createStatement() 메서드를 통해 질의문을 작성및 실행할 Statment 객체를 생성
Statement stmt = conn.createStatement();
PreparedStatement stmt = conn.preparedStatement(
"SELECT * FROM MEMBERS WHERE MNAME=?");
stmt.setString(1, request.getParameter("name"));
Statement외에도 PreparedStatement 객체를 사용할 수 있는데 이때는 Connection 구현 객체의 preparedStatement() 메서드를 통해 생성할 수 있다. 이때는 매게변수로 쿼리문을 넣어주어야 하며 추후에 넣을 데이터는 ?로 표시후 set... 메서드를 통해 입력받을 수 있다.
4. Statement 객체의 executeQuery() 메서드를 통해 질의문을 실행하고 그 결과는 ResultSet 객체로 받는다.
ResultSet rs = stmt.executeQuery("SELECT MNO, MNAME, EMAIL FROM MEMBERS");
while (rs.next()) { ... }
Statment의 executeQuery 메서드의 매게변수로 쿼리문을 넣어주면 그결과 ResultSet 객체로 반환된다.
DriverManager를 통해 Connection 생성하기 - 적용
package spms.servlets;
import ...
@WebServlet("/member/list")
public class MemberListServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
@Override
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
Connection conn = null;
Statement stmt = null;
ResultSet rs = null;
try {
DriverManager.registerDriver(new com.mysql.cj.jdbc.Driver());
conn = DriverManager.getConnection(
"jdbc:mysql://localhost/studydb?serverTimezone=UTC",
"username",
"password");
stmt = conn.createStatement();
rs = stmt.executeQuery("SELECT * FROM MEMBERS");
response.setContentType("text/html; charset=UTF-8");
PrintWriter out = reponse.getWriter();
while(rs.next()) {
out.print(rs.getInt("MNO") + ",");
out.print(rs.getString("MNAME") + ",");
}
} catch (Exception e) {
throw new ServletException(e);
} finally {
try {if (rs != null) rs.close();} catch(Exception e) {}
try {if (stmt != null) stmt.close();} catch(Exception e) {}
try {if (conn != null) conn.close();} catch(Exception e) {}
}
}
}
다음은 studydb라는 이름의 데이터베이스로 부터 멤버의 정보를 받아와 화면에 띄우는 예제이다. DB를 연결하고 쿼리문을 생성, 실행하는 과정에서 예외가 발생하므로 예외를 처리해주는 try-catch 구문을 사용해야 한다. 이때 만약 Connection, Statement, ResultSet 선언을 try 블록 안에서 할 경우, try 블록 밖에서는 사용할 수 없으므로 try-catch 구문밖에서 null 값으로 선언 후, 예외 처리를 위해 try-catch 구문 안에서 생성한다. 또한 예외 발생 여부와 무관하게 DB를 사용하는데 사용한 객체를 모두 종료 해주어야 하므로 finally 블록 안에서 해당 객체들을 종료한다.
DriverManager를 통한 Connection 객체 생성의 문제점
하나의 어플리케이션에는 여러개의 서블릿이 있을 것이고, 이 서블릿들이 대부분 JDBC를 사용할 것이다. 이때 서블릿마다 JDBC 드라비어를 등록하고, 새로운 커넥션을 생성하고 다시 완료하는 과정은 매우 비효율적이다. 그러므로 Connection Pool이라는 것을 사용하여 Connection 객체를 제사용하는 방식을 많이 이용하며, 자바는 DBCP(Database Connection Pool)에서 DataSource 객체의 커넥션 풀을 제공한다.
Connection Pool (커넥션 풀)
커넥션 풀을 커넥션을 미리 만들어 놓고, 커넥션 객체가 필요할때마다 해당 객체를 재사용하는 것이다. 이렇게 하면 불필요하게 커넥션 객체가 많이 생성되는 것을 방지 할 수 있다.
DBConnectionPool.java
public class DBConnectionPool {
String url;
String username;
String password;
Queue<Connection> connList = new LinkedList<Connection>();
public DBConnectionPool(String driver, String url, String username, String password) throws Exception {
this.url = url;
this.username = username;
this.password = password;
Class.forName(driver);
}
public Connection getConnection() throws Exception {
if (!connList.isEmpty()) {
Connection conn = connList.poll();
if (conn.isValid(10)) {
return conn;
}
}
return DriverManager.getConnection(url, username, password);
}
public void returnConnection(Connection conn) throws Exception {
connList.offer(conn);
}
public void closeAll() {
for (Connection conn : connList) {
try { conn.close(); } catch (Exception e) {}
}
}
}
- DBConnectionPool 생성자 : 드라이버 경로와 DB정보(URL/사용자이름/비밀번호)를 매게변수로 받아 필드값을 초기화하고, 드라이버를 등록한다.
- getConnection() : connList에 Connection 객체가 있을경우, 꺼내서 리턴한다. 없을 경우 새로운 Connection 객체를 생성하여 리턴한다.
- returnConnection() : 사용한 Connection 객체를 커넥션 풀에 다시 리턴한다.
package spms.servlets;
import ...
@WebServlet("/member/list")
public class MemberListServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
DBConnectionPool pool;
public void setPool(DBConnectionPool pool) {
this.pool = pool;
}
@Override
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
Connection conn = null;
Statement stmt = null;
ResultSet rs = null;
try {
conn = pool.getConnection();
stmt = conn.createStatement();
rs = stmt.executeQuery("SELECT * FROM MEMBERS");
poo.returnConnection(conn);
response.setContentType("text/html; charset=UTF-8");
PrintWriter out = reponse.getWriter();
while(rs.next()) {
out.print(rs.getInt("MNO") + ",");
out.print(rs.getString("MNAME") + ",");
}
} catch (Exception e) {
throw new ServletException(e);
} finally {
try {if (rs != null) rs.close();} catch(Exception e) {}
try {if (stmt != null) stmt.close();} catch(Exception e) {}
try {if (conn != null) conn.close();} catch(Exception e) {}
}
}
}
회원 정보를 화면에 보여주는 코드이다. 커넥션 풀을 이용해 Connection 객체를 받고(getConnection()) 다 사용한 후, 다시 반환한다.(returnConnection())
DBCP와 DataSource
DBCP(Database Connection Pool)은 외부 라이브러리로 커넥션 풀을 제공한다. 커넥션 풀에 관한 더욱 상세한 설정이 가능하고, 프로그래머가 직접 커넥션풀을 만들 필요가 없어 편리하다. DBCP는 Commons-dbcp, tomcat-dbcp hikai-dbcp 등 다향하게 존재하지만 Commons-dbcp를 기준으로 설명하겠다.
DBCP를 사용하기 위해서는 당연히 먼저 외부에 다운로드를 받아 프로그램 내부에 라이브러리로 등록해야 한다.
https://commons.apache.org/proper/commons-dbcp/index.html
DBCP – Overview
The DBCP Component Many Apache projects support interaction with a relational database. Creating a new connection for each user can be time consuming (often requiring multiple seconds of clock time), in order to perform a database transaction that might ta
commons.apache.org
(주의. commons-dbcp는 하위호환성이 없어 1.x 버전의 코드와 2.x 버전의 코드가 다르다. 이 글에서 dbcp2 버전을 기준으로 설명하겠다.)
라이브러리를 사용하기 위해 서버의 Conext.xml 파일에 등록을 해주어야 한다.
<Context>
<Resource name="jdbc/studydb" auth="Container" type="javax.sql.DataSource"
maxTotal="100" maxIdle="30" maxWaitMillis="10000"
username="study" password="dkq041675!" driverClassName="com.mysql.cj.jdbc.Driver"
url="jdbc:mysql://127.0.0.1:3306/studydb?serverTimezone=UTC"/>
</Context>
- maxTotal : 동시에 사용할 수 있는 최대 커낵션의 개수(default=8)
- maxIdle : Connection Pool에 최대로 유지될 수 있는 커넥션 개수(default=8)
- maxWaitMillis : pool에 Connection이 없어 대기할 경우, 최대 대기 시간 (default=무한)
- initialSize : 최초 풀 생성시 풀에 채워 넣을 커넥션의 개수 (default =0)
DBCP는 자바의 DataSource 클래스를 통해 Connection을 제공한다.
public class DBCPTest {
DataSource ds;
public void setDataSource(DataSource ds) {
this.ds = ds;
}
public void execute() {
Connection conn = null;
PreparedStatement pstmt = null;
try {
conn = ds.getConnection();
pstmt = conn.preparedStatement(...);
pstmt.executeUpdate();
} catch ... // 생략
}
}
마무리
DB에 접속하여 쿼리를 보내는 과정은 크게 4과정을 이뤄진다.
- 드라이버 로드
- 커넥션 생성
- 쿼리 생성
- 쿼리 실행
이 중에서 드라이버를 로드해서 커넥션을 생성하는 과정은 모든 코드에서 동일하게 반복된다. 이를 없이기 위해 사용하는 것이 커넥션 풀이다.
커넥션 풀을 직접 작성하여 사용할수도 있지만 외부 라이브러리인 DBCP를 이용해 더욱 편리하게 사용할 수 있다.
'WEB' 카테고리의 다른 글
[WEB] 스프링 컨테이너 (Spring Container) (0) | 2022.04.19 |
---|---|
[WEB] ClassNotFoundException: com.mysql.cj.jdbc.Driver (0) | 2022.03.02 |
[WEB] HTTP요청과 HttpServlet (0) | 2022.02.05 |
[WEB] 서블릿 프로그래밍 (0) | 2022.02.01 |
[WEB] 웹 어플리케이션과 HTTP 프로토콜 (0) | 2022.01.28 |