[스프링 DB 2편] 데이터 접근 기술 - 스프링 JdbcTemplate
포스트
취소

[스프링 DB 2편] 데이터 접근 기술 - 스프링 JdbcTemplate

JdbcTemplate 소개

SQL을 직접 사용하는 경우를 위해 스프링이 제공하는 라이브러리

장점

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

단점

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

JdbcTemplate 설정

build.gradle에 아래와 같이 라이브러리를 추가하자.

//JdbcTemplate 추가
implementation 'org.springframework.boot:spring-boot-starter-jdbc'

추가로 JDBC를 사용하려면 특정 DB에 대한 드라이버가 필요하니,
이번엔 H2 DB에 대한 드라이버를 추가하자.

//H2 데이터베이스 추가
runtimeOnly 'com.h2database:h2'

JdbcTemplate 적용1 - 기본

매퍼

JdbcTemplate은 매퍼라는 것이 필요하다.

RowMapper라는 클래스를 통해 생성한다.
제네릭으로 내가 데이터를 설정할 클래스를 설정해주고,
rs에서 컬럼명을 지칭해서 set + 자료형으로 이루어진 메소드명으로 값을 가져온다.
이 때 rsResultSet을 의미한다.

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

단일 조회

JdbcTemplatequeryForObject 메소드를 사용하자.
파라미터로는 쿼리, 매퍼, 인자들을 지정하면 된다.

파라미터로 넘길 변수가 a, b, c가 있다면 아래와 같이 된다.

template.queryForObject(sql, 매퍼, a, b, c)

목록 조회

JdbcTemplatequery 메소드를 사용하자.
파라미터로는 쿼리, 매퍼, 인자들을 지정하면 된다.

파라미터로 넘길 변수가 a, b, c가 있다면 아래와 같이 된다.

template.query(sql, 매퍼, a, b, c)

등록, 수정, 삭제

JdbcTemplateupdate 메소드를 사용하자.
파라미터로는 쿼리, 매퍼, 인자들을 지정하면 된다.

파라미터로 넘길 변수가 a, b, c가 있다면 아래와 같이 된다.

template.update(sql, 매퍼, a, b, c)

등록할 때 PK 값 가져오기

데이터를 저장할 때 PK 값을 가져오고 싶을 때가 있을 것이다.
그럴 때는 다른 update 메소드를 사용하자.
GeneratedKeyHolder를 사용해서 PK 값을 가져오는 메소드가 따로 있다.

우선 update를 실행할 때 람다를 통해서 아래와 같이 데이터를 등록한다.

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

그러면 데이터가 저장됬을 때 keyHolder에 PK 값이 저장된다.

long key = keyHolder.getKey().longValue();

JdbcTemplate 적용2 - 동적 쿼리 문제

실무에서 개발하다 보면 검색할 때 조건이 매우 다양한 것을 알 수 있다.
특히 관리자용 페이지를 개발하다 보면 해당 페이지가 관리하는 서비스의 규모가 커질수록
관리하는 데이터가 많아지기 때문에 검색 조건도 다양해진다.

그런데 검색 조건이 다양하다고 그걸 또 다 쓰는 것은 아니다.
만약에 검색 조건이 5개가 있다고 치면
1,3,5를 쓰는 경우도 있을 것이고, 1,2,4를 쓰는 경우도 있을 것이고, 안 쓰는 경우도 있을 것이다.

이런 경우에 문제점은 조건절을 만드는 것이다.
어느 순간에 WHERE를 넣을지 AND를 넣을지 개발자가 완전히 수동으로 확인해줘야 한다.
어찌보면 JdbcTemplate 사용 시 제일 귀찮은 점으로 볼 수 있다.

JdbcTemplate 적용3 - 구성과 실행

@Configuration을 통해서 환경설정을 하는 클래스를 만들어주자.
해당 클래스 내부에서 서비스나 리포지토리를 빈으로 등록하고,
메인 애플리케이션 클래스에서 @Import로 해당 환경설정을 활성화 시켜주자.

