JdbcTemplate
은 스프링 프레임워크에서 제공하는 JDBC(Java Database Connectivity) 작업을 보다 쉽게 처리할 수 있도록 도와주는 클래스다. JDBC는 자바 언어를 통해 데이터베이스에 접근하고 SQL 쿼리를 실행하는 데 사용되는 표준 API다. 그러나 JDBC를 직접 사용할 경우에는 반복적인 작업과 예외 처리 등으로 인해 코드가 복잡해질 수 있다.
아래 코드는 순수 JDBC를 이용한 코드다.
@Override
public void save(final long roomId, final long pieceId, final Square square) {
final String query = "INSERT INTO board (room_id, square, piece_id) VALUES (?, ?, ?)";
final Connection connection = getConnection();
try (final PreparedStatement preparedStatement = connection.prepareStatement(query,
Statement.RETURN_GENERATED_KEYS)) {
preparedStatement.setLong(1, roomId);
preparedStatement.setString(2, square.getName());
preparedStatement.setLong(3, pieceId);
preparedStatement.executeUpdate();
} catch (SQLException exception) {
throw new RuntimeException(exception);
} finally {
closeConnection(connection);
}
}
코드에서 알 수 있듯이 많은 양의 코드를 작성해야 한다. 뿐만 아니라 매 쿼리마다 Connection을 불러오고 SQL의 파라미터를 매핑해야 하는 수고로움이 존재한다.
JdbcTemplate
은 이러한 문제를 해결하기 위해 JDBC 작업을 단순화하고, 반복적인 코드를 줄여주는 역할을 한다.
@Override
public void save(final long roomId, final long pieceId, final Square square) {
final String query = "INSERT INTO board (room_id, square, piece_id) VALUES (?, ?, ?)";
jdbcTemplate.update(query, roomId, square.getName(), pieceId);
}
주요 기능은 다음과 같다.
SQL 쿼리 실행: JDBC를 사용하여 데이터베이스에 SQL 쿼리를 실행할 수 있다. JdbcTemplate
을 사용하면 SQL 쿼리를 실행하고 결과를 처리하는 과정이 간결해진다.
파라미터 바인딩: SQL 쿼리에 파라미터를 전달하여 동적으로 쿼리를 생성할 수 있다. JdbcTemplate
은 파라미터 바인딩을 지원하여 보안과 코드의 유연성을 향상시킨다.
예외 처리: JDBC 작업 중 발생할 수 있는 예외를 처리하기 위한 메커니즘을 제공한다. JdbcTemplate
은 JDBC 예외를 스프링의 DataAccessException으로 변환하여 편리한 예외 처리를 제공한다.
트랜잭션 관리: 스프링의 트랜잭션 매니저와 함께 사용하여 데이터베이스 트랜잭션을 관리할 수 있다. JdbcTemplate
은 트랜잭션 경계를 설정하고 롤백 및 커밋을 처리하는 데 도움을 준다.
JdbcTemplate
을 사용하면 JDBC 작업을 보다 간편하게 처리할 수 있으며, 스프링의 핵심 기능과 통합하여 개발 생산성을 향상시킬 수 있다.
JdbcTemplate 클래스를 활용한 JDBC 프로그래밍의 방법에 대해 알아보자.
DAO 클래스에서 JdbcTemplate을 초기화하는지 알아보자. 대부분 애플리케이션에서는 스프링이 DAO 클래스에 Bean에 주입해야 하는 DataSource 객체를 생성자에 넘긴 뒤 JdbcTemplate 인스턴스를 만든다.
public class JdbcTemplate {
public JdbcTemplate(DataSource dataSource) {
this.setDataSource(dataSource);
this.afterPropertiesSet();
}
public JdbcTemplate(DataSource dataSource, boolean lazyInit) {
this.setDataSource(dataSource);
this.setLazyInit(lazyInit);
this.afterPropertiesSet();
}
}
스프링이 DataSource를 주입하면 JdbcTemplate도 초기화돼 사용할 수 있다.
JdbcTemplate의 queryForObject 메서드를 이용하여 단일 객체를 조회할 수 있다.
public ReservationTime findById(long timeId) {
String sql = "SELECT id, start_at FROM reservation_time WHERE id = ?";
return jdbcTemplate.queryForObject(sql, ReservationTime.class, timeId);
}
첫 번째 매개변수는 쿼리문이며, 두 번째 매개변수는 조회 결과를 매핑할 클래스 타입, 세 번째 매개변수를 이용하여 쿼리문에 바인딩할 파라미터를 전달할 수 있다.
KeyHolder는 JDBC 작업을 수행할 때 자동으로 생성되는 DB의 키(Auto-Increment)를 보관하는 인터페이스다. 주로 INSERT 문을 실행한 후에 자동으로 생성된 키 값을 가져오는 데 사용된다.
KeyHolder keyHolder = new GeneratedKeyHolder();
jdbcTemplate.update(connection -> {
PreparedStatement ps = connection.prepareStatement(
"insert into customers (first_name, last_name) values (?, ?)",
new String[]{"id"});
ps.setString(1, customer.getFirstName());
ps.setString(2, customer.getLastName());
return ps;
}, keyHolder);
Long id = keyHolder.getKey().longValue();
GeneratedKeyHolder
클래스는 내부적으로 ArrayList로 구현되어 있고, ArrayList는 Thread-Safe하지 않기 때문에 주의해야 한다.
public class GeneratedKeyHolder implements KeyHolder {
private final List<Map<String, Object>> keyList;
public GeneratedKeyHolder() {
this.keyList = new ArrayList(1);
}
//...
}
하지만 여전히 위 KeyHolder를 이용한 방법은 가독서이 많이 떨어진다. 내부에 try-catch가 들어가게 된다면 가독성이 더 안좋아진다.
JdbcTemplate은 insert를 위해 SimpleJdbcInsert를 제공한다. 사용하기 위해서는 DataSource를 주입해야 한다.
@Repository
public class ReservationDao implements ReservationRepository {
private final JdbcTemplate jdbcTemplate;
private final SimpleJdbcInsert simpleJdbcInsert;
private final KeyHolder keyHolder = new GeneratedKeyHolder();
public ReservationDao(JdbcTemplate jdbcTemplate, DataSource dataSource) {
this.jdbcTemplate = jdbcTemplate;
this.simpleJdbcInsert = new SimpleJdbcInsert(dataSource)
.withTableName("reservation")
.usingGeneratedKeyColumns("id");
}
}
위 KeyHolder의 예시 코드를 다음과 같이 변경할 수 있다.
@Override
public Reservation save(final Reservation reservation) {
Map<String, Object> params = new HashMap<>();
params.put("name", reservation.getName());
params.put("date", reservation.getDate());
params.put("time_id", reservation.getTime().getId());
long id = simpleJdbcInsert.executeAndReturnKey(params).longValue();
return new Reservation(
id,
reservation.getName(),
reservation.getDate(),
reservation.getTime()
);
}
코드가 획기적으로 감소하고, 가독성도 좋아진 것을 느낄 수 있다. 다만, 저장해야할 칼럼이 많아지면 직접 Map으로 put()
하는 방식은 비효율적일 수 있다. 이를 위해 SqlParameterSource
를 제공한다.
SqlParameterSource
의 구현체 중 하나인 MapSqlParameterSource
를 사용하면 Map parameter 생성 시, 메서드 체이닝으로 가독성 있게 생성할 수 있다.
@Override
public Reservation save(final Reservation reservation) {
SqlParameterSource params = new MapSqlParameterSource()
.addValue("name", reservation.getName())
.addValue("date", reservation.getDate())
.addValue("time_id", reservation.getTime().getId());
long id = simpleJdbcInsert.executeAndReturnKey(params).longValue();
return new Reservation(
id,
reservation.getName(),
reservation.getDate(),
reservation.getTime()
);
}
SqlParameterSource
의 구현체 중 하나인 BeanPropertySqlParameterSource
를 사용할 수 있다. BeanPropertySqlParameterSource
는 이전 방식인 MapSqlParameterSource
보다 더 간단하게 사용할 수 있지만, Bean으로 등록 가능한 객체에만 적용할 수 있다.
Bean으로 등록 가능하기 위해서는 (getter 메서드) + (생성자 또는 setter 메서드)가 필요하다.
@Override
public Reservation save(final Reservation reservation) {
SqlParameterSource params = new BeanPropertySqlParameterSource(reservation);
long id = simpleJdbcInsert.executeAndReturnKey(params).longValue();
return new Reservation(
id,
reservation.getName(),
reservation.getDate(),
reservation.getTime()
);
}
일반적으로 DB에서 조회를 할 때는 값 하나만 조회하기보다는 한 row나 여러 row를 질의한 후 각 row를 도메인 객체나 엔티티로 변환해 사용한다. 스프링의 RowMapper<T>
를 사용하면 JDBC ResultSet을 간한 POJO 객체로 매핑할 수 있다.
List<Customer> customers = jdbcTemplate.query(
"select id, first_name, last_name from customers",
(resultSet, rowNum) -> {
Customer customer = new Customer(
resultSet.getLong("id"),
resultSet.getString("first_name"),
resultSet.getString("last_name")
);
return customer;
});
queryForObject와 마찬가지로 세 번째 매개변수를 이용하여 쿼리문에 바인딩할 파라미터를 전달할 수 있다.
RowMapper의 경우 별도로 선언하여 사용할 수 있다.
private final RowMapper<Customer> rowMapper = (resultSet, rowNum) -> {
Customer customer = new Customer(
resultSet.getLong("id"),
resultSet.getString("first_name"),
resultSet.getString("last_name"));
return customer;
};
public List<Customer> findFirstName() {
List<Customer> customers = jdbcTemplate.query(
"select id, first_name, last_name from customers where first_name = ?",
rowMapper, firstName
);
}
별도로 RowMapper로 사용할 경우, 중복된 코드를 줄일 수 있다.
JdbcTemplate의 update 메서드를 이용하여 INSERT, UPDATE, DELETE 쿼리를 실행할 수 있다.
아래는 update 메서드를 이용해 DELETE 쿼리를 실행하는 함수이다.
public boolean deleteById(long timeId) {
String sql = "DELETE FROM reservation_time WHERE id = ?";
int deleteId = jdbcTemplate.update(sql, timeId);
return deleteId != 0;
}
JDBC 드라이버는 동일한 여러 호출을 일괄 처리할 경우 성능이 향상된다. Update를 일괄 처리로 그룹화하기 batch를 활용할 수 있다.
Batch는 jdbcTemplate에서 batchUpdate()
를 활용하여 구현할 수 있다.
public int[] batchUpdate(final List<ReservationTime> times) {
return this.jdbcTemplate.batchUpdate(
"update reservation_time set start_at = ? where id = ?",
new BatchPreparedStatementSetter() {
public void setValues(PreparedStatement ps, int i) throws SQLException {
ReservationTime time = times.get(i);
ps.setTime(1, Time.valueOf(time.getStartAt()));
ps.setLong(2, time.getId());
}
public int getBatchSize() {
return times.size();
}
}
);
}