JdbcTemplate이란?
JdbcTemplate은 SQL을 직접 사용하여 JDBC를 다루는 편리한 방법 중 하나이다.
JdbcTemplate은 JDBC를 직접 사용할 때 발생하던 여러 가지 반복 문제를 해결해 주고, 트랜잭션을 위한 커넥션 동기화와 스프링 예외 변환기 등의 기능을 자동으로 실행해 준다.
장점
- JdbcTemplate은 스프링으로 JDBC를 사용할 때 자동으로 포함되는 spring-jdbc 라이브러리에 속해있기 때문에 별도의 복잡한 설정 없이 사용할 수 있다.
- 템플릿 콜백 패턴을 사용해서 JDBC를 직접 사용할 때 발생하는 대부분의 반복 작업을 대신 처리해 준다.
- 개발자는 SQL을 작성하고 전달할 파라미터를 정의하여 응답 값을 매핑하기만 하면 된다.
- 트랜잭션을 위한 커넥션 동기화, 스프링 예외 변환기를 자동으로 실행한다.
동적 SQL을 작성하기가 어렵다는 단점도 있다.
이 문제는 추후 MyBatis, QueryDSL 등의 다른 기술로 해결할 수 있다. 그러나 복잡하지 않은 SQL을 처리할 때는 단순하게 JdbcTemplate을 사용하는 것도 좋은 선택지가 될 것이다.
JdbcTemplate 사용 설정
build.gradle에 JdbcTemplate을 포함한 spring-jdbc 라이브러리를 추가한다.h2 DB를 사용할 것이므로 h2 DB도 추가했다.
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
runtimeOnly 'com.h2database:h2'
application.properties에 dataSource를 가져오기 위한 DB 접근 URL, USERNAME, PASSWORD를 미리 설정해 준다.
spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.username=sa
spring.datasource.password=
의존관계 설정
JdbcTemplate은 데이터소스(DataSource)가 필요하다.
JdbcPostRepository의 생성자에서 dataSource를 주입받고, 생성자 내부에서 JdbcTemplate을 생성하는 방법을 이용한다.
JdbcTemplate을 스프링 빈으로 직접 등록하고 주입받아도 되지만, 관례상 이 방법을 많이 사용한다고 한다.
@Repository
public class JdbcPostRepository implements PostRepository {
private final NamedParameterJdbcTemplate template;
public JdbcPostRepository(DataSource dataSource){
this.template = new NamedParameterJdbcTemplate(dataSource);
}
//Repository 로직 생략
}
예제에 사용할 테이블
예제에 사용할 테이블
id는 자동 증가하도록 설정해줬다.
CREATE TABLE post (
id BIGINT generated by default as identity,
writerId VARCHAR(20) NOT NULL,
title VARCHAR(100) NOT NULL,
content Longtext NOT NULL,
create_date TIMESTAMP NOT NULL,
modified_date TIMESTAMP NOT NULL,
views BIGINT DEFAULT 0,
primary key (id)
);
기본 사용법 알아보기
template.update()
- 데이터를 변경할 때는 update를 사용한다.
- INSERT, UPDATE, DELETE SQL에 사용한다.
template.queryForObject()
- 데이터를 하나 조회할 때 사용한다. 즉, 검색 결과 tuple이 하나일 때 사용한다.
- 결과가 없으면 EmptyResultDataAccessException 예외가 발생한다.
- 결과가 둘 이상이면 IncorrectResultSizeDataAccessException 예외가 발생한다.
template.query()
- 데이터를 하나 이상 조회할 때 사용한다. 데이터를 조회해서 list로 반환한다.
- 결과가 없을 시 빈 컬렉션을 반환한다.
RowMapper()
- 데이터 조회 결과를 객체로 변환할 때 사용한다.
- RowMapper가 필요한 메서드에 직접 람다식을 작성해도 되지만, 메서드로 빼서 사용하는 게 좋겠다.
private RowMapper<Post> postRowMapper2(){
return ((rs, rowNum) -> {
Post post = new Post();
post.setId(rs.getLong("id"));
post.setWriterId(rs.getString("writerId"));
post.setTitle(rs.getString("title"));
post.setContent(rs.getString("content"));
post.setCreate_date(rs.getTimestamp("create_date").toLocalDateTime());
post.setModified_date(rs.getTimestamp("modified_date").toLocalDateTime());
post.setViews(rs.getLong("views"));
return post;
});
}
파라미터 ' ? ' 바인딩 예제
DB에 보낼 SQL을 직접 String타입으로 작성하고, 변하는 값은 ' ? '으로 대체한다.
이후 ' ? ' 부분은 순서대로 값을 설정해 준다.
public Optional<Post> findById(Long id) {
String sql = "SELECT * FROM POST where id=? ";
Post post = template.queryForObject(sql, postRowMapper(), id);
}
여기서 queryForObject의 postRowMapper())는 쿼리로 조회한 하나의 tuple을 Post 객체 형식으로 변환하여 post에 담아준다.
public Post updatePost(Long id, Post updateParam) {
String sql = "UPDATE post SET title=?, content=?, MODIFIED_DATE=? where id=?";
template.update(sql,
updateParam.getTitle(),
updateParam.getContent(),
updateParam.getModifiedDate(),
id);
}
여기서 중요한 것은 첫 번째 '? '가 title 이므로 update()에서 첫 번째로 title값을 넘겼고, 두 번째 '? '가 content 이므로 두 번째로 content 값을 넘겼다. 나머지 파라미터들도 순서대로 바인딩해줘야 한다.
그런데 만약 updateParam.getTitle()과 updateParam.getContent()의 순서가 실수로 바뀌었다고 생각해 보자. 그렇게 되면 제목에 content값이 들어가고 내용에 title 값이 들어갈 것이다. 물론 이렇게 짧은 sql에서는 그럴 일이 없을 수도 있다. 그러나 만약 SQL문이 길어지고 복잡해진다면? SQL문에 '? '가 10개만 들어가도 실수할 확률이 늘어나고 헷갈릴 것 같다.
JdbcTemplate은 이런 문제를 해결하기 위해 NamedParameterJdbcTemplate라는 이름을 지정해서 파라미터를 바인딩하는 기능을 제공한다.
이 아래서부터는 '? '로 바인딩하는 기초적인 방법 대신 NamedParameterJdbcTemplate으로 이름을 지정하고 Map, MapSqlParameterSource, BeanPropertyRowMapper 등으로 객체를 바인딩하는 방법을 사용할 것이다.
NamedParameterJdbcTemplate
위에서 설명했듯이 ' ? ' 대신 :title과 같이 바인딩해야 하는 파라미터의 이름을 지정할 수 있다. 이 방법을 사용하면 순서대로 데이터를 넣어야 해서 헷갈리는 일도 없을 것이고, 어느 자리에 어떤 값이 들어가야 하는지 명확해서 좋다.
그래도 순서는 맞춰주는 것이 좋지만, Map과 비슷한 방식으로 값을 전달하기 때문에 순서가 변해도 상관없다.
위에서 본 updatePost() 메서드를 SQL을 이름 지정 방식으로 바꾸면 아래와 같이 변한다.
public Post updatePost(Long id, Post updateParam) {
String sql = "UPDATE post SET title=:title, content=:content, MODIFIED_DATE=:modified_date" +
" where id=:id";
SqlParameterSource param = new MapSqlParameterSource()
.addValue("title", updateParam.getTitle())
.addValue("content", updateParam.getContent())
.addValue("modified_date", LocalDateTime.now())
.addValue("id", id);
template.update(sql, param);
return findById(id).get();
}
코드가 조금 길어졌지만, 개발을 할 때 가장 중요한 것은 모호함을 제거하여 코드를 명확하게 하는 것이 유지보수 관점에서 매우 중요할 것이다. 또한 내가 내 코드를 오랜만에 보거나 다른 사람이 봐도 알아보기 쉬울 것이다.
이제부터는 작성자가 만든 JdbcPostRepository 코드를 리뷰하며 파라미터 바인딩 방법들을 알아보자.
코드 리뷰 + 설명
Map / queryForObject()
Long 형 id 값을 파라미터로 받아서 DB에서 해당 id를 가진 tuple을 불러오는 메서드이다.
- 파라미터 바인딩 해야 할 값이 하나밖에 없기 대문에 단순히 하나의 K - V 셋을 가진 Map을 파라미터로 넘겨준다.
- Map.of를 사용해서 Map자료구조 생성과 동시에 값을 넣어줬다.
- queryForObject로 하나의 tuple을 불러와서 post 형식으로 바꿔주기 위해 postRowMapper()을 파라미터로 넘겨줬다.
public Optional<Post> findById(Long id) {
String sql = "SELECT * FROM POST where id=:id ";
try {
Map<String, Object> param = Map.of("id", id);
//queryForObject : 하나의 객체를 가져올때
Post post = template.queryForObject(sql, param, postRowMapper());
return Optional.of(post);
} catch (EmptyResultDataAccessException e) {
return Optional.empty();
}
}
BeanPropertyRowMapper
- 자바빈 프로퍼티 규약을 통해서 자동으로 파라미터 객체를 생성한다.
- Post 클래스에 getXxx() 메서드를 찾아서 xxx와 매칭시킨다. [ ex) getTitle() -> title ]
- Key : column 명, Value : 값
private RowMapper<Post> postRowMapper() {
return BeanPropertyRowMapper.newInstance(Post.class); //camel 변환 지원
}
단, BeanPropertyRowMapper는 SQL로 조회할 데이터에 Post의 모든 필드가 존재할 때만 사용할 수 있다.
하나라도 빠져있으면 MapSqlParameterSource를 사용해야 한다.
MapSqlParameterSource
post의 id와 title, content를 담은 updateParam을 파라미터로 담아서 해당 post를 수정하는 메서드이다.
- updateParam에서 받은 title, content와 현재 시각, 수정할 post의 id를 SQL문의 파라미터로 바인딩해야 한다.
- SqlParameterSource를 생성해서 각각의 값을 추가해 준다.
- 이 메서드는 조회를 하지 않기 때문에 update(sql, param)만 넘기고 RowMapper는 필요 없다.
public Post updatePost(Long id, Post updateParam) {
String sql = "UPDATE post SET title=:title, content=:content, MODIFIED_DATE=:modified_date" +
" where id=:id";
SqlParameterSource param = new MapSqlParameterSource()
.addValue("title", updateParam.getTitle())
.addValue("content", updateParam.getContent())
.addValue("modified_date", LocalDateTime.now())
.addValue("id", id);
template.update(sql, param);
return findById(id).get();
}
Map을 이용한 간단한 쿼리
대부분의 로직은 SQL 자체로 실행하고 WHERE문에서 사용할 파라미터만 전달한다.
public void addView(Long postId) {
String sql = "UPDATE post SET views=views+1 where id=:id";
template.update(sql, Map.of("id",postId));
}
public void deletePost(Long postId) {
String sql = "DELETE FROM post WHERE id=:id";
template.update(sql, Map.of("id",postId));
}
GeneratedKey설정 / GeneratedKeyHolder설정
저장할 post 객체를 받아서 DB에 저장하고, 저장된 객체를 반환하는 메서드이다.
- Id는 generated key로 설정했기 때문에 파라미터 바인딩 하지 않는다. 그래도 자동으로 id가 생성되므로 BeanPropertyRowMapper을 사용할 수 있다.
- DB에서 자동으로 이전보다 1 증가한 값을 PK로 설정해 주고, 이 값을 보관하기 위해 KeyHolder를 사용한다.
- KeyHolder도 update()에 같이 넘겨준다.
- long key = keyHolder.getKey(). longValue()로 보관했던 키 값을 가져와서 post의 Id로 설정해 주고 반환한다.
public Post save(Post post) {
post.setCreate_date(LocalDateTime.now());
post.setModified_date(LocalDateTime.now());
String sql = "INSERT INTO post( writerId, title, content, create_date, modified_date, views)" +
"values(:writerId, :title, :content, :create_date, :modified_date, :views) ";
SqlParameterSource param = new BeanPropertySqlParameterSource(post);
KeyHolder keyHolder = new GeneratedKeyHolder();
template.update(sql, param, keyHolder);
long key = keyHolder.getKey().longValue();
post.setId(key);
return post;
}
query() / 동적 쿼리
파라미터로 받은 Searchform form에 부합하는 모든 post를 찾아서 반환한다.
SearchForm은 searchCode(검색방법)와 searchWord(검색어)로 이루어져 있다.
- StringUtils.hasText()로 검색어가 있는지 판단한 후 검색어가 없을 시 맨 위에 정의한 sql을 그대로 전달한다. 맨 위의 sql은 그냥 모든 post를 불러오는 SQL이기 때문에 모든 post를 list로 받아올 수 있다.
- 조건에 따라 맨 위에서 선언한 sql에 String을 추가해 준다. ( 맨 앞에 띄어쓰기에 주의해야 한다. )
- LOWER()를 이용해서 소문자로 변환한다.
- Like % xxx% 는 String의 contains와 비슷하다. concat을 이용해서 % 와 바인딩할 값을 붙여준다.
public List<Post> findAll(Searchform form) {
String sql = "SELECT id, writerId, title, content, create_date, modified_date, views FROM POST";
if(!StringUtils.hasText(form.getSearchWord())){
return template.query(sql, postRowMapper());
}
String searchCode = form.getSearchCode();
String searchWord = form.getSearchWord();
SqlParameterSource param = new BeanPropertySqlParameterSource(form);
switch(searchCode){
case "title" : {
sql+= " WHERE LOWER(title) LIKE LOWER(concat('%',:searchWord,'%'))";
}break;
case "content" :{
sql+= " WHERE LOWER(content) LIKE LOWER(concat('%',:searchWord,'%'))";
}break;
case "writerId" :{
sql+= " WHERE LOWER(writerId)=LOWER(:searchWord)";
}break;
case "titleAndContent" :{
sql+= " WHERE LOWER(title) LIKE LOWER(concat('%',:searchWord,'%')) or LOWER(content) LIKE LOWER(concat('%',:searchWord,'%'))";
}break;
}
return template.query(sql, param, postRowMapper());
}
조금 더 복잡한 동적 쿼리
여기서 중요한 것은 아무런 추가 조건이 없을 때는 where도 들어가지 않아야 한다는 점과, and를 붙이는 것, 띄어쓰기이다.
public List<Item> findAll(ItemSearchCond cond) {
String itemName = cond.getItemName();
Integer maxPrice = cond.getMaxPrice();
SqlParameterSource param = new BeanPropertySqlParameterSource(cond);
String sql = "select id, item_name, price, quantity from item";
//동적 쿼리
if (StringUtils.hasText(itemName) || maxPrice != null) {
sql += " where";
}
boolean andFlag = false;
if (StringUtils.hasText(itemName)) {
sql += " item_name like concat('%',:itemName,'%')";
andFlag = true;
}
if (maxPrice != null) {
if (andFlag) {
sql += " and";
}
sql += " price <= :maxPrice";
}
log.info("sql={}", sql);
return template.query(sql, param, itemRowMapper());
}
이 글은 강의를 듣고 JdbcTemplate을 공부한 뒤 개인 사이드 프로젝트에 적용해 보고 정리한 글이다.
JdbcTemplate을 사용한 repository 깃허브 링크
https://github.com/HSRyuuu/my_first_board/blob/master/src/main/java/hello/board/repository/jdbctemplate/JdbcPostRepository.java
(참고) 인프런 - 김영한 님 스프링 DB 2편
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-db-2/dashboard
'Database > 데이터 접근 기술' 카테고리의 다른 글
[SQL/Mysql, MariaDB] 드라이버 연결 (0) | 2023.07.25 |
---|---|
[MyBatis] 동적쿼리, 기타 문법 (0) | 2023.05.15 |
[MyBatis] 마이바티스 기본 사용법 (2) | 2023.05.15 |
[JDBC] 커넥션 풀, DataSource (0) | 2023.05.01 |
[JDBC] 자바 데이터베이스 표준 (0) | 2023.05.01 |