[Spring] JdbcTemplate의 기본

nimoh·2023년 3월 18일
4
post-thumbnail

Spring에서 SQL을 사용할 때 JdbcTemplate는 간단하면서 좋은 선택지이다. JdbcTemplate은 트랜잭션 관리 및 리소스 관리등을 자동으로 해주는 등 매우 편리하게 JDBC를 사용할 수 있게 도와준다.

JdbcTemplate을 사용하는 이유

JdbcTemplate을 사용하지 않고 Connection으로 DB에 접근하는 경우 코드는 다음과 같다.

  • 기존의 방법
	@Override
    public Member save(Member member) {
        String sql = "INSERT INTO MEMBER(member_id, money) VALUES (?, ?)";

        Connection con = null;
        PreparedStatement pstmt = null;

        try {
            con = getConnection();
            pstmt = con.prepareStatement(sql);
            pstmt.setString(1, member.getMemberId());
            pstmt.setInt(2, member.getMoney());
            pstmt.executeUpdate();
            return member;
        } catch (SQLException e) {
            throw exceptionTranslator.translate("save", sql, e);
        } finally {
            // TCP/IP 커넥션으로 연결되기 때문에 자원 해제해줘야 함
            close(con, pstmt, null); // 현재 코드에는 없지만 자원을 해제해주는 사용자 정의 메서드
        }
    }

위에서 예시를 든 save() 뿐만 아니라 DB에 접근하는 모든 메서드에서 Connection을 가져오고, PreparedStatement를 가져와서 sql을 수행하고 자원을 해제해주는 로직을 작성해주어야한다.
중복 코드가 난무하고 너무 복잡하지 않겠는가?
이럴 때 도입할 수 있는 DB 접근 기술 중 하나가 바로 JdbcTemplate 이다.

  • JdbcTemplate 사용
		...
        private final JdbcTemplate template;		
        public MemberRepository(DataSource dataSource) {
        	this.template = new JdbcTemplate(dataSource);
    	}
		...
 
		@Override
   		public Member save(Member member) {
            String sql = "INSERT INTO MEMBER(member_id, money) VALUES (?, ?)";
            template.update(sql, member.getMemberId(), member.getMoney());
        return member;
    }

JdbcTemplate를 생성하였다. JdbcTemplate은 생성자에서 DateSource를 필요로 하기 때문에 주입해주었다.

JdbcTemplate을 사용하지 않은 코드와 비교해보아라. 얼마나 간단해졌는지.
JdbcTemplate을 사용하면 사용하지 않는 자원(커넥션 등)을 자동으로 해제해준다.

JdbcTemplate의 장점

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

JdbcTemplate의 단점

  • 동적 쿼리문을 해결하기 어렵다.

단점을 해결하기위해...

동적 쿼리문의 수월한 작성을 위해 다른 DB 접근 기술을 사용할 수 있다.
예를 들어 MyBatis와 JPA+QueryDSL 을 사용할 수 있다.

JdbcTemplate 사용법

Insert

다음은 JdbcTemplate을 이용한 Insert 메서드이다.

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

item 테이블의 item_name, price, quantity 컬럼에 각각 쿼리 파라미터로 값을 지정해준다.

이 파라미터는 직접 넣어주는 것이 아닌, PreparedStatement 객체를 통해 넣어줘야한다.

코드를 보면

            ps.setString(1, item.getItemName());
            ps.setInt(2, item.getPrice());
            ps.setInt(3, item.getQuantity());

가 있는데 ?의 순서대로 1, 2, 3 .. 의 인덱싱을 적어줘야한다.

Update

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

update 메서드의 경우 insert보다는 단순하다. JdbcTemplate의 update메서드를 사용하여 sql문과 각각의 파라미터 값을 직접 넣어주면된다.

Select

단 건 조회

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

하나의 결과를 조회할 때에는 JdbcTemplate의 queryForObject() 를 사용하면된다. 매개변수로 sql문, RowMapper, 쿼리 파라미터(?)를 차례로 넣어줘야한다.

여러 개 조회

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

모든 결과를 조회하는 데에는 여러가지 상황이 있을 수 있다. 현재 애플리케이션은 모두 출력, 최대 가격 검색, 이름 검색을 구현해야한다. 따라서 쿼리를 동적으로 변경해줘야하기 때문에 매우 복잡해보이는 로직이 작성되었다.

어쨌든 단 건 조회와는 다르게 JdbcTemplatequery 메서드를 사용하여 sql, RowMapper, 쿼리 파라미터 를 넘겨주었다.

  • RowMapper
    • RowMapper는 말 그대로 테이블 결과 Row와 객체를 매핑해준다.

    • 여기서는 결과가 담긴 ResultSet에서 해당하는 컬럼명을 찾아 객체로 매핑해준다.

    • RowMapper 메서드를 굳이 따로 만들어 줄 필요는 없지만 코드의 유지보수성을 위해 메서드로 추출하였다.

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

