JdbcTemplate 소개와 설정
SQL을 직접 사용하는 경우에 스프링이 제공하는 JdbcTemplate은 아주 좋은 선택지다.
JdbcTemplate 은 JDBC를 매우 편리하게 사용할 수 있게 도와준다
(이전에 DB1 마지막편에서 리포지토리의 반복되는코드를 스프링의 템플릿을 도입해서 해결했다)
장점
설정의 편리함
JdbcTemplate은 spring-jdbc 라이브러리에 포함되어 있는데,
이 라이브러리는 스프링으로 JDBC를 사용할 때 기본으로 사용되는 라이브러리이다.
그리고 별도의 복잡한 설정 없이 바로 사용할 수 있다.
반복 문제 해결
JdbcTemplate은 템플릿 콜백 패턴을 사용해서,
JDBC를 직접 사용할 때 발생하는 대부분의 반복 작업을 대신 처리해준다.
개발자는 SQL을 작성하고, 전달할 파리미터를 정의하고, 응답 값을 매핑하기만 하면 된다.
우리가 생각할 수 있는 대부분의 반복 작업을 대신 처리해준다.
(커넥션 획득,
statement를 준비하고 실행,
결과를 반복하도록 루프를 실행,
커넥션 종료, statement , resultset 종료,
트랜잭션 다루기 위한 커넥션 동기화 ,
예외 발생시 스프링 예외 변환기 실행)
단점
동적 SQL을 해결하기 어렵다.
이제 직접 JdbcTemplate을 설정하고 적용하면서 이해해보자
build.gradle
//JdbcTemplate 추가
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
//H2 데이터베이스 추가
runtimeOnly 'com.h2database:h2'
org.springframework.boot:spring-boot-starter-jdbc 를 추가하면
JdbcTemplate이 들어있는 spring-jdbc 가 라이브러리에 포함된다.
(JdbcTemplate은 spring-jdbc 라이브러리만 추가하면 된다. 별도의 추가 설정 과정은 없다.)
여기서는 H2 데이터베이스에 접속해야 하기 때문에 H2 데이터베이스의 클라이언트 라이브러리(Jdbc Driver)도 추가하자.
JdbcTemplate 적용1
JdbcTemplate을 사용해서 메모리에 저장하던(Map에 저장하던) 데이터를 데이터베이스에 저장해보자.
ItemRepository 인터페이스가 있으니 이 인터페이스를 기반으로 JdbcTemplate을 사용하는 새로운 구현체를 개발하자
/**
* JdbcTemplate
*/
@Slf4j
@Repository
public class JdbcTemplateItemRepositoryV1 implements ItemRepository {
private final JdbcTemplate template;
public JdbcTemplateItemRepositoryV1(DataSource dataSource) {
// JdbcTemplate는 생성할떄 DataSource가 필요하다. 내부에서 커넥션만들고, 등등의 반복작업을 위해 DataSource가 필요하기 때문
this.template = new JdbcTemplate(dataSource);
}
@Override
public Item save(Item item) {
String sql = "insert into item(item_name ,price,quantity) values (?,?,?)";
KeyHolder keyHolder = new GeneratedKeyHolder(); // DB에서 직접 생성해준 키값을 받아오기 위해 필요한 keyHolder
template.update(connection -> {
//자동 증가 key (이런게 있구나하고 넘어간다)
PreparedStatement ps = connection.prepareStatement(sql,
new String[]{"id"}); //sql과 동시에 키의 컬럼명도 넣어준다.
ps.setString(1, item.getItemName());
ps.setInt(2, item.getPrice());
ps.setInt(3, item.getQuantity());
return ps;
}, keyHolder);
long key = keyHolder.getKey().longValue(); //그다음 keyHolder를 통해 생성된 키값을 꺼내온다.
item.setId(key);
return item;
}
@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);
}
@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); // queryForObject는 조회하는게 하나일때사용
// select의 결과를 item객체에 매핑하기위한 매퍼관련 코드(함수)가 필요하다.
//queryForObject()에서 없는 데이터에 대해 접근하려고하면 EmptyResultDataAccessException가 발생한다.
return Optional.of(item); // 값이 있으면 실행되는부분, Optional객체에 item객체를 담아서 반환
} catch (EmptyResultDataAccessException e) {
return Optional.empty(); // 비어있는 Optional객체를 반환
}
}
private RowMapper<Item> itemRowMapper() { //jdbcTemplate를 사용할때 resultSet을 매핑하기 위해 필요한 로우매퍼
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;
})
}
@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"; //이름조건이 있거나 가격조건이 있다면 where절을 추가해준다.
}
boolean andFlag = false; // 앞의 조건도 존재해서 and를 추가해줘야하는지 말아야하는지 체크용
List<Object> param = new ArrayList<>();
if (StringUtils.hasText(itemName)) {
sql += " item_name like concat('%',?,'%')";
// concat(%?%)를 추가해서 아이템이름 조건이 들어간 아이템을 찾기위함
param.add(itemName); // ?가 들어갔으므로 해당 파라미터를 넣어주기 위해 리스트에 값 저장
andFlag = true; // 조건 들어갔으므로 and체크용을 true로 변환
}
if (maxPrice != null) {
if (andFlag) {
sql += " and";
}
sql += " price <= ?";
param.add(maxPrice);
}
log.info("sql={}", sql);
return template.query(sql, itemRowMapper(),param.toArray()); // 조회하는게 여러개일때는 query()를 이용;
//Object배열을 파라미터로 같이 넘겨준다.
}
}
private final JdbcTemplate template;
public JdbcTemplateItemRepositoryV1(DataSource dataSource) {
// JdbcTemplate는 생성할떄 DataSource가 필요하다. 내부에서 커넥션만들고, 등등의 반복작업을 위해 DataSource가 필요하기 때문
this.template = new JdbcTemplate(dataSource);
}
JdbcTemplate 은 데이터소스( dataSource )가 필요하다.
JdbcTemplateItemRepositoryV1() 생성자를 보면 dataSource 를 의존 관계 주입 받고
생성자 내부에서 JdbcTemplate 을 생성한다.
스프링에서는 JdbcTemplate 을 사용할 때 관례상 이 방법을 많이 사용한다.
물론 JdbcTemplate 을 스프링 빈으로 직접 등록하고 주입받아도 된다.
@Override
public Item save(Item item) {
String sql = "insert into item(item_name ,price,quantity) values (?,?,?)";
KeyHolder keyHolder = new GeneratedKeyHolder(); // DB에서 직접 생성해준 키값을 받아오기 위해 필요한 keyHolder
template.update(connection -> {
//자동 증가 key (이런게 있구나하고 넘어간다)
PreparedStatement ps = connection.prepareStatement(sql,
new String[]{"id"}); //sq과 동시에 키의 컬럼명도 넣어준다.
ps.setString(1, item.getItemName());
ps.setInt(2, item.getPrice());
ps.setInt(3, item.getQuantity());
return ps;
}, keyHolder);
long key = keyHolder.getKey().longValue(); //그다음 keyHolder를 통해 생성된 키값을 꺼내온다.
item.setId(key);
return item;
}
template.update() : 데이터를 변경할 때는 update() 를 사용하면 된다.
INSERT , UPDATE , DELETE SQL에 사용한다.
template.update() 의 반환 값은 int 인데, 영향 받은 로우 수를 반환한다
데이터를 저장할 때 PK 생성에 identity (auto increment) 방식을 사용하기 때문에,
PK인 ID 값을 개발자가 직접 지정하는 것이 아니라 비워두고 저장해야 한다.
그러면 데이터베이스가 PK인 ID를 대신 생성해준다.
문제는 이렇게 데이터베이스가 대신 생성해주는 PK ID 값은 데이터베이스가 생성하기 때문에,
데이터베이스에 INSERT가 완료 되어야 생성된 PK ID 값을 확인할 수 있다.
KeyHolder 와 connection.prepareStatement(sql, new String[]{"id"}) 를 사용해서 id 를 지정해주면
INSERT 쿼리 실행 이후에 데이터베이스에서 생성된 ID 값을 조회할 수 있다.
물론 데이터베이스에서 생성된 ID 값을 조회하는 것은 순수 JDBC로도 가능하지만, 코드가 훨씬 더 복잡하다.
JdbcTemplate이 제공하는 SimpleJdbcInsert 라는 훨씬 편리한 기능이 있으므로
대략 이렇게 사용한다 정도로만 알아두면 된다.
@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);
}
데이터를 변경할 때는 update() 를 사용하면 된다.
? 에 바인딩할 파라미터를 순서대로 전달하면 된다.
update의 파라미터를 보면 Obejct... args로 되어있다.
즉 가변인자를 받는다는것이므로 한개를 넣어도되고 여러개를 넣어도된다.
@Nullable까지 있으므로 파라미터를 넘기지않아도 된다.
반환 값은 해당 쿼리의 영향을 받은 로우 수 이다.
여기서는 where id=? 를 지정했기 때문에 영향 받은 로우수는 최대 1개이다.
@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); // queryForObject는 조회하는게 하나일때사용
// select의 결과를 item객체에 매핑하기위한 매퍼관련 코드(함수)가 필요하다.
//queryForObject()에서 없는 데이터에 대해 접근하려고하면 EmptyResultDataAccessException가 발생한다.
return Optional.of(item); // 값이 있으면 실행되는부분, Optional객체에 item객체를 담아서 반환
} catch (EmptyResultDataAccessException e) {
return Optional.empty(); // 비어있는 Optional객체를 반환
}
}
private RowMapper<Item> itemRowMapper() { //jdbcTemplate를 사용할때 resultSet을 매핑하기 위해 필요한 로우매퍼
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;
})
}
template.queryForObject() --> 결과 로우가 하나일 때 사용한다
RowMapper 는 데이터베이스의 반환 결과인 ResultSet을 객체로 변환한다.
JDBC를 직접 사용할 때 ResultSet 를 사용했던 부분을 떠올리면 된다.
if (rs.next()) {
Member member = new Member();// sql 결과를 매핑해줄 객체 생성
member.setMemberId(rs.getString("member_id")); //ResultSet을 이용해서 sql결과를 조회할 수 있다.
member.setMoney(rs.getInt("money"));
return member;
}
결과로우가 하나일때 다음과 같이 ResultSet의 커서를 이용해서 값을 객체에 매핑했었다.
차이가 있다면 다음과 같이 JdbcTemplate이 다음과 같은 루프를 돌려주고,
while (rs.next() == resultSet이 끝날때까지) {
rowMapper(rs,rowNum);
}
개발자는 RowMapper 를 구현해서 그 내부 코드(ResultSet을 이용해서 객체를 매핑하는 코드)만 채운다고 이해하면 된다.
queryForObject()의 결과가 없으면 EmptyResultDataAccessException 예외가 발생한다.
결과가 둘 이상이면 IncorrectResultSizeDataAccessException 예외가 발생한다
(queryForObject()를 사용했으므로 결과 로우가 하나여야만함)
ItemRepository.findById() 인터페이스는 결과가 없을 때 Optional 을 반환해야 한다.
따라서 결과가 없으면 예외를 잡아서 Optional.empty 를 대신 반환하면 된다.
@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"; //이름조건이 있거나 가격조건이 있다면 where절을 추가해준다.
}
boolean andFlag = false; // 앞의 조건도 존재해서 and를 추가해줘야하는지 말아야하는지 체크용
List<Object> param = new ArrayList<>();
if (StringUtils.hasText(itemName)) {
sql += " item_name like concat('%',?,'%')";
// concat(%?%)를 추가해서 아이템이름 조건이 들어간 아이템을 찾기위함
param.add(itemName); // ?가 들어갔으므로 해당 파라미터를 넣어주기 위해 리스트에 값 저장
andFlag = true; // 조건 들어갔으므로 and체크용을 true로 변환
}
if (maxPrice != null) {
if (andFlag) {
sql += " and";
}
sql += " price <= ?";
param.add(maxPrice);
}
log.info("sql={}", sql);
return template.query(sql, itemRowMapper(),param.toArray()); // 조회하는게 여러개일때는 query()를 이용;
//Object배열을 파라미터로 같이 넘겨준다.
}
template.query() 결과가 하나 이상일 때 사용한다.
RowMapper 는 데이터베이스의 반환 결과인 ResultSet 을 객체로 변환한다.
결과가 없으면 빈 컬렉션을 반환한다.
return template.query(sql, itemRowMapper(),param.toArray()); // 조회하는게 여러개일때는 query()를 이용;
//Object배열을 파라미터로 같이 넘겨준다.
파라미터가 가변인자로 설정되어있을때 배열을 파라미터로 받을 수 있는것 같다.
4가지 상황에 따른 SQL을 동적으로 생성해야 한다. (검색조건이 2개일때,각각 1개씩일때,검색조건이 없을때)
동적 쿼리가 언듯 보면 쉬워 보이지만, 막상 개발해보면 생각보다 다양한 상황을 고민해야 한다.
예를 들어서 어떤 경우에는 where 를 앞에 넣고 어떤 경우에는 and 를 넣어야 하는지 등을 모두 계산해야 한다
그리고 각 상황에 맞추어 파라미터도 생성해야 한다.
물론 실무에서는 이보다 훨씬 더 복잡한 동적 쿼리들이 사용된다.
참고로 이후에 설명할 MyBatis의 가장 큰 장점은 SQL을 직접 사용할 때 동적 쿼리를 쉽게 작성할 수 있다는 점이다.
실행전 설정
수동빈등록,설정파일등록
jdbcTemplate를 사용하는 구현체를 빈으로 등록해야한다.
새로운 설정파일을 만들고, Application에서 @import를 이용해서 해당 설정파일을 등록한다.
@Configuration
@RequiredArgsConstructor
public class JdbcTemplateV1Config {
private final DataSource dataSource;
@Bean
public ItemService itemService() {
return new ItemServiceV1(itemRepository());
}
@Bean
public ItemRepository itemRepository() {
return new JdbcTemplateItemRepositoryV1(dataSource);
}
}
@Import(JdbcTemplateV1Config.class)
@SpringBootApplication(scanBasePackages = "hello.itemservice.web")
public class ItemServiceApplication {
데이터베이스 접근 설정
application.properties
spring.profiles.active=local
spring.datasource.url=jdbc:h2:tcp://localhost/~/practiceDB2
spring.datasource.username=sa
spring.datasource.password=123
이렇게 설정만 하면 스프링 부트가 해당 설정을 사용해서
커넥션 풀과 DataSource , 트랜잭션 매니저를 스프링 빈으로 자동 등록한다
실행후 테스트
상품을 추가해보면
h2 데이터베이스에 해당 데이터가 잘 저장되는것을 확인할 수 있다.
(itemA,B는 스프링프로필이 local일때 자동으로 들어가게 설정해둔 테스트 데이터이다.)
(참고로 서버를 다시 시작할 때 마다 TestDataInit 이 실행되기 때문에 itemA , itemB 도 데이터베이스에 계속 추가된다. 메모리와 다르게 서버가 내려가도 데이터베이스는 유지되기 때문이다.)
SQL 로그 확인
JdbcTemplate이 실행하는 SQL 로그를 확인하려면 application.properties 에 다음을 추가하면 된다.
main , test 설정이 분리되어 있기 때문에 둘다 확인하려면 두 곳에 모두 추가해야 한다
logging.level.org.springframework.jdbc=debug
'인프런 > 스프링 DB 2편' 카테고리의 다른 글
6) 데이터 접근기술 - JPA (0) | 2024.02.08 |
---|---|
5)데이터 접근 기술 - MyBatis (0) | 2023.03.08 |
4) 데이터 접근 기술 - 테스트 (0) | 2023.03.07 |
3) 데이터 접근 기술 - 스프링 JdbcTemplate (2) (0) | 2023.03.07 |
1) 데이터 접근 기술 - 시작 (0) | 2023.03.06 |
댓글