[Tomat] JDBC DataSource 사용 방법

 

[ 소개 ]

- JNDI DataSource 구성은 본 글에서 광범위하게 다루고 있는데, 톰캣 사용자의 피드백에 따르면 배결 구성에 대한 세부사항은 까다로울 수 있다.

- 다음은 인기있는 데이터베이스에 대한 몇가지 예시 구성과 DB 사용에 대한 몇가지 일반적인 팁이다.

- Tomcat 7.x 와 Tomcat 8.x 는 서로 다른버전의 Apache Commons DBCP 라이브러리를 사용하기 때문에 JNDI 리소스 구성이 다소 변경되었다.
- 이전 JNDI 리소스 구성을 Tomcat 11 에서 동작하게 하려면 아래 예제의 구문과 일치하도록 수정해야 할 가능성이 높다. (마이그레이션 가이드 참조)

- 또한 일반적인 JNDI DataSource 구성과 이 튜토리얼의 내용은 사용자가 ContextHost 설정 참조문서, 특히 이후 레퍼런스의 Automatic Application Deployment 섹션을 읽고 이해했다고 가정한다.



[ DriverManager, 서비스 제공자 매커니즘 과 메모리 누수 ]

- java.sql.DriverManagerService Provider (서비스제공자) 매커니즘을 제공한다.
- 이 기능은 META-INF/service/java.sql.Driver 파일을 제공하여 자신의 존재를 알리는 사용 가능한 모든 JDBC 드라이버가 자동으로 검색/로드/등록 되므로 JDBC Connection 을 생성하기전에 데이터베이스 드라이버를 명시적으로 로드할 필요가 없게 해준다.
- 하지만, 이 구현은 서블릿 컨테이너 환경의 모든 Java 버전에서 근본적으로 문제가 있는데, java.sql.DriverManager 가 드라이버를 단 한번만 검색한다는 것이다.

- Apache Tomcat에 포함된 JRE Memory Leak Prevention Listener 는 Tomcat 시작중에 드라이버 검색을 트리거 하여 이 문제를 해결하며, 기본적으로 활성화 되어있다.
- 이는 공통 클래스 로더와 그 상위 로더에서 볼수 있는 라이브러리들만 데이터베이스 드라이버 검색 대상이 된다는걸 의미하고, 여기에는 $CATALINA_HOME/lib, $CATALINA_BASE/lib, Class_Path, Module_Path 가 포함되며, WEB_Application(WEB-INF/lib) 과 (구성된경우)공유 클래스 로더에 패키지된 드라이버들은 노출되지 않으며 자동으로 로드되지 않는다.
- 이 기능을 비활성화 하게되면 JDBC를 사용하는 첫번째 WEB_Application 에 의해 검색이 트리거되어 해당 WEB_Application 이 다시 로드될때와 이 기능에 의존하는 다른 WEB_Application 에서 장애가 발생할 수 있다.

- 따라서 WEB-INF/lib 디렉토리에 데이터베이스 드라이버가 있는 WEB_Application 은 Service_Provider 매커니즘에 의존할 수 없으며 드라이버를 명시적으로 등록해야 한다.

- java.sql.DriverManager 의 드라이버 목록도 메모리 누수의 원인으로 알려져 있다.
- WEB_Application 이 등록한 모든 드라이버는 WEB_Application 이 중지될 때 등록 해제되어야 한다.
- Tomat 은 WEB_Application 이 중지될 때 WEB_Application 클래스 로더가 로드한 JDBC 드라이버를 자동으로 발견하고 등록 해제하려고 시도하지만 Application 이 ServletContextListener 를 통해 스스로 이 작업을 수행할 것으로 예상한다.



[ Database Connection Pool (DBCP 2) 구성 ]

- Apache Tomcat 의 기본 데이터베이스 Connection Pool 은 Apache Commons 프로젝트의 라이브러리에 의존하며 아래 라이브러리가 사용된다.
 1) Commons DBCP 2
 2) Commons Pool 2

- 이 라이브러리들은 $CATALINA_HOME/lib/tomcat-dbcp.jar 단일 JAR 파일에 있으며, Connection Pool 에 필요한 Class 만 포함되었고 Application 과 충돌을 방지하기 위해 package 이름이 변경되었다.

- DBCP_2 는 JDBC_4.1 을 지원한다.

1004lucifer

1. 설치

- 설정 파라미터의 전체 목록은 DBCP 2 문서를 참고.


2. 데이터베이스 Connection Pool 누수 방지

