토비의 스프링 Chapter 3.6 정리

종명·2021년 4월 28일
0

스프링의 JdbcTemplate

템플릿/콜백 패턴의 기본적인 동작방식, 만드는 방법을 알아봤으니 이번에는 스프링에서 제공하는 템플릿/콜백 기술을 살펴보자. 스프링은 JDBC를 이용하는 DAO에서 사용할 수 있도록 준비된 다양한 템플릿과 콜백을 제공한다.

그동안 만들었던 JdbcContext를 스프링에 제공하는 JdbcTemplate으로 바꿔보자.
DataSource를 DI받아서 JdbcTemplate 생성자의 파라미터로 주입해주면 된다.

public class UserDao {
    private DataSource dataSource;
    private JdbcTemplate jdbcTemplate;

    public void setDataSource(DataSource dataSource) {
        this.jdbcTemplate = new JdbcTemplate(dataSource);
        this.dataSource = dataSource;
    }
    ...

update()

deleteAll()에 먼저 적용해보자.

    public void deleteAll() throws SQLException {
        this.jdbcTemplate.update(new PreparedStatementCreator() {
            @Override
            public PreparedStatement createPreparedStatement(Connection con) throws SQLException {
                return con.prepareStatement("delete from users");
            }
        });
    }

앞에서 만들었던 executeSql()은 SQL 문장만 전달하면 미리 준비된 콜백을 만들어서 호출하는 것까지 해주는 편리한 메소드였다. JdbcTemplate에도 기능이 비슷한 메소드가 존재한다.

    public void deleteAll() throws SQLException {
        this.jdbcTemplate.update("delete from users");
    }

add() 메소드에 대한 편리한 메소드도 제공한다.

    public void add(final User user) throws SQLException {
        this.jdbcTemplate.update("insert into users(id, name, password) values(?, ?, ?)",
                user.getId(), user.getName(), user.getPassword());
    }

queryForInt()

getCount()는 SQL 쿼리를 실행하고 ResultSet을 통해 결과 값을 가져오는 코드다. 이런 작업흐름을 가진 코드에서 사용할 수 있는 템플릿은 PreparedStatementCreator, ResultSetExtractor 콜백을 파라미터로 받는 query()이다.

콜백을 만드느라 익명 내부 클래스가 두번 등장하여 복잡해보이지만 getCount() 메소드에 있던 코드중에서 변하는 부분이 콜백으로 만들어져서 제공된다고 생각하면 이해하기 쉽다.

  • 앞에서 만든 lineReadTemplate()과 유사하게 두번 쨰 콜백에서 리턴되는 값은 결국 템플릿 메소드의 결과로 다시 리턴된다.
  • ResultSetExtractor는 제네릭스 타입 파라미터를 갖는다. ResultSet에서 추출할 수 있는 값의 타입이 다양하기 때문에 타입 파라미터를 사용한 것이다.
    public int getCount() {
        return this.jdbcTemplate.query(new PreparedStatementCreator() {
            @Override
            public PreparedStatement createPreparedStatement(Connection con) throws SQLException {
                return con.prepareStatement("select count(*) from users");
            }
        }, new ResultSetExtractor<Integer>() {
            @Override
            public Integer extractData(ResultSet rs) throws SQLException, DataAccessException {
                rs.next();
                return rs.getInt(1);
            }
        });
    }

JdbcTemplate는 이런 기능을 가진 콜백을 내장하고 있는 queryForInt()라는 편리한 메소드를 제공한다.(현재는 deprecated 되어 queryForObject 를 이용하면 된다.)

    public int getCount() {
        return this.jdbcTemplate.queryForObject("select count(*) from users", Integer.class);
    }

queryForObject()

이번엔 get() 메소드에 JdbcTemplate을 적용해보자. 일단 SQL은 바인딩이 필요한 치환자를 가지고 있다. 이것은 add()에서 사용했던 방법을 적용하면 될 것 같다. 남은 것은 ResultSet에 복잡한 User Object를 만드는 작업이다. 이를 위해 ResultSetExtractor 콜백 대신 RowMapeer 콜백을 사용하겠다.

    public User get(String id) {
        return this.jdbcTemplate.queryForObject("select * from users where id = ?", new Object[]{id},
                new RowMapper<User>() {
                    @Override
                    public User mapRow(ResultSet rs, int rowNum) throws SQLException {
                        User user = new User();
                        user.setId(rs.getString("id"));
                        user.setName(rs.getString("name"));
                        user.setPassword(rs.getString("password"));
                        return user;
                    }
                });
    }

query()

기능 정의와 테스트 작성

id로 정렬된 모든 사용자 정보를 가져오는 getAll() 메소드를 추가해보려고 한다. 먼저 테스트를 작성해보자.

    @Test
    public void testGetAll() {
        userDao.deleteAll();

        userDao.add(user1); // gyumee
        List<User> userList1 = userDao.getAll();
        assertThat(userList1.size(), is(1));
        checkSameUser(user1, userList1.get(0));

        userDao.add(user2); // leegw700
        List<User> userList2 = userDao.getAll();
        assertThat(userList2.size(), is(2));
        checkSameUser(user1, userList2.get(0));
        checkSameUser(user2, userList2.get(1));

        userDao.add(user3); // bumjin
        List<User> userList3 = userDao.getAll();
        assertThat(userList3.size(), is(3));
        
        checkSameUser(user3, userList3.get(0)); // user3의 id가 알파벳순으로 가장 빠르다.
        checkSameUser(user1, userList3.get(1));
        checkSameUser(user2, userList3.get(2));
    }

