Spring/[인프런 김영한 스프링 DB 2편 - 데이터 접근 활용 기술]

[인프런 김영한 스프링 DB 2편 - 데이터 접근 활용 기술] 데이터 접근 기술 - 스프링 JdbcTemplate

h2boom 2024. 11. 21. 14:54

스프링 JdbcTemplate

  • JdbcTemplate 특징
    • 장점
      • 설정의 편리함 - 스프링 JDBC 사용 시 기본으로 사용되는 라이브러리로 별도의 복잡한 설정이 필요없다.
      • 반복 문제 해결 - 템플릿 콜백 패턴을 통해 JDBC의 반복 문제를 해결해주기에
        SQL 작성, 파라미터 정의, 응답 값 매핑만 하면된다.
    • 단점
      • 동적 SQL을 해결하기 어렵다.

 

@Slf4j
public class JdbcTemplateItemRepositoryV1 implements ItemRepository {

    private final JdbcTemplate template;

    public JdbcTemplateItemRepositoryV1(DataSource dataSource) {
        this.template = new JdbcTemplate(dataSource);
    }
    
    ...
}
  • JdbcTemplate을 사용한 Repository 예제
    • 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 -> {
        //JdbcTemplate에서 자동 증가 키를 사용하는 방법
        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;
}
  • save()
    • template.update()는 INSERT, UPDATE, DELETE SQL과 같이 데이터를 변경할 때 사용한다.
    • PK(ID 값) 생성 시 auto_increment 방식을 사용하는 경우 PK를 개발자가 직접 지정하지 않고 비워두고 저장하면 DB에서 PK를 대신 생성해준다.
      • DB에 INSERT가 완료되면 생성된 PK를 확인할 수 있다.
      • JdbcTemplate 사용 시 KeyHolder와 connection.prepareStatement(sql, new String[]{"id"})를 사용해서 id를 지정해주면 INSERT 쿼리 실행 이후 DB에서 생성된 ID 값을 조회할 수 있다.이 방법 외에도 JdbcTemplate이 제공하는 SimpleJdbcInsert라는 훨씬 편리한 기능이 존재한다.

 

@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();
    }
}
  • findById()
    • template.queryForObject()는 결과 로우가 하나일 때 사용한다.
      • RowMapper는 데이터베이스의 반환 결과인 ResultSet을 객체로 변환한다.
      • 결과가 없으면 EmptyResultDataAccessException 예외가 발생한다.
      • 결과가 2개 이상이면 IncorrectResultSizeDataAccessException 예외가 발생한다.

 

@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());
}
  • findAll()
    • template.query()는 결과가 하나 이상일 때 사용한다.
      • 결과가 없는 경우 빈 컬렉션을 반환한다.
    • 동적 쿼리 로직은 경우의 수도 많을 뿐더러 복잡하다.
      • MyBatis를 사용하면 SQL을 직접 사용할 때 동적 쿼리를 쉽게 작성할 수 있다.

 

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.setPrice(rs.getInt("quantity"));

        return item;
    });
}
  • itemRowMapper()
    • DB의 조회 결과를 객체로 변환할 때 사용한다.
    • JDBC를 직접 사용해서 ResultSet을 사용하는 것과 차이점은 JdbcTemplate이 루프를 돌려주기에 RowMapper 내부 코드만 구현하면 된다.

 

logging.level.org.springframework.jdbc=debug
  • JdbcTemplate이 실행하는 SQL 로그를 확인하기 위해 application.properties에 내용 추가

이름 지정 파라미터(NamedParameterJdbcTemplate)

  • JdbcTemplate을 기본으로 사용하면 파라미터를 순서대로 바인딩한다.
    • ex) update item set item_name=?, price=?, where id=? 인 경우
      SQL의 ? 순서대로 첫 번째 파라미터는 itemName, 두번째는 price, 마지막은 id를 넣어줘야 한다.
    • 파라미터를 순서대로 바인딩하다보면 순서가 바뀌는 경우 심각한 문제가 발생한다.
      • 개발을 할 때는 코드를 줄이는 편리함보다 모호함을 제거해서 명확하게 만드는 작업이 유지보수 관점에서 더 중요하다.

 

  • 파라미터를 순서대로 바인딩하다 생기는 문제를 보완하기 위해 NamedParameterJdbcTemplate으로 이름을 지정해서 파라미터를 바인딩하는 기능을 제공한다.
  • NamedParameterJdbcTemplate는 이름을 기준으로 파라미터 바인딩을 하기에 순서가 바뀌더라도 상관없다.

 

