3 분 소요


JdbcTemplate 소개

JdbcTemplate은 JDBC를 매우 편리하게 사용할 수 있게 도와준다.

장점

  • 설정의 편리함
    • JdbcTemplate은 spring-jdbc 라이브러리에 포함되어 있는데, 이 라이브러리는 스프링으로 JDBC를 사용할 때 기본으로 사용되는 라이브러리이다. 그리고 별도의 복잡한 설정 없이 바로 사용할 수 있다.
  • 반복 문제 해결
    • JdbcTemplate은 템플릿 콜백 패턴을 사용해서, JDBC를 직접 사용할 때 발생하는 대부분의 반복 작업을 대신 처리해준다.
    • 개발자는 SQL을 작성하고, 전달할 파라미터를 정의하고, 응답 값을 매핑하기만 하면 된다.
    • 대부분의 반복 작업을 대신 처리해준다.
      • 커넥션 획득
      • statement를 준비하고 실행
      • 결과를 반복하도록 루프를 실행
      • 커넥션 종료, statement, resultset 종료
      • 트랜잭션 다루기 위한 커넥션 동기화
      • 예외 발생시 스프링 예외 변환기 실행

단점

  • 동적 SQL을 해결하기 어렵다.


적용1 - 기본

메모리에 저장하던 데이터 -> 데이터베이스에 저장

Repository

JdbcTemplateItemRepositoryV1

/**
 * 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 (?, ?, ?)";

        KeyHolder keyHolder = new GeneratedKeyHolder();
        template.update(con -> {
            // 자동 증가 키
            PreparedStatement ps = con.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;
        };
    }
}
  • JdbcTemplatedataSource가 필요하다.

save()

  • template.update()
    • INSERT, UPDATE, DELETE SQL에 사용한다.
  • 데이터를 저장할 때 PK 생성에 identity(auto increment) 방식을 사용하기 때문에, PK인 ID 값을 개발자가 직접 지정하는 것이 아니라 비워두고 저장해야 한다.
  • 이렇게 데이터베이스가 대신 생성해주는 PK ID 값은, 데이터베이스에 INSERT가 완료 되어야 생성된 PK ID 값을 확인할 수 있다.
  • KeyHolderconnection.prepareStatement(sql, new String[]{"id"})를 사용해서 id를 지정해주면 INSERT 쿼리 실행 이후에 데이터베이스에서 생성된 ID 값을 조회할 수 있다.
  • 참고로, JdbcTemplate이 제공하는 SimpleJdbcInsert라는 편리한 기능이 있으므로 대략 이렇게 사용한다 정도로 알아만 두면 된다.

findById()

  • template.queryForObject()
    • 결과 로우가 하나일 때 사용.
    • RowMapper는 데이터베이스의 반환 결과인 ResultSet을 객체로 변환한다.
    • 결과가 없으면 EmptyResultDataAccessException 예외가 발생한다.
    • 결과가 둘 이상이면 IncorrectResultSizeDataAccessException 예외가 발생한다.
  • ItemRepository.findById() 인터페이스는 결과가 없을 때 Optional을 반환해야 한다. 따라서, 결과가 없으면 예외를 잡아서 Optional.empty를 대신 반환하면 된다.

findAll()

  • template.query()
    • 결과가 하나 이상일 때 사용
    • RowMapper는 데이터베이스의 반환 결과인 ResultSet을 객체로 변환한다.
    • 결과가 없으면 빈 컬렉션을 반환한다.

itemRowMapper()

데이터베이스의 조회 결과를 객체로 변환할 때 사용.
JdbcTemplate이 다음과 같은 루프를 돌려주고, 개발자는 RowMapper를 구현해서 그 내부 코드만 채운다고 이해하면 된다.

while(resultSet  끝날  까지) {
    rowMapper(rs, rowNum)
}


적용2 - 동적 쿼리 문제

결과를 검색하는 findAll()에서 어려운 부분은 사용자가 검색하는 값에 따라서 실행하는 SQL이 동적으로 달라져야 한다는 점이다.

// 검색 조건 X
select id, item_name, price, quantity from item

// 상품명으로 검색
select id, item_name, price, quantity from item
    where item_name like concat('%',?,'%')

// 최대 가격으로 검색
select id, item_name, price, quantity from item
    where price <= ?

// 상품명, 최대 가격   검색
select id, item_name, price, quantity from item
    where item_name like concat('%',?,'%')
        and price <= ?

결과적으로 4가지 상황에 따른 SQL을 동적으로 생성해야 한다.


적용3 - 구성과 실행

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

ItemServiceApplication - 변경

//@Import(MemoryConfig.class)
@Import(JdbcTemplateV1Config.class)
@SpringBootApplication(scanBasePackages = "hello.itemservice.web")
public class ItemServiceApplication {}


<출처 : 인프런 - 스프링 DB 2편 : 데이터 접근 활용 기술(김영한)>

댓글남기기