스프링은 Jdbc를 이용한 dao에서 사용할 수 있도록 준비된 다양한 템플릿과 콜백을 제공한다.
자주 사용되는 패턴을 가진 콜백은 다시 템플릿에 결합시켜서 간단한 메소드 호출만으로 사용이 가능하도록 만들어져있다.
공식 문서
JdbcTemplate (Spring Framework 6.0.6 API)
인터페이스 알아보기
친절한 공식 문서에 따르면 읽기는 query, 쓰기는 update, DDL은 execute 인터페이스를 사용한다고 한다.
UserDao에 읽기와 쓰기 sql을 실행하도록 JdbcTemplate을 적용해보았다.
✅ JdbcTemplate 의존성 주입하기
public class UserDao {
private JdbcTemplate jdbcTemplate;
public void setDataSource(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
// ..
}
Updating
Updating은 쓰기 관련 작업이기 때문에 반환값이 없다. (쿼리-명령 분리 규칙)
update() 인터페이스를 사용할 때는 실행 결과를 매핑하는 등의 작업이 필요하지 않다.
update() 매개변수에는 PreparedStatementCreator 혹은 final String sql & 치환자 값 배열을 넘겨줄 수 있다.
코드를 뜯어보면 큰 주석으로 구분하고 있다.
✅ void deleteAll
deleteAll은 치환자가 없다. string sql을 사용하는 편이 훨씬 간결하다.
public void deleteAll() throws SQLException {
// Methods dealing with static SQL (java.sql.Statement)
// StatementCallback 사용
jdbcTemplate.update("delete from users");
}
PreparedStatement도 사용할 수 있다.
뜯어보니 PreparedStatementCreator는 PreparedStatement를 생성하는 추상 메소드를 하나 가진 함수형 인터페이스였다.
람다식으로도 표현 가능하다.
public void deleteAll() throws SQLException {
// Methods dealing with prepared statements
// PreparedStatementCallback 사용
jdbcTemplate.update(new PreparedStatementCreator() {
@Override
public PreparedStatement createPreparedStatement(Connection con) throws SQLException {
return con.prepareStatement("delete from users");
}
});
}
✅ void add
add는 치환자가 있는 sql을 실행해야 한다.
마찬가지로 PreparedStatement 혹은 final String sql과 매개변수를 넘겨줄 수 있다.
public void add(User user) throws SQLException {
// static sql을 사용하는 콜백
jdbcTemplate.update("insert into users(id, name, password) values(?,?,?)",
user.getId(), user.getName(), user.getPassword());
}
public void add(User user) throws SQLException {
// PreparedStatement를 사용하는 콜백
jdbcTemplate.update(con -> {
PreparedStatement ps = con.prepareStatement("insert into users(id, name, password) values(?,?,?)");
ps.setString(1, user.getId());
ps.setString(2, user.getName());
ps.setString(3, user.getPassword());
return ps;
});
}
Querying
Updating은 쓰기 관련 작업이기 때문에 반환값이 없다. (쿼리-명령 분리 규칙)
update() 인터페이스를 사용할 때는 실행 결과를 매핑하는 등의 작업이 필요하지 않다.
update() 매개변수에는 PreparedStatementCreator 혹은 final String sql & 치환자 값 배열 을 넘겨줄 수 있다.
✅ int getCount
getCount는 치환자가 없고 정수값을 반환한다.
query도 마찬가지로 PreparedStatement나 final String sql을 넘길 수 있다.
query 인터페이스에 PreparedStatement, ResultSetExtractor<Integer>를 넘겨준다.
ResultSetExtractor는 sql의 실행 결과인 ResultSet을 사용해 어떻게 값을 추출할 것인지 정의한다.
인텔리제이가 람다로 바꾸라고 하는 것을 보니 함수형 인터페이스이다.
public int getCount() throws SQLException {
// PreparedStatement 사용
// PreparedStatement, ResultSetExtractor<Integer> 넘겨줌
return this.jdbcTemplate.query(
con -> con.prepareStatement("select count(*) from users"),
rs1 -> { rs1.next(); return rs1.getInt(1); }
);
}
✅ User get
User get은 치환자가 있고 User 타입을 반환한다.
오브젝트 하나를 반환하므로 queryForObject를 사용했다.
치환자는 sql문 바로 뒤에 Object[] 배열로 넘긴다.
마지막 파라미터는 RowMapper인데, 다른 메소드에서도 사용돼서 메소드로 추출했다.
RowMapper는 반복문 안에서 사용되는 콜백이다.
데이터베이스에서 데이터를 읽으면 rs에 담기는데, 로우 하나를 User에 매핑하는 역할을 한다.
이 콜백은 사용자가 반환받고 싶어하는 타입에 따라 달라지기 때문에 제네릭 타입을 사용했다.
public User get(String id) throws SQLException, EmptyResultDataAccessException {
return this.jdbcTemplate.queryForObject(
"select * from users where id = ?",
new Object[]{id},
getUserRowMapper()
);
}
private static RowMapper<User> getUserRowMapper() {
return (rs, rowNum) -> {
User user = new User();
user.setId(rs.getString("id"));
user.setName(rs.getString("name"));
user.setPassword(rs.getString("password"));
return user;
};
}
✅ List<User> getAll
getAll은 치환자가 없고 List<User>를 반환한다.
public List<User> getAll() {
return this.jdbcTemplate.query(
"select * from users order by id",
getUserRowMapper()
);
}
private static RowMapper<User> getUserRowMapper() {
return (rs, rowNum) -> {
User user = new User();
user.setId(rs.getString("id"));
user.setName(rs.getString("name"));
user.setPassword(rs.getString("password"));
return user;
};
}
RowMapper가 정말 반복에 사용되는 콜백인지 살펴보자.
getAll이 사용하는 jdbcTemplate에 정의된 query(sql, RowMapper) 메소드이다.
public <T> List<T> query(String sql, RowMapper<T> rowMapper) throws DataAccessException {
return query(sql, new RowMapperResultSetExtractor<T>(rowMapper));
}
// 어댑터
private static class RowCallbackHandlerResultSetExtractor implements ResultSetExtractor<Object> {
private final RowCallbackHandler rch;
public RowCallbackHandlerResultSetExtractor(RowCallbackHandler rch) {
this.rch = rch;
}
public Object extractData(ResultSet rs) throws SQLException {
while (rs.next()) {
this.rch.processRow(rs);
}
return null;
}
}
while 문에서 ResultSet가 존재할 때까지 rs를 탐색하고 있다.
내부적으로 우리가 넘겨준 콜백이 실행되고 있다.
콜백 재사용
본문 설명 중에 콜백 재사용이 있었다.
클래스 구조가 좀 복잡한 것 같아서 우선 JdbcTemplate.java 소스 안에서만 콜백 재사용을 살펴보았다.
아마 처음 템플릿으로 분리한 메소드는 execute인 것 같다.
코드를 살펴보면 매개변수의 타입에 따라 여러 execute가 존재한다. (Connection, String sql, PreparedStatement)
execute 메소드 내부를 보면 리소스를 준비하고 예외 처리하고 리소스를 닫는 작업을 확인할 수 있다.
//-------------------------------------------------------------------------
// Methods dealing with prepared statements
//-------------------------------------------------------------------------
public <T> T execute(PreparedStatementCreator psc, PreparedStatementCallback<T> action)
throws DataAccessException {
Assert.notNull(psc, "PreparedStatementCreator must not be null");
Assert.notNull(action, "Callback object must not be null");
if (logger.isDebugEnabled()) {
String sql = getSql(psc);
logger.debug("Executing prepared SQL statement" + (sql != null ? " [" + sql + "]" : ""));
}
Connection con = DataSourceUtils.getConnection(getDataSource());
PreparedStatement ps = null;
try {
Connection conToUse = con;
if (this.nativeJdbcExtractor != null &&
this.nativeJdbcExtractor.isNativeConnectionNecessaryForNativePreparedStatements()) {
conToUse = this.nativeJdbcExtractor.getNativeConnection(con);
}
ps = psc.createPreparedStatement(conToUse);
applyStatementSettings(ps);
PreparedStatement psToUse = ps;
if (this.nativeJdbcExtractor != null) {
psToUse = this.nativeJdbcExtractor.getNativePreparedStatement(ps);
}
T result = action.doInPreparedStatement(psToUse);
handleWarnings(ps);
return result;
}
catch (SQLException ex) {
// Release Connection early, to avoid potential connection pool deadlock
// in the case when the exception translator hasn't been initialized yet.
if (psc instanceof ParameterDisposer) {
((ParameterDisposer) psc).cleanupParameters();
}
String sql = getSql(psc);
psc = null;
JdbcUtils.closeStatement(ps);
ps = null;
DataSourceUtils.releaseConnection(con, getDataSource());
con = null;
throw getExceptionTranslator().translate("PreparedStatementCallback", sql, ex);
}
finally {
if (psc instanceof ParameterDisposer) {
((ParameterDisposer) psc).cleanupParameters();
}
JdbcUtils.closeStatement(ps);
DataSourceUtils.releaseConnection(con, getDataSource());
}
}
그리고 execute에 넘겨주는 콜백은 sql을 생성하는 부분일 것이다.
그런데 sql의 종류에 따라 조회 타입과 쓰기 타입이 많이 사용되므로 두 개의 패턴이 중복해서 나타났을 것이고,
이를 다시 분리해 템플릿에 넣은 듯하다.
통합된 콜백 안에서 템플릿인 execute()를 호출하고 있다.
PreparedStatementCreator psc, final PreparedStatementSetter pss, final ResultSetExtractor<T> rse)
throws DataAccessException {
Assert.notNull(rse, "ResultSetExtractor must not be null");
logger.debug("Executing prepared SQL query");
return execute(psc, new PreparedStatementCallback<T>() {
public T doInPreparedStatement(PreparedStatement ps) throws SQLException {
ResultSet rs = null;
try {
if (pss != null) {
pss.setValues(ps);
}
rs = ps.executeQuery();
ResultSet rsToUse = rs;
if (nativeJdbcExtractor != null) {
rsToUse = nativeJdbcExtractor.getNativeResultSet(rs);
}
return rse.extractData(rsToUse);
}
finally {
JdbcUtils.closeResultSet(rs);
if (pss instanceof ParameterDisposer) {
((ParameterDisposer) pss).cleanupParameters();
}
}
}
});
}
마지막으로 SQL에 넘겨줄 수 있는 다양한 매개변수 형태를 처리하기 위해 메소드를 오버로딩해서 정의해놓았다.
public <T> T query(PreparedStatementCreator psc, ResultSetExtractor<T> rse) throws DataAccessException {
return query(psc, null, rse);
}
public <T> T query(String sql, PreparedStatementSetter pss, ResultSetExtractor<T> rse) throws DataAccessException {
return query(new SimplePreparedStatementCreator(sql), pss, rse);
}
public <T> T query(String sql, Object[] args, int[] argTypes, ResultSetExtractor<T> rse) throws DataAccessException {
return query(sql, newArgTypePreparedStatementSetter(args, argTypes), rse);
}
public <T> T query(String sql, Object[] args, ResultSetExtractor<T> rse) throws DataAccessException {
return query(sql, newArgPreparedStatementSetter(args), rse);
}
public <T> T query(String sql, ResultSetExtractor<T> rse, Object... args) throws DataAccessException {
return query(sql, newArgPreparedStatementSetter(args), rse);
}
public void query(PreparedStatementCreator psc, RowCallbackHandler rch) throws DataAccessException {
query(psc, new RowCallbackHandlerResultSetExtractor(rch));
}
public void query(String sql, PreparedStatementSetter pss, RowCallbackHandler rch) throws DataAccessException {
query(sql, pss, new RowCallbackHandlerResultSetExtractor(rch));
}
public void query(String sql, Object[] args, int[] argTypes, RowCallbackHandler rch) throws DataAccessException {
query(sql, newArgTypePreparedStatementSetter(args, argTypes), rch);
}
public void query(String sql, Object[] args, RowCallbackHandler rch) throws DataAccessException {
query(sql, newArgPreparedStatementSetter(args), rch);
}
public void query(String sql, RowCallbackHandler rch, Object... args) throws DataAccessException {
query(sql, newArgPreparedStatementSetter(args), rch);
}
public <T> List<T> query(PreparedStatementCreator psc, RowMapper<T> rowMapper) throws DataAccessException {
return query(psc, new RowMapperResultSetExtractor<T>(rowMapper));
}
public <T> List<T> query(String sql, PreparedStatementSetter pss, RowMapper<T> rowMapper) throws DataAccessException {
return query(sql, pss, new RowMapperResultSetExtractor<T>(rowMapper));
}
public <T> List<T> query(String sql, Object[] args, int[] argTypes, RowMapper<T> rowMapper) throws DataAccessException {
return query(sql, args, argTypes, new RowMapperResultSetExtractor<T>(rowMapper));
}
public <T> List<T> query(String sql, Object[] args, RowMapper<T> rowMapper) throws DataAccessException {
return query(sql, args, new RowMapperResultSetExtractor<T>(rowMapper));
}
public <T> List<T> query(String sql, RowMapper<T> rowMapper, Object... args) throws DataAccessException {
return query(sql, args, new RowMapperResultSetExtractor<T>(rowMapper));
}
라이브러리 코드를 뜯어보는게 처음이라 굉장히 낯설었지만
배운 내용을 어렵지 않게 찾을 수 있어 되게 흥미로웠다.
잘 이해한 것 같아서 뿌듯하다 - !
'Spring&SpringBoot > <토비의 스프링 3.1 Vol1.1>, 이일민' 카테고리의 다른 글
좋은 예외 처리에 대한 고찰 (0) | 2023.03.21 |
---|---|
낙관적인 예외 처리 (0) | 2023.03.21 |
콜백 분리 연습 (0) | 2023.03.13 |
반복되는 부분으로부터 반복되지 않는 부분을 분리하는 패턴 (0) | 2023.03.13 |
템플릿 콜백 패턴으로 가독성 높이기 (0) | 2023.03.13 |