GitHub

https://github.com/Choidongjun0830

Spring

[Spring DB2] 데이터 접근 기술 - 스프링 JdbcTemplate-1

gogi masidda 2024. 3. 14. 20:42

SQL을 직접 사용하는 경우에 스프링이 제공하는 JdbcTemplate는 JDBC를 매우 편리하게 사용할 수 있도록 도와주어 좋은 선택지다.

 

장점

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

단점

  • 동적 SQL을 해결하기 어렵다.
//JdbcTemplate 추가
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
//H2 데이터베이스 추가
runtimeOnly 'com.h2database:h2'

이 코드를 'build.gradle'에 추가하면 사용 가능

 

JdbcTemplateItemRepositoryV1

/**
 * JdbcTemplate 구현
 */
@Slf4j
public class JdbcTemplateItemRepositoryV1 implements ItemRepository {

    private final JdbcTemplate template;

    public JdbcTemplateItemRepositoryV1(DataSource dataSource) { //생성자에 dataSource가 필요
        this.template = new JdbcTemplate(dataSource);
    }

    @Override
    public Item save(Item item) {
        String sql = "insert into item(item_name, price, quantity) values (?,?,?)";
        //DB에서 생성해준 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);
        //데이터베이스에 insert가 완료되어야 키 값을 확인할 수 있어서 insert후에 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();
        }

    }

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

    @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()); //query는 리스트 반환, 쿼리문이 실패일 때만 DataAccessException을 던짐.
    }
}

save()

  • 데이터를 저장할 때 PK인 id 값은 데이터베이스 자체에서 auto increment 방식을 사용하기 때문에 데이터가 insert되어야 PK값을 알 수 있다. 그래서 KeyHolder와 'connection.prepareStatement(sql, new String[]{"id"})를 사용해서 id를 지정해주면 insert 이후에 데이터베이스에서 생성된 id 값을 알 수 있다.

findById()

  • template.quertForObject()
    • 결과 row가 하나일 때 사용
    • RowMapper는 데이터베이스의 반환 결과인 ResultSet을 객체로 변환한다.
    • 결과가 없으면, 그리고 결과가 둘 이상이면 예외가 발생한다.

findAll()

  • template.query()
    • 결과가 하나 이상일 때 사용한다. (하나 이상이니까 하나일 때도 사용됨)
    • RowMapper는 데이터베이스의 반환 결과인 ResultSet을 객체로 변환한다. (RowMapping시에 필요한 resultset이 끝날 때까지 반복문을 돌리는 것은 JdbcTemplate에서 해준다.)
    • 결과가 없으면 빈 컬렉션을 반환한다.
    • 동적 쿼리 문제
      • 필터링을 할 때 사용자가 넣은 값에 따라 동적으로 쿼리가 달라져야 한다. where를 넣고 어떤 상황에는 and를 넣어줘야 하고, 각 상황에 맞춰서 파라미터도 생성해야 한다.
      • 이후에 배우는 Mybatis를 사용할 때의 큰 장점은 SQL을 직접 작성할 때 동적 쿼리를 쉽게 작성할 수 있다는 것이다.

JdbcTemplate 실행

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

원래 데이터베이스가 아닌 메모리를 사용해서 MemoryConfig를 사용했지만, 지금은 JdbcTemplate를 이용하기 때문에 새로운 Config를 만든다.

@Import(JdbcTemplateV1Config.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);
	}

}

그리고 main메서드가 있는 곳에도 @Import를 바꾼다.

 

결과

데이터베이스와 잘 연동된 것을 확인할 수 있다.

728x90