Spring의 JDBCTemplate

hoyong.eom·2023년 8월 20일
0

스프링

목록 보기
34/59
post-thumbnail

Spring

JDBCTemplate

JdbcTemplate는 spring-jdbc 라이브러리에 포함되어있다. 이 라이브러리는 스프링으로 JDBC를 사용할떄 기본으로 사용되는 라이브러리이다. 그리고 별도의 복잡한 설정 없이 바로 사용할 수 있다.

JdbcTemplate는 템플릿 콜백 패턴을 사용해서 JDBC를 직접 사용할때 발생하는 대부분의 반복 작업을 대신 처리해준다.
따라서 개발자는 SQL을 작성하고 전달할 파라미터를 정의해서 응답값을 맵핑하기만 하면 된다.

기존에 고전적으로 JDBC를 사용해서 발생했던 아래의 문제점들을 대부분 해결해준다.

  • 커넥션 획득 반복 작업
  • statement를 준비하고 실행
  • 결과를 반복하도록 루프 실행
  • 커넥션 종료, statement, resultset 종료
  • 트랜잭션을 다루기 위한 커넥션 동기화
  • 예외 발생시 스프링 예외 변환기 실행

그러나, 동적 sql 쿼리문을 작성하기는 여전히 어렵기 때문에 이를 보완하기 위해서는 MyBatis를 사용해야한다.

JdbcTemplate 설정

implementation 'org.springframework.boot:spring-boot-starter-jdbc'

org.springframework.boot:spring-boot-starter-jdbc를 추가하면 JdbcTemplate가 들어있는 spring-jdbc가 라이브러리에 포함된다.

JdbcTemplate는 spring-jdbc라이브러리만 추가하면 된다. 별도의 추가 설정은 필요하지 않다.

JdbcTemplate 적용1 - 기본

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는 데이터소스가 필요하다
  • 스프링에서 JdbcTemplate은 관례상 datasource를 의존성 주입받아 생성자를 이용해서 생성한다. 주로 이 방법을 많이 사용한다.
  • JdbcTemplate를 스프링빈으로 직접 등록해서 사용해도 된다.

save()

  • template.update() : 데이터를 변경할때는 update()를 사용한다.
    - INSERT,UPDATE, DELETE SQL에 사용한다.
    • template.update() 의 반환값은 int이며, 영향받은 로우 수를 반환한다.
  • 데이터를 저장할때 PK생성에 identity(auto increment)방식을 사용하기 때문에 PK인 ID값을 개발자가 직접 지정하는것이 아니라 비워두고 저장해야한다. 그러면 데이터베이스가 PK인 ID를 대신 생성해준다.
  • 문제는 이렇게 데이터베이스가 대신 생성해주는 PK ID 값은 데이터베이스가 생성하기 떄문에 데이터베이스에 INSERT가 완료되어야 생성된 PK ID 값을 확인할 수 있다.
  • KeyHolder와 connection.prepareStatement(sql, new Stringp[]{"id"}를 사용해서 id를 지정해주면 INSERT 쿼리 실행 이후에 데이터베이스에서 생성된 ID 값을 조회할 수 있다.

update()

  • template.update()를 사용해서 데이터를 변경한다.
  • ?에 바인딩할 파라미터를 순서대로 전달한다.

findById()

  • template.queryForObject()를 사용해서 조회한다.
    - queryForObject()는 결과 로우가 하나일때사용한다.
    • RowMapper는 데이터베이스의 반환 결과인 ResultSet을 객체로 변환한다.
    • 결과가 없으면 EmptyResultDataAccessException 예외가 발생한다.
    • 결과각 둘 이상이면 IncorrectResultSizeDataAccessException예외가 발생한다.

findAll()

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

itemRowMapper()
데이터베이스의 조회 결과를 객체로 변환할때 사용한다.
JDBC를 직접 사용할떄 ResultSEt를 사용했던 부분과 동일하다.
차이가 있다면 JdbcTemplate가 아래와 같이 루프를 돌려주고 개발자는 RowMapper를 구현해서 그 내부 코드만 채운다고 이해하면 된다.

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

참고)
JDBCTemplate을 이용해서 동적 쿼리를 지원하기는 쉽지 않다.. 따라서 동적 쿼리를 사용해야한다면(대부분 사용할것) MyBatis를 사용하자.

