새소식

framework/spring

[Spring] JDBC Template

  • -

JDBC Template

스프링이 제공하는 JdbcTemplate은 JDBC를 편리하게 사용할 수 있게 도와준다.

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

의존성 설정

JdbcTemplate을 사용하기 위해서는 의존성을 설정해줘야 한다.
build.gradle에 다음 내용을 추가해준다.

dependencies {
    // JdbcTemplate
    implementation ‘org.springframework.boot:spring-boot-starter-jdbc’
}

JdbcTemplate 기능

JdbcTemplate는 대표적으로 다음과 같은 기능을 지원한다.

  • JdbcTemplate : 순서 기반 파라미터 바인딩 지원
  • NamedParameterJdbcTemplate : 이름 기반 파라미터 바인딩 지원
  • SimpleJdbcInsert : insert SQL을 편리하게 사용 가능
  • SimpleJdbcCall : 스토어드 프로시저 호출 가능

JdbcTemplate 사용

JdbcTemplate은 순서 기반 파라미터 바인딩을 지원한다.
JdbcTemplate 생성 시점에 DataSource를 넘겨주어야 한다.

다음과 같이 사용할 수 있다.

public class JdbcTemplateItemRepository {

    private final JdbcTemplate template;

    public JdbcTemplateItemRepository(DataSource dataSource) {
        this.template = new JdbcTemplate(dataSource);
    }

    // insert
    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;
    }

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

    // select
    public Optional<Item> findById(Long id) {
        String sql = "select id, item_name, price, quantity from item where id = ?";
        try {
            // queryForObject() : 결과가 없으면 예외 발생.
            Item item = template.queryForObject(sql, itemRowMapper(), id);
            return Optional.of(item);
        } catch (EmptyResultDataAccessException e) {
            return Optional.empty();
        }
    }

    // select
    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());
    }

    // resultset to Object
    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;
        };
    }
}

데이터를 조작하는 명령(INSERT, UPDATE, DELETE SQL)은 JdbcTemplate.update() 메서드를 사용한다.
반환값은 int로 영향을 받은 행 수를 반환한다.
save() 메서드에서 보듯이 자동 증가 컬럼이 있다면 해당 컬럼은 SQL 문에서 비워준다.
insert 수행 이후 저장된 PK 값을 받기 위해 KeyHoldercon.prepareStatement(sql, new String[]{"id"})를 통해 자동 증가된 컬럼을 지정해주면, insert 이후 자동 증가된 컬럼 값이 KeyHolder에 담긴다.

파라미터 바인딩을 위해서는 JdbcTemplate 메서드의 args 부분에 ?에 바인딩 할 파라미터를 순서대로 전달하여 사용하면 된다.

하지만 이는 파라미터를 순서대로 바인딩 하기 때문에 순서가 맞지 않아 오류가 발생할 경우가 생길 수 있다는 문제가 있다.

NamedParameterJdbcTemplate

JdbcTemplate의 순서 기반 파라미터 바인딩의 문제를 보완하기 위해 NamedParameterJdbcTemplate을 제공한다.

NamedParameterJdbcTemplate도 역시 생성 시점에 DataSource를 넘겨주어야 한다.

public class JdbcTemplateItemRepository {

    private final NamedParameterJdbcTemplate template;

    public JdbcTemplateItemRepositoryV2(DataSource dataSource) {
        this.template = new NamedParameterJdbcTemplate(dataSource);
    }

    // 빈 프로퍼티 기반 파라미터 매핑
    public Item save(Item item) {
        String sql = "insert into item (item_name, price, quantity) " +
                "values (:itemName, :price, :quantity)";
        // BeanPropertySqlParameterSource를 사용할 수 있다.
        SqlParameterSource param = new BeanPropertySqlParameterSource(item);
        KeyHolder keyHolder = new GeneratedKeyHolder();
        template.update(sql, param, keyHolder);
        Long key = keyHolder.getKey().longValue();
        item.setId(key);
        return item;
    }

    // 파라미터 이름 기반 매핑
    public void update(Long itemId, ItemUpdateDto updateParam) {
        String sql = "update item " +
                "set item_name=:itemName, price=:price, quantity=:quantity " +
                "where id=:id";
        // MapSqlParamterSource를 사용할 수 있다.
        SqlParameterSource param = new MapSqlParameterSource()
                .addValue("itemName", updateParam.getItemName())
                .addValue("price", updateParam.getPrice())
                .addValue("quantity", updateParam.getQuantity())
                .addValue("id", itemId);
        template.update(sql, param);
    }

    public Optional<Item> findById(Long id) {
        String sql = "select id, item_name, price, quantity from item where id = :id";
        try {
            // Map을 사용하여 sql 파라미터 이름에 바인딩할 수 있다.
            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();
        }
    }

    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() {
        // BeanPropertyRowMapper는 리플렉션 등의 기능을 이용하여 ResultSet의 결과를 통해 자바빈 규약에 맞추어 데이터를 변환한다.
        // ResultSet 필드 이름과 변환 객체의 필드 이름이 다를 경우, sql에 as를 통해 별칭을 주어 해결한다.
        return BeanPropertyRowMapper.newInstance(Item.class); // camelcase 지원
    }
}

findById 메서드에서와 같이 단순히 Map을 사용해서 NamedParameterJdbcTemplate 메서드에 넘겨서 이름 기반으로 파라미터 바인딩을 할 수 있다.

또한 update 메서드와 같이 MapSqlParameterSource를 사용하고, 이를 NamedParameterJdbcTemplate에 넘겨줄 수도 있다.
MapSqlParameterSourceMap과 유사한데, SQL 타입을 지정할 수 있는 등 SQL에 좀 더 특화된 기능을 제공한다.

save 메서드와 같이BeanPropertySqlParameterSource를 사용하는 방법도 있다.
BeanPropertySqlParameterSource은 자바 빈 프로퍼티 규약을 통해서 자동으로 파라미터 객체를 생성하는 방법이다.
BeanPropertySqlParameterSource 생성 시점에 특정 객체를 인자로 받으면, 해당 객체의 프로퍼티를 통해 파라미터를 자동으로 만들어낸다. ex) getItemName() -> key : itemName, value : 'itemName 값'

SimpleJdbcInsert

SimpleJdbcInsertinsert SQL을 직접 작성하지 않아도 되도록 돕는 기능이다.

SimpleJdbcInsert 생성 시점에 DataSource와 데이터를 저장할 테이블 이름, key를 생성하는 PK 컬럼, insert SQL에 사용할 컬럼들의 이름을 지정한다.
insert SQL에 사용할 컬럼 이름들은 생략할 수 있으며, 특정 값만 저장하고 싶을 때 사용할 수 있다.

public class JdbcTemplateItemRepositoryV3 implements ItemRepository {

    private final NamedParameterJdbcTemplate template;
    private final SimpleJdbcInsert jdbcInsert;

    public JdbcTemplateItemRepositoryV3(DataSource dataSource) {
        this.template = new NamedParameterJdbcTemplate(dataSource);
        this.jdbcInsert = new SimpleJdbcInsert(dataSource)
                .withTableName("item")
                .usingGeneratedKeyColumns("id")
                .usingColumns("item_name", "price", "quantity"); // 생략 가능
    }

    public Item save(Item item) {
        SqlParameterSource param = new BeanPropertySqlParameterSource(item);
        Number key = jdbcInsert.executeAndReturnKey(param);
        item.setId(key.longValue());
        return item;
    }
}

SimpleJdbcCall

추가로 스토어드 프로시저를 사용할 수 있도록 SimpleJdbcCall을 지원한다.

[Spring] JDBC Template

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.