+ 배경지식
https://splendidlolli.tistory.com/561
Java에서 DB와 연결되기 위해서는 Jdbc api를 거쳐야 한다.
개발자는 Jdbc api를 사용하여 DB에 접근(query 보내기 등)할 수 있다.
DB에 연결하고(Connection을 얻고), Connection으로부터 Statement 객체를 생성해 query문을 날릴 수 있으며, query를 수행해 ResultSet을 얻어낼 수 있다(=> 아니면 단순히 query를 수행)
만약 ResultSet을 얻어냈다면 ResultSet에 담긴 내용을 원하는 형태로 매핑하여 원하는 query수행 결과를 얻어낼 수 있다.
위 배경지식에 설명한 내용을 직접 일일이 작성해서 query를 보낼 수 있다.
그런데 Spring은 Jdbc의 Template을 제공하기 때문에 꼭 귀찮게 그러지 않아도 된다.
https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/jdbc/core/JdbcTemplate.html
- Spring JdbcTemplate을 이용하려면 당연히 Spring 기능을 사용한다.
- 즉 JdbcTemplate Bean이 Spring container에 등록된다. 이때 Springboot를 사용한다면 DataSoucre, DB정보 등은 application.yml 파일 등에 적어두면 된다. Spring이 yml 파일에서 정보를 읽어와 알아서 Connection을 얻어오고 Bean으로 잘 등록한다. (AutoConfiguration)
이 포스팅에서는 JdbcTemplate의 동작을 간단히 확인해 볼 것이고,
직접 작성한 위 과정이 결국 Template으로 제공되고 있다는 것을 직접 확인할 것이다!
시작!
JDBC api 직접 사용하여 쿼리 날려보기
직접 Connection을 얻어 findAll하는 query를 날려보는 과정이다.
설명은 주석에!
@Override
public List<Customer> findAll() {
List<Customer> allCustomers = new ArrayList<>();
try (
// 먼저 Connection을 얻어왔다.
var connection = dataSource.getConnection();
// Connection으로부터 Statement를 얻어왔다.
var statement = connection.prepareStatement("select * from customers");
// Statement로부터 ResultSet을 얻어왔다.
var resultSet = statement.executeQuery()
) {
// 얻은 resultSet을 돌면서 원하는 형식으로 매핑해주었다.
while(resultSet.next()){
mapToCustomer(allCustomers, resultSet);
}
// 나는 이 블록을 빠져나가면 자동으로 Connection이 close()되는 HikariCP를 사용했기 때문에
// close()는 명시하지 않아도 되었지만, 분명히 connection을 close()해줘야 함은 잊지말자
} catch (SQLException throwable) {
logger.error("Got error while closing connection", throwable);
throw new RuntimeException(throwable);
}
// 원하는 형태로 리턴!
return allCustomers;
}
DB Connection을 얻어오고 Statement를 얻어와서 원하는 query를 수행하는 과정을 실습한 것이다.
Java에서 DB와 연결되어 query를 수행하는 과정은 이런식이다.
그런데, 매번 이렇게 Connection얻고, Statment얻어서 처리하고, ResultSet 얻어서 원하는 형식으로 또 매핑하고, 예외처리할 부분은 예외처리해주고, close할 부분 해주고...
이런 과정을 매번 코딩하지 않아도 된다.
JDBC는 이러한 Template을 제공하기 때문이다.
JdbcTemplate를 사용하도록 위 코드를 바꾸면, 이렇게나 간결해진다.
@Override
public List<Customer> findAll() {
return jdbcTemplate.query("select * from customers", customerRowMapper);
}
JdbcTemplate 내부적으로 저런 로직을 제공하기 때문이다.
그럼 뜯어보자!
JdbcTemplate의 query() 메서드 살펴보기
Jdbc Template을 사용하면 이렇게 간결해진다.
@Override
public List<Customer> findAll() {
return jdbcTemplate.query("select * from customers", customerRowMapper);
// jdbc template에서는 query()라는 메서드를 제공
// sql와 RowMapper를 전달
}
내부적으로 어떻게 구성되어있길래 이게 가능할까?
결국 Connection 얻기, Statment 사용하기, ResultSet 처리, Connection close하는 부분이 모두 Template으로 제공되기 떄문이다!
=> 슬슬 확인해보자.
방금 사용한 JdbcTemplate의 query() 메서드를 확인해보자.
반환하는 값이 result로 감싸져있다.
@Override
public <T> List<T> query(String sql, RowMapper<T> rowMapper) throws DataAccessException {
return result(query(sql, new RowMapperResultSetExtractor<>(rowMapper)));
}
잠깐! 왜 result(...)를 반환할까? result()로 들어가보자.
그렇구나. 인자(query(...)가) null인지 아닌지를 먼저 검증해주고 있다. 오케이!
result()가 뭔지는 이정도로만 하자. 인자가 null인지 검증.
private static <T> T result(@Nullable T result) {
Assert.state(result != null, "No result");
return result;
}
그럼 다시 JdbcTemplate의 query()메서드로 돌아와서 (이전과 같은 코드임)
result() 속에 있는 query()를 보자.
인자로 sql문과 RowMapperResultSetExtractor 객체를 받고 있다.
이 query() 메서드는 뭘까? 들어가보자.
@Override
public <T> List<T> query(String sql, RowMapper<T> rowMapper) throws DataAccessException {
return result(query(sql, new RowMapperResultSetExtractor<>(rowMapper)));
}
저 query() 메서드는 이렇게 되어있다
설명을 주석으로 달아보겠다.
@Override
@Nullable
public <T> T query(final String sql, final ResultSetExtractor<T> rse) throws DataAccessException{
// 먼저 인자가 null인지 먼저 검증해주고 있네
Assert.notNull(sql, "SQL must not be null");
Assert.notNull(rse, "ResultSetExtractor must not be null");
if (logger.isDebugEnabled()) {
logger.debug("Executing SQL query [" + sql + "]");
}
// 콜백이 존재한다!
// 메서드 내에 이 콜백클래스인 QueryStatementCallback을 구현하고 있구나.
// 이 콜백을 좀 더 들여다보자.
/**
* Callback to execute the query.
*/
class QueryStatementCallback implements StatementCallback<T>, SqlProvider {
@Override
@Nullable
// doInStatement라는 메서드를 구현하고 있다!
// 인자를 보니까 반가운 Statement가 보인다.
public T doInStatement(Statement stmt) throws SQLException {
// 반가운 ResultSet도 보인다!
ResultSet rs = null;
try {
// ResultSet을 얻어내고 있다! stmt.executeQuery(sql)을 통해서..! 익숙한 부분!
rs = stmt.executeQuery(sql);
return rse.extractData(rs);
}
finally {
// 뭔가 ResultSet을 close하는 부분도 보인다!!
JdbcUtils.closeResultSet(rs);
}
}
@Override
public String getSql() {
return sql;
}
}
// 결국 리턴하는 값을 보니까 excute(인자 콜백)을 리턴한다.
// 메서드 내부에서 구현한 콜백을 사용하여 execute에 넣어주고 있다.
return execute(new QueryStatementCallback(), true);
}
그럼 리턴하는 값인 execute(new QueryStatementCallback(), true)를 확인해보자.
execute()가 콜백을 인자로 받아서 어떤 동작을 하길래,
JdbcTemplate의 query()메서드를 통해 손쉽게 sql문을 실행할 수 있는 걸까?
아래는 해당 execute() 메서드다.
와! 익숙한 로직!
직접 DataSource를 통해 connection을 얻어내고, statement를 얻고, resultSet을 얻었던
직접 모든걸 손수 작성했던 그 코드랑 동일한 로직이다!
설명을 위해 주석을 달아두겠다.
@Nullable
private <T> T execute(StatementCallback<T> action, boolean closeResources) throws DataAccessException {
Assert.notNull(action, "Callback object must not be null");
// DataSource로부터 Connection을 얻어내는 부분!! (익숙!!)
Connection con = DataSourceUtils.getConnection(obtainDataSource());
Statement stmt = null;
try {
// connection으로부터 Statement를 얻어내는 부분!! (익숙!!)
stmt = con.createStatement();
applyStatementSettings(stmt);
// 와 아무튼 stmt로부터 result를 얻는 부분이다!!
// T는 내가 사용하고자 하는 RowMapper<T>의 T다! (앞에 언급한 메서드 참고)
// 기억하기 : 내가 jdbc template의 어떤 query() 메서드를 쓰기 위해서 RowMapper를 정의해서 전달했었다.
T result = action.doInStatement(stmt);
handleWarnings(stmt);
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.
String sql = getSql(action);
JdbcUtils.closeStatement(stmt);
stmt = null;
DataSourceUtils.releaseConnection(con, getDataSource());
con = null;
throw translateException("StatementCallback", sql, ex);
}
finally {
// 손수 닫아줘야 할 경우 close하는 부분까지 있음!!
if (closeResources) {
JdbcUtils.closeStatement(stmt);
DataSourceUtils.releaseConnection(con, getDataSource());
}
}
}
그래서 이름이 JdbcTemplate이구나!!
마지막으로 요약!
JdbcTemplate을 나름대로 설명해보자면 이렇다.
Java에서 Jdbc api를 통해 DB에 접근하는 방식인 Connection 얻고, Statement를 통해 쿼리 날리고, ResultSet 처리 필요하면 한다. 쿼리 날린 후 마지막으로 Statement와 Connection을 닫아주는 그 전형적인 과정을 편하게 이용할 수 있도록 템플릿으로 제공한 것이다.
이 글에서는 List를 받아오는 query() 중 하나만 뜯어보았지만,
수행하려는 query에 따라 내부적으로 사용하는 Template이 다르다.
그런데 아무튼 그 query들을 편하게 날릴 수 있도록 적절한 Template을 제공한다는 것이다!
한계점?
다만 JDBC Template을 이용하면 자바코드상에 SQL문이 박혀 들어간다.
코드와 쿼리가 막 섞여있으면 유지보수가 힘들어진다.
쿼리를 관리하기 위해서 QueryMapper 라이브러리가 나오게 되었는데, 대표적인 것이 Mybatis다.
다음 글에서 간단히 QueryMapper를 사용하는 방식을 공부한다.
.
.
QueryMapper를 사용하는 방법은 크게 두가지다.
- Annotation 사용
- Xml 파일 사용
그 중에서 xml파일로 쿼리문을 한번에 모아 관리하고,
이 쿼리문을 QueryMapper 중 Mybatis를 통해 매핑해서 쓰는 방법을 정리해보았다.
↓
Springboot Mybatis 사용해보기 ─ xml 파일로 쿼리문 관리
'JAVA > Application' 카테고리의 다른 글
[JPA] Entity는 영속성 컨텍스트에 저장/조회한다! (4) | 2023.01.25 |
---|---|
Springboot Mybatis 사용해보기 ─ xml 파일로 쿼리문 관리 (0) | 2023.01.23 |
Spring MVC | Controller는 어떻게 요청을 처리하는 걸까? | Controller와 Servlet (0) | 2023.01.19 |
Spring AOP 쉽게 이해하기 | 그리고 간단한 예제코드 (0) | 2023.01.16 |
post 요청 후 406 Not Acceptable 해결 - Dto에 Getter 붙이기 (0) | 2023.01.16 |