참고)
JdbcTemplate를 사용하기 위해서는 데이터소스가 필요한데, 스프링부트는 데이터소스와 트랜잭션 매니저를 자동으로 스프링빈에 등록해준다.

// application.properties
spring.profiles.active=local
spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.username=sa

참고)
JdbcMTemplate가 실행하는 SQL로그를 확인하려면 application.properties에 다음과 같이 추가하면 된다.

#jdbcTemplate sql log
logging.level.org.springframework.jdbc=debug

이름지정 JDBCTemplate

jdbcTemplate를 기본적으로 사용하면 파라미터를 순서대로 바인딩 한다.
예를 들어 아래와 같은 코드를 보자.

String sql = "update item set item_name=?, price=?, quantity=? where id=?";
template.update(sql,
 itemName,
 price,
 quantity,
 itemId);

위에서는 순서대로 잘 바인딩 되어 있지만 , 위 순서가 잘못된다면 매우 심각한 문제가 발생할 수 있다.

이러한 문제를 보완하기 위해서 NamedParameterJdbcTemplate라는 이름을 지정해서 파라미터를 바인딩하는 기능을 제공한다.

예시 코드를 보면 아래와 같다.

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 변환 지원
    }
}
  • this.template = new NamedParameterJdbcTemplate(dataSource)가 핵심코드이다.
  • NamedParameterJdbcTemplate도 내부에 dataSource가 필요하다.
  • JdbcTemplateItemRepositoryV2 생성자를 보면 의존관계 주입은 dataSource를 받고 내부에서 NamedParameterJdbcTemplate를 생성해서 가지고 있다. 스프링에서는 JdbcTemplate 관련 기능을 사용할때 관례상 이러한 방법을 많이 사용한다.
  • NamedParameterJdbcTemplate를 스프링빈으로 직접 등록하고 주입받아도 된다.

save()

  • SQL에서 ? 대신에 :파라미터이름으로 받는 것을 확인할 수 있다.
String sql = "insert into item(item_name, price, quantity) " +
                "values (:itemName, :price, :quantity)";

추가로 NamedParameterJdbcTemplate는 데이터베이스가 생성해주는 키를 매우 쉽게 조회하는 기능도 제공해준다.

JdbcTemplate - 이름 지정파라미터2

파라미터를 전달하면 Map처럼 key,value 데이터 구조를 만들어서 전달해야한다.
여기서 key는 :파라미터이름으로 지정한 파리미터의 이름이고, value는 해당 파라미터의 값이 된다.

아래의 코드를 보면 파라미터를 전달하는것을 확인할 수 있다.

template.update(sql, param, keyHolder);

이름 지정 바인딩에서 자주 사용하는 파라미터의 종류는 크게 3가지가 있다.

  • Map
  • SqlParameterSource
    - MapSqlParameterSource
    • BeanPropertySqlParameterSource

1. Map

단순히 Map을 이용하는 경우는 아래와 같다.

Map<String, Object> param = Map.of("id", id);
Item item = template.queryForObject(sql, param, itemRowMapper())

2. MapSqlParameterSource

Map과 유사한데, SQL 타입을 지정할 수 있는 등 SQL에 좀 더 특화된 기능을 제공한다.
SqlParameterSource인터페이스의 구현체이다.
MapSqlParameterSource는 메서드 체인을 통해 편리한 사용법도 제공한다.

SqlParameterSource param = new MapSqlParameterSource()
 .addValue("itemName", updateParam.getItemName())
 .addValue("price", updateParam.getPrice())
 .addValue("quantity", updateParam.getQuantity())
 .addValue("id", itemId); //이 부분이 별도로 필요하다.
template.update(sql, param);

3. BeanPropertySqlParameterSource

자바빈 프로퍼티 규약을 통해서 자동으로 파라미터 객체를 생성한다.
예를 들어 getItemNAme() ,getPrice()가 있으면 다음과 같은 데이터를 자동으로 만들어낸다.

  • key=itemName, value=상품명 값
  • key=price, value= 가격 값
SqlParameterSource param = new BeanPropertySqlParameterSource(item);
KeyHolder keyHolder = new GeneratedKeyHolder();
template.update(sql, param, keyHolder)

BeanPropertySqlParameterSource가 가장 편리하기 때문에 좋아보이지만 BeanPropertySqlParameterSource는 항상 사용할 수 없다. 예를 들어,
SQL에 :id를 바인딩해야하는데 바인딩 객체에 id가 없다면 사용할 수 없고, MapSqlParameterSource를 사용해야한다.