- 데이터베이스 Connection Pool 은 데이터베이스에 대한 연결들의 Pool 을 생성하고 관리한다.
- 이미 존재하는 데이터베이스 Connection 을 재활용하고 재사용 하는것이 새로운 Connection 을 맺는것보다 효율적이다.

- Connection Pool 에는 한가지 문제가 있는데 WEB_Application 은 ResultSet, Statement, Connection 을 명시적으로 닫아야 한다.
- WEB_Application 이 이러한 리소스들을 닫지 않으면 재사용을 위해서 다시 사용할 수 없으며, 이는 데이터베이스 Connection Pool Leak(누수) 가 발생하고 사용가능한 Connection 이 더이상 없는경우 WEB_Application 의 DB 연결이 실패할 수 있게된다.

- 이 문제에 대한 해결책이 있는데, Apache Commons DBCP_2 는 이러한 (abandoned) 방치된 데이터베이스 Connection 을 추적하고 복구하도록 구성할 수 있다.
- Connection 을 복구할 수 있을뿐만 아니라, 이러한 리소스들을 Close 하지 않은 코드에 대한 StackTrace 를 생성할 수도 있다.


- (abandoned) 버려진 데이터베이스 Connection 이 제거되고 재활용 되도록 DBCP_2 DataSource 를 구성하려면 <Resource> 에 아래 속성 중 하나 또는 모두 추가하면 된다.

removeAbandonedOnBorrow=true

removeAbandonedOnMaintenance=true

- 위 두개의 속성의 기본값은 false 이며, removeAbandonedOnMaintenance 는 timeBetweenEvictionRunsMillis 의 값이 (+)양수 값으로 설정하여 Pool 유지관리를 활성화 해야 효과가 있다. (속성의 자세한 설명은 DBCP 2 문서 참고)


removeAbandonedTimeout 속성을 사용하여 데이터베이스 Connection 이 버려진 것으로 간주되기 까지의 유휴시간(초)를 설정할 수 있다. (기본값: 300초)

removeAbandonedTimeout="60"


- 데이터베이스의 Connection 리소스를 방치한 코드의 StackTrace 를 DBCP_2 가 로깅하기 원한다면 logAbandoned 속성을 true 로 설정할 수 있다. (기본값: fasle)

logAbandoned="true"


★ 작성자 내용 추가 

- 최근에는 JPA 나 MyBatis 의 프레임워크가 잘되어 있어 Connection 의 누수가 거의 없기에 위 Abandoned 관련 옵션을 설정하지 않는것을 권장한다.
- 특히나 쿼리 수행시간이 removeAbandonedTimeout 시간을 넘어가게되면 해당 Connection 은 끊어지고 새로운 Connection 을 연결하게 되는데 트랜잭션 기능을 사용하고 있는중에 removeAbandonedTimeout 제한에 걸리게 된다면 해당 트랜잭션의 요청은 무조건 실패하게 된다.


3. MySQL DBCP 2 예제

0) 소개

- 정상 동작한다고 보고된 MySQL 버전 (작성자: 내용이 거의 업데이트 되지 않았다.)
 1) MySQL 3.23.47, InnoDB 사용 MySQL 3.23.47, MySQL 3.23.58, MySQL 4.0.1 alpha
 2) Connector/J 3.0.11-stable (공식 JDBC 드라이버)
 3) mm.mysql 2.0.14 (오래된 3rd party JDBC 드라이버)

- 아래 내용 수행전 $CATALINA_HOME/lib 디렉토리에 JDBC 드라이버 JAR 파일을 복사해야 한다.


1) MySQL 설정

- 아래 내용과 다르게 작업 시 문제가 발생할 수 있으므로 이 지침을 반드시 따라야 한다.

- user, database, test table 을 새로 만든다. MySQL user 는 반드시 password가 있어야 하며, 빈 password 로 연결을 시도하면 드라이버가 실패한다.

   참고: 테스트가 완료되면 위의 사용자를 제거해야 한다.


- 다음으로 테스트 데이터를 testdata 테이블에 넣는다.


2) Contetxt 설정

- 아래와 같이 Context 에 리소스에 대한 선언을 추가하여 Tomcat 에 JNDI DataSource 를 구성한다.


3) web.xml 설정

- 테스트_Application 을 위해 WEB-INF/web.xml 을 생성한다.


4) Test 코드

- 나중에 사용할 간단한 test.jsp 페이지를 만든다.

