
Spring을 공부하게 되면서 데이터베이스에 연결시켜야하는 일이 생겼다. 이때 Spring 에서의 JDBC api의 활용을 도와주는 JdbcTemplate을 학습해보고자 한다.
또한 jdbcTemplate을 점점더 사용하면서 알게된 정보드를 추가적으로 업데이트 할 예정이다.
과거에 JDBC API를 직접 사용할 땐 다음과 같은 절차가 필요했다:
이러한 방식은 반복적이며 리소스 관리가 불편하고 실수를 유발할 수 있다.
JdbcTemplate를 사용하면 리소스 획득, 연결 관리, 예외 처리 등 과 같은 작업은 고려하지 않고 쿼리와 응답 처리만 고민할 수 있게 도와준다.
기본적으로 DataSource를 사용하여 연결을 사용한다. 이때 DataSource는 기존에 연결하던 방식인 DriverManager의 대체제이다. Connection을 통해서 연결을 가능하게 해주는 인터페이스이다.
Spring에서 기본으로 사용하는 구현체는 HiKariDataSource이다. 이 구현체는 성능이 가장 좋으며 , Connection pool을 제공한다. connection 비용은 비싸기에 미리 여러개를 빌려둔다음에 반납하는 구조이다.
Jdbc를 사용하기 위해서는 앞서 몇가지 설정을 해주어야한다.
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
jdbc 의존성을 추가하게 된다면 사용가능하다.
의존성을 추가해주게 되며 자연스럽게 DataSource Bean객체와 JdbcTemplate Bean 객체를 Spring에서 등록해주게 된다.
따라서 필요시 컨트롤러 생성자에서 주입을 해주거나 필드에서 @AutoWired을 통해 주입을 해주면 사용할 수 있다.
나는 이번 공부에서 연결한 DB는 H2 인메모리 DB이다.
application.properties에 다음과 같은 속성을 적용해주어야 한다.
spring.h2.console.enabled=true
// 브라우저에서 localhost:8080/h2-console을 사용하겠다는 의미
spring.datasource.url=jdbc:h2:mem:test
//spring의 datasource 에 대한 연결을 할 주소를 설정하는 과정
보이는 것 과 같이 앞서 설명한 DataSource가 Connection을 사용하여 연결할 주소를 설정해주는 모습이다.
JdbcTemplate을 활용하여 이제 DB에 접근해보자. 제공해주는 메서드를 활용한다면 기존에 사용하던 jdbc Api 보다 더 편리한 기능을 제공받을 수 있다.
query 와 관련된 메서드는 SELECT를 사용하기 위해 사용한다.
queryForObject()반환 값이 오직 하나일 경우 - 단일 객체 조회
쿼리문을 실행 후 반환하는 값이 단일일 경우에 사용하는 메서드이다.
만약 해당 객체가 없다면 EmptyResultDataAccessException를 발생시킨다.

위의 사진은 라이브러리에 정의되어 있는queryForObject인데 반환값이 단일 객체를 반환하도록 되어있다.
queryForObject 활용(1)
int rowCount = this.jdbcTemplate.queryForObject("select count(*) from t_actor", Integer.class);
queryForObject 활용(2)
int countOfActorsNamedJoe = this.jdbcTemplate.queryForObject(
"select count(*) from t_actor where first_name = ?", Integer.class, "Joe");
Object 배열안에 넣어주어도 차례로 바인딩 된다.queryForObject 활용(3)
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);
RowMapper select 결과로 first_name과 last_name의 값이 나오게 되는데 이를 RowMapper를 통해 Actor 객체로 만들어주는 과정이다.
해당 람다식 안에서 반환되는 newActor를 그대로 queryForObject 반환값으로 사용한다. 따라서 select 를 통해서 2가지 칼럼값이 나왔지만 단일 객체로 반환하였기에 사용이 가능하다.
RowMapper에 관해서는 아래에서 더 자세히 다뤄보겠다.
칼럼이 하나일 때는 Spring이 ResultSet에서 그 단일 값을 자동으로 꺼내주는 내부 로직이 있어서 RowMapper 없이 바로 queryForObject(..., String.class) 처리가 가능하다.
하지만 칼럼이 두 개 이상이면 어떤 필드에 어떤 값을 넣어야 할지 모르기 때문에 반드시 **개발자가 직접 매핑 방식(RowMapper)을 알려줘야 한다.
int rowCount = jdbcTemplate.queryForObject("select count(*) from customers", Integer.class);
String lastName = jdbcTemplate.queryForObject("select last_name from customers where id = ?", String.class, id);
이 경우는 학습을 위해 억지로 시도해본 경우이지만, 실제로 이런 방식으로 활용할 일은 없을 것 같다.
public String getLastName(Long id) {
//TODO : 주어진 Id에 해당하는 customers의 lastName을 반환
String sql = "SELECT last_name,first_name from customers WHERE id = ?";
String lastName = jdbcTemplate.queryForObject(sql,(rs,rowNum)->{return rs.getString("last_name");}, id);
return lastName;
}
Customer customer = jdbcTemplate.queryForObject( "select id, first_name, last_name from customers where id = ?", (resultSet, rowNum) -> { Customer customer = new Customer( resultSet.getLong("id"), resultSet.getString("first_name"), resultSet.getString("last_name") ); return customer; }, id);
query()0개 이상의 값들이 조회를 통해 나올 것을 기대하고 사용하는 메서드이다.
따라서 기존 queryForObject(sql,Stirng.class) 와 같은 단일 값 처리에 관해서는 사용이 불가능하다.

