프레임워크/Spring

[Spring] JdbcTemplate이란 무엇인가?

멍토 2021. 4. 28.

나는 이전까지 JDBC를 이용하여 프로젝트를 진행했었다.(아는게 이거밖에 없었다.)

JDBC는 데이터베이스 종류에 영향을 받지 않고 정해진 API를 이용하여 처리하는 것이다.

이러한 JDBC는 추상화를 통해 영향을 덜 받게 만들었지만 불편함이 남아있었는데 그것은 아래와 같다.

  1. Connection으로 연결하기
  2. 쿼리를 작성 후 PreparedStatement로 실행하기
  3. resultSet을 이용하여 결과 처리하기
  4. Connection 닫아주기

의 과정이 필요하다. 편리함을 겪기 전까지는 당연하다고 생각이 들었던 내용이지만 JDBC Template을 써보니 중복되는 부분이 많이 제거되어 편리함을 느꼈다.

JdbcTemplate은 JDBC와 마찬가지로 데이터를 저장하기 위해 도와주는 API이다. 다른점이라면 SQL Mapper라는 점이다.

SQL Mapper는 SQL을 직접 작성하고 Object의 필드를 매핑하여 데이터를 객체화 하는것이다.

간단한 예시를 살펴보면 아래와 같다.

public class PieceDao() {
    public Connection connection() {
        Connection connection = null;
        String server = "localhost:13306";
        String database = "db_name";
        String option = "?useSSL=false&serverTimezone=UTC&useUnicode=true&characterEncoding=utf8";
        String userName = "root";
        String password = "root";

        try {
            Class.forName("com.mysql.cj.jdbc.Driver");
        } catch (ClassNotFoundException e) {
            System.err.println(" !! JDBC Driver load 오류: " + e.getMessage());
            e.printStackTrace();
        }

        try {
            connection = DriverManager
                .getConnection("jdbc:mysql://" + server + "/" + database + option, userName,
                               password);
        } catch (SQLException e) {
            System.err.println("연결 오류:" + e.getMessage());
            e.printStackTrace();
        }

        return connection;
    }

    public void closeConnection(Connection connection) {
        try {
            if (connection != null) {
                connection.close();
            }
        } catch (SQLException e) {
            System.err.println("con 오류:" + e.getMessage());
        }
    }

    protected void disconnect(PreparedStatement preparedStatement, ResultSet resultSet) throws SQLException {
        if (!Objects.isNull(preparedStatement)) {
            preparedStatement.close();
        }
        if (!Objects.isNull(resultSet)) {
            resultSet.close();
        }
    }

    public void addPieces(final int boardId, final Map<Position, Piece> pieces) throws SQLException {
        Connection connection = connection();
        PreparedStatement preparedStatement = null;
        connection.setAutoCommit(false);
        try {
            for (Piece piece : pieces.values()) {
                if (Objects.isNull(piece)) {
                    continue;
                }
                String query = "INSERT INTO piece(board_id, piece_position, piece_symbol) VALUES (?, ?, ?)";
                preparedStatement = connection.prepareStatement(query);
                addPieceDataInit(boardId, preparedStatement, piece);
                preparedStatement.executeUpdate();
                preparedStatement.close();
            }
        } finally {
            disconnect(preparedStatement, null);
            closeConnection(connection);
        }
    }

    private void addPieceDataInit(final int boardId, final PreparedStatement preparedStatement,
        final Piece piece) throws SQLException {
        preparedStatement.setInt(1, boardId);
        preparedStatement.setString(2, piece.position().changedPositionToString());
        preparedStatement.setString(3, piece.symbol());
    }
}

이 코드는 체스게임에서 말들의 정보를 저장하기 위한 코드이다.

JDBC로 작성하면 위와 같이 길어지는 부분이 있다.

이러한 코드를 JdbcTemplate으로 바꾸면 아래와 같아진다.

@Repository
public class PieceDao {
    private final JdbcTemplate jdbcTemplate;

