기존의 JdbcTemplate
의 파라미터,리턴값을 참조해 나만의 JdbcTemplate
을 만들어보았다.
RowMapper
, PreparedStatementSetter
, ResultSetCallback
등 다양한 함수형 인터페이스를 사용해 코드 중복을 줄이려 했다.
public class JdbcTemplate {
private final JdbcExecutor jdbcExecutor;
private final DefaultStatementSetter defaultStatementSetter = new DefaultStatementSetter();
public JdbcTemplate(final DataSource dataSource) {
this.jdbcExecutor = new JdbcExecutor(dataSource);
}
public <T> List<T> query(final String sql, final RowMapper<T> rowMapper,
final Object... args) {
return query(sql, defaultStatementSetter.getSetter(args), rowMapper);
}
public <T> List<T> query(final String sql,
final PreparedStatementSetter statementSetter,
final RowMapper<T> rowMapper) {
final ResultSetCallback<List<T>> resultSetCallback = rs -> {
List<T> result = new ArrayList<>();
while (rs.next()) {
result.add(rowMapper.mapRow(rs));
}
return result;
};
return jdbcExecutor.find(sql, statementSetter, resultSetCallback);
}
public <T> T queryForObject(final String sql, final RowMapper<T> rowMapper,
final Object... args) {
return queryForObject(sql, defaultStatementSetter.getSetter(args), rowMapper);
}
public <T> T queryForObject(final String sql,
final PreparedStatementSetter statementSetter,
final RowMapper<T> rowMapper) {
final ResultSetCallback<T> resultSetCallback = rs -> {
rs.next();
return rowMapper.mapRow(rs);
};
return jdbcExecutor.find(sql, statementSetter, resultSetCallback);
}
public Integer executeUpdate(final String sql, final Object... args) {
return executeUpdate(sql, defaultStatementSetter.getSetter(args));
}
public Integer executeUpdate(final String sql,
final PreparedStatementSetter statementSetter) {
return jdbcExecutor.update(sql, statementSetter);
}
public DataSource getDataSource() {
return jdbcExecutor.getDataSource();
}
}
명령 쿼리 로직을 한 곳에서 관리하고 싶어 콜백 패턴으로 JdbcExecutor를 추출했다.
public class JdbcExecutor {
private static final Logger log = LoggerFactory.getLogger(JdbcExecutor.class);
private final DataSource dataSource;
public JdbcExecutor(DataSource dataSource) {
this.dataSource = dataSource;
}
public <T> T find(String sql, PreparedStatementSetter statementSetter,
ResultSetCallback<T> resultSetCallback) {
return executeOrThrow(sql, statementSetter, statement -> {
try (final ResultSet rs = statement.executeQuery()) {
return resultSetCallback.execute(rs);
}
});
}
public Integer update(final String sql,
final PreparedStatementSetter statementSetter) {
return executeOrThrow(sql, statementSetter, PreparedStatement::executeUpdate);
}
private <T> T executeOrThrow(final String sql, final PreparedStatementSetter setter,
final PreparedStatementCallback<T> statementCallback) {
Connection connection = DataSourceUtils.getConnection(dataSource);
try (final PreparedStatement statement = connection.prepareStatement(sql)) {
setter.setValues(statement);
return statementCallback.execute(statement);
} catch (SQLException e) {
log.error(e.getMessage(), e);
throw new DataAccessException(e);
} finally {
DataSourceUtils.releaseConnection(connection, dataSource);
}
}
public DataSource getDataSource() {
return dataSource;
}
}
PreparedStatementSetter
라는 함수형 인터페이스를 활용해 람다 형태로 PreparedStatement
인자를 세팅했다.
처음엔 DefaultStatementSetter
로직을 JdbcTemplate
안에 뒀는데, 기능 단위로 분리하고 싶어 클래스로도 추출했다.
콜백 패턴을 적용하고 나니 오히려 로직이 복잡해진 것 같기도 하다.
@FunctionalInterface
public interface PreparedStatementSetter {
void setValues(PreparedStatement p) throws SQLException;
}
public class DefaultStatementSetter {
PreparedStatementSetter getSetter(Object... args) {
return stmt -> {
try {
for (int i = 0; i < args.length; i++) {
stmt.setObject(i + 1, args[i]);
}
} catch (NullPointerException e) {
throw new SQLException(e);
}
};
}
}
@FunctionalInterface
public interface PreparedStatementCallback<T> {
T execute(PreparedStatement statement) throws SQLException;
}
@FunctionalInterface
public interface ResultSetCallback<T> {
T execute(ResultSet resultSet) throws SQLException;
}
@FunctionalInterface
public interface RowMapper<T> {
T mapRow(ResultSet rs) throws SQLException;
}
테스트에서는 리턴값을 검증하는 방법이 까다로워, 우선은 Mock 형태로 객체들이 close() 되었는지 여부만 확인했습니다.
class JdbcTemplateTest {
private final DataSource dataSource = mock(DataSource.class);
private JdbcTemplate jdbcTemplate;
@BeforeEach
void setUp() {
jdbcTemplate = new JdbcTemplate(dataSource);
}
@DisplayName("queryForObject 호출 시 결과 반환 후 connection, statement, resultSet이 close된다.")
@Test
void queryForObject() throws SQLException {
//given
Connection connection = mock(Connection.class);
PreparedStatement statement = mock(PreparedStatement.class);
ResultSet resultSet = mock(ResultSet.class);
when(dataSource.getConnection()).thenReturn(connection);
when(connection.prepareStatement(anyString())).thenReturn(statement);
when(statement.executeQuery()).thenReturn(resultSet);
//when
final var select = "select id, account, password, email from users where id = ?";
jdbcTemplate.queryForObject(select, mock(RowMapper.class), 1L);
//then
verify(statement).close();
verify(resultSet).close();
verify(connection).close();
}
@DisplayName("query 호출 시 결과 반환 후 connection, statement, resultSet이 close된다.")
@Test
void query() throws SQLException {
//given
Connection connection = mock(Connection.class);
PreparedStatement statement = mock(PreparedStatement.class);
ResultSet resultSet = mock(ResultSet.class);
when(dataSource.getConnection()).thenReturn(connection);
when(connection.prepareStatement(anyString())).thenReturn(statement);
when(statement.executeQuery()).thenReturn(resultSet);
//when
final var select = "select id, account, password, email from users where account = ?";
jdbcTemplate.query(select, mock(RowMapper.class), "hunch");
//then
verify(statement).close();
verify(resultSet).close();
verify(connection).close();
}
@DisplayName("executeUpdate 호출 시 결과 반환 후 connection, statement이 close된다.")
@Test
void executeUpdate() throws SQLException {
//given
Connection connection = mock(Connection.class);
PreparedStatement statement = mock(PreparedStatement.class);
when(dataSource.getConnection()).thenReturn(connection);
when(connection.prepareStatement(anyString())).thenReturn(statement);
when(statement.executeUpdate()).thenReturn(5);
//when
final var select = "update users set account = ?";
Integer integer = jdbcTemplate.executeUpdate(select, mock(RowMapper.class), "hunch");
//then
assertThat(integer).isEqualTo(5);
verify(statement).close();
verify(connection).close();
}
}
여러 방식으로 중복된 코드를 줄일 수 있구나...
콜백/템플릿 패턴을 통해 중복로직을 분리 하는 것이 재밌었다.
try-catch
문을 어떻게 줄일 수 있을지 고민했지만 해답을 못찾았던 적 있었다. 템플릿/콜백 패턴을 이용하면 가능하다! 다시 제네릭을 포함한 람다나 익명클래스로 리팩토링 해보고 싶다.