JdbcTemplate - 이름 지정 파라미터 1

JdbcTemplate이 파리미터를 바인딩할 때는 순서 지정 바인딩이름 지정 바인딩이 있다.

순서 지정 바인딩

순서대로 자동으로 파라미터가 바인딩되는 방식이다.
만약에 update item set item_name=?, price=?, quantity=? where id=?라는 쿼리가 있다고 가정해보자.
그러면 해당 쿼리를 실행하려면 다음과 같이 값을 설정할 것이다.

template.update(sql, itemName, price, quantity, itemId);

그런데 만약에 여기서 price와 quantity의 순서가 바뀌었다고 가정해보자.

template.update(sql, itemName, quantity, price, itemId);

만약 실제로 이런 상황이 발생한다면 개발 시점에 알 수 있을까?
대비를 미리 잘 해둔다면 찾기 쉽겠지만 영어가 가득한 상황에서 찾기는 쉽지 않을 것이다.
정말 심각할 때는 실제로 운영에 반영되고 찾게 되는 상황이 발생할 수도 있다.
그러면 코드만 고치는 게 아니라 DB도 복구해야 하는 등 일이 엄청 커질 수 있다.

이름 지정 바인딩

순서 지정 바인딩의 문제를 고치기 위해 이름 지정 바인딩 방식이 등장했다.
NamedParameterJdbcTemplate라는 클래스를 사용하면 이름 지정 바인딩 방식을 사용할 수 있다.

JdbcTemplate - 이름 지정 파라미터 2

NamedParameterJdbcTemplate라는 클래스를 통해서 JdbcTemplate에서 이름 지정 바인딩을 하는 방식을 알아보자.

매퍼

우선 매퍼를 사용하는 방식부터 차이가 크다.
기본적으로 RowMapper를 사용하는 것은 동일하다.

다만 기존에는 ResultSet을 통해서 클래스에 값을 설정하는 방식이었다.
하지만 이름 지정 바인딩 방식에서는 BeanPropertyRowMapper를 통해서 클래스 자체를 전달한다.
BeanPropertyRowMapper를 사용하면 item_iditemId로 변환하는 camel-case 변환도 지원해준다.

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

이름 지정 바인딩에서의 파라미터

이름 지정 바인딩 방식에서는
MapSqlParameterSource라는 인터페이스가 적극적으로 사용된다.
Map은 단순히 키와 값을 설정하면 되니 넘어가자.

SqlParameterSource 인터페이스는 값을 Map처럼 설정하는 방식과 자바빈 프로퍼티 규약을 기준으로 설정하는 방식이 있다.

Map처럼 설정하는 방식은 MapSqlParameterSource라는 클래스를 사용한다.
해당 클래스는 빌더 패턴을 사용한다는 것이 중요하다.
그래서 아래와 같이 생성과 동시에 값을 설정한다.

SqlParameterSource param = new MapSqlParameterSource()
.addValue("itemName", itemName)
.addValue("price", price)
.addValue("quantity", quantity)
.addValue("id", itemId);

그런 다음에 값을 넘기면 된다.

template.update(sql, param);

자바빈 프로퍼티 규약의 경우에는 값을 넘기기 위해 클래스를 만들어서 getter 메소드를 통해 값을 설정하는 방식이다.
해당 방식은 BeanPropertySqlParameterSource라는 클래스를 사용한다.
만약에 Item이라는 클래스가 있고 price라는 필드가 있다고 가정해보자.
이 때 getPrice라는 메소드가 존재한다면 price를 사용하는 곳에 자동으로 값을 설정해준다.
값은 컨트롤러나 서비스에서 설정해주고 리포지토리에서는 그저 사용하기만 하면 된다.

SqlParameterSource param = new BeanPropertySqlParameterSource(item);