    public PieceDao(final JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    public void addPieces(long boardId, Map<Position, Piece> pieces) {
        String sql = "INSERT INTO piece(board_id, piece_position, piece_symbol) VALUES (?, ?, ?)";
        List<Position> positions = new ArrayList<>(pieces.keySet());
        jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() {
            @Override
            public void setValues(PreparedStatement preparedStatement, int i) throws SQLException {
                Piece piece = pieces.get(positions.get(i));
                preparedStatement.setInt(1, boardId);
                preparedStatement.setString(2, piece.position().changedPositionToString());
                preparedStatement.setString(3, piece.symbol());
            }

            @Override
            public int getBatchSize() {
                return pieces.size();
            }
        });
    }
}

속도를 위한 BatchInsert를 적용했음에도 불구하고 불필요한 처리작업이 사라져 코드가 간결해졌다.

아래는 추가적인 사용법이다.

기본적으로 제공되는 타입은 아래와 같이 가져올 수 있다.

int rowCount = this.jdbcTemplate.queryForObject("select count(*) from t_actor", Integer.class);

int countOfActorsNamedJoe = this.jdbcTemplate.queryForObject(
        "select count(*) from t_actor where first_name = ?", Integer.class, "Joe");

String lastName = this.jdbcTemplate.queryForObject(
        "select last_name from t_actor where id = ?",
        String.class, 1212L);

다음은 객체를 가져올때이다.

기존적으로 제공되는 타입과 다르게 지정을 해줘야 한다.

Actor actor = jdbcTemplate.queryForObject(
        "select first_name, last_name from t_actor where id = ?",
        (resultSet, rowNum) -> {
            Actor newActor = new Actor();
            newActor.setFirstName(resultSet.getString("first_name"));
            newActor.setLastName(resultSet.getString("last_name"));
            return newActor;
        },
        1212L);

여러개의 데이터를 가져오기 위해서는 queryForObject가 아니라 query로 동작하면 된다.

List<Actor> actors = this.jdbcTemplate.query(
        "select first_name, last_name from t_actor",
        (resultSet, rowNum) -> {
            Actor actor = new Actor();
            actor.setFirstName(resultSet.getString("first_name"));
            actor.setLastName(resultSet.getString("last_name"));
            return actor;
        });

여기서 위에처럼 2개의 로직을 작성하게 된다면 중복이 발생하게 된다. 그래서 RowMapper를 이용하여 중복을 줄일 수 있다.

private final RowMapper<Actor> actorRowMapper = (resultSet, rowNum) -> {
    Actor actor = new Actor();
    actor.setFirstName(resultSet.getString("first_name"));
    actor.setLastName(resultSet.getString("last_name"));
    return actor;
};

public List<Actor> findAllActors() {
    return this.jdbcTemplate.query( "select first_name, last_name from t_actor", actorRowMapper);
}

insert, update, delete와 같은 경우는 update 하나를 이용해서 동작시킨다.

this.jdbcTemplate.update(
        "insert into t_actor (first_name, last_name) values (?, ?)",
        "Leonor", "Watling");

this.jdbcTemplate.update(
        "update t_actor set last_name = ? where id = ?",
        "Banjo", 5276L);

this.jdbcTemplate.update(
        "delete from t_actor where id = ?",
        Long.valueOf(actorId));

DDL을 사용할 때는 excute를 이용한다.

this.jdbcTemplate.execute("create table mytable (id integer, name varchar(100))");

물론 나중에 가면 JPA를 이용해서 이것조차 사용하지 않다고 알고 있다.

그렇지만 지금은 JdbcTemplate을 써본 입장으로써 JDBC와 차이점과 느낀점을 남기고 싶었다.

좀 더 자세한 내용을 알고 싶다면 아래의 공식문서를 찾아보면 좋을 것 같다.

출처 : https://docs.spring.io/spring-framework/docs/current/reference/html/data-access.html#jdbc-JdbcTemplate

https://docs.spring.io/spring-framework/docs/current/reference/html/data-access.html#jdbc-JdbcTemplate-examples-query

댓글

💲 광고입니다.