BeanPropertyRowMapper

기존의 itemRowMapper()함수가 아래 처럼 개선되었다.

private RowMapper<Item> itemRowMapper() {
 return BeanPropertyRowMapper.newInstance(Item.class); //camel 변환 지원
}

BeanPropertyRowMapper는 ResultSet의 결과를 받아서 자바빈 규약에 맞추어 데이터를 변환한다. 예를 들어, 데이터베이스에서 조회한 결과가 select id, price라고 한다면 다음과 같은 코드를 작성해준다.

Item item = new Item();
item.setId(rs.getLong("id"));
item.setPrice(rs.getInt("price"));

별칭

그런데, select item_name의 경우 setItem_name()이라는 메서드가 없기 때문에 문제가 발생할 수 있는데 이런 경우 개발자가 조회 SQL에서 alias를 사용해서 수정할 수 있다.

select item_name as itemName

실제로 이 방법은 매우 유용하지만, 이러한 관례의 불일치는 스프링에서 어느정도 보완해준다.

실제로 이 부분이 관례로 많이 사용되다보니(자바:카멜케이스, DB:스네이크케이스) BeanPropertyRowMapper는 언더스코어 표기법을 카멜로 자동으로 변환해준다.
따라서 select item_name으로 조회해도 setItemName()에 문제 없이 들어간다.

SimpleJdbcInsert

JdbcTemplate는 INSERT SQL를 직접 작성하지 않아도 되도록 SimpleJdbcInsert라는 편리한 기능을 제공한다.

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"); //생략 가능
    }

    @Override
    public Item save(Item item) {
        SqlParameterSource param = new BeanPropertySqlParameterSource(item);
        Number key = jdbcInsert.executeAndReturnKey(param);
        item.setId(key.longValue());
        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 변환 지원
    }
}
  • this.jdbcInsert = new SimpleJdbcInsert(dataSource)가 핵심코드이다.
this.jdbcInsert = new SimpleJdbcInsert(dataSource)
 .withTableName("item")
 .usingGeneratedKeyColumns("id");
// .usingColumns("item_name", "price", "quantity"); //생략 가능
  • withTableName : 데이터를 저장할 테이블 명을 지정한다.
  • usingGeneratedKeyColumns : Key를 생성하는 PK컬럼 명을 지정한다.
  • usingColumns : INSERT SQL에 사용할 컬럼을 지정한다. 특정 값만 저장하고 싶을 때 사용한다.

SimpleJdbcInsert는 생성 시점에 데이터베이스 테이블의 메타 데이터를 조회한다. 따라서 어떤 컬럼이 있는지 확인 할 수 있으므로 usingColumns를 생략할 수 있다. 만약 특정 컬럼만 지정해서 저장하고 싶다면 suingColumns를 사용하면 된다.

SimpleJdbcCall

프로시저 호출을 편리하게 할 수 있다

참고 : https://docs.spring.io/spring-framework/reference/data-access/jdbc/simple.html#jdbc-simple-jdbc-call-1

참고
JdbcTemplate 공식 매뉴얼
https://docs.spring.io/spring-framework/docs/current/reference/html/data-access.html#jdbc-JdbcTemplate

단건 조회 - 숫자

int rowCount = jdbcTemplate.queryForObject("select count(*) from t_actor",
Integer.class)

하나의 로우를 조회할떄는 queryForObject()를 사용한다. 지금처럼 조회대상이 객체라아니라 단순 데이터 하나라면 타입을 Integer.class, String.class와 같이 지정해주면 된다.

단건 조회 - 숫자 조회, 파라미터 바인딩

int countOfActorsNamedJoe = jdbcTemplate.queryForObject(
 "select count(*) from t_actor where first_name = ?", Integer.class,
"Joe");

기타

임의의 SQL을 실행할떄는 execute()를 사용하면 된다. 테이블을 생성하는 DDL에 사용할 수 있다.

jdbcTemplate.execute("create table mytable (id integer, name varchar(100))")

스토어드 프로시저 호출

jdbcTemplate.update(
 "call SUPPORT.REFRESH_ACTORS_SUMMARY(?)",
 Long.valueOf(unionId));

참고

해당 포스팅은 아래의 강의를 공부하여 정리한 내용입니다.
김영한님의 SpringDB2-JDBC

0개의 댓글