참고
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를 편리하게 호출할 수 있음.
•
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을 처리함
◦
PreparedStatement에 sql과 ‘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
복사