"org.apache.commons.dbcp2"에 있는 "BasicDataSource"를 이용하여 데이터베이스 커넥션 풀(Pool)을 생성하여 이용하겠습니다.
1. "test2" 프로젝트의 "pom.xml"을 오픈하여 "commons-dbcp2"는 주석 처리를 해제하고 "tomcat-jdbc"는 주석처리합니다.
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-dbcp2</artifactId>
<version>2.8.0</version>
</dependency>
<!--
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-jdbc</artifactId>
<version>8.5.68</version>
</dependency>
-->
작업을 위해 JNDI 이용을 위해 설정한 정보들을 주석처리합니다.
2. "Servers"에서 "Tomcat8-config"이나 "Tomcat9-config"의 "server.xml"의 "<GlobalNamingResources>"태그안에 "<Resource>"를 주석처리합니다.
<!--
<Resource name="jdbc/test"
auth="Container"
type="javax.sql.DataSource"
driverClassName="org.mariadb.jdbc.Driver"
url="jdbc:mariadb://localhost:3306/test"
username="root"
password="password"
initialSize="20"
maxTotal="100"
maxIdle="100"
minIdle="20"
maxWaitMillis="30000"
/>
-->
"context.xml"의 "<Context>"태그안에 "<ResourceLink>"를 주석처리합니다.
<!--
<ResourceLink global="jdbc/test" name="jdbc/test" type="javax.sql.DataSource"/>
-->
"web.xml"의 를 주석처리합니다.
<!--
<resource-ref>
<description>MariaDB Connection Pool</description>
<res-ref-name>jdbc/test</res-ref-name>
<res-type>javax.sql.DataSource</res-type>
<res-auth>Container</res-auth>
</resource-ref>
-->
3. "test2" 프로젝트의 "Java Resources/src/main/java"에서 "com.hom.project.test2.database"에서 오른쪽 버튼을 클릭하여 컨텍스트 메뉴 [New > Class]를 클릭하고 "TestDatabaseDataSource.java"를 생성합니다.