public class JdbcTemplateItemRepositoryV2 implements ItemRepository {
    private final NamedParameterJdbcTemplate template;

    public JdbcTemplateItemRepositoryV2(DataSource dataSource) {
        this.template = new NamedParameterJdbcTemplate(dataSource);
    }
    
    ...
}
  • 이름 지정 파라미터 기능인 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;
}
  • save()
    • SQL에서 ? 대신 :파라미터이름을 받는다.
      • :파라미터이름은 객체의 필드명과 일치해야 한다.
    • NamedParameterJdbcTemplate은 DB가 생성해주는 PK를 쉽게 조회하는 기능도 제공해준다.
    • BeanPropertySqlParameterSource 사용

 

@Override
public void update(Long itemId, ItemUpdateDto updateParam) {
    String sql = "update item " +
            "set item_name=:itemName, price=:price, quantity=:quantity " +
            "where id=:id";

    MapSqlParameterSource param = new MapSqlParameterSource()
            .addValue("itemName", updateParam.getItemName())
            .addValue("price", updateParam.getPrice())
            .addValue("quantity", updateParam.getQuantity())
            .addValue("id", itemId);

    template.update(sql, param);
}
  • update() 
    • 이름 지정 파라미터에서 파라미터를 전달하려면 Map처럼 key, value 데이터 구조를 만들어서 전달해야 한다.
      • key는 :파라미터이름으로 지정한 파라미터의 이름이고 value는 해당 파라미터의 값이 된다.
        • ex) :itemName과 addValue()의 key 값인 "itemName"이 일치해야하고 value에는 itemName의 값이 들어간다.
    • MapSqlParameterSource 사용

 

@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();
    }
}
  • findById()
    •  Map 사용

 

  • 이름 지정 바인딩에서 자주 사용하는 파라미터 종류 3가지
    • Map
      • findById() 예제에서 사용
    • SqlParameterSource
      • MapSqlParameterSource : Map과 유사하며 SQL 타입을 지정하는 등 SQL에 조금 더 특화되어 있으며 메소드 체인을 통한 편리한 사용법도 제공한다.
        • update() 예제에서 사용
      • BeanPropertySqlParameterSource : 자바빈 프로퍼티 규약을 통해서 자동으로 파라미터 객체를 생성한다.
        • ex) getItemName()이 있는 경우 데이터를 자동으로 만들어낸다.
          key = itemName, value = itemName의 값
        • save() 예제에서 사용

 

  • BeanPropertySqlParameterSource가 가장 좋아보이지만 항상 사용할 수 있는 것은 아니다.
    • 만약 :id를 바인딩해야하는데 해당 객체에 id 필드가 없는 경우 사용할 수 없다.

 

private RowMapper<Item> itemRowMapper() {
    //카멜케이스 변환 지원
    return BeanPropertyRowMapper.newInstance(Item.class);
}

//select id, price의 경우
Item item = new Item();
item.setId(rs.getLong("id"));
item.setPrice(rs.getInt("price"));
  • BeanPropertyRowMapper로 ResultSet의 결과를 받아서 자바빈 규약에 맞춰 데이터를 변환한다.
    • ex) select id, price의 경우 해당 코드를 작성해주고 DB에서 조회한 결과 이름을 기반으로 setId(), setPrice()처럼 자바빈 프로퍼티 규약에 맞춘 메소드를 호출한다.
    • DB 컬럼명과 객체 명이 다른 경우 as 사용
      • ex) select item_name의 경우 setItem_name()이라는 메소드가 없기 때문에 별칭 as를 사용해서 SQL 조회 결과의 이름을 변경하는 방식을 사용한다.
      • ex) DB에는 memeber_name이고 객체에서는 username인 경우 => select member_name as username
    • DB에서는 주로 언더스코어_ 를 사용하는 snake_case 표기법을 사용하고 자바에서는 camelCase를 사용하기에 언더 스코어 표기법을 카멜 표기법으로 자동 변환해준다.
      ex) select item_name => setItemName()으로 자동 변환