쿼리 작성 시 차이점

순서 지정 바인딩 방식에서는 where id = ?처럼 물음표로 값을 설정할 위치를 지정해줬다.
하지만 이름 지정 바인딩 방식에서는 where id = :id처럼 :필드명으로 매핑할 이름을 직접 명시해준다.

등록할 때 PK 값 가져오기

이름 지정 바인딩 방식을 사용할 때도 동일하게 GeneratedKeyHolder를 사용하면 된다.
template.update(sql, param, keyHolder);처럼 쿼리를 실행하고,
Long key = keyHolder.getKey().longValue();처럼 값을 가져오면 된다.

주의할 점

순서 지정 바인딩을 사용할 때는 JdbcTemplateJdbcTemplate를 그대로 주입했다.
이름 지정 바인딩을 사용할 때는 JdbcTemplateNamedParameterJdbcTemplate를 주입하는 것을 주의하자.
아니면 처음부터 NamedParameterJdbcTemplate를 선언하면 된다.

Jdbc Template - SimpleJdbclnsert

JdbcTemplate은 INSERT SQL를 직접 작성하지 않아도 되도록 SimpleJdbcInsert라는 편리한 기능을 제공한다.
SimpleJdbcInsert라는 클래스를 사용하면 된다.
참고로 스프링에서는 JdbcTemplate를 사용할 때 데이터를 저장할 때는 관례 상 이 방법을 많이 사용한다.

생성자

생각보다 단순한데 테이블명과 등록 시 반환할 PK의 컬럼명을 명시하면 된다. 아래는 그 예시다.

this.jdbcInsert = 
    new SimpleJdbcInsert(dataSource)
        .withTableName("item") //테이블명 지정
        .usingGeneratedKeyColumns("id"); //PK가 되는 컬럼명 지정

그리고 SimpleJdbcInsert는 생성 시점에 DB 테이블의 메타 데이터를 조회할 수 있다.
그래서 usingColumns라는 메소드를 통해서 INSERT가 실행될 때 저장되는 컬럼들을 지정할 수 있다.
전체가 아닌 일부 컬럼만 저장하고 싶을 때 사용하며, 생략 가능하다.

데이터 등록하기

단순히 executeAndReturnKey 메소드를 실행하면 된다.
파라미터를 지정하는 것은 BeanPropertySqlParameterSource로 하면 된다.
실행하면 아래와 같은 모습이 된다.

Number key = jdbcInsert.executeAndReturnKey(param);

메소드명을 보면 알 수 있듯이 PK 값을 반환한다.
해당 값은 Number 클래스로 받아서
해당 클래스에서 제공하는 longValue()intValue()같은 메소드로 형변환해서 사용하면 된다.

JdbcTemplate 주요 기능

  • JdbcTemplate
    • 순서 기반 파라미터 바인딩을 지원
  • NamedParameterJdbcTemplate
    • 이름 기반 파라미터 바인딩을 지원
    • 가장 권장되는 방식
  • SimpleJdbcInsert
    • INSERT SQL을 편리하게 사용 가능
  • SimpleJdbcCall
    • 스토어드 프로시저를 편리하게 호출 가능
    • 공식 문서

JdbcTemplate 정리

실무에서 쿼리를 직접 실행할 때는 JdbcTemplate를 사용하자.
실제로 JPA같은 ORM 기술을 사용할 때도 종종 쿼리를 직접 사용해야 할 때가 있는데,
그럴 때 JdbcTemplate이 많이 쓰인다.

다만 JdbcTemplate의 최대 단점이 문제인데 바로 동적 쿼리다.
만약 JPA같은 ORM 기술을 쓰는 게 아니라면 이럴 때는 MyBatis라는 기술을 사용하자.
JdbcTemplate의 최대 단점인 동적 쿼리 문제를 해결해주면서 쿼리도 편하게 작성할 수 있게 도와준다.

출처

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.