-
데이터 접근 기술- 스프링 JdbcTemplateBackEnd/Spring DB 2023. 2. 1. 06:56반응형
JdbcTemplate 장단점
장점
- spring-jdbc 라이브러리(스프링으로 jdbc를 사용할 때 기본으로 사용되는 라이브러리)에 포함되어 있어 별도의 복잡한 설정 없이 사용할 수 있는 설정의 편리함을 제공합니다.
- 템플릿 콜백 패턴을 사용해서 JDBC를 직접 사용할 때 발생하는 대부분의 반복 작업(커넥션 획득, statement를 준비하고 실행, 결과를 반복하도록 루프 실행, 트랜잭션 다루기 위한 커넥션 동기화, 예외 발생 시 스프링 예외 변환기 실행)을 대신 처리해줍니다.
단점
- 동적 SQL을 다루기가 어렵습니다.
JdbcTemplate 주요 기능
- JdbcTemplate: 순서 기반 파라미터 바인딩을 지원합니다.
- NamedParameterJdbcTemplate: 이름 기반 파라미터 바인딩을 지원합니다. (권장)
- SimpleJdbcInsert: INSERT SQL을 편리하게 사용할 수 있습니다.
- SimpleJdbcCall: 스토어드 프로시저를 편리하게 호출할 수 있습니다.
JdbcTemplate 적용
의존성 추가(build.gradle)
//JdbcTemplate 추가 implementation 'org.springframework.boot:spring-boot-starter-jdbc'
로그 추가
JdbcTemplate이 실행하는 SQL 로그를 확인하려면 application.properties에 다음을 추가하면 됩니다.
#jdbcTemplate sql log logging.level.org.springframework.jdbc=debug
JdbcTemplate를 사용해서 메모리에 저장하던 데이터를 H2 데이터베이스에 저장합니다. ItemRepository 인터페이스를 기반으로 JdbcTemplate을 사용하는 새로운 구현체를 개발합니다. 스프링 JdbcTemplate 사용 방법은 공식 메뉴얼을 참고하시면 됩니다.
package hello.itemservice.repository.jdbctemplate; import hello.itemservice.domain.Item; import hello.itemservice.repository.ItemRepository; import hello.itemservice.repository.ItemSearchCond; import hello.itemservice.repository.ItemUpdateDto; import lombok.extern.slf4j.Slf4j; import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.RowMapper; import org.springframework.jdbc.support.GeneratedKeyHolder; import org.springframework.jdbc.support.KeyHolder; import org.springframework.util.StringUtils; import javax.sql.DataSource; import java.sql.PreparedStatement; import java.util.ArrayList; import java.util.List; import java.util.Optional; /** * JdbcTemplate */ @Slf4j 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(); } } @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()); } 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; }); } }
- this.template = new JdbcTemplate(dataSource): JdbcTemplate은 데이터소스가 필요합니다. 생성자를 보면 dataSource를 의존 관계 주입 받고 생성자 내부에서 JdbcTemplate를 생성합니다. 스프링에서는 JdbcTemplate를 사용할 때 관례상 이 방법을 많이 사용하지만, JdbcTemplate을 스프링 빈으로 직접 등록하고 주입받아도 됩니다.
- template.update(): 데이터를 변경할 때는 update()를 사용합니다. INSERT, UPDATE, DELETE SQL에 사용하며, 반환 값은 int로 영향 받은 로우 수입니다.
- KeyHolder와 connection.prepareStatement(sql, new String[]{"id"})를 사용해서 id를 지정해주면 INSERT 쿼리 실행 이후에 데이터베이스에서 생성된 ID 값을 조회할 수 있습니다.
- template.queryForObject(): 결과 로우가 하나일 때 사용합니다. RowMapper는 데이터베이스의 반환 결과인 ResultSet을 객체로 변환합니다. 결과가 없으면 EmptyResultDataAccessException 예외를, 결과가 둘 이상이면 IncorrectResultSizeDataAccessException 예외가 발생합니다.
- template.query(): 결과가 하나 이상일 때 사용합니다. 결과가 없으면 빈 컬렉션을 반환합니다.
신규 구현체로 적용하기 위해 신규 Config 파일을 생성하고, ItemServiceApplication에 설정된 Config 클래스를 변경합니다. 이후 포스팅부터 해당 내용은 skip 하겠습니다.
package hello.itemservice.config; import hello.itemservice.repository.ItemRepository; import hello.itemservice.repository.jdbctemplate.JdbcTemplateItemRepositoryV1; import hello.itemservice.repository.memory.MemoryItemRepository; import hello.itemservice.service.ItemService; import hello.itemservice.service.ItemServiceV1; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import javax.sql.DataSource; @Configuration @RequiredArgsConstructor public class JdbcTemplateV1Config { private final DataSource dataSource; @Bean public ItemService itemService() { return new ItemServiceV1(itemRepository()); } @Bean public ItemRepository itemRepository() { return new JdbcTemplateItemRepositoryV1(dataSource); } }
//@Import(MemoryConfig.class) @Import(JdbcTemplateV1Config.class) @SpringBootApplication(scanBasePackages = "hello.itemservice.web") public class ItemServiceApplication {...}
NamedParameterJdbcTemplate
JdbcTemplate을 기본으로 사용하면 파라미터를 순서대로 바인딩 합니다. 아래 코드에서 itemName, price, quantity는 SQL에 있는 ? 순서대로 바인딩 됩니다. 파라미터를 순서대로 바인딩 하는 것은 편리하지만, 순서가 맞지 않아 버그가 발생할 수도 있으니 주의해서 사용해야 합니다.
String sql = "update item set item_name=?, price=?, quantity=? where id=?"; template.update(sql, itemName, price, quantity, itemId);
JdbcTemplate은 이런 문제를 보완하기 위해 NamedParameterJdbcTemplate라는 이름을 지정해서 파라미터를 바인딩 하는 기능을 제공합니다.
package hello.itemservice.repository.jdbctemplate; import hello.itemservice.domain.Item; import hello.itemservice.repository.ItemRepository; import hello.itemservice.repository.ItemSearchCond; import hello.itemservice.repository.ItemUpdateDto; import lombok.extern.slf4j.Slf4j; import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.jdbc.core.BeanPropertyRowMapper; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.RowMapper; import org.springframework.jdbc.core.namedparam.BeanPropertySqlParameterSource; import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; import org.springframework.jdbc.core.namedparam.SqlParameterSource; import org.springframework.jdbc.support.GeneratedKeyHolder; import org.springframework.jdbc.support.KeyHolder; import org.springframework.util.StringUtils; import javax.sql.DataSource; import java.sql.PreparedStatement; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Optional; /** * NamedParameterJdbcTemplate * SqlParameterSource * - BeanPropertySqlParameterSource * - MapSqlParameterSource * Map * * BeanPropertyRowMapper * */ @Slf4j 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(); } } @Override public List<Item> findAll(ItemSearchCond cond) { String itemName = cond.getItemName(); Integer maxPrice = cond.getMaxPrice(); SqlParameterSource param = new BeanPropertySqlParameterSource(cond); String sql = "select id, item_name, price, quantity from item"; //동적 쿼리 if (StringUtils.hasText(itemName) || maxPrice != null) { sql += " where"; } boolean andFlag = false; if (StringUtils.hasText(itemName)) { sql += " item_name like concat('%',:itemName,'%')"; andFlag = true; } if (maxPrice != null) { if (andFlag) { sql += " and"; } sql += " price <= :maxPrice"; } log.info("sql={}", sql); return template.query(sql, param, itemRowMapper()); } private RowMapper<Item> itemRowMapper() { return BeanPropertyRowMapper.newInstance(Item.class); //camel 변환 지원 } }
- SQL에서 ? 대신 :파라미터이름을 사용합니다.
- 파라미터를 전달하려면 Map처럼 Key, Value 데이터 구조를 만들어서 전달해야 합니다.
- 이름 지정 바인딩에서 자주 사용하는 파라미터의 종류는 크게 3가지(Map, MapSqlParameterSource, BeanPropertySqlParameterSource)가 있습니다.
Map
java.util.Map입니다.
// findById() 코드 Map<String, Object> param = Map.of("id", id); Item item = template.queryForObject(sql, param, itemRowMapper());
MapSqlParameterSource
Map과 유사하나 SQL 타입을 지정할 수 있는 등 SQL에 좀 더 특화된 기능을 제공합니다. SqlParameterSource 인터페이스의 구현체로 메서드 체인을 통해 편리한 사용법도 제공합니다.
// update() 코드 SqlParameterSource param = new MapSqlParameterSource() .addValue("itemName", updateParam.getItemName()) .addValue("price", updateParam.getPrice()) .addValue("quantity", updateParam.getQuantity()) .addValue("id", itemId); //이 부분이 별도로 필요하다. template.update(sql, param);
BeanPropertySqlParameterSource
SqlParameterSource 인터페이스의 구현체로, 자바빈 프로퍼티 규약을 통해서 자동으로 파라미터 객체를 생성합니다. 예를 들어 getItemName(), getPrice()가 있으면 다음과 같은 데이터를 자동으로 만들어냅니다.
- key=itemName, value=상품명 값
- key=price, value=가격 값
// save() 코드 SqlParameterSource param = new BeanPropertySqlParameterSource(item); KeyHolder keyHolder = new GeneratedKeyHolder(); template.update(sql, param, keyHolder);
BeanPropertyRowMapper
BeanPropertyRowMapper는 ResultSet의 결과를 받아서 자바빈 규약에 맞추어 데이터를 변환합니다. 데이터베이스에서 조회한 결과 이름을 기반으로 setId(), setPrice()처럼 자바빈 프로퍼티 규약에 맞춘 메서드를 호출하는 것입니다.
// [AS-IS] 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; }); } // [TO-BE] private RowMapper<Item> itemRowMapper() { return BeanPropertyRowMapper.newInstance(Item.class); //camel 변환 지원 }
SimpleJdbcInsert와 SimpleJdbcCall에 대한 사용 방법은 스프링 공식 메뉴얼을, 해당 프로젝트에 SimpleJdbcInsert 적용 코드는 github repository를 참고하시면 됩니다.
[참고 정보]
반응형'BackEnd > Spring DB' 카테고리의 다른 글
데이터 접근 기술- JPA (2) 2023.02.04 데이터 접근 기술- MyBatis (0) 2023.02.02 데이터 접근 기술- Memory (0) 2023.01.31 JdbcTemplate (0) 2023.01.28 예외(Exception) (2) (0) 2023.01.28