✅ JdbcTemplate
앞에서 만들었던 JdbcContext와 유사하지만 훨씬 강력하고 편리한 기능을 제공해준다. JdbcTemplate은 스프링 프레임워크에서 제공하는 데이터베이스 연동을 위한 도구이다.
- SQL 쿼리 실행과 결과 처리를 단순화하는 간결한 API를 제공한다.
- 데이터베이스 연결 및 트랜잭션 관리를 자동으로 처리하여 개발자가 별도로 구현할 필요가 없다.
- JDBC(Java Database Connectivity)를 기반으로 동작하며, 데이터베이스 종류에 상관없이 사용할 수 있다.
- PreparedStatement를 활용하여 SQL 쿼리의 매개변수화와 자동 리소스 관리를 수행한다.
- 데이터베이스 조회 결과를 자바 객체로 매핑하기 위한 RowMapper 인터페이스를 제공하며, 개발자는 결과를 편리하게 처리할 수 있다.
- JdbcTemplate
public class UserDao { private DataSource dataSource; private JdbcTemplate jdbcTemplate; public void setDataSource(DataSource dataSource) { this.dataSource = dataSource; this.jdbcTemplate = new JdbcTemplate(dataSource); }
UserDao에 JdbcTemplate을 DI 받고, JdbcTemplate은 객체를 생성할 때 인수로 dataSource 객체를 DI 받는다.
🔹deleteAll() 적용
- JdbcTemplate 적용
public void deleteAll() throws SQLException { String sql = "DELETE FROM users"; //콜백 객체 생성 this.jdbcTemplate.update(new PreparedStatementCreator() { public PreparedStatement createPreparedStatement(Connection con) throws SQLException { return con.prepareStatement(sql); } }); }
앞에서 JdbcContext에서 작업한 것과 같이
deletAll()
메소드는 콜백 객체를 생성하고 템플릿 메소드인 update를 호출한다. 그 이후 템플릿에서 작업이 진행되다가 콜백 메소드를 만나 preparedStatement를 실행하고 그 결과를 다시 템플릿에게 반환한다.콜백을 직접 구현하지 않고, JdbcTemplate이 가지고 있는 내장 콜백을 사용할 수도 있다.
- JdbcTemplate, 내장 함수
public void deleteAll() throws SQLException { String sql = "DELETE FROM users"; //콜백 객체 생성을 내장 함수가 담당한다. this.jdbcTemplate.update(sql); }
템플릿 메소드인
update()
의 내장함수를 사용해서 JdbcContext에서 쿼리문만 매개변수로 주면 콜백 객체 생성과 템플릿 메소드 호출까지 한 번에 처리할 수 있는 로직을 JdbcTemplate은 기본적으로 제공하고 있다.
🔹add() 적용
- add()
public void add(final User user) throws ClassNotFoundException, SQLException{ String sql = "INSERT INTO users(id, name, password) values(?,?,?)"; this.jdbcTemplate.update(sql, user.getId(), user.getName(), user.getPassword()); }
update()
템플릿 메소드는 PreparedStatement의 값을 바인딩하기 위해 메소드 오버로딩이 정의되어 있다. PreparedStatement의 바인딩 순서에 따라 매개변수로 값을 넣으면 자동으로 바인딩된다.
- UserDaoTest
@BeforeEach public void setUp() { System.out.println("setUp():" + this); DataSource dataSource = new SingleConnectionDataSource( "jdbc:mariadb://localhost:3306/toby?characterEncoding=UTF-8", "root", "1234", true); dao = new UserDao(); dao.setDataSource(dataSource); }
기존에 JdbcContext를 준비했던 과정을 삭제했다. 이제는 UserDao에 DI된 JdbcTemplate을 사용하기 때문이다. 테스트를 수행하면 결과가 잘 출력된다.
👉 이러한 템플릿 메소드는 변경되는 부분이 쿼리문 밖에 존재하지 않으므로 재사용성이 뛰어나다. 또 기본적으로 제공해주는 JdbcTemplate을 통해 직접 구현해야 하는 부분이 적어졌다.
✅ getCount(), jdbcTemplate.queryForInt()
getCount() 메소드는 아직까지 템플릿/콜백 패턴을 적용하지 않았다. SQL 쿼리를 실행하고 ResultSet을 통해 결과 값을 가져오는 코드이다. 이때 사용되는 JdbcTemplate의 템플릿 메소드는 PreparedStatementCreator 콜백과 ResultSetExtractor 콜백을 파라미터로 받는 query() 템플릿 메소드이다.
PreparedStatementCreator 콜백은 쿼리를 실행해주는 콜백이고, ResultSetExtractor 콜백은 쿼리를 수행하고 반환된 ResultSet 객체를 통해 원하는 결과값을 만들어 템플릿에게 전달하는 콜백이다. 클라이언트/템플릿/콜백 3단계 구조에서 콜백이 만들어 내는 결과는 템플릿을 통해 클라이언트에게 전달될 수 있으므로 템플릿에게 콜백의 결과를 전달하는 것이다.
- getCount()
//== 테이블 정보 개수 조회 ==// public int getCount() throws SQLException { String sql = "SELECT COUNT(*) FROM users"; return this.jdbcTemplate.query( //첫 번째 콜백 객체 new PreparedStatementCreator() { public PreparedStatement createPreparedStatement(Connection con) throws SQLException{ return con.prepareStatement(sql); } //두 번째 콜백 객체 }, new ResultSetExtractor<Integer>() { public Integer extractData(ResultSet rs) throws SQLException{ rs.next(); return rs.getInt(1); } }); }
getCount()
메소드를 호출한 곳에서 받아볼 결과가 int이므로 템플릿/콜백 패턴을 통해 반환된 결과(rs.getInt(1);
)를 다시 반환하기 위해서 템플릿/콜백 메소드를 return함으로써 그 결과를 반환하도록 했다.getCount() 메소드에서 변하는 부분은 두 가지이다.
- 어떤 쿼리를 수행할 것인가?
- 수행된 쿼리의 결과로 어떤 결과를 추출해 내고 싶은가?
따라서 변하는 부분이 두 가지이므로 콜백 객체도 두 가지가 생겨나는 것이다. ResultSetExtractor 콜백은 제네릭을 적용하였다. ResultSetExtractor 콜백 객체에 제네릭으로 지정한 타입은 메소드에 적용되
query()
템플릿에도 적용되어 리턴 타입이 Integer로 바뀌게 된다.그러나 이렇게 계속 콜백 객체가 많아지면 코드가 복잡해지지 않을까? 이를 위해 JdbcTemplate이 지원하는 또 다른 메소드가 있다.
queryForInt()queryForInt()
메소드는 Spring Framework 4.3 버전부터 Deprecated되었으며, Spring Framework 5.0부터는 완전히 제거되었습니다. 따라서 최신 버전의 Spring Framework를 사용하고 계신다면, 이 메소드를 사용할 수 없습니다.따라서 query() 메소드를 사용하여 람다식으로 ResultSet 객체에서 집합의 첫 번째 열의 값을 추출하여 List에 저장하고, DataAccessUtils의 singleResult() 메소드를 사용하여 리스트에서 단일 결과값을 반환 받도록 만들어 보았습니다.
public int getCount() throws SQLException { String sql = "SELECT COUNT(*) FROM users"; List<Integer> result = jdbcTemplate.query(sql, (rs, rowNum) -> rs.getInt(1)); return (int)DataAccessUtils.singleResult(result); }
✅ jdbcTemplate.queryForObject()
쿼리를 수행하고 반환 받은 ResultSet 객체로부터 값을 바인딩하여 User 객체를 만들어야 한다. 이때 사용할 수 있는 것이 RowMapper 콜백이다. ResultSetExtractor은 ResultSet을 한 번 전달 받아 알아서추출 작업을 모두 진행하고 최종 결과만 리턴해주면 되는 데, RowMapper은 ResultSet의 로우 하나를 매핑하여 사용할 수 있으며 여러 번 호출할 수 있다.
queryForObject()책에서 기술되어있는 queryForObject() 메소드의 동작 방식은 더 이상 지원하지 않습니다. object[] 방식으로 하는 것이 아니라 가변인자 방식으로 구현되어있습니다. 그래서 그냥 다른 방법으로 구현해보도록 하겠습니다.
//==RowMapper 객체를 생성하기 위한 메소드==// private RowMapper<User> userRowMapper() { return ((rs, rowNum) -> { User user = new User(); user.setId(rs.getString("id")); user.setName(rs.getString("name")); user.setPassword(rs.getString("password")); return user; }); }
userRowMapper
메서드는RowMapper<User>
를 생성하기 위한 메서드입니다. 람다 표현식을 사용하여RowMapper
를 구현하고,ResultSet
에서 데이터를 추출하여User
객체에 매핑한 후 반환합니다.public Optional<User> get(String id) throws SQLException { String sql = "SELECT * FROM users WHERE id = ?"; try (Stream<User> stream = jdbcTemplate.queryForStream(sql, userRowMapper(), id)) { return stream.findFirst(); } catch (DataAccessException e) { return Optional.empty(); } }
jdbcTemplate.queryForStream()
메서드를 사용하여 쿼리 결과를 스트림으로 가져옵니다. 이 메서드는 sql, RowMapper, 그리고 쿼리에 전달할 매개변수인 id를 인자로 받습니다.findFirst()
메서드를 호출하여 스트림에서 첫 번째 요소를 Optional로 반환합니다. 이를 통해 조회된 사용자가 있으면 해당 사용자를, 없으면Optional.empty()
를 반환합니다.DataAccessException은 데이터 액세스 예외를 처리하기 위한 예외 처리 블록입니다. 예외가 발생하면
Optional.empty()
를 반환합니다.queryForStream
메서드를 사용하여 스트림으로 결과를 가져오므로, 대량의 결과를 처리할 때 유용합니다.
✅ 테스트 작성
getAll() 메소드를 작성하기 전에 TDD 방식으로 테스트를 먼저 작성해보자. getAll()은 테이블의 모든 로우를 다 가져오는 메소드이다. 그렇기에 여러 개의 User 객체를 담을 수 있는 컬렉션인 List 타입으로 반환하는 것이 가장 적절하다. 또한 기본키인 id 순으로 정렬해서 가져올 수 있도록 하자.
- UserDaoTest, getAll()
@Test public void getAll() throws ClassNotFoundException, SQLException{ dao.deleteAll(); dao.add(user1); List<User> users1 = dao.getAll(); assertEquals(users1.size(), 1); checkSameUser(user1, users1.get(0)); dao.add(user2); List<User> users2 = dao.getAll(); assertEquals(users2.size(), 2); checkSameUser(user1, users2.get(0)); checkSameUser(user2, users2.get(1)); dao.add(user3); List<User> users3 = dao.getAll(); assertEquals(users3.size(), 3); checkSameUser(user3, users3.get(0)); checkSameUser(user1, users3.get(1)); checkSameUser(user2, users3.get(2)); } private void checkSameUser(User user1, User user2) { assertEquals(user1.getId(), user2.getId()); assertEquals(user1.getName(), user2.getName()); assertEquals(user1.getPassword(), user2.getPassword()); }
setUp() 메소드에 User 객체 3개를 생성하고 준비해두었다. add() 메소드로 User 객체를 하나씩 DB에 저장하고 getAll() 메소드로 모든 User 객체를 List로 가지고 온 뒤 List의 사이즈와 저장한 User 객체의 수가 같은지 비교한다.
저장한 User 객체와 DB에서 가져온 User 객체의 정보가 일치하는지 동등성을 checkSameUser() 메소드를 통해서 비교하는 테스트이다.
- getAll()
//== 테이블에 있는 전체 User 정보 가져오기 public List<User> getAll() throws DataAccessException, SQLException { String sql = "SELECT * FROM users ORDER BY id DESC"; return this.jdbcTemplate.query(sql, userRowMapper()); }
query()
메서드는 쿼리 결과를 한 번에 가져와서ResultSetExtractor
를 통해 처리하며, 그 결과를 리스트로 반환합니다. 각 결과 행은RowMapper
를 통해 객체로 매핑되고, 이들 객체는 리스트에 추가됩니다.RowMapper
는ResultSet
에서 데이터를 읽어와서 원하는 객체로 변환하는 인터페이스입니다. 각 행마다RowMapper
의mapRow()
메서드가 호출되어 결과를 객체로 변환하고, 이들 객체는 리스트에 추가됩니다. 따라서query()
메서드는RowMapper
를 통해 객체의 리스트를 생성하고 반환합니다.userRowMapper() 메소드는 쿼리를 실행하고 반환된 ResultSet 객체를 통해 User 객체로 바인딩하여 반환하는 메소드이다. 이를 통해 DB의 모든 User 정보를 조회한 후 내부적으로 List 컬렉션에 저장하여 반환한다.
- JdbcTemplate.class
@Override public <T> List<T> query(String sql, RowMapper<T> rowMapper) throws DataAccessException { return result(query(sql, new RowMapperResultSetExtractor<>(rowMapper))); }
위와 같이 List로 반환되는 것을 알 수 있다.
- JdbcTemplate.class
- addAndGet()
@Test public void addAndGet() throws ClassNotFoundException, SQLException { System.out.println("addAndGet(): " + this); dao.deleteAll(); assertEquals(dao.getCount(), 0); dao.deleteAll(); assertEquals(dao.getCount(), 0); dao.add(user1); dao.add(user2); assertEquals(dao.getCount(), 2); Optional<User> Optuserget1 = dao.get(user1.getId()); if(!Optuserget1.isEmpty()) { User userget = Optuserget1.get(); assertEquals(user1.getName(), userget.getName()); assertEquals(user1.getPassword(), userget.getPassword()); } Optional<User> Optuserget2 = dao.get(user2.getId()); if(!Optuserget2.isEmpty()) { User userget = Optuserget2.get(); assertEquals(user2.getName(), userget.getName()); assertEquals(user2.getPassword(), userget.getPassword()); } }
UserDao의
get()
메소드가 Optinal<User>으로 반환 타입이 변경되었기 때문에 addAndGet() 메소드도 오류가 난다. User 객체의 Id를 통해서 Optinal 클래스의get()
메소드로 Optional 타입으로 반환되면,isEmpty()
메소드로 비어있는지 체크하고get()
메소드를 통해서 User 객체를 반환하면 된다.
📖토비 스프링 3.1 -p259~268
🚩jhcode33의 toby-spring-study.git으로 이동하기
Uploaded by N2T