    private void checkSameUser(User givenUser, User actualUser) {
        assertThat(givenUser.getId(), is(actualUser.getId()));
        assertThat(givenUser.getName(), is(actualUser.getName()));
        assertThat(givenUser.getPassword(), is(actualUser.getPassword()));
    }

getAll() 메소를 만들어보자.

    public List<User> getAll() {
        return this.jdbcTemplate.query("select * from users order by id", new RowMapper<User>() {
            @Override
            public User mapRow(ResultSet rs, int rowNum) throws SQLException {
                User user = new User();
                user.setId(rs.getString("id"));
                user.setName(rs.getString("name"));
                user.setPassword(rs.getString("password"));
                return user;
            }
        });
    }

테스트 보완

스프링의 개발자 로드 존슨은 테스트를 작성할 떄 항상 네거티브 테스트부터 만드는 습관이 있다고 한다. 정상적인 조건의 테스트부터 만들면 테스트가 성공하는 것을 보고 쉽게 만족해서 예외적인 상황은 빼먹고 넘어가기가 쉽기 때문이다. 예외상황에 대한 테스트를 자꾸 빼먹는 개발자라면 의도적으로 예외적인 조건에 대해 먼저 테스트를 만드는 습관을 들이는 것도 좋다.


getAll()의 쿼리를 실행했는데 아무런 데이터가 없다면 어떻게 할 것인가? 정하기 나름이다. 일단 query() 라는 템플릿에서는 크기가 0인 List 오브젝트를 돌려준다. getAll()은 query()r가 돌려주는 결과를 그대로 리턴하도록 하고 테스트에는 검증 코드를 추가하자

    @Test
    public void testGetAll() {
        userDao.deleteAll();
        
        List<User> userList0 = dao.getAll();
        assertThat(users0.size(), is(0));
        ...
    }

그런데 getAll()에서 query()의 결과에 손댈 것도 아니면서 굳이 검증 코드를 추가해야 할까?


물론이다, 테스트 코드를 만드는 것이 좋다. UserDao를 사용하는 입장에서는 내부적인 것에는 관심이 없고 getAll()이라는 메소드가 어떻게 동작하는지에만 관심이 있다. UserDaoTest에서는 UserDao의 getAll() 메소드에 기대하는 동작방식에 대한 검증이 먼저다. 그리고 이렇게 해두면 query() 대신 다른 방법으로 구현을 바꿔도 동일한 기능을 하는 UserDao인지 확인이 가능하다.

재사용 가능한 콜백의 분리

UserDao 클래스를 보면 try/catch/finally가 있던 떄에 비해 코드의 양이 줄었을 뿐 아니라 메소드의 기능을 파악하기 쉽게 되었다.

중복제거

get()과 getAll()의 RowMapper의 내용이 같다. 중복을 제거해보자.

public class UserDao {
    private RowMapper<User> userRowMapper = new RowMapper<User>() {
        @Override
        public User mapRow(ResultSet rs, int rowNum) throws SQLException {
            User user = new User();
            user.setId(rs.getString("id"));
            user.setName(rs.getString("name"));
            user.setPassword(rs.getString("password"));
            return user;
        }
    };

    public User get(String id) {
        return this.jdbcTemplate.queryForObject("select * from users where id = ?", new Object[]{id}, userRowMapper);
    }

    public List<User> getAll() {
        return this.jdbcTemplate.query("select * from users order by id", this.userRowMapper);
    }
    ...
}

템플릿/콜백 패턴과 UserDao

아래는 최종적으로 완성된 UserDao클래스다. 템플릿/콜백 패턴과 DI를 이용해 예외처리, 리소스 관리, 유연한 DataSource활용 방법까지 제공하면서 깔끔한 코드가 되었다.

public class UserDao {
    private JdbcTemplate jdbcTemplate;

    public void setDataSource(DataSource dataSource) {
        this.jdbcTemplate = new JdbcTemplate(dataSource);
    }

    private RowMapper<User> userRowMapper = new RowMapper<User>() {
        @Override
        public User mapRow(ResultSet rs, int rowNum) throws SQLException {
            User user = new User();
            user.setId(rs.getString("id"));
            user.setName(rs.getString("name"));
            user.setPassword(rs.getString("password"));
            return user;
        }
    };

    public void add(final User user) {
        this.jdbcTemplate.update("insert into users(id, name, password) values (?, ?, ?)",
                user.getId(), user.getName(), user.getPassword());
    }

    public User get(String id) {
        return this.jdbcTemplate.queryForObject("select * from users where id = ?", 
                new Object[]{id}, userRowMapper);
    }

    public List<User> getAll() {
        return this.jdbcTemplate.query("select * from users order by id", this.userRowMapper);
    }

    public void deleteAll() {
        this.jdbcTemplate.update("delete from users");
    }

    public int getCount() {
        return this.jdbcTemplate.queryForObject("select count(*) from users", Integer.class);
    }
}
  • UserDao에는 User 정보를 DB에 넣거나 가져오거나 조작하는 핵심적인 기능만 담겨있다.
  • JdbcTemplate에는 JDBC API를 사용하는 방식, 예외처리, 리소스 반납, DB연결을 어떻게 가져올지에 대한 책임과 관심이 있다. 따라서 변경이 일어난다 해도 UserDao 코드에는 아무런 영향이 없다. 그런 면에서 책임이 다른 코드와는 낮은 결합도를 유지하고 있다. 다만 JdbcTemplate이라는 템플릿 클래스를 직접 이용한다는 면에서 특정 템플릿/콜백 구현에 대한 강한 결합을 가지고 있다.

0개의 댓글