[jdbc 미션] jdbc 라이브러리: 공통된 부분을 추상화
공통된 부분과 아닌 부분을 분리
어떤 부분을 공통으로 사용하여 추상화 할 것인지 아닌지를 결정해야 한다. 아래와 같은 부분들은 공통으로 추상화할 수 있다.
- Connection 생성
- Statement 준비 및 실행
- ResultSet 생성
- 예외 처리
- Connection, Statement, ResultSet 객체 close
이 미션을 하면서는 기존에 사용하던 jdbcTemplate과 미션 가이드의 도움을 많이 받았다. 실제로 추상화 할 부분과 아닌 부분을 결정하는 것은 추상화 단계의 1단계라는 생각이 들었다.
사용한 자원은 닫는다
Connection, PreparedStatement, ResultSet 등의 사용한 자원은 닫아주어야 한다. 자원이 닫히지 않고 메모리를 계속 차지한다면 메모리 누수가 발생할 수 있고 예상치 못한 문제가 발생할 수 있기 때문이라고 한다. 뭐든지 파일도, 커넥션도 열었던 것은 닫아주자! 기존에는 try~catch~finally
구문으로 자원을 처리하였으나 java7 이상부터는 try with resource 문법이 추가되어 finally 대신 자원을 처리해줄 수 있다.
public void update(String sql, Object... args) {
try (Connection conn = dataSource.getConnection();
PreparedStatement pstmt = createPreparedStatement(conn, sql, args)) {
log.debug("query : {}", sql);
pstmt.executeUpdate();
} catch (SQLException e) {
log.error(e.getMessage(), e);
throw new RuntimeException(e);
}
}
private PreparedStatement createPreparedStatement(Connection conn, String sql, Object... args) throws SQLException {
PreparedStatement pstmt = conn.prepareStatement(sql);
setPreparedStatement(pstmt, args);
return pstmt;
}
private void setPreparedStatement(PreparedStatement pstmt, Object[] args) throws SQLException {
int index = 1;
for (Object arg : args) {
pstmt.setObject(index, arg);
index += 1;
}
}
PreparedStatement에서 셋팅을 해주어야하는 경우 메소드로 분리하여 처리할 수 있다.
가변인자, 제네릭
2~3월에 객체지향 자바 수업에서 가변인자를 처음 봤다. ...
으로 여러개의 인자를 받을 수 있다. 같은 타입의 인자가 몇개가 들어올지 모르는 경우 가변인자를 사용해서 다양하게 함수를 사용할 수 있다. 위에 코드 예시에도 있듯이 가변인자로 받는 경우 배열로 받아진다.
아래의 코드 예시에서 처럼 제네릭 타입을 사용하여 원하는 객체타입을 반환할 수 있다.
템플릿메소드 패턴
해당 메소드를 오버라이드하여 사용하거나 메소드가 1개인 인터페이스를 사용하여 람다식을 사용하여 함수형의 방식으로 인자로 전달해 줄 수 있다.
public interface StateCallBack<T> {
T doInStatement(Statement stmt, ResultSet rs) throws SQLException;
}
// JdbcTemplate Class
private <T> T execute(StateCallBack<T> action, String sql, Object... args) {
try (Connection conn = dataSource.getConnection();
PreparedStatement pstmt = createPreparedStatement(conn, sql, args);
ResultSet rs = pstmt.executeQuery()
) {
return action.doInStatement(pstmt, rs);
} catch (SQLException e) {
log.error(e.getMessage(), e);
throw new RuntimeException(e);
}
}
public <T> T query(String sql, RowMapper<T> rowMapper, Object... args) {
return execute(((stmt, rs) -> {
log.debug("query : {}", sql);
if (rs.next()) {
return rowMapper.mapRow(rs);
}
return null;
}), sql, args);
}
실제로 JdbcTemplate 메소드 안을 살펴보면 아래와 같은 코드로 작성되어 있다.
//실제 jdbcTemplate 라이브러리에서 사용되는 메소드
@FunctionalInterface
public interface ConnectionCallback<T> {
@Nullable
T doInConnection(Connection con) throws SQLException, DataAccessException;
}
결과
기존에는 메소드마다 중복되어 작성되었다. 부분들을 공통된 부분과 아닌 부분으로 분리하여 추상화를 하여 라이브러리화 하여 `후`와 같이 좀 더 변화하는 로직에 집중할 수 있는 코드가 되었다.
// 전
public void insert(User user) {
final String sql = "insert into users (account, password, email) values (?, ?, ?)";
Connection conn = null;
PreparedStatement pstmt = null;
try {
conn = dataSource.getConnection();
pstmt = conn.prepareStatement(sql);
log.debug("query : {}", sql);
pstmt.setString(1, user.getAccount());
pstmt.setString(2, user.getPassword());
pstmt.setString(3, user.getEmail());
pstmt.executeUpdate();
} catch (SQLException e) {
log.error(e.getMessage(), e);
throw new RuntimeException(e);
} finally {
try {
if (pstmt != null) {
pstmt.close();
}
} catch (SQLException ignored) {}
try {
if (conn != null) {
conn.close();
}
} catch (SQLException ignored) {}
}
}
// 후
public void insert(User user) {
final String sql = "insert into users (account, password, email) values (?, ?, ?)";
jdbcTemplate.insert(sql, user.getAccount(), user.getPassword(), user.getEmail());
}