누군가 SQL 코드의 순서를 변경하게 된다면 어떻게 될까 => ( price와 quantity의 순서를 변경했다.)
Before
String sql = "update item set item_name=?, price=?, quantity=? where id=?";
template.update(sql,
itemName,
price,
quantity,
itemId);
After
String sql = "update item set item_name=?, quantity=?, price=? where id=?";
template.update(sql,
itemName,
price,
quantity,
itemId);
- 결과적으로 price 와 quantity 가 바뀌는 매우 심각한 문제가 발생한다. 이런 일이 없을 것 같지만, 실무에서는 파라미터가 10~20개가 넘어가는 일도 아주 많다. 그래서 미래에 필드를 추가하거나, 수정하면서 이런 문제가 충분히 발생할 수 있다.
- 버그 중에서 가장 고치기 힘든 버그는 데이터베이스에 데이터가 잘못 들어가는 버그다. 이것은 코드만 고치는 수준이 아니라 데이터베이스의 데이터를 복구해야 하기 때문에 버그를 해결하는데 들어가는 리소스가 어마어마하다.
- 개발을 할 때는 코드를 몇줄 줄이는 편리함도 중요하지만, 모호함을 제거해서 코드를 명확하게 만드는 것이 유지보수 관점에서 매우 중요하다.
- 이처럼 파라미터를 순서대로 바인딩하는 것은 편리하기는 하지만, 순서가 맞지 않아서 버그가 발생할 수도 있으므로 주의해서 사용해야 한다
이름 지정 바인딩
전체 코드
/**
* NamedParameterJdbcTemplate
* SqlParameterSource
* - BeanPropertySqlParameterSource
* - MapSqlParameterSource
* Map
*
* BeanPropertyRowMapper
*
*/
@Slf4j
public class JdbcTemplateItemRepositoryV2 implements ItemRepository {
//private final JdbcTemplate template;
private final NamedParameterJdbcTemplate template; //이름기반 JDBCTemplate
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)";
//item 이름으로 파라미터 생성
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); //cammel 변환 지원
}
}
- JdbcTemplateItemRepositoryV2 는 ItemRepository 인터페이스를 구현했다.
- this.template = new NamedParameterJdbcTemplate(dataSource)
- NamedParameterJdbcTemplate 도 내부에 dataSource 가 필요하다.
- JdbcTemplateItemRepositoryV2 생성자를 보면 의존관계주입은 dataSource를 받고 내부에서 NamedParameterJdbcTemplate을 생성해서 가지고 있다. 스프링에서는 JdbcTemplate 관련 기능을 사용할 때 관례상 이 방법을 많이 사용한다.
- 물론 NamedParameterJdbcTemplate 을 스프링 빈으로 직접 등록하고 주입받아도 된다
Save()
@Override
public Item save(Item item) {
String sql = "insert into item(item_name, price, quantity) "
+ "values (:itemName, :price, :quantity)";
//item 이름으로 파라미터 생성
SqlParameterSource param = new BeanPropertySqlParameterSource(item);
KeyHolder keyHolder = new GeneratedKeyHolder();
template.update(sql, param, keyHolder);
long key = keyHolder.getKey().longValue();
item.setId(key);
return item;
}
- SQL에서 다음과 같이 ? 대신에 :파라미터 이름을 받는 것을 확인할 수 있다.
- 추가로 NamedParameterJdbcTemplate은 데이터베이스가 생성해주는 키를 매우 쉽게 조회하는 기능도 제공해준다
이름 지정 파라미터 종류
파라미터를 전달하려면 Map 처럼 key , value 데이터 구조를 만들어서 전달해야 한다. 여기서 key는 :파 리히터 이름으로 지정한, 파라미터의 이름이고 , value는 해당 파라미터의 값이 된다.
크게 3가지 종류가 있다
- Map
- SqlParameterSource
- MapSqlParameterSource
- BeanPropertySqlParameterSource
Map
findById() 코드에서 확인할 수 있다.
@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을 전달해 주면 된다.
MapSqlParameterSource
update() 코드에서 확인할 수 있다.
@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);
}
- Map 과 유사한데, SQL 타입을 지정할 수 있는 등 SQL에 좀 더 특화된 기능을 제공한다.
- SqlParameterSource 인터페이스의 구현체이다.
- MapSqlParameterSource는 메서드 체인을 통해 편리한 사용법도 제공한다.
BeanPropertySqlParameterSource
save(), findAll() 코드에서 확인할 수 있다.
@Override
public Item save(Item item) {
String sql = "insert into item(item_name, price, quantity) "
+ "values (:itemName, :price, :quantity)";
//item 이름으로 파라미터 생성
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 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());
}
- JAVA 빈 프로퍼티 규약을 통해서 자동으로 파라미터 객체를 생성한다.
- 예) ( getXxx() -> xxx, getItemName() -> itemName )
- 예를 들어서 getItemName() , getPrice()가 있으면 다음과 같은 데이터를 자동으로 만들어낸다
- SqlParameterSource 인터페이스의 구현체이다.
- 여기서 보면 BeanPropertySqlParameterSource 가 많은 것을 자동화해주기 때문에 가장 좋아 보이지만, BeanPropertySqlParameterSource를 항상 사용할 수 있는 것은 아니다.
- 예를 들어서 update() 에서는 SQL에 :id를 바인딩해야 하는데, update()에서 사용하는 ItemUpdateDto 에는 itemId 가 없다. 따라서 BeanPropertySqlParameterSource를 사용할 수 없고, 대신에 MapSqlParameterSource를 사용했다
BeanPropertyRowMapper
private RowMapper<Item> itemRowMapper() {
return BeanPropertyRowMapper.newInstance(Item.class); //cammel 변환 지원
}
- BeanPropertyRowMapper 는 ResultSet의 결과를 받아서 JAVA 빈 규약에 맞추어 데이터를 변환한다.
예를 들어서 데이터베이스에서 조회한 결과가 select id, price 라고 하면 다음과 같은 코드를 작성해준다. (실제로는 리플렉션 같은 기능을 사용한다.)
Item item = new Item();
item.setId(rs.getLong("id"));
item.setPrice(rs.getInt("price"));
- 데이터베이스에서 조회한 결과 이름을 기반으로 setId() , setPrice() 처럼 JAVA 빈 프로퍼티 규약에 맞춘 메서드를 호출하는 것이다.
별칭
그런데 select item_name 의 경우 setItem_name()이라는 메서드가 없기 때문에 골치가 아프다. 이런 경우 개발자가 조회 SQL을 다음과 같이 고치면 된다.
select item_name as itemName
별칭 as 를 사용해서 SQL 조회 결과의 이름을 변경하는 것이다. 실제로 이 방법은 자주 사용된다. 특히 데이터베이스 컬럼 이름과 객체 이름이 완전히 다를 때 문제를 해결할 수 있다. 예를 들어서 데이터베이스에는 member_name이라고 되어 있는데 객체에 username이라고 되어 있다면 다음과 같이 해결할 수 있다.
select member_name as username
이렇게 데이터베이스 컬럼 이름과 객체의 이름이 다를 때 별칭( as )을 사용해서 문제를 많이 해결한다. JdbcTemplate 은 물론이고, MyBatis 같은 기술에서도 자주 사용된다.
관례의 불일치
JAVA 객체는 카멜( camelCase ) 표기법을 사용한다. itemName처럼 중간에 낙타 봉이 올라와 있는 표기법이다.
반면에 관계형 데이터베이스에서는 주로 언더스코어를 사용하는 snake_case 표기법을 사용한다. item_name처럼 중간에 언더스코어를 사용하는 표기법이다.
이 부분을 관례로 많이 사용하다 보니 BeanPropertyRowMapper 는 언더스코어 표기법을 카멜로 자동 변환해준다.
따라서 select item_name으로 조회해도 setItemName()에 문제없이 값이 들어간다.
정리하면 snake_case 는 자동으로 해결되니 그냥 두면 되고, 컬럼 이름과 객체 이름이 완전히 다른 경우에는 조회 SQL에서 별칭을 사용하면 된다.
구성과 실행
JdbcTemplateV2Config
@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
//@Import(MemoryConfig.class)
//@Import(JdbcTemplateV1Config.class)
@Import(JdbcTemplateV2Config.class)
@SpringBootApplication(scanBasePackages = "hello.itemservice.web")
public class ItemServiceApplication {
...
}
- JdbcTemplateV2Config.class를 설정으로 사용하도록 변경되었다.
- @Import(JdbcTemplateV1Config.class) @Import(JdbcTemplateV2Config.class)
'스프링 DB 2편(데이터 접근 활용 기술)' 카테고리의 다른 글
| Ch02. 스프링 JdbcTemplate - JdbcTemplate 기능 정리 (0) | 2022.06.25 |
|---|---|
| Ch02. 스프링 JdbcTemplate - JdbcTemplate(SimpleJdbcInsert) (0) | 2022.06.25 |
| Ch02. 스프링 JdbcTemplate - JdbcTemplate 적용 (0) | 2022.06.25 |
| Ch02. 스프링 JdbcTemplate - JdbcTemplate 소개와 설정 (0) | 2022.06.25 |
| Ch01. 시작 - 데이터베이스 테이블 생성 (0) | 2022.06.23 |