JdbcTemplate - 이름 지정 파라미터

기존 JdbcTemplate을 사용하면 다음과 같은 문제점이 있다.

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

이 경우 누군가 파라미터로 들어가는 quantityprice의 순서를 변경한다면 DB에서 가격이 수량으로, 수량이 가격으로 바뀌어 들어가게되고 이를 복구하는 것은 쉽지않을 것이다.

이를 해결하기 위해 JdbcTemplate은 다양한 방법을 지원한다.

// private final JdbcTemplate template;
private final NamedParameterJdbcTemplate template;

방법은 바로 NamedParameterJdbcTemplate을 사용하는 것이다.

NamedParameterJdbcTemplate 역시 생성자에 dataSource를 필요로 한다.

사용법

NamedParameterJdbcTemplate 을 사용할 때에는 sql문에 쿼리파라미터를 ? 가 아닌 파라미터의 이름을 지정해주면 된다.

        String sql = "insert into item (item_name, price, quantity) " +
                     "values (:itemName, :price, :quantity)";

sql 작성은 위와 같고 이 sql을 사용하여 쿼리를 수행하는 데에는 크게 3가지 방법이 있다.

  • BeanPropertySqlParameterSource
	@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;
}

BeanPropertySqlParameterSource을 사용하면 매우 쉽게 parameter를 설정할 수 있다. 위 코드를 보면 BeanPropertySqlParameterSource을 생성하면서 item 객체를 넘겨주었다. item에는 getItemName, getPrice, getQuantity가 있는데 get 메서드와 쿼리 파라미터에서 지정해준 이름이 동일하다면 자동으로 매칭해준다.

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

MapSqlParameterSource()를 사용하면 메서드 체이닝 방식으로 쿼리 파라미터를 추가해줄 수 있다. 기존 JdbcTemplate과 유사하지만 파라미터 이름을 가지고 대상을 추가하기 때문에 더욱 직관적이다.

  • Map 사용

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

간단하게 Map 컬렉션 프레임워크를 사용해도 param을 넣어줄 수 있다.

💡 아래와 같이 BeanPropertyRowMapper를 사용하면 자동으로 ResultSet과 객체를 연결해준다.
 private RowMapper<Item> itemRowMapper() {
          return BeanPropertyRowMapper.newInstance(Item.class);
}

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

숫자 하나와 파라미터 바인딩 예시이다.

  • 단건 조회 - 문자 조회
    String lastName = jdbcTemplate.queryForObject(
            "select last_name from t_actor where id = ?",
            String.class, 1212L);

문자 하나와 파라미터 바인딩 예시이다.

  • 단건 조회 - 객체 조회
  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);

객체 하나를 조회한다. 결과를 객체로 매핑해야 하므로 RowMapper 를 사용해야 한다. 여기서는 람다를
사용했다.

  • 목록 조회 - 객체
  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;
						});

여러 로우를 조회할 때는 query() 를 사용하면 된다. 결과를 리스트로 반환한다.

결과를 객체로 매핑해야 하므로 RowMapper 를 사용해야 한다. 여기서는 람다를 사용했다.

  • 목록 조회 - 객체
  private final RowMapper<Actor> actorRowMapper = (resultSet, rowNum) -> {
      Actor actor = new Actor();
      actor.setFirstName(resultSet.getString("first_name"));
      actor.setLastName(resultSet.getString("last_name"));
      return actor;
  };
  public List<Actor> findAllActors() {
      return this.jdbcTemplate.query("select first_name, last_name from t_actor",actorRowMapper);
	}

여러 로우를 조회할 때는 query() 를 사용하면 된다. 결과를 리스트로 반환한다.

여기서는 RowMapper 를 분리했다. 이렇게 하면 여러 곳에서 재사용 할 수 있다.

변경(INSERT, UPDATE, DELETE)

데이터를 변경할 때는 jdbcTemplate.update() 를 사용하면 된다. 참고로 int 반환값을 반환하는데,
SQL 실행 결과에 영향받은 로우 수를 반환한다.

  • 등록
  jdbcTemplate.update(
          "insert into t_actor (first_name, last_name) values (?, ?)",
          "Leonor", "Watling");
  • 수정
  jdbcTemplate.update(
          "update t_actor set last_name = ? where id = ?",
          "Banjo", 5276L);
  • 삭제
  jdbcTemplate.update(
          "delete from t_actor where id = ?",
          Long.valueOf(actorId));

기타 기능

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

  • DDL
jdbcTemplate.execute("create table mytable (id integer, name varchar(100))");
  • 스토어드 프로시저 호출
    jdbcTemplate.update(
            "call SUPPORT.REFRESH_ACTORS_SUMMARY(?)",
            Long.valueOf(unionId));
profile
부족함을 인정하는 순간이 성장의 시작점이다.

0개의 댓글