JdbcTemplate - SimpleJdbcInsert

  • 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).longValue();
        item.setId(key.longValue());

        return item;
    }
    
    ...
}
  • SimpleJdbcInsert 예제
    • DataSource를 필요로 하며 데이터를 저장할 테이블 명, PK 컬럼명, INSERT SQL에 사용할 컬럼명을 지정한다.
      • SimpleJdbcInsert는 생성 시점에 DB 테이블의 메타 데이터를 조회하기에 어떤 컬럼이 있는지 확인할 수 있으므로 생략할 수 있으며 특정 컬럼만 지정해서 저장하고 싶은 경우에만 usingColums를 사용한다.
    • save()
      • executeAndReturnKey()로 INSERT SQL을 실행하고 생성된 PK 값을 편리하게 조회할 수 있다.

JdbcTemplate 기능 정리

  • JdbcTemplate 주요 기능
    • JdbcTemplate : 순서 기반 파라미터 바인딩을 지원한다.
    • NamedParameterJdbcTemplate : 이름 기반 파라미터 바인딩을 지원한다. (권장)
    • SimpleJdbcInsert : INSERT SQL을 편리하게 사용할 수 있다.
    • SimpleJdbcCall : 스토어드 프로시저를 편리하게 호출할 수 있다.

 

  • 스토어드 프로시저 : 일련의 쿼리를 하나의 메소드처럼 실행하기 위한 쿼리 집합이다.

 

//단건 조회 - 숫자 조회
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");

//단건 조회 - 문자 조회
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);
  • 단건 조회 예제
    • 단건 조회 시 queryForObject() 사용
      • 조회 대상이 객체가 아닌 단순 데이터의 경우 데이터 타입을 지정해준다.
      • 조회 대상이 객체라면 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;
});

//목록 조회 - 객체
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를 사용하며 RowMapper는 람다로 사용할 수 도 있고 메소드로 별도로 분리해서 사용할 수 도 있다.

 

//등록
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));
  • 변경(INSERT, UPDATE, DELETE)
    • 데이터를 변경할 때는 jdbcTemplate.update()를 사용하며 반환 값은 SQL 실행 결과에 영향을 받은 로우 수이다.

 

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

//스토어드 프로시저 호출
jdbcTemplate.update(
	"call SUPPORT.REFRESH_ACTORS_SUMMARY(?)",
	Long.valueOf(unionId))
  • 기타 기능
    • 임의의 SQL을 실행할 때는 execute()를 사용하면 된다.
    • ex) 테이블을 생성하는 DDL 쿼리, 스토어드 프로시저 ...

정리

  • template.queryForObject()는 결과 로우가 하나일 때 사용한다.
  • template.query()는 결과 로우가 하나 이상일 때 사용한다.

 

  • JdbcTemplate 주요 기능
    • JdbcTemplate : 순서 기반 파라미터 바인딩을 지원한다.
    • NamedParameterJdbcTemplate : 이름 기반 파라미터 바인딩을 지원한다. (권장)
    • SimpleJdbcInsert : INSERT SQL을 편리하게 사용할 수 있다.
    • SimpleJdbcCall : 스토어드 프로시저를 편리하게 호출할 수 있다.
  • 순서 기반 파라미터 바인딩보다 이름 기반 파라미터 바인딩을 사용하는 것이 안전하다.

 

  • 이름 기반 파라미터 바인딩 사용 시 SQL에서 ? 대신 :파라미터이름을 받으며 :파라미터이름은 객체의 필드명과 일치해야 한다.
  • 이름 지정 바인딩에서 자주 사용하는 파라미터 종류 3가지
    • Map
    • SqlParameterSource
      • MapSqlParameterSource : Map과 유사하며 SQL 타입을 지정하는 등 SQL에 조금 더 특화되어 있으며 메소드 체인을 통한 편리한 사용법도 제공한다.
      • BeanPropertySqlParameterSource : 자바빈 프로퍼티 규약을 통해서 자동으로 파라미터 객체를 생성한다.
        • snake_case를 camelCase로 자동 변환해준다.

출처 : [인프런 김영한 스프링 DB 2편 - 데이터 접근 활용 기술]

https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-db-2/dashboard

 

스프링 DB 2편 - 데이터 접근 활용 기술 강의 | 김영한 - 인프런

김영한 | 백엔드 개발에 필요한 DB 데이터 접근 기술을 활용하고, 완성할 수 있습니다. 스프링 DB 접근 기술의 원리와 구조를 이해하고, 더 깊이있는 백엔드 개발자로 성장할 수 있습니다., 백엔드

www.inflearn.com