위의 사진과 같이 List 의 반환값을 보여주고 있다. 해당 리스트 안에서 0개이상의 값들을 넣어 반환하도록 사용할 수 있다.
query() 활용(1)
List<Actor> actors = this.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;
});```
* 첫번째 값으로 sql 문을 넣어준다.
* 두번째 값으로는 `RowMapper`를 넣어주어 받은 값을 활용할 수 있다.
**query() 활용(2)**
```java
private final RowMapper<Customer> rowMapper = (rs, rowNum) -> {
Customer customer = new Customer(rs.getLong("id"), rs.getString("first_name"), rs.getString("last_name"));
return customer;
};
public List<Customer> findCustomerByFirstName(String firstName) {
String sql = "select id, first_name, last_name from customers where first_name = ?";
//TODO : firstName을 기준으로 customer를 list형태로 반환
List<Customer> customers = jdbcTemplate.query(sql, rowMapper, firstName);
return customers;
}
RowMapper를 따로 재정의하여 사용할 수 있다.RowMapper는 ResultSet 의 값에 있는 데이터의 매핑을 도와주는 인터페이스이다.
쿼리문이 실행된다면 반환값으로 ResultSet에 값을 넣어서 데이터를 받아오게 된다.
이때 해당 jdbcTemplate이 ResultSet 값에서 결과를 한줄 씩 RowMapper에게 넘겨주게 된다.
RowMapper는 mapRow()메서드를 사용하며 데이터를 매핑하여 원하는 값으로 가공하는 기능을 제공한다.
mapRow()는 인자를 두가지 받아 작동하는데, 데이터 값이 들어있는 resultSet 과 해당이 몇번째 행인지 rowNum 이라는 값을 받게 된다. rowNum을 활용해 현재 매핑하고 있는 데이터가 몇번째인지에 대한 정보를 가지고 특정 상황의 분기점에서 활용할 수 있다.
정리해서, 기존 jdbc에서 while(resultSet.next())을 하며 resultSet.getString()을 하던 과정의 부분을 담당하는 인터페이스라고 생각하면 된다.
쿼리문 중 insert,delete,update를 하는 과정에서 사용된다.
update()jdbcTemplate.update()를 통해서 값을 넣거나 삭제,혹은 수정을 할 수 있다.
모두 반환 값으로 해당 쿼리문으로 값이 변경된 테이블 행의 개수가 반환된다.
update() 활용(1)
jdbcTemplate.update(
"insert into t_actor (first_name, last_name) values (?, ?)",
"Leonor", "Watling");
쿼리의 insert문을 실행하게 된다면 DB에서 생성하여 넣어주는 값인 primary key인 Id 값이 필요한 경우가 있다.
이때 활용가능한 것이 KeyHolder이다. KeyHolder는 인터페이스인데 이때 주로 사용하는 구현체가 GeneratedKeyHolder 이다.
KeyHolder keyHolder = new GeneratedKeyHolder();
사용가능한 메서드로써는 다음과 같다.
| 메서드 이름 | 반환 타입 | 설명 |
|---|---|---|
getKey() | Number | 첫 번째 키의 값만 단독으로 반환 주로 단일 키 (예: id)만 있는 경우 사용 |
getKeyAs(Class<T>) | T (제네릭) | getKey()의 타입 안전 버전 예: getKeyAs(Long.class) |
getKeys() | Map<String, Object> | 첫 번째 row의 키-값 전체 map 반환 |
getKeyList() | List<Map<String, Object>> | 자동 생성 키들의 모든 row를 리스트로 반환 보통은 1개 row지만 여러 row도 가능 |
이제 공부한 KeyHolder를 사용해보자면
update() 활용(2)
jdbcTemplate.update(connection->{
PreparedStatement preparedStatement = connection.prepareStatement(sql,new String[]{"id"});
preparedStatement.setString(1, customer.getFirstName());
preparedStatement.setString(2,customer.getLastName());
return preparedStatement;
},keyHolder);
PreparedStatementConstructer가 필요하다.KeyHolder 객체를 넣어주면 된다.PreparedStatementConstructer 의 구현체를 뜯어보면
PreparedStatementCreator preparedStatementCreator = new PreparedStatementCreator() {
@Override
public PreparedStatement createPreparedStatement(Connection con) throws SQLException {
return null;
}
};
createPreparedStateMent() 메서드를 구현해야한다.
이제 이 구현을 람다식을 통해서 update문 안에서 바로 정의하고 사용해보자.
바로 위에 있는 코드 update() 활용(2) 의 코드에서
connection->{
PreparedStatement preparedStatement = connection.prepareStatement(sql,new String[]{"id"});
preparedStatement.setString(1, customer.getFirstName());
preparedStatement.setString(2,customer.getLastName());
return preparedStatement;
}
PreparedStatement를 connection을 통하여 생성하고 이때 값을 직접 주입해주게 된다.
prepareStatement() 메서드 안에는 2가지 인자가 필요한데
prepareStatement()안에 인자를 넣어주고 반환하게 된다면,
jdbcTemplate.update()가 DB가 다시 반납해준 값을 keyHolder에 넣어주게 된다.
현재는 id 값 하나를 넣어주라고 부탁하였기 때문에 keyHolder에는 값 하나만 존재할 것이다.
따라서 keyHolder.getKey().longValue() 를 한다면 방금 가져온 자동생성된 값을 가져올 수 있다.
만약 prepareStatement(sql,new String[]{"id","id2"}) 와 같이 2개의 key값에 대한 요구를 한다면 keyHolder에 2개의 값이 저장되어 들어올 것이다. 이때 입력한 "id","id2"는 db에서 생성해주는 auotoGenerated 값이어야한다. 만약 값이 db에서 생성해주는 값이 아니라면 Null 이 들어있던가 특정 DB에서 예외가 터지게 된다.
다시 돌아와 만약 값 2개가 keyHolder에 잘 들어와 있다면
Map<String, Object> keys = keyHolder.getKeys(); // 내부적으로는 첫 번째 row의 keyMap
Long id = ((Number) keys.get("id")).longValue();
Long id2 = ((Number) keys.get("id2")).longValue();
을 통해서 들어있는 값을 가져올 수 있다. 값이 하나라면 이 부분들이 생략되어
keyHolder.getKey().longValue()
처럼 사용해주면 된다.
batchUpdate()여러 값에 따른 여러 쿼리를 저장해두었다 한번에 모두 실행시키고 싶다면 사용한다.
batchUpdate 활용(1)
List<Object[]> splitUpNames = Arrays.asList("John Woo", "Jeff Dean", "Josh Bloch", "Josh Long")
.stream()
.map(name -> name.split(" "))
.collect(Collectors.toList());
jdbcTemplate.batchUpdate("INSERT INTO customers(first_name, last_name) VALUES (?,?)", splitUpNames);
List<Object[]> 값이 들어가 List를 순회하며 리스트 안에 있는 각 배열의 값을 차례로 쿼리 문에 바인딩하여 실행한다.주로 단순한 CREATE,DROP을 sql문을 실행하기 위해 사용한다.
ConnectionCallback, PreparedStatementCallback 등을 직접 구현할 수 있다고 한다.
트랜잭션 경계 내에서 연결을 얻거나 세부 컨트롤 가능하다고 하는데 이는 추후에 공부하며 추가하겠다.
jdbcTemplate.execute("DROP TABLE customers IF EXISTS");
JdbcTemplate은 JDBC 반복 작업을 줄여주는 스프링의 헬퍼 클래스queryForObject()는 단일 row 반환, query()는 리스트 반환RowMapper는 DB 결과(ResultSet)를 Java 객체로 매핑해주는 전략update()는 INSERT/UPDATE/DELETE 시 사용KeyHolder를 사용하면 자동 생성된 키(id 등)를 받을 수 있음
오 진짜 좋은데요???