Java 프레임워크 만들기 - JSP

자바 제네릭 메소드와 리플렉션을 이용한 자동 PreparedStatement 생성 1 - Java Generic Method Reflection PreparedStatement

carrotweb 2021. 7. 16. 22:12
728x90
반응형

PreparedStatement 자동 생성과 실행으로 DAO에서는 쿼리문만 작성하게 되었습니다. 또한 반복적인 코드를 줄임으로써 개발 시간이 단축되었습니다.

이어서 PreparedStatement에서 Select 쿼리문을 처리하는 메소드를 만들어 보겠습니다.

Select 쿼리문도 Insert, Update, Delete 쿼리문처럼 쿼리를 실행하지만 쿼리 결과는 ResultSet 객체로 받습니다.

또한 Select 쿼리문의 결과에는 한개 또는 여러개의 행(row)이 올 수 있고 한 개의 행(row)에 한 개의 컬럼(column)만 올 수 있습니다.

그래서 Select 쿼리 처리 결과에 따라 메소드를 구분하여 만들어야 합니다.

1. ResultSet 객체에서 한개의 행(row)를 받아 객체로 생성하여 전달하는 메소드

2. ResultSet 객체에서 한개의 행(row)에 한개의 컬럼(column)를 받아 전달하는 메소드

3. ResultSet 객체에서 여러개의 행(row)를 받아 객체로 생성하고 리스트 객체로 전달하는 메소드

MyBatis(마이바티스)에서도 쿼리 결과에 따라 selectOne과 selectList로 구분하여 사용합니다.

첫번째로 ResultSet 객체에서 한개의 행(row)을 받아 객체로 생성하여 전달하는 메소드인 executeQueryOne()메소드를 생성하겠습니다.

PreparedStatement에서 사용되는 쿼리문

SELECT MBR_ID, MBR_PWD, MBR_PWD_SALT, MBR_NM FROM MBR_ACCOUNT_TB WHERE MBR_ID=?

 

"?"를 "#{객체의 맴버 필드}"으로 파라미터로 된 쿼리문

SELECT MBR_ID, MBR_PWD, MBR_PWD_SALT, MBR_NM FROM MBR_ACCOUNT_TB WHERE MBR_ID=#{id}

 

쿼리문을 실행하면 쿼리 결과가 테이블의 컬럼(column)명으로 오기 때문에 객체의 맴버 필드와 일치하지 않아 값을 설정할 수 없습니다.

그래서 쿼리문에 alias(별칭)를 이용하여 객체의 맴버 필드를 추가합니다.

SELECT MBR_ID AS id, MBR_PWD AS password, MBR_PWD_SALT AS passwordSalt, MBR_NM AS name FROM MBR_ACCOUNT_TB WHERE MBR_ID=#{id}

그리고 "UserVO.java"에 추가된 등록자, 등록일, 수정자, 수정일도 가져오게 추가합니다.

SELECT MBR_ID AS id, MBR_PWD AS password, MBR_PWD_SALT AS passwordSalt, MBR_NM AS name, REG_ID AS registrationId, REG_DTM AS registrationDateTime, MOD_ID AS modificationId, MOD_DTM AS modificationDateTime FROM MBR_ACCOUNT_TB WHERE MBR_ID=#{id}

 

데이터베이스 컨넥션 객체를 가져옵니다.

Connection connection = getConnection();

 

이전에 만든 createPreparedStatement()메소드를 이용하여 PreparedStatement를 생성합니다.

PreparedStatement preparedStatement = createPreparedStatement(connection, query, parameterObject);
if (preparedStatement != null) {
	System.out.println("PreparedStatement를 생성함");
} else {
	System.out.println("PreparedStatement를 생성하지 못함");
}

자세한 설명은 이전 "자바 패턴 매치와 리플렉션을 이용한 자동 PreparedStatement 생성 1"를 참조하세요.

생성된 PreparedStatement를 실행하여 ResultSet 객체를 받습니다.

ResultSet resultSet = preparedStatement.executeQuery();

 

ResultSet 객체에서 전체 row 수를 확인하여 한 개일 때만 처리하게 합니다.

