Search
Duplicate
📒

[Database Study] 04-4. JdbcTemplate - 소개와 적용

상태
미진행
수업
Database Study
주제
JDBC
4 more properties
참고

JDBC 기술은 반복을 부른다.

NOTE
앞서 여러 추상화 과정들을 통해 코드를 개선해나가는 작업을 해왔다
그렇지만 아직까지 개선의 여지가 남아있는데 아래의 코드를 보자
@Override public Member findById(String memberId){ String sql = "select * from member where member_id = ?"; PreparedStatement pstmt = null; ResultSet rs = null; try { Connection conn = getConnection(); pstmt = conn.prepareStatement(sql); pstmt.setString(1, memberId); rs = pstmt.executeQuery(); log.info("Connection = {}, class = {}", conn,conn.getClass()); if (rs.next()) { Member member = new Member(); member.setMemberId(rs.getString("member_id")); member.setMoney(rs.getInt("money")); return member; }else{ throw new NoSuchElementException("member not found memberId = " + memberId); } } catch (SQLException e) { log.error("error", e); throw exTranslator.translate("find", sql, e); }finally { JdbcUtils.closeResultSet(rs); JdbcUtils.closeStatement(pstmt); } }
Java
복사
길다..
위 코드를 살펴보면 반복되는 부분이 계속 생기는걸 알수있다
Connection 조회, 동기화
PreparedStatement 생성 및 파라미터 바인딩
쿼리 실행
ResultSet 바인딩
예외 발생 시, 스플이 예외 변환기 실행
리소스 종료
이러한 구문들은 실제 비즈니스 로직 사이에 적용되어있다. 따라서 이것들은 따로 메서드로 빼서 처리할 수 없다. 이런 경우에는 템플릿 콜백 패턴을 이용해서 처리할 수 있다!

JdbcTemplate을 이용한 코드 리팩토링

NOTE
@Override public Member findById(String memberId) { String sql = "select * from member where member_id = ?"; return template.queryForObject(sql, memberRowMapper(), memberId); }
Java
복사
위의 코드를 단축시킨 것
@Slf4j public class MemberRepositoryV5 implements MemberRepository{ private final JdbcTemplate jdbcTemplate; public MemberRepositoryV5(DataSource dataSource) { this.jdbcTemplate = new JdbcTemplate(dataSource); } @Override public Member save(Member member) { String sql = "insert into member(member_id, money) values(?,?)"; jdbcTemplate.update(sql,member.getMemberId(), member.getMoney()); return member; } @Override public Member findById(String memberId){ String sql = "select * from member where member_id = ?"; return jdbcTemplate.queryForObject(sql, memberRowMapper(), memberId); } @Override public void update(String memberId, int money){ String sql = "update member set money = ? where member_id = ?"; jdbcTemplate.update(sql, memberId, money); } @Override public void delete(String memberId){ String sql = "delete from member where member_id=?"; jdbcTemplate.update(sql, memberId); } private RowMapper<Member> memberRowMapper() { return (rs, rowNum) -> { Member member = new Member(); member.setMemberId(rs.getString("member_id")); member.setMoney(rs.getInt("money")); return member; }; } }
Java
복사
전체 코드
Jdbc템플릿을 사용하면 불필요한 코드를 제거해준다

정리

NOTE
JdbcTemplate트랜잭션을 위한 Connection 동기화, 예외 발생시 스프링 예외 변환기도 자동으로 실행해준다
Service 계층의 순수성
트랜잭션 추상화 + 트랜잭션 AOP 덕분에 Service 계층의 순수성을 최대한 유지하면서 Service 계층에서 트랜잭션을 사용할 수 있다
스프링에 제공하는 예외 추상화 + 예외 변환기 덕분에, DB접근 기술이 변경되어도 Service 계층의 순수성을 유지하면서, 예외도 사용
서비스 계층이 Repository 인터페이스에 의존한 덕분에 향후 다른 기술로 변경되어도 Service계층은 유지할 수 있다
Repository에서 JDBC를 사용하는 반복 코드가 JdbcTemplate에 의해 제거되었다.

JdbcTemplate 소개와 설정

