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 예외가 발생한다.
- template.queryForObject()는 결과 로우가 하나일 때 사용한다.
@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을 직접 사용할 때 동적 쿼리를 쉽게 작성할 수 있다.
- template.query()는 결과가 하나 이상일 때 사용한다.
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를 넣어줘야 한다. - 파라미터를 순서대로 바인딩하다보면 순서가 바뀌는 경우 심각한 문제가 발생한다.
- 개발을 할 때는 코드를 줄이는 편리함보다 모호함을 제거해서 명확하게 만드는 작업이 유지보수 관점에서 더 중요하다.
- ex) update item set item_name=?, price=?, where 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 사용
- SQL에서 ? 대신 :파라미터이름을 받는다.
@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의 값이 들어간다.
- key는 :파라미터이름으로 지정한 파라미터의 이름이고 value는 해당 파라미터의 값이 된다.
- MapSqlParameterSource 사용
- 이름 지정 파라미터에서 파라미터를 전달하려면 Map처럼 key, value 데이터 구조를 만들어서 전달해야 한다.
@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() 예제에서 사용
- ex) getItemName()이 있는 경우 데이터를 자동으로 만들어낸다.
- MapSqlParameterSource : Map과 유사하며 SQL 타입을 지정하는 등 SQL에 조금 더 특화되어 있으며 메소드 체인을 통한 편리한 사용법도 제공한다.
- Map
- 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()으로 자동 변환
- ex) select id, price의 경우 해당 코드를 작성해주고 DB에서 조회한 결과 이름을 기반으로 setId(), setPrice()처럼 자바빈 프로퍼티 규약에 맞춘 메소드를 호출한다.
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 값을 편리하게 조회할 수 있다.
- DataSource를 필요로 하며 데이터를 저장할 테이블 명, PK 컬럼명, INSERT SQL에 사용할 컬럼명을 지정한다.
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를 사용해야 한다.
- 단건 조회 시 queryForObject() 사용
//목록 조회 - 객체
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는 람다로 사용할 수 도 있고 메소드로 별도로 분리해서 사용할 수 도 있다.
- 여러 로우를 조회할 때는 query()를 사용하며 리스트로 결과를 반환한다.
//등록
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