주제 : 이전에 배운 내용을 기반으로 데이터 접근 기술들을 학습한다.
적용 데이터 접근 기술
- JdbcTemplate
- MyBatis
- JPA, Hibernate
- 스프링 데이터 JPA
- Querydsl
여기에는 크게 2가지 분류가 있다.
1. SQL Mapper 주요기능
- 개발자는 SQL만 작성하면 해당 SQL의 결과를 객체로 편리하게 매핑해준다.
- JDBC를 직접 사용할 때 발생하는 여러가지 중복을 제거해주고, 기타 개발자에게 여러가지 편리한 기능을 제공한다.
2. ORM 주요 기능
- JdbcTemplate이나 MyBatis 같은 SQL 매퍼 기술은 SQL을 개발자가 직접 작성해야 하지만, JPA를 사용하면 기본적인 SQL은 JPA가 대신 작성하고 처리해준다.
- 개발자는 저장하고 싶은 객체를 마치 자바 컬렉션에 저장하고 조회하듯이 사용하면 ORM 기술이 데이터베이스에 해당 객체를 저장하고 조회해준다.
- JPA는 자바 진영의 ORM 표준이고, Hibernate(하이버네이트)는 JPA에서 가장 많이 사용하는 구현체이다. 자바에서 ORM을 사용할 때는 JPA 인터페이스를 사용하고, 그 구현체로 하이버네이트를 사용한다고 생각하면 된다.
- 스프링 데이터 JPA, Querydsl은 JPA를 더 편리하게 사용할 수 있게 도와주는 프로젝트이다.
프로젝트 구조
도메인 분석
Item
@Data public class Item { private Long id; private String itemName; private Integer price; private Integer quantity; public Item() { } public Item(String itemName, Integer price, Integer quantity) { this.itemName = itemName; this.price = price; this.quantity = quantity; } }
Item은 상품 자체를 나타내는 객체이다. 이름, 가격, 수량을 속성으로 가지고 있다.
리포지토리 분석
ItemRepository 인터페이스
public interface ItemRepository { Item save(Item item); void update(Long itemId, ItemUpdateDto updateParam); Optional<Item> findById(Long id); List<Item> findAll(ItemSearchCond cond); }
- 메모리 구현체에서 향후 다양한 데이터 접근 기술 구현체로 변경하기 위해 리포지토리에
인터페이스를 도입했다.- 각각의 기능은 메서드 이름으로 충분히 이해가 될 것이다.
ItemSearchCond
@Data public class ItemSearchCond { private String itemName; private Integer maxPrice; public ItemSearchCond() { } public ItemSearchCond(String itemName, Integer maxPrice) { this.itemName = itemName; this.maxPrice = maxPrice; } }
- 검색 조건으로 사용된다. 상품명, 최대 가격이 있다. 상품명의 일부만 포함되어도 검색이 가능해야 한다.
cond->condition을 줄여서 사용했다.ItemUpdateDto
@Data public class ItemUpdateDto { private String itemName; private Integer quantity; public ItemUpdateDto() { } public ItemUpdateDto(String itemName, Integer price, Integer quantity) { this.itemName = itemName; this.price = price; this.quantity = quantity; } }
- 상품을 수정할 때 사용하는 객체이다.
- 단순히 데이터를 전달하는 용도로 사용되므로 DTO를 뒤에 붙였다.
- DTO는 기능은 없고 데이터를 전달만 하는 용도로 사용되는 객체를 뜻한다.
- 업데이트 작업에서는 전체 상품 정보가 아닌 일부 정보만을 업데이트해야 할 때가 있습니다. 그래서 업데이트를 위한 DTO 클래스를 따로 사용하는 것입니다.
MemoryItemRepository
@Repository public class MemoryItemRepository implements ItemRepository { private static final Map<Long, Item> store = new HashMap<>(); //static private static long sequence = 0L; //static @Override public Item save(Item item) { item.setId(++sequence); store.put(item.getId(), item); return item; } @Override public void update(Long itemId, ItemUpdateDto updateParam) { // ItemUpdateDto에게서 데이터 전송받는다. Item findItem = findById(itemId).orElseThrow(); findItem.setItemName(updateParam.getItemName()); findItem.setPrice(updateParam.getPrice()); findItem.setQuantity(updateParam.getQuantity()); } @Override public Optional<Item> findById(Long id) { return Optional.ofNullable(store.get(id)); } @Override public List<Item> findAll(ItemSearchCond cond) { String itemName = cond.getItemName(); Integer maxPrice = cond.getMaxPrice(); return store.values().stream() .filter(item -> { if (ObjectUtils.isEmpty(itemName)) { return true; } return item.getItemName().contains(itemName); }).filter(item -> { if (maxPrice == null) { return true; } return item.getPrice() <= maxPrice; }) .collect(Collectors.toList()); } public void clearStore() { store.clear(); } }서비스 분석
ItemService
public interface ItemService { Item save(Item item); void update(Long itemId, ItemUpdateDto updateParam); Optional<Item> findById(Long id); List<Item> findItems(ItemSearchCond itemSearch); }ItemServiceV1
@Service @RequiredArgsConstructor public class ItemServiceV1 implements ItemService { private final ItemRepository itemRepository; @Override public Item save(Item item) { return itemRepository.save(item); } @Override public void update(Long itemId, ItemUpdateDto updateParam) { itemRepository.update(itemId, updateParam); } @Override public Optional<Item> findById(Long id) { return itemRepository.findById(id); } @Override public List<Item> findItems(ItemSearchCond cond) { return itemRepository.findAll(cond); } }
ItemServiceV1서비스 구현체는 대부분의 기능을 단순히 리포지토리에 위임한다.
컨트롤러 분석
HomeController
@Controller @RequiredArgsConstructor public class HomeController { @RequestMapping("/") public String home() { return "redirect:/items"; } }
- 단순히 홈으로 요첨이 왔을 때
items로 이동ItemController
@Controller @RequestMapping("/items") @RequiredArgsConstructor public class ItemController { private final ItemService itemService; @GetMapping public String items(@ModelAttribute("itemSearch") ItemSearchCond itemSearch, Model model) { List<Item> items = itemService.findItems(itemSearch); model.addAttribute("items", items); return "items"; } @GetMapping("/{itemId}") public String item(@PathVariable long itemId, Model model) { Item item = itemService.findById(itemId).get(); model.addAttribute("item", item); return "item"; } @GetMapping("/add") public String addForm() { return "addForm"; } @PostMapping("/add") public String addItem(@ModelAttribute Item item, RedirectAttributes redirectAttributes) { Item savedItem = itemService.save(item); redirectAttributes.addAttribute("itemId", savedItem.getId()); redirectAttributes.addAttribute("status", true); return "redirect:/items/{itemId}"; } @GetMapping("/{itemId}/edit") public String editForm(@PathVariable Long itemId, Model model) { Item item = itemService.findById(itemId).get(); model.addAttribute("item", item); return "editForm"; } @PostMapping("/{itemId}/edit") public String edit(@PathVariable Long itemId, @ModelAttribute ItemUpdateDto updateParam) { itemService.update(itemId, updateParam); return "redirect:/items/{itemId}"; } }
- 상품을 CRUD하는 컨트롤러이다
스프링 부트 설정 분석
@Configuration public class MemoryConfig { @Bean public ItemService itemService() { return new ItemServiceV1(itemRepository()); } @Bean public ItemRepository itemRepository() { return new MemoryItemRepository(); } }
ItemServiceV1,MemoryItemRepository를 스프링 빈으로 등록하고 생성자를 통해 의존관계를 주입한다.
SQL을 직접 사용하는 경우에 스프링이 제공하는JdbcTemplate은 좋은 선택지이다.JdbcTemplate은JDBC를 매우 편리하게 사용할 수 있게 도와준다.장점
- 설정의 편리함
- 반복 문제 해결
JdbcTemplate은 템플릿 콜백 패턴을 사용해서, JDBC를 직접 사용할 때 발생하는 대부분의 반복 작업을 대신 처리해준다.- 개발자는 SQL을 작성하고, 전달할 파라미터를 정의하고, 응답 값을 매핑하기만 하면 된다.
- 우리가 생각할 수 있는 대부분의 반복 작업을 대신 처리해준다.
설정
build.gradle에 들어가서org.springframework.boot:spring-boot-starter-jdbc를 추가하면 JdbcTemplate이 들어있는 spring-jdbc 가 라이브러리에 포함된다.- 여기서는 H2 데이터베이스에 접속해야 하기 때문에 H2 데이터베이스의 클라이언트 라이브러리(Jdbc Driver)도 추가하자.
runtimeOnly 'com.h2database:h2'H2 데이터베이스에
item테이블을 생성해야 한다.drop table if exists item CASCADE; create table item ( id bigint generated by default as identity, item_name varchar(10), price integer, quantity integer, primary key (id) );
- 이제부터 본격적으로
JdbcTemplate을 사용해서 메모리에 저장하던 데이터를 저장해보자.ItemRepository인터페이스를 기반으로JdbcTemplate을 사용하는 새로운 구현체를 개발하자.@Slf4j @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 (?, ?, ?)"; // JDBC template 은 KeyHolder 를 넣어주어야 한다. // 여기에는 ID를 넣지 않았기 때문에 ID를 자동생성하는 값을 넣고 꺼내와야 한다. ID를 세팅해서 반환해 주어야 한다. 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); // 여기서 keyHolder를 넘겼기 때문에 꺼내올 수 있다. // 아래 코드. item에 key값 넣어주기 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; }; } }기본
JdbcTemplateItemRepositoryV1은ItemRepository인터페이스를 구현했다.this.template = new JdbcTemplate(dataSource)JdbcTemplate은 데이터소스(dataSource)가 필요하다.JdbcTemplateItemRepositoryV1()생성자를 보면dataSource를 의존 관계 주입 받고 생성자 내부에서JdbcTemplate을 생성한다. 스프링에서는JdbcTemplate을 사용할 때 관례상 이 방법을 많이 사용한다.save 메서드 분석
KeyHolder는 JDBC 작업 중 생성된 키를 보유하는 데 사용되는 인터페이스입니다. 이것은 주로 데이터베이스에서 자동으로 생성된 키(예: 자동 증가 열 또는 시퀀스)를 포함하는 경우에 사용됩니다.
코드에서 KeyHolder를 사용하여 자동 생성된 키를 가져올 수 있습니다.- 여기서의 KeyHolder는 template.update(...) 메서드 내부의 콜백 함수에서 사용됩니다. 이 콜백 함수는 SQL 쿼리를 실행하고, 해당 쿼리에서 생성된 키를 KeyHolder에 저장합니다. 그런 다음
keyHolder.getKey().longValue()를 사용하여 해당 키를 얻어옵니다.- 간단히 말하면, 이 코드는 새로운 아이템을 데이터베이스에 저장할 때, 데이터베이스에서 자동으로 생성된 아이템의 식별자(주로 기본 키)를 얻어오기 위해
KeyHolder를 사용하는 것입니다. 이렇게 얻은 키를 사용하여 Item 객체에 설정한 후에 해당 객체를 반환합니다콜백 함수란?
콜백 함수(callback function)는 다른 함수나 메서드의인자(argument)로 전달되어 특정 이벤트가 발생했을 때 호출되는 함수를 말합니다. 즉, 어떤 작업을 수행하는 동안에 다른 코드가 필요한 경우, 그 코드를 콜백 함수로 전달하여 필요한 때에 호출될 수 있도록 하는 것입니다.- 현재 JDBC의
JdbcTemplate에서의 콜백 함수는update()메서드에 전달되는 함수입니다. 이 함수는 JDBC 작업을 수행하고 그 결과를 처리합니다. 이 경우에는update()메서드에 전달되는 콜백 함수가 JDBC 작업을 정의하고 있습니다. 콜백 함수는PreparedStatement를 생성하고 설정하며, 해당 작업을JdbcTemplate에 의해 실행됩니다. 이때JDBC작업을 수행하는 과정에서 자동 생성된 키를KeyHolder에 저장하고, 이를 통해 나중에 얻어오는 작업을 수행합니다.PreparedStatement ps = connection.prepareStatement(sql, new String[ ]{"id"});
new String[ ]{"id"}는 JDBC에서 SQL 쿼리를 실행할 때, 자동으로 생성된 키(예: 데이터베이스의 자동 증가 열)를 반환하도록 하는 방법 중 하나입니다.- 여기서 "id"는 쿼리에서 자동으로 생성된 키를 나타내는 열의 이름입니다. 이것은 일반적으로 데이터베이스 테이블에서 기본 키(Primary Key)가 자동으로 생성되는 경우에 사용됩니다.
- 이 설정을 통해 SQL 쿼리가 실행된 후에 자동으로 생성된 키를 얻을 수 있습니다. PreparedStatement 객체에 쿼리를 전달하면서 두 번째 인자로 이 배열을 전달하면 됩니다.
결과를 검색하는 findAll()에서 어려운 부분은 사용자가 검색하는 값에 따라서 실행하는 SQL이 동적으로 달라져야 한다는 점입니다. 이는 MyBatis에서 쉽게 작성 가능
실제 코드가 동작하도록 구성
JdbcTemplateV1Config
@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); } }
ItemRepository구현체로 JdbcTemplateItemRepositoryV1 이 사용되도록 했다. 이제 메모리 저장소(MemoryConfig)가 아니라 실제 DB에 연결하는 JdbcTemplate이 사용된다.ItemServiceApplication - 변경
//@Import(MemoryConfig.class) @Import(JdbcTemplateV1Config.class) @SpringBootApplication(scanBasePackages = "hello.itemservice.web") public class ItemServiceApplication {}데이터베이스 접근 설정
src/main/resources/application.propertiesspring.profiles.active=local spring.datasource.url=jdbc:h2:tcp://localhost/~/test spring.datasource.username=sa
- 이렇게 설정만 하면 스프링 부트가 해당 설정을 사용해서 커넥션 풀과
DataSocure트랜잭션 매니저를 스프링 빈으로 자동 설정한다.(스프링 부트의 자동 리소스 등록)로그 추가
JdbcTemplate이 실행하는 SQL 로그를 확인하려면 application.properties 에 다음을 추가하면 된다. main , test 설정이 분리되어 있기 때문에 둘다 확인하려면 두 곳에 모두 추가해야 한다.#jdbcTemplate sql log logging.level.org.springframework.jdbc=debug
1) 순서대로 바인딩
JdbcTemplate을 기본으로 사용하면 파라미터를 순서대로 바인딩한다. 이때, 누군가 다음과 같이 SQL 코드의 순서를 변경했다고 가정해보자.(price와quantity의 순서를 변경)String sql = "update item set item_name=?, quantity=?, price=? where id=?"; template.update(sql,itemName,price,quantity,itemId);
- 결과적으로
price와quantity가 바뀌는 매우 심각한 문제가 발생한다. 그래서 미래에 필드를 추가하거나, 수정하면서 이런 문제가 충분히 발생할 수 있다.- 해결책 : 이름 지정 바인딩
2) 이름 지정 바인딩
JdbcTemplate은 이런 문제를 보완하기 위해NamedParameterJdbcTemplate라는 이름을 지정해서 파라미터를 바인딩 하는 기능을 제공한다.@Slf4j @Repository public class JdbcTemplateItemRepositoryV2 implements ItemRepository { private final NamedParameterJdbcTemplate template; // JdbcTemplate을 NameParameterJdbcTemplate로 교체 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)"; // "values (:itemName, :price, :quantity)"; 추가되었다. ?가 아니라 이름 기반으로 명확하게 지정해주었다. SqlParameterSource param = new BeanPropertySqlParameterSource(item); //Item에 있는 객체를 기반으로 파라미터를 만든다.(BeanPropertySqlParameterSource) 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) { Integer maxPrice = cond.getMaxPrice(); String itemName = cond.getItemName(); 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 변환 지원 } }
NameParameterJdbcTemplate로 교체String sql = "insert into item (item_name, price, quantity) " + "values (:itemName, :price, :quantity)";를?,?,?가 아니라 이름 지정new BeanPropertySqlParameterSource를 추가
Item에 있는 객체를 기반으로 파라미터를 만든다.
이름 지정 파라미터 2
이름 지정 파라미터
파라미터를 전달하려면Map처럼Key,value데이터 구조를 만들어서 전달해야 한다. 여기서key는:파라미터이름으로 지정한, 파라미터의 이름이고,value는 해당 파라미터의 값이 된다.이름 지정 바인딩에서 자주 사용하는 파라미터의 종류는 크게 3가지가 있다.
MapSqlParameterSource
MapSqlParameterSourceBeanPropertySqlParameterSource
1. Map
단순히Map을 사용한다.
findById()코드에서 확인할 수 있다.Map<String, Object> param = Map.of("id", id); Item item = template.queryForObject(sql, param, itemRowMapper());
Map<String, Object> param = Map.of("id", id);: 이 부분은 쿼리를 실행할 때 사용할 매개변수를 저장하는 Map을 생성합니다. 이 맵은 쿼리의 매개변수 이름과 값을 매핑합니다. 여기서는 "id"라는 키에 해당하는 값을 id로 설정한 Map을 생성합니다.
Item item = template.queryForObject(sql, param, itemRowMapper());: 이 코드는 JDBC의 JdbcTemplate을 사용하여 데이터베이스에서 아이템을 조회합니다. queryForObject 메서드는 데이터베이스에서 하나의 행을 가져오는 데 사용됩니다. 이 메서드에는 세 가지 매개변수가 필요합니다.
- 첫 번째 매개변수 sql: 실행할 SQL 쿼리입니다. 여기서는 데이터베이스에서 아이템을 조회하는 쿼리일 것입니다.
- 두 번째 매개변수 param: 쿼리 실행에 사용할 매개변수 맵입니다. 여기서는 위에서 생성한 매개변수 맵을 사용하여 id 값을 전달합니다.
- 세 번째 매개변수 itemRowMapper(): 쿼리 결과를 객체로 매핑하는 데 사용되는 RowMapper입니다. 이 메서드는 아이템 객체와 데이터베이스 테이블 간의 매핑을 수행합니다.
- 이렇게 하면 데이터베이스에서 특정 아이템을 조회할 때 Map을 사용하여 매개변수를 전달하고, 그 결과를 Item 객체로 변환하여 가져올 수 있습니다.
2. MapSqlParameterSource
Map 과 유사한데, SQL 타입을 지정할 수 있는 등 SQL에 좀 더 특화된 기능을 제공합니다.SqlParameterSource인터페이스의 구현체입니다.MapSqlParameterSource는 메서드 체인을 통해 편리한 사용법도 제공합니다.
updatae()코드에서 확인할 수 있다.SqlParameterSource param = new MapSqlParameterSource() .addValue("itemName", updateParam.getItemName()) .addValue("price", updateParam.getPrice()) .addValue("quantity", updateParam.getQuantity()) .addValue("id", itemId); //이 부분이 별도로 필요하다. template.update(sql, param);
- MapSqlParameterSource 클래스는 SqlParameterSource 인터페이스를 구현한 것으로, 이를 사용하여 SQL 쿼리에서 사용할 매개변수를 설정합니다. 이 때, MapSqlParameterSource는 키-값 쌍을 사용하여 매개변수를 설정할 수 있으므로, 편리하게 매개변수를 전달할 수 있습니다
왜
findByid에서는 단순히 map,update에서는 MapSqlParameterSource를 썼는가?
findById메서드는 간단한 조회용 쿼리이므로Map을 사용하여 매개변수를 전달하고,update메서드는 복잡한 수정용 쿼리이므로MapSqlParameterSource를 사용하여 매개변수를 전달하는 것입니다.
3. BeanPropertySqlParameterSource
자바빈 프로퍼티 규약을 통해서 자동으로 파라미터 객체를 생성합니다.
예를 들어서getItemName(),getPrice()가 있으면 다음과 같은 데이터를 자동으로 만들어냅니다.
key=itemName, value=상품명 값key=price, value=가격 값
SqlParameterSource인터페이스의 구현체입니다.
save(),findAll()코드에서 확인할 수 있습니다.SqlParameterSource param = new BeanPropertySqlParameterSource(item); KeyHolder keyHolder = new GeneratedKeyHolder(); template.update(sql, param, keyHolder);
- 여기서 보면
BeanPropertySqlParameterSource가 많은 것을 자동화 해주기 때문에 가장 좋아보이지만,BeanPropertySqlParameterSource를 항상 사용할 수 있는 것은 아니다.- 예를 들어서
update()에서는 SQL에 :id 를 바인딩 해야 하는데,update()에서 사용하는ItemUpdateDto에는itemId가 없다. 따라서BeanPropertySqlParameterSource를 사용할 수 없고, 대신에MapSqlParameterSource를 사용했다.추가 BeanPropertyRowMapper
JdbcTemplateItemRepositoryV2 - itemRowMapper()private RowMapper<Item> itemRowMapper() { return BeanPropertyRowMapper.newInstance(Item.class); //camel 변환 지원 }
- 이 메서드는
BeanPropertyRowMapper클래스를 사용하여RowMapper를 생성합니다.BeanPropertyRowMapper는 기본적으로 객체의 프로퍼티(필드)와 열의 이름을 매핑하여 자동으로 객체를 생성합니다.- 예를 들어서 데이터베이스에서 조회한 결과가
select id,price라고 하면 데이터베이스에서 조회한 결과 이름을 기반으로setId(),setPrice()처럼 자바빈 프로퍼티 규약에 맞춘 메서드를 호출하는 것이다.
@Configuration @RequiredArgsConstructor public class JdbcTemplateV2Config { private final DataSource dataSource; @Bean public ItemService itemService(){ return new ItemServiceV1(itemRepository()); } @Bean public ItemRepository itemRepository(){ return new JdbcTemplateItemRepositoryV2(dataSource); } }
- 앞서 개발한
JdbcTemplateItemRepositoryV2를 사용하도록 스프링 빈에 등록한다.ItemServiceApplication - 변경
@Slf4j @Import(JdbcTemplateV2Config.class) @SpringBootApplication(scanBasePackages = "hello.itemservice.web") public class ItemServiceApplication { public static void main(String[] args) { SpringApplication.run(ItemServiceApplication.class, args); } @Bean @Profile("local") public TestDataInit testDataInit(ItemRepository itemRepository) { return new TestDataInit(itemRepository); } }
JdbcTemplateV2Config.class를 설정으로 사용하도록 변경되었다.@Import(JdbcTemplateV1Config.class)->@Import(JdbcTemplateV2Config.class)
JdbcTemplate은 INSERT SQL을 직접 작성하지 않아도 되도록 SimpleJdbcInsert라는 편리한 기능을 제공한다.
JdbcTemplateItemRepositoryV3
@Slf4j @Repository public class JdbcTemplateItemRepositoryV3 implements ItemRepository { private final NamedParameterJdbcTemplate template; //SimpleJdbcInsert 이걸 사용하면서 엄청 많은 코드를 줄였음 private final SimpleJdbcInsert jdbcInsert; public JdbcTemplateItemRepositoryV3(DataSource dataSource){ this.template = new NamedParameterJdbcTemplate(dataSource); this.jdbcInsert = new SimpleJdbcInsert(dataSource) .withTableName("item") .usingGeneratedKeyColumns("id"); } @Override public Item save(Item item) { SqlParameterSource param = new BeanPropertySqlParameterSource(item); Number key = jdbcInsert.executeAndReturnKey(param); item.setId(key.longValue()); 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) { Integer maxPrice = cond.getMaxPrice(); String itemName = cond.getItemName(); 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); } }
JdbcTemplateItemRepositoryV3은ItemRepository인터페이스를 구현했다.this.jdbcInsert = new SimpleJdbcInsert(dataSource): 생성자를 보면 의존관계 주입은dataSource를 받고 내부에서SimpleJdbcInsert을 생성해서 가지고 있다. 스프링에서는 JdbcTemplate 관련 기능을 사용할 때 관례상 이 방법을 많이 사용한다.SimpleJdbcInsert
this.jdbcInsert = new SimpleJdbcInsert(dataSource) .withTableName("item") .usingGeneratedKeyColumns("id"); // .usingColumns("item_name", "price", "quantity"); //생략 가능
withTableName: 데이터를 저장할 테이블 명을 지정한다.usingGeneratedKeyColumns: key 를 생성하는 PK 컬럼 명을 지정한다.usingColumns: INSERT SQL에 사용할 컬럼을 지정한다. 특정 값만 저장하고 싶을 때 사용한다. 생략할 수 있다.save()
jdbcInsert.executeAndReturnKey(param)을 사용해서 INSERT SQL을 실행하고, 생성된 키 값도 매우 편리하게 조회할 수 있다.JdbcTemplateV3Config
@Configuration @RequiredArgsConstructor public class JdbcTemplateV3Config { private final DataSource dataSource; @Bean public ItemService itemService(){ return new ItemServiceV1(itemRepository()); } @Bean public ItemRepository itemRepository(){ return new JdbcTemplateItemRepositoryV3(dataSource); } }ItemServiceApplication - 변경
@Slf4j @Import(JdbcTemplateV3Config.class) @SpringBootApplication(scanBasePackages = "hello.itemservice.web") public class ItemServiceApplication {}