Java 프레임워크 만들기 - JSP

자바 데이터베이스 커넥션 풀 매니저 만들기 5 - Java Database Connection Pool Manager, 이클립스(Eclipse)

carrotweb 2021. 6. 18. 21:29
728x90
반응형

커넥션 풀에서 모든 커넥션을 사용하고 있다면 커넥션 대기 시간(maxWait)에 따라 대기해야 합니다. 데이터베이스와 시스템의 성능이 좋을 경우 커넥션을 대기하지 않고 추가로 커넥션을 생성하여 사용하는 것이 좋습니다. 그렇지만 무한정 커넥션을 생성하는 것은 데이터베이스와 시스템에 부하가 발생하여 문제가 될 수 있습니다. 그래서 동시에 사용할 수 있는 최대 커넥션 개수(maxActive)와 커넥션이 반환될 때 최대로 유지될 수 있는 커넥션 개수(maxIdle)을 설정하여 효율적으로 관리되게 하겠습니다.

1. "test2" 프로젝트의 "Java Resources/src/main/resources"에서 "/com/home/project/test2/config/database-maria.properties"파일에 커넥션 대기 시간(Connection Pool) 크기를 추가합니다.

id=databaseMaria
driverClassName=org.mariadb.jdbc.Driver
url=jdbc:mariadb://localhost:3306/test
username=root
password=password
initialSize=1
maxWait=1000
maxActive=2
maxIdle=1

"maxActive"는 동시에 사용할 수 있는 최대 커넥션 개수

"maxIdle"는 커넥션이 반환될 때 최대로 유지될 수 있는 커넥션 개수

테스트를 위해 동시에 사용할 수 있는 최대 커넥션 개수(maxActive)는 2개로, 커넥션이 반환될 때 최대로 유지될 수 있는 커넥션 개수(maxIdle)을 1개로 설정합니다.

2. "test2" 프로젝트의 "Java Resources/src/main/java"에서 "com.hom.project.test2.database"에 있는"TestDatabasePoolManager.java"의 "loadProperties()"메소드에 동시에 사용할 수 있는 최대 커넥션 개수(maxActive)커넥션이 반환될 때 최대로 유지될 수 있는 커넥션 개수(maxIdle)을 추가하고 기본값을 설정하게 합니다.

/**
 * 데이터베이스 매니저 프로퍼티를 로드하고 검증합니다.
 * @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;
			}
			String initialSize = databaseInfo.getProperty("initialSize");
			if (initialSize == null || initialSize.trim().isEmpty()) {
				System.out.println("데이터베이스 프로퍼티에 initialSize가 없어 기본값(8)으로 설정합니다.");
				databaseInfo.setProperty("initialSize", "8");
			}
			String maxWait = databaseInfo.getProperty("maxWait");
			if (maxWait == null || maxWait.trim().isEmpty()) {
				System.out.println("데이터베이스 프로퍼티에 maxWait가 없어 기본값(무제한)으로 설정합니다.");
				databaseInfo.setProperty("maxWait", "0");
			}
			String maxActive = databaseInfo.getProperty("maxActive");
			if (maxActive == null || maxActive.trim().isEmpty()) {
				System.out.println("데이터베이스 프로퍼티에 maxActive가 없어 기본값(8)으로 설정합니다.");
				databaseInfo.setProperty("maxActive", "8");
			}
			String maxIdle = databaseInfo.getProperty("maxIdle");
			if (maxIdle == null || maxIdle.trim().isEmpty()) {
				System.out.println("데이터베이스 프로퍼티에 maxIdle가 없어 기본값(8)으로 설정합니다.");
				databaseInfo.setProperty("maxIdle", "8");
			}

			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;
}

"maxActive"와 "maxIdle"는 기본값으로 커넥션 수를 8개로 설정합니다.

커넥션 풀에서 모든 커넥션을 사용하고 있을 때 커넥션 풀의 개수(connections.size())와 동시에 사용할 수 있는 최대 커넥션 개수(maxActive)를 비교하여 추가로 커넥션을 생성하고 커넥션 풀에 추가하고 리턴하게 합니다.

3. "test2" 프로젝트의 "Java Resources/src/main/java"에서 com.hom.project.test2.database"에 있는 "TestDatabasePoolManager.java"의 "getConnection()"메소드를 수정합다.

/**
 * 데이터베이스 커넥션 풀에서 데이터베이스 연결 객체를 가져옵니다.
 * @return 데이터베이스 연결 객체
 */