데이터소스 객체와 데이터베이스 프로퍼티, 커넥션 풀이 생성되었는지 확인하기 위해 부울값 필드를 추가하고 커넥션 풀 생성 여부 확인 메서드를 추가합니다.
/**
* 데이터소스 객체
*/
private BasicDataSource dataSource = null;
/**
* 데이터베이스 프로퍼티
*/
private Properties databaseInfo = new Properties();
/**
* 데이터베이스 커넥션 풀 생성 여부
*/
private boolean initDatabasePool = false;
/**
* 데이터베이스 커넥션 풀 생성 여부를 가져옵니다.
* @return 데이터베이스 커넥션 풀 생성 여부
*/
public boolean isInitDatabasePool() {
return initDatabasePool;
}
데이터베이스 프로퍼티(properties) 파일을 읽어 프로퍼티로 로드하게 메소드를 추가합니다.
/**
* 데이터베이스 매니저 프로퍼티를 로드하고 검증합니다.
* @param databaseManagerPath 데이터베이스 매니저 프로퍼티 경로
* @return 프로퍼티 로드 여부
*/
private boolean loadProperties(String databaseManagerPath) {
boolean result = true;
InputStream inputStream = getClass().getResourceAsStream(databaseManagerPath);
if (inputStream != null) {
try {
databaseInfo.load(inputStream);
System.out.println("데이터베이스 프로퍼티를 읽음");
String id = databaseInfo.getProperty("id");
if (id == null || id.trim().isEmpty()) {
System.out.println("데이터베이스 프로퍼티에 id가 없음");
result = false;
}
String driverClassName = databaseInfo.getProperty("driverClassName");
if (driverClassName == null || driverClassName.trim().isEmpty()) {
System.out.println("데이터베이스 프로퍼티에 driverClassName이 없음");
result = false;
}
String url = databaseInfo.getProperty("url");
if (url == null || url.trim().isEmpty()) {
System.out.println("데이터베이스 프로퍼티에 url이 없음");
result = false;
}
String username = databaseInfo.getProperty("username");
if (username == null || username.trim().isEmpty()) {
System.out.println("데이터베이스 프로퍼티에 username이 없음");
result = false;
}
String password = databaseInfo.getProperty("password");
if (password == null || password.trim().isEmpty()) {
System.out.println("데이터베이스 프로퍼티에 password가 없음");
result = false;
}
setPropertyInit("initialSize", "0");
setPropertyInit("maxWaitMillis", "0");
setPropertyInit("maxTotal", "8");
setPropertyInit("maxIdle", "8");
setPropertyInit("validationQuery", "");
setPropertyInit("validationQueryTimeoutSeconds", "-1");
setPropertyInit("poolStatements", "False");
setPropertyInit("maxOpenPreparedStatements", "-1");
setPropertyInit("maxConnLifetimeMillis", "-1");
setPropertyInit("minIdle", "0");
setPropertyInit("testOnBorrow", "False");
setPropertyInit("testOnReturn", "False");
setPropertyInit("testOnCreate", "False");
setPropertyInit("testWhileIdle", "False");
setPropertyInit("evictionPolicyClassName", "org.apache.commons.pool2.impl.DefaultEvictionPolicy");
setPropertyInit("timeBetweenEvictionRunsMillis", "-1");
setPropertyInit("numTestsPerEvictionRun", "3");
setPropertyInit("minEvictableIdleTimeMillis", "1800000");
setPropertyInit("softMinEvictableIdleTimeMillis", "-1");
setPropertyInit("removeAbandonedOnBorrow", "False");
setPropertyInit("removeAbandonedTimeout", "300000");
setPropertyInit("logAbandoned", "False");
if (!result) {
System.out.println("데이터베이스 프로퍼티 정보가 정확하지 않음");
}
} catch (IOException e) {
System.out.println("데이터베이스 프로퍼티를 읽지 못함");
result = false;
e.printStackTrace();
} finally {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
} else {
System.out.println("데이터베이스 프로퍼티를 읽지 못함");
result = false;
}
return result;
}
프로퍼티의 키가 값이 없을 경우 프로퍼티 키와 기본값을 설정할 수 있게 "setPropertyInit()"메소드를 생성합니다.
/**
* 프로퍼티 키가 비어 있을 경우에 기본값을 설정합니다.
* @param propertyKey 프로퍼티 키
* @param defaultValue 기본값
*/
private void setPropertyInit(String propertyKey, String defaultValue) {
String propertyValue = databaseInfo.getProperty(propertyKey);
if (propertyValue == null || propertyValue.trim().isEmpty()) {
if (defaultValue == null || defaultValue.trim().isEmpty()) {
System.out.println("데이터베이스 프로퍼티에 " + propertyKey + "가 없어 기본값(공백)으로 설정합니다.");
defaultValue = "";
} else {
System.out.println("데이터베이스 프로퍼티에 " + propertyKey + "가 없어 기본값(" + defaultValue + ")으로 설정합니다.");
}
databaseInfo.setProperty(propertyKey, defaultValue);
}
}
"id"로 여러개의 데이터베이스들을 구분하여 관리 할 수 있도록 메소드를 추가합니다.
/**
* 데이터베이스 프로퍼티에 id를 가져옵니다.
* @return id
*/
public String getId() {
return databaseInfo.getProperty("id") == null ? "" : databaseInfo.getProperty("id").trim();
}
org.apache.commons.dbcp2.BasicDataSource"를 이용하여 데이터소스를 생성하는 메소드를 추가합니다.
"BasicDataSource"의 인스턴스를 생성하고 데이터베이스 드라이버와 데이터베이스 연결 정보를 설정합니다.
String driverClassName = databaseInfo.getProperty("driverClassName");
String url = databaseInfo.getProperty("url");
String username = databaseInfo.getProperty("username");
String password = databaseInfo.getProperty("password");
dataSource = new BasicDataSource();
// 데이터베이스 드라이버
dataSource.setDriverClassName(driverClassName);
// 데이터베이스와 연결 URL
dataSource.setUrl(url);
// 데이터베이스와 연결 계정
dataSource.setUsername(username);
// 데이터베이스와 연결 패스워드
dataSource.setPassword(password);
풀(Pool)에서 생성하고 유지되는 커넥션 수와 대기 시간을 설정합니다.
String initialSizeInfo = databaseInfo.getProperty("initialSize");
int initialSize = Integer.parseInt(initialSizeInfo);
String maxTotalInfo = databaseInfo.getProperty("maxTotal");
int maxTotal = Integer.parseInt(maxTotalInfo);
String maxIdleInfo = databaseInfo.getProperty("maxIdle");
int maxIdle = Integer.parseInt(maxIdleInfo);
String maxWaitMillisInfo = databaseInfo.getProperty("maxWaitMillis");
long maxWaitMillis = Long.parseLong(maxWaitMillisInfo);
String minIdleConnection = databaseInfo.getProperty("minIdle");
int minIdle = Integer.parseInt(minIdleConnection);
// 최초에 데이터베이스와 연결되는 커넥션 수
dataSource.setInitialSize(initialSize);
// 동시에 사용할 수 있는 최대 커넥션 개수
dataSource.setMaxTotal(maxTotal);
// 커넥션이 반환될 때 최대로 유지될 수 있는 커넥션 개수
dataSource.setMaxIdle(maxIdle);
// 커넥션을 가져오기 위해 대기하기 최대 시간
dataSource.setMaxWaitMillis(maxWaitMillis);
// 커넥션이 반환될 때 최소로 유지될 수 있는 커넥션 개수
dataSource.setMinIdle(minIdle);
커넥션이 유효한지 검사하는 쿼리와 응답시간을 설정합니다.
String validationQuery = databaseInfo.getProperty("validationQuery");
String validationQueryTimeoutSecondsInfo = databaseInfo.getProperty("validationQueryTimeoutSeconds");
int validationQueryTimeoutSeconds = Integer.parseInt(validationQueryTimeoutSecondsInfo);
// 커넥션이 유효한지 검사하기 위한 쿼리
dataSource.setValidationQuery(validationQuery);
// 커넥션이 유효한지 검사하는 쿼리가 실행되고 응답을 기다리는 시간
dataSource.setValidationQueryTimeout(validationQueryTimeoutSeconds);
커넥션마다 쿼리문을 풀링할지 여부와 풀링할 쿼리문 수를 설정합니다.
String poolStatementsInfo = databaseInfo.getProperty("poolStatements");
boolean poolStatements = Boolean.parseBoolean(poolStatementsInfo);
String maxOpenPreparedStatementsInfo = databaseInfo.getProperty("maxOpenPreparedStatements");
int maxOpenPreparedStatements = Integer.parseInt(maxOpenPreparedStatementsInfo);
// 커넥션마다 PreparedStatement 풀링 여부
dataSource.setPoolPreparedStatements(poolStatements);
// 커넥션마다 최대로 풀링할 PreparedStatement의 개수
dataSource.setMaxOpenPreparedStatements(maxOpenPreparedStatements);
커넥션의 생성후 최대로 이용 가능한 시간을 설정합니다.
String maxConnLifetimeMillisInfo = databaseInfo.getProperty("maxConnLifetimeMillis");
long maxConnLifetimeMillis = Long.parseLong(maxConnLifetimeMillisInfo);
// 커넥션의 생성후 최대로 이용 가능한 시간
dataSource.setMaxConnLifetimeMillis(maxConnLifetimeMillis);
풀(Pool)에서 사용되지 않는 커넥션들에 대한 유효성 검사를 설정합니다.
// 풀에서 사용되지 않는 커넥션들에 대한 유효성 검사
String testOnBorrowInfo = databaseInfo.getProperty("testOnBorrow");
boolean testOnBorrow = Boolean.parseBoolean(testOnBorrowInfo);
String testOnReturnInfo = databaseInfo.getProperty("testOnReturn");
boolean testOnReturn = Boolean.parseBoolean(testOnReturnInfo);
String testOnCreateInfo = databaseInfo.getProperty("testOnCreate");
boolean testOnCreate = Boolean.parseBoolean(testOnCreateInfo);
String testWhileIdleInfo = databaseInfo.getProperty("testWhileIdle");
boolean testWhileIdle = Boolean.parseBoolean(testWhileIdleInfo);
// 풀에서 커넥션을 가져올 때 커넥션이 유효한지 검사 여부
dataSource.setTestOnBorrow(testOnBorrow);
// 풀에 커넥션을 반환할 때 커넥션이 유효한지 검사 여부
dataSource.setTestOnReturn(testOnReturn);
// 풀에서 커넥션을 생성한 후 커넥션이 유효한지 검사 여부
dataSource.setTestOnCreate(testOnCreate);
// 풀에서 사용되지 않는 커넥션들에 대해 유효한지 검사 여부 (유효하지 않으면 제거)
dataSource.setTestWhileIdle(testWhileIdle);
풀(Pool)에서 사용되지 않는 커넥션들에 대한 추출 조건을 설정합니다.
// 풀에서 사용되지 않는 커넥션들에 대한 추출(제거)
String evictionPolicyClassName = databaseInfo.getProperty("evictionPolicyClassName");
String timeBetweenEvictionRunsMillisInfo = databaseInfo.getProperty("timeBetweenEvictionRunsMillis");
long timeBetweenEvictionRunsMillis = Long.parseLong(timeBetweenEvictionRunsMillisInfo);
String numTestsPerEvictionRunInfo = databaseInfo.getProperty("numTestsPerEvictionRun");
int numTestsPerEvictionRun = Integer.parseInt(numTestsPerEvictionRunInfo);
String minEvictableIdleTimeMillisInfo = databaseInfo.getProperty("minEvictableIdleTimeMillis");
long minEvictableIdleTimeMillis = Long.parseLong(minEvictableIdleTimeMillisInfo);
String softMinEvictableIdleTimeMillisInfo = databaseInfo.getProperty("softMinEvictableIdleTimeMillis");
long softMinEvictableIdleTimeMillis = Long.parseLong(softMinEvictableIdleTimeMillisInfo);
// 추출 정책 클래스 명 설정
dataSource.setEvictionPolicyClassName(evictionPolicyClassName);
// 풀에서 사용되지 않는 커넥션들에 대해 추출하는 Evictor 스레드의 검사 주기 (-1이면 검사하지 않음)
dataSource.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis);
// Evictor 스레드의 검사 주기 때마다 풀에서 사용되지 않는 커넥션 중 검사할 커넥션 수
dataSource.setNumTestsPerEvictionRun(numTestsPerEvictionRun);
// 풀에서 사용되지 않는 커넥션이 최소로 유지될 수 있는 시간 (최소 유지 시간이 초과되면 제거)
dataSource.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis);
// 풀에서 사용되지 않는 커넥션이 추출되기 전에 최소로 유지될 수 있는 시간
dataSource.setSoftMinEvictableIdleTimeMillis(softMinEvictableIdleTimeMillis);
풀(Pool)에서 버려진 커넥션들에 대한 복구 조건을 설정합니다.
// 풀에서 버려진 커넥션들에 대한 복구(재생)
String removeAbandonedOnBorrowInfo = databaseInfo.getProperty("removeAbandonedOnBorrow");
boolean removeAbandonedOnBorrow = Boolean.parseBoolean(removeAbandonedOnBorrowInfo);
String removeAbandonedTimeoutInfo = databaseInfo.getProperty("removeAbandonedTimeout");
int removeAbandonedTimeout = Integer.parseInt(removeAbandonedTimeoutInfo);
// 풀에서 커넥션을 가져올 때 버려진 커넥션을 찾아 제거할지 여부
dataSource.setRemoveAbandonedOnBorrow(removeAbandonedOnBorrow);
// 풀에서 버려진 커넥션으로 인정할 수 있는 시간
dataSource.setRemoveAbandonedTimeout(removeAbandonedTimeout);
풀(Pool)에서 버려진 커넥션들에 대한 로그 저장 여부를 설정합니다.
String logAbandonedInfo = databaseInfo.getProperty("logAbandoned");
boolean logAbandoned = Boolean.parseBoolean(logAbandonedInfo);
// 버려진 커넥션에 대한 로그 저장 여부
dataSource.setLogAbandoned(logAbandoned);
데이터소스를 시작하여 데이터베이스 커넥션 풀(Pool)을 생성합니다.
try {
dataSource.start();
initDatabasePool = true;
System.out.println("데이터베이스 커넥션 풀을 생성함");
} catch (SQLException e) {
e.printStackTrace();
System.out.println("시스템 문제로 인해 데이터베이스 커넥션 풀을 생성하지 못함");
}
위에서 설명한 코드 전체를 "initDatabasePool()"메소드로 생성합니다.
/**
* 데이터베이스 커넥션 풀을 생성합니다.
*/
private void initDatabasePool() {
{Code}
}

