반응형
6. 커넥션 풀로 성능 개선
- 웹은 클라이언트의 요청에 서버가 응답하는 구조→ Connection 객체 생성 때마다 네트워크 통신이 이뤄지며, 시간이 걸리는 작업들이 수반됨 == 시스템 성능에 큰 영향을 미침
⇒ ’커넥션 풀’로 성능 개선 필요
1) 커넥션 풀과 JNDI
(1) 커넥션 풀
- JSP 프로그래밍에서, 커넥션 풀은 WAS가 제공하는 것을 이용하는 게 좋음
→ WAS 하나에 여러 개의 웹 애플리케이션을 구동시키는 경우가 많아,
각 자원을 한번에 관리하는 게 절약도 되고 관리하기가 쉬움 - WAS가 시작될 때, 미리 생성한 커넥션 객체를 사용하므로 웹 애플리케이션 실행 속도 빨라짐
+ 클라이언트의 동시 요청이 많아지더라도 좀 더 응답이 수월함
→ 성능 향상 효과가 커서, 웹 뿐만 아니라 게임에서도 많이 사용
(2) JNDI
- Java Naming and Directory Interface-자바에서 객체나 데이터의 이름만으로 찾아 쓸 수 있는 디렉토리 서비스
- 객체 이름과 실제 객체를 연결해주는 역할
- 대부분의 WAS는 커넥션 풀을 비롯한 여러 자원을 JNDI로 제공
→ 이름과 실제 객체와의 연결은 외부 설정 파일에서 관리
[다른 객체로 연결하거나 세부 설정을 바꿀 때 소스코드 수정/컴파일 불필요]
*비슷한 예-DNS
- Domain Name System - 웹에서 도메인을 입력하면 이것을 통해 웹 서버의 IP 주소를 얻어와 해당 주소로 접속 가능
(3) WAS의 JNDI를 통해 커넥션 풀을 사용하는 방법
- WAS(톰캣)가 시작할때 server.xml과 context.xml에 설정한 대로 커넥션 풀 생성
- JSP 코드에서 JNDI 서버(WAS가 제공)로부터 데이터소스 객체를 얻어 옴
- 데이터소스로부터 커넥션 객체를 가져옴
- 데이터베이스(DB) 작업을 수행
- 모든 작업이 끝나면, 커넥션 객체를 풀로 반환
2) 커넥션 풀 설정
(1) server.xml 파일에 코드 추가
//{톰캣 홈 디렉터리}/conf/server.xml
<GlobalNamingResources>
...기존 내용 유지...
<Resource auth="Container"
driverClassName="oracle.jdbc.OracleDriver"
type="javax.sql.DataSource"
initialSize="0"
minIdle="5"
maxTotal="20"
maxIdle="20"
maxWaitMillis="5000"
url="jdbc:oracle:thin:@localhost:1521:xe"
name="dbcp_myoracle"
username="hello"
password="1234" />
</GlobalNamingResources>
- <GlobalNamingResources>는 전역 자원을 등록하는 영역
→ 이 엘리먼트 안에 등록한 자원은, 이 서버에서 구동되는 모든 웹 애플리케이션에서 사용 가능
- Type 속성으로 지정한 javax.sql.DataSource: 물리적인 데이터소스와의 연결을 생성해주는 자바 표준 인터페이스
→ driverClassName 속성으로 지정한 oracle.jdbc.OracleDriver 클래스가 이 인터페이스를 구현하고 있음
→ 오라클의 OracleDriver 클래스가 커넥션 풀 기능을 구현하고,
우리는 자바 표준 인터페이스인 DataSource 형태로 받아 이용하는 것 - <Resource>엘리먼트의 다양한 속성
driverClassName | JDBC 드라이버의 클래스명 |
type | 데이터소스로 사용할 클래스명 |
initialSize | 풀의 최초 초기화 과정에서 미리 만들어놓을 연결의 개수 (기본값은 0) |
minIdle | 최소한으로 유지할 연결 개수 (기본값은 0) |
maxTotal | 동시에 사용할 수 있는 최대 연결 개수 (기본값은 8) |
maxIdle | 풀에 반납할 때 최대로 유지될 수 있는 연결 개수 (기본값은 8) |
maxWaitMillis | 새로운 요청이 들어왔을 때 얼만큼 대기할지를 밀리초 단위로 기술 |
url | 오라클 연결을 위한 URL |
name | 생성할 자원(여기서는 풀)의 이름 |
username | 계정 아이디 |
password | 계정 패스워드 |
(2) context.xml 파일에 코드 추가
//{톰캣 홈 디렉터리}/conf/context.xml
<Context>
...기존 내용 유지...
<ResourceLink global="dbcp_myoracle" name="dbcp_myoracle"
type="javax.sql.DataSource" />
</Context>
- 풀의 이름과 데이터소스로 사용할 클래스명 기술
- global 속성에 앞서 server.xml에서 등록한 커넥션 풀 전역 자원의 이름을 명시해야 함
(3) 이클립스에서 웹 서버 다시 생성
- Servers 뷰에서 기존 서버 삭제
- 새 서버 생성
- 프로젝트 탐색기에서 Servers 폴더를 열고 server.xml과 context.xml을 열어보면, 편집한 내용이 그대로 적용되어 있음
3) 커넥션 풀 동작 검증
- 사용하는 멤버 변수들과 close( ) 메소드는 JDBConnect 클래스와 같음
- JDBConnect가 JDBC 드라이버를 로드하고 DB와의 연결을 직접 만들었다면, DBConnPool은 JNDI로부터 데이터소스를 찾은 후 데이터소스로부터 연결을 얻음
// Java Resources/src/common/DBConnPool.java
package common;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.Statement;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.sql.DataSource;
public class DBConnPool {
public Connection con;
public Statement stmt;
public PreparedStatement psmt;
public ResultSet rs;
// 기본 생성자
public DBConnPool() {
try {
// 커넥션 풀(DataSource) 얻기
Context initCtx = new InitialContext(); //1.
Context ctx = (Context)initCtx.lookup("java:comp/env"); //2.
DataSource source = (DataSource)ctx.lookup("dbcp_myoracle"); //3.
// 커넥션 풀을 통해 연결 얻기
con = source.getConnection(); //4.
System.out.println("DB 커넥션 풀 연결 성공");
}
catch (Exception e) {
System.out.println("DB 커넥션 풀 연결 실패");
e.printStackTrace();
}
}
// 연결 해제(자원 반납)
public void close() {
try {
if (rs != null) rs.close();
if (stmt != null) stmt.close();
if (psmt != null) psmt.close();
if (con != null) con.close(); // 자동으로 커넥션 풀로 반납됨
System.out.println("DB 커넥션 풀 자원 반납");
}
catch (Exception e) {
e.printStackTrace();
}
}
}
- InitialContext라는 객체 생성: JNDI(자바 네이밍 서비스)에서 이름과 실제 객체를 연결해주는 개념이 Context 이고 InitialContext는 네이밍 서비스를 이용하기 위한 시작점
→ lookup( ) 메소드에 이름을 건네 원하는 객체를 찾아 올 수 있음 - “java:comp/env{현재 웹 애플리케이션의 루트 디렉토리}”라는 이름을 인수로 Context 객체를 얻음
→ 현재 웹 애플리케이션이 사용할 수 있는 모든 자원은 “java.comp/env”아래에 위치함 - “java:comp/env” 아래에 위치한 “dbcp_myoracle” 자원을 얻어옴: 이 자원이 바로 앞서 설정한 데이터소스(커넥션 풀)
→ “dbcp_myoracle”은 context.xml 파일에 추가한 <ResourceLink>에 있는 name 속성의 값 - 데이터소스를 통해 풀에 연결되어 있는 연결 객체를 얻어와 멤버 변수에 저장하면, 생성 과정이 마무리 됨
- 다 쓴 연결을 반납함 → Connection객체의 close( )만 호출해주면 됨 [풀을 사용하지 않을 때와 같음]
(2) ConnectionTest.jsp
// WebContent/ConnectionTest.jsp
<%@ page import="common.JDBConnect"%>
<%@ page import="common.DBConnPool"%> //1.새로 만든 DBConnPool 클래스 임포트
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
...생략...
<h2>커넥션 풀 테스트</h2>
<%
DBConnPool pool = new DBConnPool();
pool.close();
%>
</body>
</html>
(3) ConnectTest.jsp 실행 결과
- server.xml, context.xml 파일을 잘못건드린 건지… 서버가 계속 오류 나면서 실행이 안 됨
(오류명: The Network Adapter could not establish the connection)
- 해결이 불가능해서, 프로젝트+서버 다시 만듬 → JDBConnect의 DB정보만 옮겨놓기 → JDBConnect.jsp, Connection.jsp 수정하고 방화벽 인바운드규칙 추가했는데도 오류.. cmd로 sql도 켰음
- 02.02-오류 해결. 프로젝트 라이브러리에 새 서버 추가하고(https://geminihoroscope.tistory.com/81), https://velog.io/@on-n-on-turtle/jsp-빨간줄-faceCet-문제-java-15, https://jhnyang.tistory.com/272 참고해서 해결함
(저번과 같은 문제였음: 방화벽→인바운드규칙 추가, [서비스]→ Oracle 붙어있는 프로그램 전부 ‘시작’ 상태로 만듬)
- 2) 커넥션 풀 설정 다시 반복하기
→ 성공! sever.xml, context.xml 파일에서 기존의 코드를 지우고 코드를 추가해버려서 오류가 났던 것 같다..
7. 간단한 쿼리 작성 및 실행
- 데이터베이스 작업: 쿼리문(SQL)을 작성하고 실행하여 그 결과를 얻어오는 일을 말함
1) JDBC에서의 쿼리문 표현
- Statement 계열의 인터페이스
Statement | 인파라미터가 없는 정적 쿼리 처리 시 사용 |
PreparedStatement | 인파라미터가 있는 동적 쿼리를 처리할 때 사용 |
CallableStatement | 프로시져(procedure)나 함수(function)를 호출할 때 사용 |
- Statement 계열의 객체로 쿼리문을 실행할 때 사용하는 메소드
- Statement와 PreparedStatement에서의 메소드 사용 방법은 약간의 차이가 있음
executeUpdate() | insert, update, delete 쿼리문 실행 시 사용 기존 코드를 변화시키거나 새로운 레코드를 입력함 → 실행 후 영향을 받은 행의 개수를 int형으로 반환 |
executeQuery() | select 쿼리문 실행 시 사용 기존 레코드 조회 시 사용 → 조회한 레코드들의 집합인 ResultSet 객체 반환 |
- JDBC에서 쿼리문은 java.sql.Statement 인터페이스로 표현되는데,
Statement 객체는 Connection 객체를 통해 얻어오도록 되어 있음 - 인파라미터(IN Parameter): 미리 작성해둔 쿼리문에서 일부 값을 나중에 결정할 수 있게 해주는 매개변수.
쿼리문에서 물음표(?)로 표현함
2) 동적 쿼리문으로 회원 추가 - sqlplus
- 작업 순서: DB 연결 → 입력값 준비 → 쿼리문 생성 → 쿼리 수행 → 연결 닫기
- PreparedStatement: 쿼리문의 틀을 준비해둔 후, 필요할 때 인파라미터를 설정해서 사용
(데이터 타입에 맞는 set 메소드 사용하기!)
//데이터 타입에 맞는 메소드
void setInt(int index, int value);
void setDate(int index, Date value);
void setString(int index, String value);
- ExeUpdate.jsp 파일
// WebContent/ExeUpdate.jsp
<%@ page import="java.sql.PreparedStatement"%>
<%@ page import="java.sql.Connection"%>
<%@ page import="common.JDBConnect"%>
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<html>
<head><title>JDBC</title></head>
<body>
<h2>회원 추가 테스트(executeUpdate() 사용)</h2>
<%
// 1.DB에 연결
JDBConnect jdbc = new JDBConnect();
// 2.테스트용 입력값 준비
String id = "test1";
String pass = "1111";
String name = "테스트용";
// 쿼리문 생성
String sql = "INSERT INTO member VALUES (?, ?, ?, sysdate)"; //3.
PreparedStatement psmt = jdbc.con.prepareStatement(sql); //4.
// 5.(아래 코드 3줄)
psmt.setString(1, id);
psmt.setString(2, pass);
psmt.setString(3, name);
// 쿼리 수행
int inResult = psmt.executeUpdate(); //6.
out.println(inResult + "행이 입력되었습니다."); //7.
// 연결 닫기
jdbc.close();
%>
</body>
</html>
1. 앞서 준비한 JDBConnect 객체 생성
2. 테이블에 입력할 값 준비
→ 원래 아래처럼 form 값을 전송받아야 하지만, DB 작업 테스트 중이니 간단하게 값을 하드코딩함
String id = request.getParameter("id");
String pass = request.getParameter("pass");
String name = request.getParameter("name");
3. 쿼리문 생성 1 - SQL문 문자열 정의
INSERT INTO member VALUES (?, ?, ?, sysdate)
//입력하라. 회원 테이블에 다음 3개의 값과 현재 시각을(?는 나중에 대입할 값)
4. 쿼리문 생성 2 - Connection 객체를 통해 PreparedStatement 객체 생성 (3.의 SQL문을 인수로 제공)
5. 쿼리문 생성 3 - 인파라미터들에 실제 값을 대입함 (setString()의 첫 번째 매개변수 = 인파라미터의 순서) →첫 번째 인파라미터가 1번이며 차례로 1씩 증가하므로, DB에서 인덱스가 1부터 시작됨
6. 쿼리문 실행 → 데이터베이스에 성공적으로 입력된 레코드의 수가 정수형으로 반환됨
(쿼리문이 update나 delete였다면, 수정/삭제된 레코드 수 반환)
7. DB 연결 해제
- ExeUpdate.jsp을 [Run On Server]로 실행한 결과
- [윈도우] + [R] → ‘cmd’ 입력 → cmd에서 “sqlplus” 입력 → user-name에 “hello”, password에 “1234” 입력 → “select * from member;” 입력
3) 정적 쿼리문으로 회원 조회 - JSP
- 정적 쿼리문: 모든 내용이 처음부터 완벽하게 정의되어, 더는 변할 게 없는 쿼리문
(1) 회원 목록 조회 테스트
- ExeQuery.jsp 파일 생성 - ExeUpdate.jsp 파일보다 쿼리문 생성 부분이 훨씬 간결함
// WebContent/ExeQuery.jsp
<%@ page import="java.sql.ResultSet"%>
<%@ page import="java.sql.Statement"%>
<%@ page import="java.sql.Connection"%>
<%@ page import="common.JDBConnect"%>
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<html>
<head><title>JDBC</title></head>
<body>
<h2>회원 목록 조회 테스트(executeQuery() 사용)</h2>
<%
// DB에 연결
JDBConnect jdbc = new JDBConnect();
// 쿼리문 생성
String sql = "SELECT id, pass, name, regidate FROM member"; //1.
Statement stmt = jdbc.con.createStatement(); //2.
// 쿼리 수행
ResultSet rs = stmt.executeQuery(sql); //3.
// 결과 확인(웹 페이지에 출력)
//5.java.sql.Date~ 부분까지
while (rs.next()) { //4.
String id = rs.getString(1);
String pw = rs.getString(2);
String name = rs.getString("name");
java.sql.Date regidate = rs.getDate("regidate");
//6.
out.println(String.format("%s %s %s %s", id, pw, name, regidate) + "<br/>");
}
// 연결 닫기
jdbc.close();
%>
</body>
</html>
- SQL문에 인파라미터가 전혀 없음
- PreparedStatement가 아닌, Statement를 생성함. 메소드는 prepareSatatment()가 아닌, createStatement()
- 쿼리 수행에 executeUpdate()가 아닌 executeQuery() 메소드를 이용하고, 결과로 ResultSet(조회 결과를 담고 있는 집합-이 예제에서는 ‘회원 목록’) 객체를 받음
- next() 메소드: ResultSet 객체에서 다음 행(레코드) 반환
- 4.에서 반환한 행에서, ID/패스워드/이름/가입 날짜를 차례로 읽어옴
- 5.에서 읽어온 회원 목록을 웹 페이지에 출력
- ExeQuery.jsp 파일을 [Run On Server]로 실행한 결과 (방화벽에서 8081 포트 열어줘야 함) → 2개의 레코드가 정상적으로 출력됨
- JDBC 프로그래밍이 진행되는 순서
- JDBC 드라이버 로드 → 데이터베이스 연결 → 쿼리문 작성 → 쿼리문(Statement 계열) 객체 생성 → 쿼리 실행 → 실행 결과 처리 → 연결 해제
- JDBC 프로그래밍이 진행되는 순서
4) ResultSet에서 결괏값 불러오기
- Select문: 조건에 맞는 레코드를 모두 선택하라는 명령 → 실행 결과로 얻은 ResultSet에는 여러 개의 레코드가 담겨 있음
- ResultSet 안의 쿼리 결과로 반환된 ResultSet에서는 커서가 첫 번째 행 부분에 위치하고, next() 메소드가 호출되면 다음 행으로 커서 이동
- → 다음 행이 있다면 true 반환, 빈 ResultSet이거나 더 이상 읽어올 수 있는 행이 없다면 false 반환
- ResultSet의 get 메소드들은 커서가 현재 가리키는 행의 걸럼값들을 읽어 옴 (컬럼의 인덱스를 사용하거나, 컬럼명을 그대로 사용해도 됨)
→ 다음 3개의 메소드로 대부분의 작업을 처리 가능
int getInt(int columnIndex) 혹은 int getInt(String columnLabel) | 지정한 인덱스 혹은 이름의 컬럼에 해당하는 값을 정수형으로 추출 |
Date getDate(int columnIndex) 혹은 Date getDate(String columnLabel) |
지정한 인덱스 혹은 이름의 컬럼에 해당하는 값을 날짜형으로 추출 |
String getString(int columnIndex) 혹은 String getString(String columnLabel) |
지정한 인덱스 혹은 이름의 컬럼에 해당하는 값을 문자열로 추출 |
반응형