이전 "자바 PreparedStatement CRUD"를 통해 PreparedStatement를 이용하여 CRUD(Create, Read, Update, Delete)쿼리문을 처리하였습니다. 그러나 코드를 보면 반복적인 작업이 많이 있습니다.
그래서 MyBatis(마이바티스)처럼 쿼리문에 "#{}"으로 파라미터를 입력하고 전달되는 객체를 이용해 자동으로 파라미터를 처리하여 PreparedStatement를 생성하게 하겠습니다.
이전에도 PreparedStatement에 "?"의 순서대로 전달되는 객체의 맴버 필드 값을 설정하였습니다.
PreparedStatement에서 Select 쿼리문은 executeQuery()메소드로 실행되고 ResultSet객체로 쿼리 결과를 받습니다. 그리고 Insert, Update, Delete 쿼리문은 executeUpdate()메소드로 실행되고 정수형(int)으로 결과를 받습니다.
그럼 Insert, Update, Delete 쿼리문을 파라미터로 처리("?"을 "#{객체의 맴버 필드}"으로 변경)하면 다음과 같습니다.
PreparedStatement에서 사용되는 쿼리문
INSERT INTO MBR_ACCOUNT_TB (MBR_ID, MBR_PWD, MBR_PWD_SALT, MBR_NM, REG_ID) VALUES (?, ?, ?, ?, 'SYSTEM')
UPDATE MBR_ACCOUNT_TB SET MBR_NM=? WHERE MBR_ID=?
DELETE FROM MBR_ACCOUNT_TB WHERE MBR_ID=?
MyBatis(마이바티스)처럼 "#{객체의 맴버 필드}"으로 파라미터로 된 쿼리문
INSERT INTO MBR_ACCOUNT_TB (MBR_ID, MBR_PWD, MBR_PWD_SALT, MBR_NM, REG_ID) VALUES (#{id}, #{password}, #{passwordSalt}, #{name}, 'SYSTEM')
UPDATE MBR_ACCOUNT_TB SET MBR_NM=#{name} WHERE MBR_ID=#{id}
DELETE FROM MBR_ACCOUNT_TB WHERE MBR_ID=#{id}
Update 쿼리문을 대상으로 설명하겠습니다.
UPDATE MBR_ACCOUNT_TB SET MBR_NM=#{name}, MOD_ID=#{modificationId}, MOD_DTM=#{modificationDateTime} WHERE MBR_ID=#{id}
수정자 아이디와 수정일자를 업데이트하기 위해 Update 쿼리문을 수정하였습니다.
그리고 "UserVO.java"에 등록자, 등록일, 수정자, 수정일이 추가되었습니다.
/**
* 등록자
*/
private String registrationId = "";
/**
* 등록일
*/
private Date registrationDateTime = null;
/**
* 수정자
*/
private String modificationId = "";
/**
* 수정일
*/
private Date modificationDateTime = null;
/**
* 등록자를 가져옵니다.
* @return 등록자
*/
public String getRegistrationId() {
return registrationId;
}
/**
* 등록자를 설정합니다.
* @param registrationId 등록자
*/
public void setRegistrationId(String registrationId) {
this.registrationId = registrationId;
}
/**
* 등록일을 가져옵니다.
* @return 등록일
*/
public Date getRegistrationDateTime() {
return registrationDateTime;
}
/**
* 등록일을 설정합니다.
* @param registrationDateTime 등록일
*/
public void setRegistrationDateTime(Date registrationDateTime) {
this.registrationDateTime = registrationDateTime;
}
/**
* 수정자를 가져옵니다.
* @return 수정자
*/
public String getModificationId() {
return modificationId;
}
/**
* 수정자를 설정합니다.
* @param modificationId 수정자
*/
public void setModificationId(String modificationId) {
this.modificationId = modificationId;
}
/**
* 수정일을 가져옵니다.
* @return 수정일
*/
public Date getModificationDateTime() {
return modificationDateTime;
}
/**
* 수정일을 설정합니다.
* @param modificationDateTime 수정일
*/
public void setModificationDateTime(Date modificationDateTime) {
this.modificationDateTime = modificationDateTime;
}
데이터베이스 컨넥션 객체를 가져옵니다.
Connection connection = getConnection();
PreparedStatement를 생성할 때는 "#{}"으로 파라미터로 된 쿼리문을 "?"로 대체(변환)해야 합니다.
"#{}"의 파라미터를 추출하기 위해 정규식으로 Pattern 객체를 생성합니다.
Pattern pattern = Pattern.compile("(#\\{\\w+\\})");
파라미터는 객체의 맴버 필드로 알파벳이나 숫자가 하나 이상 오기 때문에 "\(역슬래시)w+"를 사용합니다.
정규식에서 "{"(중괄호)가 정규식이 아닌 문자로 인식하기 위해서는 역슬래시(\)가 앞에 있어야 합니다.
문자열에서 역슬래시(\)가 문자로 인식되기 위해서 역슬래시(\)가 앞에 있어야 합니다.
괄호("(", ")")는 그룹입니다.
Pattern객체를 이용해 문자열에서 패턴을 찾는 Matcher객체를 생성합니다.
Matcher matcher = pattern.matcher(query);
Matcher의 replaceAll()메소드를 이용해 패턴과 일치하는 모든 부분을 "?"로 대체(변환)합니다.
String preparedQuery = matcher.replaceAll("?");
UPDATE MBR_ACCOUNT_TB SET MBR_NM=#{name} WHERE MBR_ID=#{id}
UPDATE MBR_ACCOUNT_TB SET MBR_NM=? WHERE MBR_ID=?
"?"로 대체(변환)된 쿼리문으로 PreparedStatement를 생성합니다.
String preparedStatement = connection.prepareStatement(preparedQuery);
전달 받은 객체에서 맴버 필드 리스트를 가져옵니다.
Class<?> objectClass = object.getClass();
Field[] Fields = objectClass.getDeclaredFields();
Matcher의 replaceAll()메소드를 이용하면 시퀀스가 마지막으로 이동하게 되어 이후 find()메서드를 사용할 수 없습니다. 그래서 reset()메소드를 이용하여 시퀀스를 처음으로 이동시킵니다.
matcher = matcher.reset();
Matcher의 find()메소드를 이용해 패턴과 일치하는 것을 찾고 파라미터(객체의 맴버 필드)를 추출합니다.
int index = 1;
while (matcher.find()) {
String parameterName = matcher.group().replace("#{", "").replace("}", "");
}
객체의 맴버 필드 리스트에서 파라미터와 일치하는 맴버 필드를 찾고 맴버 필드에 접근할 수 있게 설정하여 맴버 필드의 값을 가져옵니다.
for (Field field : Fields) {
String fieldName = field.getName();
if (parameterName.equals(fieldName)) {
field.setAccessible(true);
Object fieldValue = null;
try {
fieldValue = field.get(object);
} catch (IllegalArgumentException | IllegalAccessException e) {
System.out.println("객체의 " + fieldName + "에서 값을 가져오지 못함");
e.printStackTrace();
}
}
}
만약 파라미터와 일치되는 맴버 필드가 없거나 맴버 필드의 값을 가져오지 못하면 PreparedStatement의 "?"에 맞는 값을 설정할 수 없기 때문에 실패 처리를 해야합니다.
맴버 필드의 값을 맴버 필드의 타입에 맞게 케스팅하여 PreparedStatement의 "?"에 대체할 값을 설정합니다.
if (fieldValue != null) {
if (field.getType().getName().equals(String.class.getName())) {
preparedStatement.setString(index++, String.class.cast(fieldValue));
} else if (field.getType().getName().equals("int")) {
preparedStatement.setInt(index++, (int)fieldValue);
} else if (field.getType().getName().equals("short")) {
preparedStatement.setShort(index++, (short)fieldValue);
} else if (field.getType().getName().equals("float")) {
preparedStatement.setFloat(index++, (float)fieldValue);
} else if (field.getType().getName().equals("long")) {
preparedStatement.setLong(index++, (long)fieldValue);
} else if (field.getType().getName().equals("double")) {
preparedStatement.setDouble(index++, (double)fieldValue);
} else if (field.getType().getName().equals("boolean")) {
preparedStatement.setBoolean(index++, (boolean)fieldValue);
} else if (field.getType().getName().equals("byte")) {
preparedStatement.setByte(index++, (byte)fieldValue);
} else if (field.getType().getName().equals(Date.class.getName())) {
preparedStatement.setTimestamp(index++, new java.sql.Timestamp(((Date)fieldValue).getTime()));
} else if (field.getType().getName().equals(java.sql.Date.class.getName())) {
preparedStatement.setDate(index++, java.sql.Date.class.cast(fieldValue));
} else if (field.getType().getName().equals(java.sql.Time.class.getName())) {
preparedStatement.setTime(index++, java.sql.Time.class.cast(fieldValue));
} else if (field.getType().getName().equals(java.sql.Timestamp.class.getName())) {
preparedStatement.setTimestamp(index++, java.sql.Timestamp.class.cast(fieldValue));
}
}
루프를 돌면서 "?" 순서로 처리하기 때문에 파라미터 인덱스를 정수형 변수로 사용합니다.
또한 "자바 동적 형(타입) 변환"에서 Class의 cast()메소드에 대해 설명하였듯이 "int", "short", "float", "long", "double", "boolean", "byte"과 같은 기본형(자료형)타입은 클래스가 아니기 때문에 정적 캐스팅으로 처리해야 합니다.
그리고 데이터베이스에서 테이블의 필드가 "datetime"이고 전달되는 객체의 맴버 필드 타입이 Date일때 setDate()메소드를 사용하면 년월일("yyyy-MM-dd")로만 저장됩니다.
if (field.getType().getName().equals(Date.class.getName())) {
preparedStatement.setDate(index, new java.sql.Date(((Date)fieldValue).getTime()));
}
그래서 년월일시분초("yyyy-MM-dd HH:mm:ss")로 저장하기 위해서 setTimestamp()메소드를 사용해야 합니다.
if (field.getType().getName().equals(Date.class.getName())) {
preparedStatement.setTimestamp(index++, new java.sql.Timestamp(((Date)fieldValue).getTime()));
}
만약 전달되는 객체의 맴버 필드 타입이 참조형 타입인 래퍼 클래스(Wrapper class)이면 다음과 같이 처리하면 됩니다.
if (field.getType().getName().equals(Integer.class.getName())) {
preparedStatement.setInt(index++, Integer.class.cast(fieldValue).intValue());
} else if (field.getType().getName().equals(Short.class.getName())) {
preparedStatement.setShort(index++, Short.class.cast(fieldValue).shortValue());
} else if (field.getType().getName().equals(Float.class.getName())) {
preparedStatement.setFloat(index++, Float.class.cast(fieldValue).floatValue());
} else if (field.getType().getName().equals(Long.class.getName())) {
preparedStatement.setLong(index++, Long.class.cast(fieldValue).longValue());
} else if (field.getType().getName().equals(Double.class.getName())) {
preparedStatement.setDouble(index++, Double.class.cast(fieldValue).doubleValue());
} else if (field.getType().getName().equals(Boolean.class.getName())) {
preparedStatement.setBoolean(index++, Boolean.class.cast(fieldValue).booleanValue());
} else if (field.getType().getName().equals(Byte.class.getName())) {
preparedStatement.setByte(index++, Byte.class.cast(fieldValue).byteValue());
}
PreparedStatement를 종료합니다.
preparedStatement.close();
데이터베이스 컨넥션 객체를 반환하고 생성된 처리된 행(row) 수를 리턴합니다.
closeConnection(connection);
return result;
1. "TestDatabaseDataSource.java"나 "TestDatabaseJNDI.java", "TestDatabaseConectionPool.java", "TestDatabasePoolManager.java", "TestDatabaseManager.java"에 추가하시면 됩니다.
추가되는 전체 소스입니다.
/**
* 쿼리문(UPDATE, INSERT, DELETE)을 실행합니다.
* @param query 쿼리 문자열
* @param parameterObject 파라미터 객체
* @return 처리된 행 수(-1: 오류, 0: 처리된 행 없음, 1이상: 처리된 행 수)
*/
public int executeUpdate(String query, Object parameterObject) {
int result = -1;
long startTime = System.currentTimeMillis();
Connection connection = getConnection();
PreparedStatement preparedStatement = createPreparedStatement(connection, query, parameterObject);
if (preparedStatement != null) {
System.out.println("PreparedStatement를 생성함");
try {
result = preparedStatement.executeUpdate();
System.out.println("Query[" + preparedStatement.toString() + "]를 실행함");
} catch (SQLException e) {
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 result;
}
/**
* PreparedStatement 객체를 샹성합니다.
* @param connection 데이터베이스 연결 객체
* @param query 쿼리 문자열
* @param parameterObject 파라미터 객체
* @return PreparedStatement 객체
*/
private PreparedStatement createPreparedStatement(Connection connection, String query, Object parameterObject) {
if (connection == null || (query == null || query.isEmpty()) || parameterObject == null) {
return null;
}
PreparedStatement preparedStatement = null;
Class<?> parameterObjectClass = parameterObject.getClass();
Field[] Fields = parameterObjectClass.getDeclaredFields();
// 쿼리문에서 파라메타(#{...})를 ?으로 변환
Pattern pattern = Pattern.compile("(#\\{\\w+\\})");
Matcher matcher = pattern.matcher(query);
String preparedQuery = matcher.replaceAll("?");
try {
// 변환된 쿼리문으로 PreparedStatement 생성
preparedStatement = connection.prepareStatement(preparedQuery);
// 쿼리문에서 파라메타(#{...})에 찾고 객체에서 동일한 이름의 맴버 필드를 찾아 PreparedStatement의 "?"에 대체할 값을 설정
matcher = matcher.reset();
int index = 1;
while (matcher.find()) {
String parameterName = matcher.group().replace("#{", "").replace("}", "");
boolean isParameterValue = false;
for (Field field : Fields) {
String fieldName = field.getName();
if (parameterName.equals(fieldName)) {
isParameterValue = setPreparedStatementParameter(preparedStatement, index++, field, parameterObject);
break;
}
}
if (!isParameterValue) {
System.out.println("객체에서 " + parameterName + "와 같은 맴버 필드를 찾지 못함");
preparedStatement.close();
preparedStatement = null;
break;
}
}
} catch (SQLException e) {
preparedStatement = null;
e.printStackTrace();
}
return preparedStatement;
}
/**
* PreparedStatement의 파리미터에 전달 받은 객체의 맴버 필드의 값을 설정합니다.
* @param preparedStatement PreparedStatement 객체
* @param index 파라미터 인덱스
* @param field 파라미터 객체의 맴버 필드
* @param parameterObject 파라미터 객체
* @return 처리여부
*/
private boolean setPreparedStatementParameter(PreparedStatement preparedStatement, int index, Field field, Object parameterObject) {
if (preparedStatement == null || index <= 0 || field == null || parameterObject == null) {
return false;
}
boolean result = false;
field.setAccessible(true);
Object fieldValue = null;
try {
fieldValue = field.get(parameterObject);
} catch (IllegalArgumentException | IllegalAccessException e) {
System.out.println("객체의 " + field.getName() + "에서 값을 가져오지 못함");
e.printStackTrace();
}
if (fieldValue != null) {
try {
if (field.getType().getName().equals(String.class.getName())) {
preparedStatement.setString(index, String.class.cast(fieldValue));
result = true;
} else if (field.getType().getName().equals("int")) {
preparedStatement.setInt(index, (int)fieldValue);
result = true;
} else if (field.getType().getName().equals(Integer.class.getName())) {
preparedStatement.setInt(index, Integer.class.cast(fieldValue).intValue());
result = true;
} else if (field.getType().getName().equals("short")) {
preparedStatement.setShort(index, (short)fieldValue);
result = true;
} else if (field.getType().getName().equals(Short.class.getName())) {
preparedStatement.setShort(index, Short.class.cast(fieldValue).shortValue());
result = true;
} else if (field.getType().getName().equals("float")) {
preparedStatement.setFloat(index, (float)fieldValue);
result = true;
} else if (field.getType().getName().equals(Float.class.getName())) {
preparedStatement.setFloat(index, Float.class.cast(fieldValue).floatValue());
result = true;
} else if (field.getType().getName().equals("long")) {
preparedStatement.setLong(index, (long)fieldValue);
result = true;
} else if (field.getType().getName().equals(Long.class.getName())) {
preparedStatement.setLong(index, Long.class.cast(fieldValue).longValue());
result = true;
} else if (field.getType().getName().equals("double")) {
preparedStatement.setDouble(index, (double)fieldValue);
result = true;
} else if (field.getType().getName().equals(Double.class.getName())) {
preparedStatement.setDouble(index, Double.class.cast(fieldValue).doubleValue());
result = true;
} else if (field.getType().getName().equals("boolean")) {
preparedStatement.setBoolean(index, (boolean)fieldValue);
result = true;
} else if (field.getType().getName().equals(Boolean.class.getName())) {
preparedStatement.setBoolean(index, Boolean.class.cast(fieldValue).booleanValue());
result = true;
} else if (field.getType().getName().equals("byte")) {
preparedStatement.setByte(index, (byte)fieldValue);
result = true;
} else if (field.getType().getName().equals(Byte.class.getName())) {
preparedStatement.setByte(index, Byte.class.cast(fieldValue).byteValue());
result = true;
} else if (field.getType().getName().equals(Date.class.getName())) {
preparedStatement.setTimestamp(index, new java.sql.Timestamp(((Date)fieldValue).getTime()));
result = true;
} else if (field.getType().getName().equals(java.sql.Date.class.getName())) {
preparedStatement.setDate(index, java.sql.Date.class.cast(fieldValue));
result = true;
} else if (field.getType().getName().equals(java.sql.Time.class.getName())) {
preparedStatement.setTime(index, java.sql.Time.class.cast(fieldValue));
result = true;
} else if (field.getType().getName().equals(java.sql.Timestamp.class.getName())) {
preparedStatement.setTimestamp(index, java.sql.Timestamp.class.cast(fieldValue));
result = true;
}
} catch (SQLException e) {
e.printStackTrace();
}
if (result) {
System.out.println("객체의 " + field.getName() + "에서 값(" + fieldValue + ")을 가져와 적용함");
}
}
return result;
}
2. "TestLoginDaoImpl.java"의 editMemberObject()메소드를 executeUpdate()메소드로 처리되게 변경합니다.
@Override
public int editMemberObject(UserVO userVO) {
int result = 0;
if (userVO != null) {
String query = "UPDATE MBR_ACCOUNT_TB SET MBR_NM=? 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.getName());
preparedStatement.setString(index++, userVO.getId());
System.out.println("PreparedStatement를 생성함");
} catch (SQLException e) {
System.out.println("PreparedStatement를 생성하지 못함");
e.printStackTrace();
}
if (preparedStatement != null) {
try {
result = preparedStatement.executeUpdate();
System.out.println("Query[" + preparedStatement.toString() + "]를 실행함");
} catch (SQLException e) {
System.out.println("Query[" + preparedStatement.toString() + "]를 실행하지 못함");
e.printStackTrace();
}
try {
preparedStatement.close();
System.out.println("PreparedStatement를 종료함");
} catch (SQLException e) {
System.out.println("PreparedStatement를 종료하지 못함");
e.printStackTrace();
}
}
testDatabaseManager.closeConnection(connection);
}
}
return result;
}
변경된 소스입니다.
@Override
public int editMemberObject(UserVO userVO) {
int result = 0;
if (userVO != null) {
String query = "UPDATE MBR_ACCOUNT_TB SET MBR_NM=#{name}, MOD_ID=#{modificationId}, MOD_DTM=#{modificationDateTime} WHERE MBR_ID=#{id}";
result = testDatabaseManager.executeUpdate(query, userVO);
}
return result;
}
3. 이전 "자바 JUnit를 이용한 단위 테스트"에서 추가한 "TestLoginDaoImplTest.java"에서 테스트 케이스인 testEditMemberObject()메소드에서 editMemberObject()메소드를 실행할 수 있게 수정합니다.
/**
* Test method for {@link com.home.project.test2.dao.TestLoginDaoImpl#editMemberObject(com.home.project.test2.vo.UserVO)}.
*/
@Test
public void testEditMemberObject() {
System.out.println("testEditMemberObject() 테스트 시작");
UserVO userVO = new UserVO();
userVO.setId("testid");
userVO.setName("홍길동1");
userVO.setModificationId("testid");
Date date = new Date();
userVO.setModificationDateTime(date);
int result = testLoginDao.editMemberObject(userVO);
assertTrue(result == 1);
System.out.println("testEditMemberObject() 테스트 성공 종료");
}
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]
testEditMemberObject() 테스트 시작
[main] 데이터베이스 커넥션 객체[1935365522, URL=jdbc:mariadb://localhost:3306/test, MariaDB Connector/J]를 가져옴
객체의 name에서 값(홍길동1)을 가져와 적용함
객체의 modificationId에서 값(testid)을 가져와 적용함
객체의 modificationDateTime에서 값(Fri Jul 16 17:17:48 KST 2021)을 가져와 적용함
객체의 id에서 값(testid)을 가져와 적용함
PreparedStatement를 생성함
Query[sql : 'UPDATE MBR_ACCOUNT_TB SET MBR_NM=?, MOD_ID=?, MOD_DTM=? WHERE MBR_ID=?', parameters : ['홍길동1','testid','2021-07-16 17:17:48.048','testid']]를 실행함
PreparedStatement를 종료함
[main] 데이터베이스 커넥션 객체[1935365522, URL=jdbc:mariadb://localhost:3306/test, MariaDB Connector/J]를 반환함
23ms 소요됨
testEditMemberObject() 테스트 성공 종료
testEditMemberObject()메소드에 대한 테스트가 성공적으로 처리되었습니다.
DAO에서 매번 동일하게 처리하는 코드들과 쿼리문의 "?"에 맞게 파라미터 값을 설정하는 코드가 없어져 개발 용이해졌습니다.
"TestLoginDaoImpl.java"의 Insert, Delete 쿼리문이 있는 addMemberObject()메소드와 deleteMemberObject()메소드들도 executeUpdate()메소드를 이용하여 수정하고 테스트해보세요.
다음은 Select 쿼리문을 처리하는 executeQuery()메소드 만들고 적용하여 테스트해보겠습니다.