public Connection getConnection() {
	Connection connection = null;

	if (initDatabasePool) {
		String maxWait = databaseInfo.getProperty("maxWait");
		int maxWaitTime = Integer.parseInt(maxWait);
		String maxActive = databaseInfo.getProperty("maxActive");
		int maxActiveConnection = Integer.parseInt(maxActive);

		long startWaitTime = System.currentTimeMillis();

		while(true) {
			Iterator<TestDatabaseConnection> iterator = connections.iterator();
			while(iterator.hasNext()) {
				TestDatabaseConnection connectionObject = iterator.next();
				if (connectionObject != null) {
					synchronized(this) {
						if (!connectionObject.isConnect()) {
							connection = connectionObject.getConnection();
							connectionObject.setConnect(true);
							System.out.println("[" + Thread.currentThread().getName() + "] 데이터베이스 커넥션 객체[" + connectionObject.toString() + "]를 가져옴");
						}
					}
					if (connection != null) {
						break;
					}
				}
			}
			if (connection == null) {
				if (maxActiveConnection > connections.size()) {
					connection = createConnection();
					if (connection != null) {
						TestDatabaseConnection connectionObject = new TestDatabaseConnection(connection, true);
						connections.addElement(connectionObject);
						System.out.println("추가로 데이터베이스와 연결된 커넥션 객체[" + connectionObject.toString() + "]를 생성하고 커넥션 풀에 추가하고 가져옴");
						break;
					}
				}

				System.out.println("[" + Thread.currentThread().getName() + "] 사용하지 않는 데이터베이스 커넥션 객체가 없어 대기함");

				long endWaitTime = System.currentTimeMillis();
				if ((endWaitTime - startWaitTime) >= maxWaitTime) {
					System.out.println("[" + Thread.currentThread().getName() + "] 대기 시간(" + (endWaitTime - startWaitTime) + "ms)이 초과되어 데이터베이스 커넥션 객체 가져오기를 중지함");
					break;
				} else {
					try {
						Thread.sleep(100);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
			} else {
				break;
			}
		}
		if (connection == null) {
			System.out.println("[" + Thread.currentThread().getName() + "] 데이터베이스 커넥션 객체를 가져오지 못함");
		}
	} else {
		System.out.println("생성된 데이터베이스 커넥션 풀이 없음");
	}

	return connection;
}

 

커넥션을 반환할 때 커넥션 풀의 개수(connections.size())와 커넥션이 반환될 때 최대로 유지될 수 있는 커넥션 개수(maxIdle)비교하여 사용하지 않는 커넥션을 데이터베이스와 연결 종료하고 커넥션 풀에서 제거 하게 합니다.

 

 

4. "test2" 프로젝트의 "Java Resources/src/main/java"에서 com.hom.project.test2.database"에 있는 "TestDatabasePoolManager.java"의 "closeConnection()"메소드를 수정합다.

/**
 * 데이터베이스 커넥션 풀에 데이터베이스 연결 객체를 반환합니다.
 * @param connection 데이터베이스 연결 객체
 */
public void closeConnection(Connection connection) {
	if (initDatabasePool) {
		Iterator<TestDatabaseConnection> iterator = connections.iterator();
		while(iterator.hasNext()) {
			TestDatabaseConnection connectionObject = iterator.next();
			if (connectionObject != null) {
				if (connectionObject.getConnection() == connection) {
					try {
						connection.clearWarnings();
					} catch (SQLException e) {
						e.printStackTrace();
					}
					connectionObject.setConnect(false);
					System.out.println("[" + Thread.currentThread().getName() + "] 데이터베이스 커넥션 객체[" + connectionObject.toString() + "]를 반환함");
					break;
				}
			}
		}

		String maxIdle = databaseInfo.getProperty("maxIdle");
		int maxIdleConnection = Integer.parseInt(maxIdle);

		if (connections.size() > maxIdleConnection) {
			Iterator<TestDatabaseConnection> removeiterator = connections.iterator();
			while(removeiterator.hasNext()) {
				TestDatabaseConnection connectionObject = removeiterator.next();
				if (connectionObject != null) {
					synchronized(this) {
						if (!connectionObject.isConnect()) {
							try {
								connectionObject.getConnection().close();
							} catch (SQLException e) {
								e.printStackTrace();
							}
							removeiterator.remove();
							System.out.println("데이터베이스와 연결 종료하고 커넥션 객체[" + connectionObject.toString() + "]를 커넥션 풀에서 제거함");
							break;
						}
					}
				}
			}
		}
	}
}

 

5. "Servers"탭에서 "tomcat8"를 선택하고 "start"버튼(start the server)을 클릭합니다. 두개의 웹 브라우저에서 "http://localhost:8080/test2/testform.do"를 입력합니다. 두개의 웹 브라우저에 아이디에는 "testid"를 패스워드에는 "testpwd"를 입력합니다.

두개의 웹 브라우저 "로그인"버튼을 순차적으로 클릭합니다.

"Console"탭을 보면 커넥션 풀에서 모든 커넥션을 사용하고 있을 때 커넥션 풀의 개수(connections.size())보다 동시에 사용할 수 있는 최대 커넥션 개수(maxActive)를 크면 추가로 커넥션을 생성하고 커넥션 풀에 추가하고 리턴하는 것을 확인 할 수 있습니다. 그리고 커넥션을 반환할 때 커넥션 풀의 개수(connections.size())가 커넥션이 반환될 때 최대로 유지될 수 있는 커넥션 개수(maxIdle)보다 크면 사용하지 않는 커넥션을 데이터베이스와 연결 종료하고 커넥션 풀에서 제거 하는 것을 확인 할 수 있습니다.

[Console]

contextPath : /test2
requestURI : /test2/loginprocess.do
changed requestURI : /loginprocess.do
ServletRequest에 속성 추가 - 속성명 : returnUrl, 속성 값 : /test2/testform.do
id : testid, password : testpwd
[http-nio-8080-exec-4] 데이터베이스 커넥션 객체[com.home.project.test2.database.TestDatabaseConnection@466109e7]를 가져옴
PreparedStatement를 생성함
Query[sql : 'SELECT MBR_ID, MBR_PWD, MBR_PWD_SALT, MBR_NM FROM MBR_ACCOUNT_TB WHERE MBR_ID=?', parameters : ['testid']]를 실행함
PreparedStatement를 종료함
testWhileProc : PreparedStatement를 생성함
{중간 생략}
contextPath : /test2
requestURI : /test2/loginprocess.do
changed requestURI : /loginprocess.do
ServletRequest에 속성 추가 - 속성명 : returnUrl, 속성 값 : /test2/testform.do
id : testid, password : testpwd
데이터베이스에 연결됨
추가로 데이터베이스와 연결된 커넥션 객체[com.home.project.test2.database.TestDatabaseConnection@7beaea6]를 생성하고 커넥션 풀에 추가하고 가져옴
PreparedStatement를 생성함
Query[sql : 'SELECT MBR_ID, MBR_PWD, MBR_PWD_SALT, MBR_NM FROM MBR_ACCOUNT_TB WHERE MBR_ID=?', parameters : ['testid']]를 실행함
PreparedStatement를 종료함
testWhileProc : PreparedStatement를 생성함
testWhileProc : PreparedStatement를 종료함
[http-nio-8080-exec-4] 데이터베이스 커넥션 객체[com.home.project.test2.database.TestDatabaseConnection@466109e7]를 반환함
데이터베이스와 연결 종료하고 커넥션 객체[com.home.project.test2.database.TestDatabaseConnection@466109e7]를 커넥션 풀에서 제거함
2048ms 소요됨
{중간 생략}
testWhileProc : PreparedStatement를 종료함
[http-nio-8080-exec-5] 데이터베이스 커넥션 객체[com.home.project.test2.database.TestDatabaseConnection@7beaea6]를 반환함
2109ms 소요됨

첫번째 웹 브라우저의 스레드 명은 "http-nio-8080-exec-4"이고 두번째 웹 브라우저의 스레드 명은 "http-nio-8080-exec-5"입니다.

"로그인"버튼을 먼저 누른 첫번째 웹 브라우저가 먼저 커넥션을 가져갑니다.

[http-nio-8080-exec-4] 데이터베이스 커넥션 객체[com.home.project.test2.database.TestDatabaseConnection@466109e7]를 가져옴

그리고 쿼리를 실행합니다.

PreparedStatement를 생성함
PreparedStatement를 종료함
testWhileProc : PreparedStatement를 생성함

두번째 웹 브라우저가 커넥션을 요청할 때 사용 가능한 커넥션이 없습니다. 그러나 최대 커넥션 개수(maxActive)가 커넥션 풀의 개수(connections.size())보다 크기 때문에 추가로 커넥션을 생성하여 커넥션 풀에 추가합니다.

데이터베이스에 연결됨
추가로 데이터베이스와 연결된 커넥션 객체[com.home.project.test2.database.TestDatabaseConnection@7beaea6]를 생성하고 커넥션 풀에 추가하고 가져옴

그리고 쿼리를 실행합니다.

PreparedStatement를 생성함
PreparedStatement를 종료함
testWhileProc : PreparedStatement를 생성함

첫번째 웹 브러우저가 쿼리를 끝내고 커넥션을 반환합니다. 그리고 커넥션 풀의 개수(connections.size())가 커넥션이 반환될 때 최대로 유지될 수 있는 커넥션 개수(maxIdle)보다 크기 때문에 데이터베이스와 연결을 종료하고 커넥션을 제거합니다.

testWhileProc : PreparedStatement를 종료함
[http-nio-8080-exec-4] 데이터베이스 커넥션 객체[com.home.project.test2.database.TestDatabaseConnection@466109e7]를 반환함
데이터베이스와 연결 종료하고 커넥션 객체[com.home.project.test2.database.TestDatabaseConnection@466109e7]를 커넥션 풀에서 제거함
2048ms 소요됨

두번째 웹 브러우저가 쿼리를 끝내고 커넥션을 반환합니다.

testWhileProc : PreparedStatement를 종료함
[http-nio-8080-exec-5] 데이터베이스 커넥션 객체[com.home.project.test2.database.TestDatabaseConnection@7beaea6]를 반환함
2109ms 소요됨

 

이처럼 동시에 사용할 수 있는 최대 커넥션 개수(maxActive)와 커넥션이 반환될 때 최대로 유지될 수 있는 커넥션 개수(maxIdle)를 이용하여 데이터베이스 커넥션 풀을 효율적으로 관리할 수 있습니다.

데이터베이스 커넥션 풀을 설정할 때 최초 생성되는 데이터베이스와 연결되는 커넥션 수(initialSize), 동시에 사용할 수 있는 최대 커넥션 개수(maxActive), 커넥션이 반환될 때 최대로 유지될 수 있는 커넥션 개수(maxIdle)를 고려해서 설정해야 합니다.

[ maxActive >= initialSize ]

maxActive(5)가 initialSize(10)보다 작다면 추가로 커넥션을 생성할 수가 없습니다.

[ maxActive = maxIdle ]

maxActive(3)가 maxIdle(5)보다 작다면 커넥션이 반환될 때 커넥션을 제거 할 수 없습니다. 그리고 maxActive(8)가 maxIdle(5)보다 크다면 커넥션이 반환될 때 maxIdle의 영향으로 커넥션을 제거합니다. 이는 커넥션을 매번 생성하고 제거함으로 시간이 지연됩니다.

순간적으로 동시 접속이 빈번하게 발생한다면 maxActive과 maxIdle를 동시에 동일하게 높이는 것이 좋습니다. 그러나 무한정 높일 수 없기 때문에 적절하게 maxActive를 maxIdle보다 크게 설정하는 것도 좋습니다.

initialSize와 maxActive, maxIdle를 동일하게 설정하거나 maxActive, maxIdle를 initialSize보다 높게 설정하시면 됩니다.

(maxActive >= maxIdle) >= initialSize

또한, maxActive는 데이터베이스의 성능과 서버(Server)의 수(이중화), WAS(Web Application Server)의 수에 따라 적설하게 설정해야 합니다.

 

이쁜만 아니라 커넥션 풀에서 최소한으로 유지할 커넥션 개수(minIdle)와 커넥션 유호성를 검사하기 위한 유호성 검사 쿼리(validationQuery) 및 주기, Evictor(축출) 주기(timeBetweenEvictionRunsMillis) 및 테스트 위치(testOnCreate, testOnBorrow, testOnReturn, testWhileIdle) 등을 추가로 개발한다면 더 안정하고 효율적인 커넥션 풀을 만들수 있을 겁니다.

 

지금까지 데이터베이스 커넥션 풀(DBCP)을 개발을 통해 시스템에서 사용되는 데이터베이스 커넥션 풀(DBCP)의 동작 원리를 이해할 수 있었습니다.

 

다음에는 오픈소스 라이브러리로 스프링 프레임워크에서도 사용하고 있는 아파치 공통 데이터베이스 커넥션 풀(Apache Commons DBCP)를 이용하여 적용하겠습니다.

 

728x90
반응형