Spring에서 SQL을 사용할 때 JdbcTemplate는 간단하면서 좋은 선택지이다. JdbcTemplate은 트랜잭션 관리 및 리소스 관리등을 자동으로 해주는 등 매우 편리하게 JDBC를 사용할 수 있게 도와준다.
JdbcTemplate을 사용하지 않고 Connection으로 DB에 접근하는 경우 코드는 다음과 같다.
@Override
public Member save(Member member) {
String sql = "INSERT INTO MEMBER(member_id, money) VALUES (?, ?)";
Connection con = null;
PreparedStatement pstmt = null;
try {
con = getConnection();
pstmt = con.prepareStatement(sql);
pstmt.setString(1, member.getMemberId());
pstmt.setInt(2, member.getMoney());
pstmt.executeUpdate();
return member;
} catch (SQLException e) {
throw exceptionTranslator.translate("save", sql, e);
} finally {
// TCP/IP 커넥션으로 연결되기 때문에 자원 해제해줘야 함
close(con, pstmt, null); // 현재 코드에는 없지만 자원을 해제해주는 사용자 정의 메서드
}
}
위에서 예시를 든 save() 뿐만 아니라 DB에 접근하는 모든 메서드에서 Connection을 가져오고, PreparedStatement를 가져와서 sql을 수행하고 자원을 해제해주는 로직을 작성해주어야한다.
중복 코드가 난무하고 너무 복잡하지 않겠는가?
이럴 때 도입할 수 있는 DB 접근 기술 중 하나가 바로 JdbcTemplate 이다.
...
private final JdbcTemplate template;
public MemberRepository(DataSource dataSource) {
this.template = new JdbcTemplate(dataSource);
}
...
@Override
public Member save(Member member) {
String sql = "INSERT INTO MEMBER(member_id, money) VALUES (?, ?)";
template.update(sql, member.getMemberId(), member.getMoney());
return member;
}
JdbcTemplate를 생성하였다. JdbcTemplate은 생성자에서 DateSource를 필요로 하기 때문에 주입해주었다.
JdbcTemplate을 사용하지 않은 코드와 비교해보아라. 얼마나 간단해졌는지.
JdbcTemplate을 사용하면 사용하지 않는 자원(커넥션 등)을 자동으로 해제해준다.
동적 쿼리문의 수월한 작성을 위해 다른 DB 접근 기술을 사용할 수 있다.
예를 들어 MyBatis와 JPA+QueryDSL 을 사용할 수 있다.
다음은 JdbcTemplate을 이용한 Insert 메서드이다.
@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;
}
item 테이블의 item_name, price, quantity 컬럼에 각각 쿼리 파라미터로 값을 지정해준다.
이 파라미터는 직접 넣어주는 것이 아닌, PreparedStatement 객체를 통해 넣어줘야한다.
코드를 보면
ps.setString(1, item.getItemName());
ps.setInt(2, item.getPrice());
ps.setInt(3, item.getQuantity());
가 있는데 ?의 순서대로 1, 2, 3 .. 의 인덱싱을 적어줘야한다.
@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);
}
update 메서드의 경우 insert보다는 단순하다. JdbcTemplate의 update메서드를 사용하여 sql문과 각각의 파라미터 값을 직접 넣어주면된다.
@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();
} }
하나의 결과를 조회할 때에는 JdbcTemplate의 queryForObject() 를 사용하면된다. 매개변수로 sql문, RowMapper, 쿼리 파라미터(?)를 차례로 넣어줘야한다.
@Override
public List<Item> findAll(ItemSearchCond cond) {
String itemName = cond.getItemName();
Integer maxPrice = cond.getMaxPrice();
String sql = "select id, item_name, price, quantity from item"; //동적 쿼리
if (StringUtils.hasText(itemName) || maxPrice != null) {
sql += " where";
}
boolean andFlag = false;
List<Object> param = new ArrayList<>();
if (StringUtils.hasText(itemName)) {
sql += " item_name like concat('%',?,'%')";
param.add(itemName);
andFlag = true;
}
if (maxPrice != null) {
if (andFlag) {
sql += " and";
}
sql += " price <= ?";
param.add(maxPrice);
}
log.info("sql={}", sql);
return template.query(sql, itemRowMapper(), param.toArray());
}
모든 결과를 조회하는 데에는 여러가지 상황이 있을 수 있다. 현재 애플리케이션은 모두 출력, 최대 가격 검색, 이름 검색을 구현해야한다. 따라서 쿼리를 동적으로 변경해줘야하기 때문에 매우 복잡해보이는 로직이 작성되었다.
어쨌든 단 건 조회와는 다르게 JdbcTemplate의 query 메서드를 사용하여 sql, RowMapper, 쿼리 파라미터 를 넘겨주었다.
RowMapper는 말 그대로 테이블 결과 Row와 객체를 매핑해준다.
여기서는 결과가 담긴 ResultSet에서 해당하는 컬럼명을 찾아 객체로 매핑해준다.
RowMapper 메서드를 굳이 따로 만들어 줄 필요는 없지만 코드의 유지보수성을 위해 메서드로 추출하였다.
private RowMapper<Item> itemRowMapper() {
return (rs, rowNum) -> {
Item item = new Item();
item.setId(rs.getLong("id"));
item.setItemName(rs.getString("item_name"));
item.setPrice(rs.getInt("price"));
item.setQuantity(rs.getInt("quantity"));
return item;
}; }
기존 JdbcTemplate을 사용하면 다음과 같은 문제점이 있다.
String sql = "update item set item_name=?, price=?, quantity=? where id=?";
template.update(sql,
itemName,
price,
quantity,
itemId);
이 경우 누군가 파라미터로 들어가는 quantity 과 price의 순서를 변경한다면 DB에서 가격이 수량으로, 수량이 가격으로 바뀌어 들어가게되고 이를 복구하는 것은 쉽지않을 것이다.
이를 해결하기 위해 JdbcTemplate은 다양한 방법을 지원한다.
// private final JdbcTemplate template;
private final NamedParameterJdbcTemplate template;
방법은 바로 NamedParameterJdbcTemplate을 사용하는 것이다.
NamedParameterJdbcTemplate 역시 생성자에 dataSource를 필요로 한다.
NamedParameterJdbcTemplate 을 사용할 때에는 sql문에 쿼리파라미터를 ? 가 아닌 파라미터의 이름을 지정해주면 된다.
String sql = "insert into item (item_name, price, quantity) " +
"values (:itemName, :price, :quantity)";
sql 작성은 위와 같고 이 sql을 사용하여 쿼리를 수행하는 데에는 크게 3가지 방법이 있다.
BeanPropertySqlParameterSource @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;
}
BeanPropertySqlParameterSource을 사용하면 매우 쉽게 parameter를 설정할 수 있다. 위 코드를 보면 BeanPropertySqlParameterSource을 생성하면서 item 객체를 넘겨주었다. item에는 getItemName, getPrice, getQuantity가 있는데 get 메서드와 쿼리 파라미터에서 지정해준 이름이 동일하다면 자동으로 매칭해준다.
MapSqlParameterSource()@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);
}
MapSqlParameterSource()를 사용하면 메서드 체이닝 방식으로 쿼리 파라미터를 추가해줄 수 있다. 기존 JdbcTemplate과 유사하지만 파라미터 이름을 가지고 대상을 추가하기 때문에 더욱 직관적이다.
@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();
} }
간단하게 Map 컬렉션 프레임워크를 사용해도 param을 넣어줄 수 있다.
💡 아래와 같이 BeanPropertyRowMapper를 사용하면 자동으로 ResultSet과 객체를 연결해준다.
private RowMapper<Item> itemRowMapper() {
return BeanPropertyRowMapper.newInstance(Item.class);
}
<int rowCount = jdbcTemplate.queryForObject("select count(*) from t_actor",Integer.class);
하나의 로우를 조회할 때는 queryForObject() 를 사용하면 된다. 지금처럼 조회 대상이 객체가 아니라
단순 데이터 하나라면 타입을 Integer.class , String.class 와 같이 지정해주면 된다.
int countOfActorsNamedJoe = jdbcTemplate.queryForObject(
"select count(*) from t_actor where first_name = ?", Integer.class,"Joe");
숫자 하나와 파라미터 바인딩 예시이다.
String lastName = 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);
객체 하나를 조회한다. 결과를 객체로 매핑해야 하므로 RowMapper 를 사용해야 한다. 여기서는 람다를
사용했다.
List<Actor> actors = 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;
});
여러 로우를 조회할 때는 query() 를 사용하면 된다. 결과를 리스트로 반환한다.
결과를 객체로 매핑해야 하므로 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);
}
여러 로우를 조회할 때는 query() 를 사용하면 된다. 결과를 리스트로 반환한다.
여기서는 RowMapper 를 분리했다. 이렇게 하면 여러 곳에서 재사용 할 수 있다.
데이터를 변경할 때는 jdbcTemplate.update() 를 사용하면 된다. 참고로 int 반환값을 반환하는데,
SQL 실행 결과에 영향받은 로우 수를 반환한다.
jdbcTemplate.update(
"insert into t_actor (first_name, last_name) values (?, ?)",
"Leonor", "Watling");
jdbcTemplate.update(
"update t_actor set last_name = ? where id = ?",
"Banjo", 5276L);
jdbcTemplate.update(
"delete from t_actor where id = ?",
Long.valueOf(actorId));
임의의 SQL을 실행할 때는 execute() 를 사용하면 된다. 테이블을 생성하는 DDL에 사용할 수 있다.
jdbcTemplate.execute("create table mytable (id integer, name varchar(100))");
jdbcTemplate.update(
"call SUPPORT.REFRESH_ACTORS_SUMMARY(?)",
Long.valueOf(unionId));