int resultSetCount = 0;
if (resultSet.last()) {
	resultSetCount = resultSet.getRow();
	resultSet.beforeFirst();
	if (resultSetCount == 1) {
		// 전체 row 수가 1개입니다.
	}
} else {
	// 전체 row 수가 0개입니다.
}

last()메소드는 ResultSet 객체의 마지막 행(row)으로 커서를 이동시킵니다.

getRow()메소드는 현재 행(row) 번호를 가져옵니다.

beforeFirst()메소드는 ResultSet 객체의 첫번째 행(row)이전으로 커서를 이동시킵니다.

전달 받은 클래스로 새로운 인스턴스를 생성합니다.

T resultTypeInstance = null;
if (resultTypeClass != null) {
	try {
		resultTypeInstance = resultTypeClass.newInstance();
	} catch (InstantiationException | IllegalAccessException e) {
		e.printStackTrace();
	}
}

 

생성된 객체에서 맴버 필드 리스트를 가져옵니다.

Field[] Fields = resultTypeClass.getDeclaredFields();

 

ResultSet 객체에서 컬럼(column)의 메타정보를 가져옵니다.

ResultSetMetaData resultSetMetaData = resultSet.getMetaData();

 

ResultSet 객체의 컬럼과 일치하는 객체의 맴버 필드를 찾고 맴버 필드에 접근할 수 있게 설정하여 맴버 필드에 값을 설정합니다.

for (int index = 1; index <= resultSetMetaData.getColumnCount(); index++) {
	String columnLabel = resultSetMetaData.getColumnLabel(index);
	for (Field field : Fields) {
		if (field.getName().equals(columnLabel)) {
			field.setAccessible(true);
			try {
				field.set(resultInstance, resultSet.getObject(index));
			} catch (IllegalArgumentException | IllegalAccessException e) {
				e.printStackTrace();
			}
		}
	}
}

ResultSet 객체의 컬럼 타입과 객체의 맴버 필드 타입이 같아야 설정됩니다. 타입 비교는 다음에 따로 설명하겠습니다.

PreparedStatement를 종료합니다.

preparedStatement.close();

 

데이터베이스 컨넥션 객체를 반환하고 생성된 객체를 리턴합니다.

closeConnection(connection);

return resultTypeInstance;

 

 

1. "TestDatabaseDataSource.java"나 "TestDatabaseJNDI.java", "TestDatabaseConectionPool.java", "TestDatabasePoolManager.java", "TestDatabaseManager.java"에 추가하시면 됩니다.

executeQueryOne()메소드에서 전달되는 클래스로 객체를 생성하여 리턴하기 위해 제네릭 메소드로 생성합니다.

 

추가되는 전체 소스입니다.

/**
 * 쿼리문(SELECT)을 실행한다.
 * @param query 쿼리 문자열
 * @param parameterObject 파라미터 객체
 * @param resultTypeClass 결과 클래스
 * @return 결과 객체 인스턴스
 */
