본문 바로가기
Spring&SpringBoot/<토비의 스프링 3.1 Vol1.1>, 이일민

JdbcTemplate에서 콜백 찾기

by 민휘 2023. 3. 13.

스프링은 Jdbc를 이용한 dao에서 사용할 수 있도록 준비된 다양한 템플릿과 콜백을 제공한다.

자주 사용되는 패턴을 가진 콜백은 다시 템플릿에 결합시켜서 간단한 메소드 호출만으로 사용이 가능하도록 만들어져있다.

 

 

공식 문서

Data Access

 

Data Access

The Data Access Object (DAO) support in Spring is aimed at making it easy to work with data access technologies (such as JDBC, Hibernate, or JPA) in a consistent way. This lets you switch between the aforementioned persistence technologies fairly easily, a

docs.spring.io

JdbcTemplate (Spring Framework 6.0.6 API)

 

JdbcTemplate (Spring Framework 6.0.6 API)

Execute a query for a result object, given static SQL. Uses a JDBC Statement, not a PreparedStatement. If you want to execute a static query with a PreparedStatement, use the overloaded JdbcOperations.queryForObject(String, Class, Object...) method with nu

docs.spring.io

 

 

인터페이스 알아보기

 

친절한 공식 문서에 따르면 읽기는 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));
	}

라이브러리 코드를 뜯어보는게 처음이라 굉장히 낯설었지만

배운 내용을 어렵지 않게 찾을 수 있어 되게 흥미로웠다.

잘 이해한 것 같아서 뿌듯하다 - !