ABOUT ME

Developer를 위한 Reference 블로그 입니다.

Today
Yesterday
Total
  • 데이터 접근 기술- 스프링 JdbcTemplate
    BackEnd/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 변환 지원
        }

     

      SimpleJdbcInsertSimpleJdbcCall에 대한 사용 방법은 스프링 공식 메뉴얼을, 해당 프로젝트에 SimpleJdbcInsert 적용 코드는 github repository를 참고하시면 됩니다.

     

    [참고 정보]

    스프링 DB 2편 - 데이터 접근 핵심 원리

    전체 소스코드

    반응형

    '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

    댓글

Designed by Tistory.