public <T> T executeQueryOne(String query, Object parameterObject, Class<T> resultTypeClass) {
	T resultTypeInstance = null;

	long startTime = System.currentTimeMillis();

	Connection connection = getConnection();

	PreparedStatement preparedStatement = createPreparedStatement(connection, query, parameterObject);
	if (preparedStatement != null) {
		System.out.println("PreparedStatement를 생성함");
		try {
			ResultSet resultSet = preparedStatement.executeQuery();
			int resultSetCount = getResultSetCount(resultSet);
			if (resultSetCount == 1) {{
				if (resultSet.next()) {
					if (resultTypeClass != null) {
						try {
							resultTypeInstance = resultTypeClass.newInstance();
						} catch (InstantiationException | IllegalAccessException e) {
							e.printStackTrace();
						}
					}

					if (resultTypeInstance != null) {
						Field[] Fields = resultTypeClass.getDeclaredFields();
						ResultSetMetaData resultSetMetaData = resultSet.getMetaData();
						for (int index = 1; index <= resultSetMetaData.getColumnCount(); index++) {
							String columnLabel = resultSetMetaData.getColumnLabel(index);
							for (Field field : Fields) {
								if (field.getName().equals(columnLabel)) {
									field.setAccessible(true);
									try {
										field.set(resultTypeInstance, resultSet.getObject(index));
										System.out.println("객체의 " + field.getName() + "에 값(" + resultSet.getObject(index) + ")을 적용함");
									} catch (IllegalArgumentException | IllegalAccessException e) {
										e.printStackTrace();
									}
								}
							}
						}
					}
				}
				System.out.println("Query[" + preparedStatement.toString() + "]를 실행함");
			} else {
				System.out.println("Query[" + preparedStatement.toString() + "]를 실행한 결과 row가 " + resultSetCount + "개로 처리하지 않음");
			}
			resultSet.close();
		} catch (SQLException e) {
			resultTypeInstance = null;
			System.out.println("Query[" + preparedStatement.toString() + "]를 실행하지 못함");
			e.printStackTrace();
		}

		try {
			preparedStatement.close();
			System.out.println("PreparedStatement를 종료함");
		} catch (SQLException e) {
			System.out.println("PreparedStatement를 종료하지 못함");
			e.printStackTrace();
		}
	} else {
		System.out.println("PreparedStatement를 생성하지 못함");
	}

	closeConnection(connection);

	long endTime = System.currentTimeMillis();
	System.out.println((endTime - startTime) + "ms 소요됨");

	return resultTypeInstance;
}
/**
 * ResultSet 객체의 전체 행(row) 수를 가져옵니다.
 * @param resultSet ResultSet 객체
 * @return 전체 행(row) 수
 */
private int getResultSetCount(ResultSet resultSet) {
	int resultSetCount = 0;

	try {
		if (resultSet.last()) {
			resultSetCount = resultSet.getRow();
			resultSet.beforeFirst();
		}
	} catch (SQLException e) {
		e.printStackTrace();
	}

	return resultSetCount;
}

 

2. "TestLoginDaoImpl.java"의 getMemberObject()메소드를 executeQueryOne()메소드로 처리되게 변경합니다.

@Override
public UserVO getMemberObject(UserVO userVO) {
	UserVO selectUserVO = null;

	if (userVO != null) {
		String query = "SELECT MBR_ID, MBR_PWD, MBR_PWD_SALT, MBR_NM FROM MBR_ACCOUNT_TB WHERE MBR_ID=?";
		Connection connection = testDatabaseManager.getConnection();
		if (connection != null) {
			PreparedStatement preparedStatement = null;

			try {
				int index = 1;
				preparedStatement = connection.prepareStatement(query);
				preparedStatement.setString(index++, userVO.getId());
				System.out.println("PreparedStatement를 생성함");
			} catch (SQLException e) {
				System.out.println("PreparedStatement를 생성하지 못함");
				e.printStackTrace();
			}

			if (preparedStatement != null) {
				ResultSet resultSet = null;
				try {
					resultSet = preparedStatement.executeQuery();
					System.out.println("Query[" + preparedStatement.toString() + "]를 실행함");
				} catch (SQLException e) {
					System.out.println("Query[" + preparedStatement.toString() + "]를 실행하지 못함");
					e.printStackTrace();
				}

				try {
					if (resultSet.next()) {
						selectUserVO = new UserVO();
						selectUserVO.setId(resultSet.getString("MBR_ID"));
						selectUserVO.setPassword(resultSet.getString("MBR_PWD"));
						selectUserVO.setPasswordSalt(resultSet.getString("MBR_PWD_SALT"));
						selectUserVO.setName(resultSet.getString("MBR_NM"));
					} else {
						System.out.println("resultSet이 없음");
					}
					resultSet.close();
				} catch (SQLException e) {
					System.out.println("resultSet를 가져오지 못함");
					e.printStackTrace();
				}

				try {
					preparedStatement.close();
					System.out.println("PreparedStatement를 종료함");
				} catch (SQLException e) {
					System.out.println("PreparedStatement를 종료하지 못함");
					e.printStackTrace();
				}
			}

			testDatabaseManager.closeConnection(connection);
		}
	}

	return selectUserVO;
}

변경된 소스입니다.

@Override
public UserVO getMemberObject(UserVO userVO) {
	UserVO selectUserVO = null;

	if (userVO != null) {
		String query = "SELECT MBR_ID AS id, MBR_PWD AS password, MBR_PWD_SALT AS passwordSalt, MBR_NM AS name, REG_ID AS registrationId, REG_DTM AS registrationDateTime, MOD_ID AS modificationId, MOD_DTM AS modificationDateTime FROM MBR_ACCOUNT_TB WHERE MBR_ID=#{id}";
		selectUserVO = testDatabaseManager.executeQueryOne(query, userVO, UserVO.class);
	}

	return selectUserVO;
}

 

3. 이전 "자바 JUnit를 이용한 단위 테스트"에서 추가한 "TestLoginDaoImplTest.java"에서 테스트 케이스인 testGetMemberObject()메소드에서 getMemberObject()메소드를 실행할 수 있게 수정합니다.

/**
 * Test method for {@link com.home.project.test2.dao.TestLoginDaoImpl#getMemberObject(com.home.project.test2.vo.UserVO)}.
 */
@Test
public void testGetMemberObject() {
	System.out.println("testGetMemberObject() 테스트 시작");

	UserVO userVO = new UserVO();
	userVO.setId("testid");

	UserVO selectUserVO = testLoginDao.getMemberObject(userVO);
	assertNotNull("데이터베이스로 부터 검색된 사용자가 없습니다.", selectUserVO);

	System.out.println("사용자 ID : " + selectUserVO.getId());
	System.out.println("사용자 이름 : " + selectUserVO.getName());
	System.out.println("사용자 패스워드 : " + selectUserVO.getPassword());
	System.out.println("사용자 패스워드 솔트 : " + selectUserVO.getPasswordSalt());
	System.out.println("등록자 : " + selectUserVO.getRegistrationId());
	System.out.println("등록일 : " + selectUserVO.getRegistrationDateTime());
	System.out.println("수정자 : " + selectUserVO.getModificationId());
	System.out.println("수정일 : " + selectUserVO.getModificationDateTime());

	System.out.println("testGetMemberObject() 테스트 성공 종료");
}

 

​4. test2 프로젝트의 [Java Resources > src/test/java]에 있는 "com.home.project.test2.util > TestLoginDaoImplTest.java"에서 마우스 오른쪽 버튼을 클릭하여 콘텍스트 메뉴에서 [Run As > JUnit Test]를 클릭합니다.

JUnit Test가 실행되면서 왼쪽에 JUnit탭이 나타나고 Error가 "0", Failures이 "0"으로 테스트가 성공합니다.

[Console]

testGetMemberObject() 테스트 시작
[main] 데이터베이스 커넥션 객체[1076835071, URL=jdbc:mariadb://localhost:3306/test, MariaDB Connector/J]를 가져옴
객체의 id에서 값(testid)을 가져와 적용함
PreparedStatement를 생성함
객체의 id에 값(testid)을 적용함
객체의 password에 값(Ti7Fm5LAk+aDnhB2BsrzINyW4c61perNa7NWLuFb2iM=)을 적용함
객체의 passwordSalt에 값(84ebf8ab23c440a8a7cfa895f5a545f5)을 적용함
객체의 name에 값(홍길동1)을 적용함
객체의 registrationId에 값(SYSTEM)을 적용함
객체의 registrationDateTime에 값(2021-04-27 22:50:46.0)을 적용함
객체의 modificationId에 값(testid)을 적용함
객체의 modificationDateTime에 값(2021-07-16 21:16:31.0)을 적용함
Query[sql : 'SELECT MBR_ID AS id, MBR_PWD AS password, MBR_PWD_SALT AS passwordSalt, MBR_NM AS name, REG_ID AS registrationId, REG_DTM AS registrationDateTime, MOD_ID AS modificationId, MOD_DTM AS modificationDateTime FROM MBR_ACCOUNT_TB WHERE MBR_ID=?', parameters : ['testid']]를 실행함
PreparedStatement를 종료함
[main] 데이터베이스 커넥션 객체[1076835071, URL=jdbc:mariadb://localhost:3306/test, MariaDB Connector/J]를 반환함
35ms 소요됨
사용자 ID : testid
사용자 이름 : 홍길동1
사용자 패스워드 : Ti7Fm5LAk+aDnhB2BsrzINyW4c61perNa7NWLuFb2iM=
사용자 패스워드 솔트 : 84ebf8ab23c440a8a7cfa895f5a545f5
등록자 : SYSTEM
등록일 : 2021-04-27 22:50:46.0
수정자 : testid
수정일 : 2021-07-16 21:16:31.0
testGetMemberObject() 테스트 성공 종료
728x90
반응형