자바 진영에서 DB에 접근하기 위해서는 JDBC를 사용한다. 그런데 실제 JDBC를 사용하여 DB에 접근할 때 필요한 코드를 보면, 실제 DB에 날리는 SQL 쿼리와 관련된 코드보다 그 외의 코드들이 양이 더 많다.
참고.
커넥션 획득, statement 준비 및 실행, resultset 조회, 커넥션 종료, 트랜잭션을 위한 커넥션 동기화, 예외 발생시 스프링 예외 변환기 실행 등이 실제 SQL 쿼리와 무관하게 해야하는 작업들이다.
위의 참고에 적힌 요소들은 DB에 접근하기 위해 필수적인 코드들이지만, 개발자 입장에서는 SQL 쿼리와 관련된 코드에만 집중하고 싶다.
따라서, 스프링은 JDBC를 직접 사용할 때 발생하는 대부분의 반복작업들을 대신 처리해주는 JDBC Template을 제공한다. JDBC Template을 사용하면, 개발자는 실행할 SQL을 작성하고, SQL에 들어갈 상수값을 세팅하여 실행시키면 된다. 그 외의 부가적인 반복업무들은 JDBC Template이 대신 처리해준다.
아래의 코드를 보자. JDBC Template을 이용하여 INSERT 쿼리를 날리는 코드이다.
@Repository
public class JdbcTemplateItemRepositoryV1 implements ItemRepository {
private final JdbcTemplate template;
public JdbcTemplateItemRepositoryV1(DataSource dataSource) {
this.template = new JdbcTemplate(dataSource);
}
@Override
public Item save(Item item) {
String sql = "insert into item (item_name, price, quantity) values (?, ?, ?)";
KeyHolder keyHolder = new GeneratedKeyHolder();
template.update(connection -> {
//자동 증가 키
PreparedStatement ps = connection.prepareStatement(sql, new String[]{"id"});
ps.setString(1, item.getItemName());
ps.setInt(2, item.getPrice());
ps.setInt(3, item.getQuantity());
return ps; }, keyHolder);
long key = keyHolder.getKey().longValue();
item.setId(key);
return item;
}
@Override
public void update(Long itemId, ItemUpdateDto updateParam) {
String sql = "update item set item_name=?, price=?, quantity=? where id=?";
template.update(sql,
updateParam.getItemName(),
updateParam.getPrice(),
updateParam.getQuantity(),
itemId);
}
@Override
public Optional<Item> findById(Long id) {
String sql = "select id, item_name, price, quantity from item where id = ?";
try {
Item item = template.queryForObject(sql, itemRowMapper(), id);
return Optional.of(item);
} catch (EmptyResultDataAccessException e) {
return Optional.empty();
}
}
}
위의 코드에서 자바코드로 SQL을 작성하고, SQL에 들어갈 상수 값들을 setString(), setInt() 등의 함수로 매핑하고 있는 것을 볼 수 있다. 그리고 template.update()나 template.queryForObject(), template().query() 와 같은 JDBC Template이 제공하는 함수로 SQL을 실행한다.
SELECT문 같은 조회 쿼리의 경우, DB에서 읽어온 값을 자바 코드상에서 객체로 사용하기위해 RowMapper<?> 클래스를 직접 정의하여 사용한다. 이렇게 하면, DB의 데이터를 객체 형태로 사용할 수 있다.
참고.
JDBC Template을 사용하기 위해서는 데이터소스(DataSource)가 필요하다. 따라서, 생성자의 인자로 DataSource를 주입받고, 이를 새로운 JdbcTemplate 객체를 생성할 때 인자로 넘겨주면 된다.
위에서 보았던 JDBC Template 사용코드에는 개발자의 실수가 발생할 수 있는 부분이 있다.
SQL에 들어가는 값을 세팅하는 과정에서 만약 실수로 개발자가 데이터 타입이 동일한 2개 이상의 파라미터들의 순서를 잘못 변경한다면 파라미터에 잘못된 값이 세팅되어버린다. 유지보수적 관점에서 코드를 명확하게 작성하지 않았기 때문인데, JDBC Template은 이런 문제를 보완할 수 있는 NamedParameterJdbcTemplate을 제공한다.
코드 예시를 보자.
@Slf4j
@Repository
public class JdbcTemplateItemRepositoryV2 implements ItemRepository {
private final NamedParameterJdbcTemplate template;
public JdbcTemplateItemRepositoryV2(DataSource dataSource) {
this.template = new NamedParameterJdbcTemplate(dataSource);
}
@Override
public Item save(Item item) {
String sql = "insert into item (item_name, price, quantity) " +
"values (:itemName, :price, :quantity)";
SqlParameterSource param = new BeanPropertySqlParameterSource(item);
KeyHolder keyHolder = new GeneratedKeyHolder();
template.update(sql, param, keyHolder);
Long key = keyHolder.getKey().longValue();
item.setId(key);
return item;
}
@Override
public void update(Long itemId, ItemUpdateDto updateParam) {
String sql = "update item " +
"set item_name=:itemName, price=:price, quantity=:quantity " +
"where id=:id";
SqlParameterSource param = new MapSqlParameterSource()
.addValue("itemName", updateParam.getItemName())
.addValue("price", updateParam.getPrice())
.addValue("quantity", updateParam.getQuantity())
.addValue("id", itemId); //이 부분이 별도로 필요하다.
template.update(sql, param);
}
@Override
public Optional<Item> findById(Long id) {
String sql = "select id, item_name, price, quantity from item where id = :id";
try {
Map<String, Object> param = Map.of("id", id);
Item item = template.queryForObject(sql, param, itemRowMapper());
return Optional.of(item);
} catch (EmptyResultDataAccessException e) {
return Optional.empty();
}
}
이전에 봤던 코드와 비교해봤을 때, setString(), setInt()와 같은 함수로 파라미터 값을 직접 세팅하는 대신, SqlParameterSource 라는 인터페이스를 사용하여 SQL의 파라미터 값을 세팅하는 것을 볼 수 있다. 보통 아래의 3개의 방법중 하나를 사용한다.
상황에 따라 적절한 방법을 사용하면 된다.
위에서 일반적인 JDBC Template의 사용법이나 NamedParameterJdbcTemplate에서 INSERT와 관련된 코드를 보면 다른 쿼리에 비해 코드가 비교적 복잡하다는 걸 알 수 있다. 그 이유는 데이터베이스에 저장되는 데이터의 PK값이 DB에 쿼리가 도달해야만 설정이 되기 때문이다. 즉, DB에 실제 쿼리를 날리기 전까지는 어떤 값으로 데이터의 PK가 설정될지 알 수 없기 때문에 이를 받아오기위한 추가적인 과정이 필요하다.
SimpleJdbcTemplate은 이 과정을 모두 대신해준다. 코드 예시는 아래와 같다.
@Slf4j
@Repository
public class JdbcTemplateItemRepositoryV3 implements ItemRepository {
private final NamedParameterJdbcTemplate template;
private final SimpleJdbcInsert jdbcInsert;
public JdbcTemplateItemRepositoryV3(DataSource dataSource) {
this.template = new NamedParameterJdbcTemplate(dataSource);
this.jdbcInsert = new SimpleJdbcInsert(dataSource)
.withTableName("item")
.usingGeneratedKeyColumns("id");
.usingColumns("item_name", "price", "quantity"); //생략 가능
}
@Override
public Item save(Item item) {
// String 타입으로 작성된 SQL이 없음!
SqlParameterSource param = new BeanPropertySqlParameterSource(item);
Number key = jdbcInsert.executeAndReturnKey(param);
item.setId(key.longValue());
return item;
}
SimpleJdbcTemplate을 사용하면 executeAndReturnKey() 메서드로 깔끔하게 쿼리 실행 후, PK값을 받아올 수 있다. 또, NamedParameterJdbcTemplate에서 사용했던 SqlParameterSource를 동일하게 사용할 수도 있다. 단, 생성자에서 SimpleJdbcTemplate 객체를 생성할 때 데이터베이스의 테이블에 대한 메타데이터를 명시해주어야한다. SimpleJdbcTemplate은 명시한 메타데이터를 이용하여 INSERT SQL 쿼리를 자동으로 만들고, 파라미터를 세팅한 뒤 SQL을 실행한다.
그 밖에도 SimpleJdbcCall이라는 DB상에 정의된 스토어드 프로시저를 호출하는 기능도 제공하는 이는 생략한다.
JDBC Template을 사용하면 JDBC를 직접 사용할 때보다 데이터베이스에 접근하는 것이 더 쉬워진 것은 사실이다.
하지만, JDBC Template은 동적 쿼리 문제를 해결할 수 없다. 동적쿼리는 상황에 따라 SQL 쿼리 문 자체가 변경되기 때문에, 자바의 if/else 등을 사용하여 모든 조건에 대해서 쿼리문을 작성하기엔 너무 비효율적이고 코드도 방대해진다.
따라서, 동적쿼리는 SQL Mapper인 MyBatis나 JPA의 JPQL을 동적으로 만들어주는 QueryDSL을 사용해야한다.