[Spring] JdbcTemplate 상세 사용법 정리

728x90

 

스프링 JdbcTemplate

- 순수 Jdbc 설정과 같다.

- SQL Mapper 중 하나이다

- Jdbc API에서 본 반복 코드 대부분을 제거한다.

- 커넥션 획득/종료, 커넥션 동기화, 스프링 예외 변환기 실행 등 반복 작업을 대신 처리해 준다.

- DataSource 를 주입받아 JdbcTemplate를 생성하여 사용한다.

- SQL 문을 직접 작성해야 한다. 

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

 

> 참고<

- Insert 문은 데이터 추가라는 단순한 작업이므로, 테이블 이름과 칼럼-값 정보로 SQL을 생성하기 쉽다.

따라서 Spring 이 자동화하기 위해 SimpleJdbcInsert 같은 유틸리티를 제공한다.

 

그렇지만 Select 는 조건, 그룹화, 정렬, 조인 등 형태가 다양하므로 자동화하기 어려워서 직접 작성한다.

Insert 문도 직접 작성해도 된다.

 

초기 설정

- build.gradle 파일에 jdbc, h2 데이터베이스 관련 라이브러리 추가

implementation 'org.springframework.boot:spring-boot-starter-jdbc'
runtimeOnly 'com.h2database:h2'

 

JdbcTemplate

순서 기반 파라미터 바인딩을 지원한다. 

이때, 파라미터 순서가 맞지 않으면 문제가 발생한다.

 

- 사용 방법

`queryForObject()` : 단건 조회

`query()` : 목록 조회 (리스트 반환)

`update()` : 데이터 변경

 

단건 조회, 단순 데이터 조회 경우

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

단건 조회, 객체 조회 경우

Actor actor = jdbcTemplate.queryForObject(
  "select first_name, last_name from t_actor where id = ?",
  (resultSet, rowNum) -> {
    Actor newActor = new Actor();
    newActor.setFirstName(resultSet.getString("first_name"));
    newActor.setLastName(resultSet.getString("last_name"));
    return newActor;
  },
  1212L);

목록 조회, 객체 조회 경우

List<Actor> actors = jdbcTemplate.query(
  "select first_name, last_name from t_actor",
  (resultSet, rowNum) -> {
    Actor actor = new Actor();
    actor.setFirstName(resultSet.getString("first_name"));
    actor.setLastName(resultSet.getString("last_name"));
    return actor;
  });

데이터 변경 경우

jdbcTemplate.update(
  "update t_actor set last_name = ? where id = ?",
  "Banjo", 5276L);

 

- JdbcTemplate 예시 코드

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

 

NamedParameterJdbcTemplate

이름 기반 파라미터 바인딩을 지원한다.

JdbcTemplate 대신 NamedParameterJdbcTemplate 을 사용하도록 권장한다.

 

- 사용 방법

SQL 에서 `?` 대신 `:파라미터이름` 을 받아서 사용한다.

insert into item (item_name, price, quantity) values (:itemName, :price, :quantity)"

 

 - 파라미터 종류

`Map`

  > Key : `:파라미터이름`, Value : `파라미터값` 의 Map 의 데이터 구조

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

 

`SqlParameterSource`

  > SqlParameterSource 인터페이스의 구현체

  > 메서드 체인을 통해 편리한 사용법

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 인터페이스의 구현체

  > 자바빈 프로퍼티 규약을 통해 자동으로 파라미터 객체 생성 (getter/setter 통해서)

  > 따라서 객체 필드를 통해 항상 바인딩할 수 없을 때는 사용할 수 없다.

  (이럴 때는 MapSqlParameterSource 를 통해 하나씩 추가해서 사용해야 한다.)

SqlParameterSource param = new BeanPropertySqlParameterSource(item);
KeyHolder keyHolder = new GeneratedKeyHolder();
template.update(sql, param, keyHolder);

 

- BeanPropertyRowMapper

ResultSet 의 결과를 받아 자바빈 프로퍼티 규약에 맞추어 데이터를 변환한다.

 

일반적으로 데이터베이스는 `snake_case` 표기법을 사용하고, 자바 객체는 `camelCase` 표기법을 사용한다.

따라서 언더스코어 표기법을 카멜 표기법으로 자동 변환해서, 객체를 생성하여 setXXX 를 호출하여 변환해 준다.

(`item.setItemName(rs.getString("item_name"))` 이렇게 자동으로 호출하여 변환해 준다.)

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

 

- NamedParameterJdbcTemplate 예시 코드

@Repository
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();
    }
  }
 
  private RowMapper<Item> itemRowMapper() {
    return BeanPropertyRowMapper.newInstance(Item.class); //camel 변환 지원
  }
}

 

SimpleJdbcInsert

Insert 문의 SQL 을 직접 작성하지 않고, 편리하게 사용할 수 있다.

DataSource 를 주입받아 SimpleJdbcInsert 를 생성하여 사용한다. 

 

생성 시점에 데이터베이스 테이블의 메타 데이터 생성

`withTableName` : 데이터를 저장할 테이블 명을 지정

`usingGeneratedKeyColumns` : key 를 생성하는 PK 칼럼 명을 지정

`usingColumns` : Insert SQL 에 사용할 칼럼을 지정 (생략 가능, 특정 값만 저장하고 싶을 때 사용)

 

Insert SQL 을 실행하는 메서드

`executeAndReturnKey` : Insert SQL 실행하고, 생성된 키 값 조회

 

- SimpleJdbcInsert 예시 코드

@Repository
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;
  }
...
}

 

SimpleJdbcCall

스토어드 프로시저를 편리하게 호출할 수 있다.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

출처 | 스프링 DB 2(김영한) - 인프런

728x90