- 위 JSP 페이지는 JSTL 의 SQL 및 Core 태그라이브러리 를 사용한다.
- Apache Tomcat Taglibs 프로젝트에서 1.1.x 이상의 릴리즈를 다운로드할 수 있으며, jstl.jarstandard.jar 파일을 Web_App 의 WEB-INF/lib 디렉토리에 추가하여 JSTL 을 사용할 수 있다.

- 마지막으로 DBTest.war 명칭의 파일을 $CATALINA_BASE/webapps 하위 디렉토리에 배포하거나 DBTest 라는 하위 디렉토리에 Web_App 을 배포한다.

- 배포가 완료되면 브라우저에서 http://localhost:8080/DBTest/test.jsp 로 접속하여 결과를 확인한다.


4. Oracle 8i, 9i, 10g

0) 소개

- Oracle은 일반적인 문제를 제외하고는 MySQL 구성에서 최소한의 변경만 필요하다.

- 오래된 Oracle 버전의 드라이버는 *.jar 파일 대신에 *.zip 파일이 배포 되었다.
- 톰캣에서는 $CATALINA_HOME_lib 에 위치한 *.jar 파일만 사용하기에 classes111.zip 또는 classes12.zip 파일은 .jar 확장자로 파일명을 변경해야 한다.
- jar 파일은 zip 파일과 다르지 않기 때문에 압축을 풀고 다시 jar 로 압축할 필요없이 단순히 이름만 변경해 주면 된다.

- Oracle 에서는 9i 이후버전에서는 oracle.jdbc.driver.OracleDriver 가 더 이상 사용되지 않으며 다음 메이저 릴리즈에서는 해당 드라이버 클래스에 대한 지원이 중단될 예정이라고 했기에 새로운 oracle.jdbc.OracleDriver 를 사용해야 한다.


1) Context 설정

- 위의 MySQL 구성과 유사한 방식으로 Context 에서 DataSource 를 정의해야 한다.
- 여기서는 thin 드라이버를 사용하여 사용자 scott, 비밀번호 tiger 로 mysid 라는 sid 에 연결하는 myoracle 이라는 DataSource 를 정의힌다. (참고: thin 드라이버에서 이 sid 는 tnsname 과 동일하지 않음)
- 사용하는 스키마는 사용자 scott 의 기본 스키마를 사용한다.

- OCI 드라이버를 사용하려면 URL 문자열에서 thin 을 oci로 변경하기만 하면 된다.


2) web.xml 설정

- Application 의 web.xml 파일을 만들때는 DTD의 요소 순서를 준수해야 한다.


3) 샘플 코드

- 위와 동일한 예제 Application 을 사용할 수 있다. (필요한 DB 인스턴스, 테이블 등을 생성한다고 가정)
- DataSource 코드를 아래와 같은 코드로 바꿀 수 있다.


5. PostgreSQL

0) 소개

- PostgreSQL 은 Oracle 과 비슷한 방식으로 구성된다.


1) 필수 파일

- Postgres JDBC jar 파일을 $CATALINA_HOME/lib 디렉토리에 복사한다.
- Oracle과 마찬가지로 DBCP_2 의 Classloader 가 해당 jar 파일을 찾을 수 있도록 해야한다.


2) Resource 구성

- 두가지 선택사항이 있는데, 모든 Tomcat Application 에서 공유되는 DataSource 를 정의하거나 특정 Application 만을 위한 DataSource를 정의하는 것이다.


2-a) 공유 Resource 설정

- 여러 Tomcat Application 에서 공유되는 DataSource 를 정의하려는 경우 이 옵션을 사용한다.

★ 작성자 내용 추가 
- 공식문서에는 나오지 않았지만 위 설정은 server.xml 파일의 <GlobalNamingResources> 안에 설정을 해준 뒤 Application 의 Context 에서 <ResourceLink> 요소를 추가하여 글로벌 DataSource 리소스에 연결할 수 있다.


2-b) Application 개별 Resource 설정

- 다른 Tomcat Application 에서는 접근이 안되는 특정 Application 의 DataSource 를 정의하려는 경우 아래와 같이 설정한다.

★ 작성자 내용 추가 
- 위 설정 내용은 server.xml 파일에 <Context>를 추가하거나 context.xml 파일에 설정할 수 있다. 공유 Resource 의 <ResourceLink> 도 마찬가지로 server.xml / context.xml 파일에 설정한다.


3) web.xml 설정


4. DataSource 접근

- 프로그래밍 방식으로 데이터소스에 엑세스 할때는 아래와 같이 JNDI 조회에 java:/comp/env 를 앞에 붙여야 한다.
- 또한 위의 리소스 정의 파일에서도 "jdbc/postgres"를 원하는 값으로 변경이 가능하다.