NOTE
JdbcTemplate는 JDBC를 매우 편리하게 사용할 수 있게 도와준다!
템플릿 콜백 패턴을 사용해서, JDBC를 직접 사용할 떄 발생하는 대부분의 반복작업을 대신 처리해준다.
connection 획득
statement를 준비하고 실행
결과를 반복하도록 루프를 실행
connection 종료, statement, resultset 종료
트랜잭션 다루기 위한 connection 동기화
예외 발생시 스프링 예외 변환기 실행
개발자는 SQL을 작성하고 전달할 파라미터를 정의하고, 응답 값을 매핑하기만 하면 된다.
동적 SQL을 해결하기 어려운 단점이 존재. (MyBatis에서 해결됨!)

주요기능

JdbcTemplate
순서 기반 파라미터 바인딩을 지원한다.
NamedParameterJdbcTemplate
이름 기반 파라미터 바인딩을 지원한다. (권장)
SimpleJdbcInsert
INSERT SQL을 편리하게 사용할 수 있다.
SimpleJdbcCall
Stored Proceduer를 편리하게 호출할 수 있음.
각종 메뉴얼
Stored Procedure
JdbcTemplate 사용 방법

JdbcTemplate 적용

NOJTE
JdbcTemplate를 적용해서 사용해보자!
public class JdbcTemplateItemRepositoryV1 implements ItemRepository { private final JdbcTemplate template; //데이터소스(dataSource)가 필요하다. public JdbcTemplateItemRepositoryV1(DataSource dataSource) { this.template = new JdbcTemplate(dataSource); } // ...
Java
복사

JdbcTemplate - update()

NOTE
update(sql, new Object[] {,,});
Java
복사
값은 sql 물음표값 즉, 바인딩 변수에 설정될 값을 넣어준다.
@Override public Item save(Item item) { String sql = "insert into item(item_name, price, quantity) values (?,?,?)"; // DB에서 생성해준 ID값을 가져오는 방법 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); // DB에 들어간 Key 값을 KeyHolder가 가지고 있고, 이 값을 KeyHolder가 Return 해준다. long key = keyHolder.getKey().longValue(); item.setId(key); return item; }
Java
복사
JdbcTemplate save() 코드
@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); }
Java
복사
JdbcTemplate update() 코드
데이터를 변경할 떄 사용한다
INSERT, UPDATE, DELETE SQL문에서 사용한다
template.update()의 반환 값은 int인데, 영향 받은 row수를 반환한다.

KeyHolder

현재 PK값 관리 전략은 Identity이다
Db에 데이터가 저장되는 순간, DB에서 PK값을 설정해준다. 따라서 Query에는 Id값을 넣지 않는다
KeyHolder는 DB에 생성된 ID값을 가지고 있는다
Identity 전략을 사용할 때는 위 형식으로 JdbcTemplate를 사용한다
익명함수로 connection을 처리함
PreparedStatementsql ‘id’값을 넘겨준 후, 파라미터에 바인딩 한다.

JdbcTemplate - queryForObject(), query()

NOTE
query(String sql, RowMapper<T> rowMapper, @Nullable Object... args);
Java
복사
sql, rowMapper, 바인딩 변수
데이터를 조회할 떄 사용한다
결과 개수에 따라 사용하는 함수가 달라진다
결과 row가 하나다queryForObject()
결과 row가 여러개다query()
RowMapper()는 데이터베이스 반환 결과인 ResultSet을 객체로 반환한다.
private RowMapper<Item> itemRowMpaaer() { 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; }; }
Java
복사
전통 방식의 ResultSet을 처리하던 while문을 해주는 코드
@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, itemRowMpaaer(), id); return Optional.of(item); } catch (EmptyResultDataAccessException e) { return Optional.empty(); } }
Java
복사
JdbcTemplate queryForObject() 코드 조회 결과가 null일수도 있으므로 Optional사용
@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, itemRowMpaaer(), param.toArray()); }
Java
복사
원래 간단한 쿼리인데, 동적쿼리 추가떄문에 복잡해짐.. JdbcTemplate query() 코드

동적 쿼리 문제

NOTE
위의 findAll()에서 코드가 매우 길어진 이유는, 검색하는 값에 따라 SQL이 동적으로 달라져야 하는 점 떄문이다.
상품명(itemName)과 최대 가격(maxPrice)를 고려해서 검색할 경우, 사용 여부에 따라 4가지 경우가 생긴다.

데이터베이스 접근 설정

NOTE
dataSource, Connection Pool, 트랜잭션 매니저는 이미 스프링 부트에서 지원하고 있지만 사용하기 위해선 코드를 등록해야 한다
spring.profiles.active=local spring.datasource.url=jdbc:h2:tcp://localhost/~/test spring.datasource.username=sa
Java
복사