지금까지 데이터베이스를 조작하기 위해 계속 무언가를 만들었는데, 사실 스프링에서는 이러한 것들을 편리하게 템플릿으로 이미 제공해준다.
일단 JdbcTemplate은 생성자로 dataSource를 받기 때문에 멤버로 JdbcTemplate을 만들어주고, 생성자에서 dataSource를 넣어서 만들어준다.
public UserDao(
DataSource dataSource) {
this.dataSource = dataSource;
this.jdbcContext = new JdbcContext(dataSource);
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
그리고는 다음과 같이 3가지 방법 중 하나로 쿼리문을 작성하면 되는데
public void deleteAll(){
this.jdbcTemplate.update("delete from users");
this.jdbcTemplate.update(con -> con.prepareStatement("delete from users"));
this.jdbcTemplate.update(new PreparedStatementCreator() {
@Override
public PreparedStatement createPreparedStatement(Connection con) throws SQLException {
return con.prepareStatement("delete from users");
}
});
}
아무리봐도 가장 위가 단순해보인다.
add()에 관한 메서드도 기존
this.jdbcContext.workWithStatementStrategy(c -> {
PreparedStatement ps = c.prepareStatement("insert into users(id, name, password) values(?, ?, ?)");
ps.setString(1, user.getId());
ps.setString(2, user.getName());
ps.setString(3, user.getPassword());
return ps;
});
방법에서
this.jdbcTemplate.update(
"insert into users(id, name, password) values(?, ?, ?)",
user.getId(), user.getName(), user.getPassword()
);
이렇게 더 간단하게 사용이 가능하다.
이렇게 Create, Update, Delete와 같이 데이터베이스의 정보가 변경되는 쿼리는 update를 통해 작성해주면 된다.
이제는 정보를 가져오는 쿼리를 작성해보자.
getCount()는 유저의 수를 가져오는 메서드였다.
수를 가져오는 쿼리는 간단하게 queryForObject()를 사용한다.
쿼리를 작성하고, 원하는 타입을 지정해주면 된다.
public int getCount() {
Integer result = jdbcTemplate.queryForObject("select count(*) from users", Integer.class);
return result == null ? 0 : result;
}
이 방법으로 user의 정보 또한 읽어올 수 있다.
public User get(String id){
return jdbcTemplate.queryForObject(
"select * from users where id = ?", (rs, rowNum) -> new User(
rs.getString("id"),
rs.getString("name"),
rs.getString("password")
), id);
}
엄청 간단해진 get() 메서드이다.
RowMapper를 상속받아서 mapRow() 메서드를 구현해야 하지만, 이 부분도 lambda로 간단하게 작성했다.
대신 queryForObject는 하나의 row만 가져오기 때문에, 해당하는 row가 없는 경우에도 에러가 발생한다.
그 때 발생하는 에러가 EmptyResultDataAccessException이기 때문에, 해당 에러를 핸들링하며 개발하면 된다.
다음 row를 가져오고 싶다면 rs.next()를 호출해가며 다음 값을 읽어오면 된다.
그러면 리스트로 user들을 조회하는 메서드를 만들어보자.
일단 TDD 느낌으로 테스트를 먼저 만들어보면
@Test
public void getAll() throws SQLException {
User user1 = new User("user1", "user1", "pw1");
User user2 = new User("user2", "user2", "pw2");
userDao.deleteAll();
userDao.add(user1);
List<User> users1 = userDao.getAll();
Assertions.assertEquals(1, users1.size());
Assertions.assertEquals(user1, users1.get(0));
userDao.add(user2);
List<User> users2 = userDao.getAll();
Assertions.assertEquals(2, users2.size());
Assertions.assertEquals(user1, users2.get(0));
Assertions.assertEquals(user2, users2.get(1));
}
그리고는 getAll() 메서드를 다음과 같이 작성해준다.
public List<User> getAll() {
return this.jdbcTemplate.query(
"SELECT * FROM users ORDER BY id",
(rs, rowNum) ->
new User(rs.getString("id"), rs.getString("name"), rs.getString("password"))
);
}
query() 메서드를 사용해 값을 가져온다.
query()는 여러 개의 로우가 결과로 나오는 일반적인 경우에 사용한다.
그렇기 때문에 리턴 타입은 List<T>이다.
query()를 사용하면 next를 사용하지 않아도, 로우의 개수만큼 RowMapper 콜백으로 데이터를 mapping 해준다.
이렇게 작성하고 테스트를 실행하면 깔끔하게 성공하는 것을 볼 수 있다.

이렇게하면 끝나지만 이 책에서는 항상 예외적인 상황까지 생각해보라고 말한다.
데이터베이스가 빈 경우에는 과연 null이 리턴될까? 빈 list가 리턴될까?의 질문이다.
사실 당연히 빈 list가 나오겠지만, 그래도 테스트 코드를 작성해보자.
@Test
public void getAllWithEmpty() {
userDao.deleteAll();
List<User> users = userDao.getAll();
Assertions.assertEquals(0, users.size());
}
이렇게 작성하고 테스트를 해보니

테스트를 통과한다.
이런 당연한 경우도, 혹시 모를 변경에 대비하여 미리 테스트 코드를 작성해두라고 한다.
package seungkyu;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.PreparedStatementCreator;
import org.springframework.lang.NonNullApi;
import javax.sql.DataSource;
import java.sql.*;
import java.util.List;
public class UserDao {
private final JdbcTemplate jdbcTemplate;
public UserDao(
DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
public void add(User user) throws SQLException {
this.jdbcTemplate.update(
"insert into users(id, name, password) values(?, ?, ?)",
user.getId(), user.getName(), user.getPassword()
);
}
public void deleteAll(){
this.jdbcTemplate.update("delete from users");
}
public User get(String id){
return jdbcTemplate.queryForObject(
"select * from users where id = ?", (rs, rowNum) -> new User(
rs.getString("id"),
rs.getString("name"),
rs.getString("password")
), id);
}
public int getCount() {
Integer result = jdbcTemplate.queryForObject("select count(*) from users", Integer.class);
return result == null ? 0 : result;
}
public List<User> getAll() {
return this.jdbcTemplate.query(
"SELECT * FROM users ORDER BY id",
(rs, rowNum) ->
new User(rs.getString("id"), rs.getString("name"), rs.getString("password"))
);
}
}
코드가 기존에 dataSource를 가져오던 때에 비해 많이 깔끔해졌다.
하지만 그럼에도 중복되는 부분이 보인다.
(rs, rowNum) -> new User(...) 부분이다.
이 정도면 깨끗하긴 하지만, 더 깨끗한 코드를 위해서가 아니라 확장을 위해서다.
앞으로 Dao에 이렇게 Mapping 하는 코드를 더 작성할텐데, 그 때마다 이 코드를 작성해 줄 수가 없기 때문이다.
private final RowMapper<User> userRowMapper = (rs, rowNum) -> new User(
rs.getString("id"),
rs.getString("name"),
rs.getString("password"));
이렇게 Mapper 인스턴스를 만들어서 사용하자.
뭔가 함수를 변수로 가지고 있는 것 같지만
private final RowMapper<User> userRowMapperRaw = new RowMapper<>() {
@Override
public User mapRow(ResultSet rs, int rowNum) throws SQLException {
return new User(
rs.getString("id"),
rs.getString("name"),
rs.getString("password"));
}
};
그냥 이렇게 RowMapper 인터페이스를 구현해서 가지고 있는 것이다.
@FunctionalInterface
public interface RowMapper<T> {
@Nullable
T mapRow(ResultSet rs, int rowNum) throws SQLException;
}
이렇게 @FunctionalInterface를 가지고 있기 때문에 가능한 일이다.
일단 이렇게해서 템플릿에 대해 공부했다.
뭔가 JDBC가 많이 나왔지만, 그래도 최대한 템플릿과 callback으로 이해해보려고 한다.
사실 JDBC는 원래도 사용할 줄 알았고, 보통은 JPA를 더 많이 사용하기에
'백엔드 > 토비의 스프링' 카테고리의 다른 글
[토비의 스프링] 3.5 템플릿과 콜백 (1) | 2025.06.02 |
---|---|
[토비의 스프링] 3.4 컨텍스트와 DI (0) | 2025.05.29 |
[토비의 스프링] 3.3 JDBC 전략 패턴의 최적화 (0) | 2025.05.26 |
[토비의 스프링] 3.2 변하는 것과 변하지 않는 것 (0) | 2025.05.25 |
[토비의 스프링] 3.1 다시 보는 초난감 DAO (0) | 2025.05.21 |