[ Non-DBCP 솔루션 ]

- 이러한 솔루션은 데이터베이스에 대한 단일 연결(테스트 이외의 용도로 권장하지 않음)만 제공하거나 다른 Pool 기술을 활용한다.



[ Oracle 8i With OCI Client ]

★ 작성자 내용 추가 

- Oracle 에서 더이상 JDBC-OCI 드라이버를 공식적으로 제공하지 않기에 해당 파트는 번역 및 내용에서 제외한다.

참고
https://www.oracle.com/kr/database/technologies/appdev/jdbc-downloads.html




[ 일반적인 문제 ]

- 아래는 데이터베이스를 사용하는 WEB_Application 에서 자주 발생하는 문제들과 해결 방법에 대한 팁 이다.

1. 간혈적인 데이터베이스 연결 실패

- Tomcat 은 JVM 에서 실행이 되며 JVM 은 더 이상 사용하지 않는 자바 객체들을 제거하기위해 주기적으로 가비지컬렉션(GC)를 수행하는데 이 때 Tomcat 내의 코드 실행이 멈춘다.
- 데이터베이스 연결 설정을 위한 최대 시간이 GC 에 걸린 시간보다 짧은경우 데이터베이스 연결 실패가 발생할 수 있다.

- GC 가 얼마나 오래 걸리는지 데이터를 수집하려면 Tomcat 을 시작할 때 CATALINA_OPTS 환경변수에 -verbose:gc 인자를 추가하고 해당 옵션이 활성화 되면 $CATALINA_BASE/logs/catalina.out 로그파일에 GC 에 대한 수행시간 데이터가 보여진다.

- JVM 이 올바르게 튜닝되었다면 99% 의 경우 GC 는 1초 미만이 걸릴테고 Full GC 발생 시 몇초가 걸릴수 있지만 10초 이상 걸리는 경우는 거의 없어야 한다.

- DBCP_2 의 경우 maxWaitMillis 파라미터를 확인하여 DB Connection 타임아웃 설정이 가능하며, 10~15초로 설정되어있는지 확인해보자. (기본값: -1, 무한)


2. 랜덤한 Connection Closed Exception

- 이는 하나의 (Request)요청이 Connection Pool 에서 DB Connection 을 가져와서 두번 close 했을 때 발생할 수 있다.
- Connection Pool 을 사용할 때 Connection 을 close 하는것은 다른 요청이 해당 Connection 을 재사용 할 수 있도록 Pool 에 다시 반환하는 것이지 실제로 연결이 닫히는 것은 아니다.
- 그리고 Tomcat 은 동시 요청을 처리하기 위해서 여러 스레드를 사용하는데 아래는 Tomcat 에서 해당 오류가 발생할 수 있는 이벤트 순서의 경우이다.


1. Thead-1 에서 실행중인 요청-1이 DB Connection 가져옴.

2. 요청-1 이 DB Connection 을 close 한다.

3. JVM 이 실행 Thread 를 Thread-2 로 전환한다.

4. Thread-2 에서 실행중인 요청-2 가 DB Connection 가져옴
  (요청-1 이 방금 close 한 동일한 Connection)

5. JVM 이 실행 Thread 를 다시 Thread-1 로 전환한다.

6. 요청-1 이 finally 블록에서 DB Connection 을 두번째로 닫는다.

7. JVM 이 실행 Thread 를 다시 Thread-2 로 전환한다.

8. Thread-2 의 요청-2 가 DB Connection 을 사용하려고 하지만 요청-1 이 close 를 했기 때문에 실패한다.


- 아래는 Connection Pool 에서 얻은 데이터베이스 연결을 사용하는 올바르게 작성된 코드이다.


3. Context vs GlobalNamingResources

- 위의 지침에서 JNDI 선언을 Context 요소에 선언했지만, 이러한 선언을 서버 구성파일의 GlobalNamingResources 섹션에 배치하는것도 가능하며 해당 섹션에 배치된 리소스는 서버의 Context 들 간에 공유된다.


4. JNDI Resource Naming 과 Realm 상호작용

- Realm 이 작동하도록 하려면, realm 은 <ResourceLink> 를 사용하여 <GlobalNamingResources> 또는 <Context> 섹션에 정의된 DataSource 를 참조해야 한다.



참고
 - https://tomcat.apache.org/tomcat-11.0-doc/jndi-datasource-examples-howto.html

댓글