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));