JAVA/Application

JdbcTemplate | 왜 Jdbc 'Template'인가? 내부 동작 간단히 살펴보기

히어로맛쿠키 2023. 1. 11. 23:45

+ 배경지식

https://splendidlolli.tistory.com/561

[JDBC] DB Connection을 얻어서 query 실행 | DriverManager과 DataSource를 통하여 (+ Connection Pool 개념)

JDBC JDBC는 Java에서 DB에 접속할 수 있게 하는 API이다. JDK에 포함되어있다. Persistence Layer(즉 DB와 연결하는 부분)을 위해 존재한 최초의 Component가 JDBC이다. 즉 JDBC는 역사가 깊은 API다. JDBC driver Java에

splendidlolli.tistory.com

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

JdbcTemplate (Spring Framework 6.0.4 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

- 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 파일로 쿼리문 관리

Springboot Mybatis 사용해보기 ─ xml 파일로 쿼리문 관리

학습 키워드 : 자바코드와 쿼리의 분리 &쿼리매퍼인 Mybatis Mybatis를 왜 이용할까? JdbcTemplate 사용시 query가 자바코드에 하드코딩되는 문제가 있다. 이때 쿼리매퍼인 Mybatis를 사용하면, 자바 코드와

splendidlolli.tistory.com

 
 
 

반응형