클래스 생성자를 통해 데이터베이스 프로퍼티 파일을 읽고 데이터베이스 커넥션 풀(Pool)을 생성하게 생성자를 추가합니다.
/**
* 생성자
* @param databaseManagerPath 데이터베이스 매니저 프로퍼티 경로
*/
public TestDatabaseDataSource(String databaseManagerPath) {
if (loadProperties(databaseManagerPath)) {
initDatabasePool();
}
}
데이터소스를 통해 커넥션을 가져오는 메소드를 추가합니다.
/**
* 데이터소스를 통해 데이터베이스 연결 객체를 가져옵니다.
* @return 데이터베이스 연결 객체
*/
public Connection getConnection() {
Connection connection = null;
try {
connection = dataSource.getConnection();
System.out.println("[" + Thread.currentThread().getName() + "] 데이터베이스 커넥션 객체[" + connection.toString() + "]를 가져옴");
} catch (SQLException e) {
System.out.println("[" + Thread.currentThread().getName() + "] 데이터베이스 커넥션 객체를 가져오지 못함");
e.printStackTrace();
}
return connection;
}
커넥션을 반환하는 메소드를 추가합니다.
/**
* 데이터소스에 데이터베이스 연결 객체를 반환합니다.
* @param connection 데이터베이스 연결 객체
*/
public void closeConnection(Connection connection) {
try {
System.out.println("[" + Thread.currentThread().getName() + "] 데이터베이스 커넥션 객체[" + connection.toString() + "]를 반환함");
connection.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
데이터소스를 파괴하는 메소드를 추가합니다.
/**
* 데이터소스를 파괴합니다.
*/
public void destroyDatabasePool() {
try {
dataSource.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
4. "test2" 프로젝트의 "Java Resources"에 "src/main/resources"에서 "/com/home/project/test2/config/database-maria.properties"파일에서 "jndi"를 주석처리하고 다른 설정들은 주석을 해제합니다. 그리고 "removeAbandonedOnBorrow", "removeAbandonedTimeout", "logAbandoned" 등 추가로 설정합니다.
id=databaseMaria
#jndi=java:/comp/env/jdbc/test
driverClassName=org.mariadb.jdbc.Driver
url=jdbc:mariadb://localhost:3306/test
username=root
password=password
initialSize=20
maxWaitMillis=-1
maxTotal=20
maxIdle=20
validationQuery=select 1
validationQueryTimeoutSeconds=-1
poolStatements=True
maxOpenPreparedStatements=40
maxConnLifetimeMillis=-1
#blockWhenExhausted=True
minIdle=20
testOnBorrow=False
testOnReturn=False
testOnCreate=False
testWhileIdle=True
evictionPolicyClassName=org.apache.commons.pool2.impl.DefaultEvictionPolicy
timeBetweenEvictionRunsMillis=-1
numTestsPerEvictionRun=3
minEvictableIdleTimeMillis=1800000
softMinEvictableIdleTimeMillis=-1
removeAbandonedOnBorrow=True
removeAbandonedTimeout=300000
logAbandoned=False
5. "test2" 프로젝트의 "Java Resources/src/main/java"에서 "com.home.project.test2.listener.TestServletContextListener"의 "contextInitialized()"메소드에 있는 "databaseMappingMap"의 인자를 "TestDatabaseJNDI"에서 "TestDatabaseDataSource"로 수정합니다.
// DBId-Database 맵
Map<String, TestDatabaseDataSource> databaseMappingMap = new HashMap<String, TestDatabaseDataSource>();
String databaseManagers = sce.getServletContext().getInitParameter("DatabaseManager");
if (databaseManagers != null && !databaseManagers.trim().isEmpty()) {
String[] databaseManagerPaths = databaseManagers.trim().split("\n|,|;|\\s+");
for (String databaseManagerPath : databaseManagerPaths) {
databaseManagerPath = databaseManagerPath.trim();
if (!databaseManagerPath.isEmpty()) {
TestDatabaseDataSource testDatabaseManager = new TestDatabaseDataSource(databaseManagerPath);
if (testDatabaseManager.isInitDatabasePool()) {
databaseMappingMap.put(testDatabaseManager.getId(), testDatabaseManager);
}
}
}
}
"contextInitialized()"메소드에서 DAO를 "databaseMappingMap"의 형 변환을 "TestDatabaseJNDI"에서 "TestDatabaseDataSource"로 수정합니다.
if (repositoryeInstance != null) {
Field[] Fields = findClass.getDeclaredFields();
for (Field field : Fields) {
TestAnnAutowired testAnnAutowired = field.getAnnotation(TestAnnAutowired.class);
TestAnnQualifier testAnnQualifier = field.getAnnotation(TestAnnQualifier.class);
if (testAnnAutowired != null && testAnnQualifier != null) {
System.out.println("[Repository Field] " + field.getType().getName() + " 타입의 " + field.getName() + " 맴버 필드을 찾음");
TestDatabaseDataSource testDatabaseManager = (TestDatabaseDataSource) databaseMappingMap.get(testAnnQualifier.value());
if (testDatabaseManager != null) {
field.setAccessible(true);
try {
field.set(repositoryeInstance, testDatabaseManager);
System.out.println("[Repository Field] " + field.getName() + "에 의존성 주입을 처리함");
} catch (IllegalArgumentException | IllegalAccessException e) {
e.printStackTrace();
System.out.println("[Repository Field] " + field.getName() + "에 의존성 주입을 처리하지 못함");
}
}
}
}
}
"contextDestroyed()"메소드에서 "databaseMappingMap"의 인자를 "TestDatabaseJNDI"에서 "TestDatabaseDataSource"로 수정합니다.
public void contextDestroyed(ServletContextEvent sce) {
System.out.println("ServletContext 파괴 실행");
@SuppressWarnings("unchecked")
Map<String, TestDatabaseDataSource> databaseMappingMap = (Map<String, TestDatabaseDataSource>)sce.getServletContext().getAttribute("DatabaseMappingMap");
Iterator<String> keyIter = databaseMappingMap.keySet().iterator();
while (keyIter.hasNext()) {
String dbId = (String) keyIter.next();
TestDatabaseDataSource testDatabaseManager = databaseMappingMap.get(dbId);
testDatabaseManager.destroyDatabasePool();
}
}
6. "test2" 프로젝트의 "Java Resources/src/main/java"에서 "com.home.project.test2.dao.TestLoginDaoImpl.java"의 멤버 필드 형을 "TestDatabaseJNDI"에서 "TestDatabaseDataSource"로 수정합니다.
@TestAnnAutowired
@TestAnnQualifier("databaseMaria")
private TestDatabaseDataSource testDatabaseManager;
7. 디비버(DBeaver) 툴을 실행하고 쿼리 스크립트에 현재 연결된 커넥션 수를 확인하면 최초에 데이터베이스와 연결되는 커넥션 수만큼 생성된 것을 확인할 수 있습니다.

결과 값이 23인 것은 디비버(DBeaver) 툴이 실행되면서 연결되는 커넥션 3개가 포함되어있기 때문입니다.
8. "Servers"탭에서 "tomcat8"를 선택하고 "start"버튼(start the server)을 클릭합니다. 웹 브라우저에서 "http://localhost:8080/test2/testform.do"를 입력합니다. 아이디에는 "testid"를 패스워드에는 "testpwd"를 입력하고 "로그인"버튼을 클릭합니다.

"Console"탭을 보면 JNDI를 통해 커넥션을 가져와 쿼리가 실행된 것을 확인할 수 있습니다.

[Console]
id : testid, password : testpwd
[http-nio-8080-exec-4] 데이터베이스 커넥션 객체[954616923, URL=jdbc:mariadb://localhost:3306/test, MariaDB Connector/J]를 가져옴
PreparedStatement를 생성함
Query[sql : 'SELECT MBR_ID, MBR_PWD, MBR_PWD_SALT, MBR_NM FROM MBR_ACCOUNT_TB WHERE MBR_ID=?', parameters : ['testid']]를 실행함
PreparedStatement를 종료함
[http-nio-8080-exec-4] 데이터베이스 커넥션 객체[954616923, URL=jdbc:mariadb://localhost:3306/test, MariaDB Connector/J]를 반환함
43ms 소요됨