✅ 테스트 보완(Negative Test)
Spring에서 네거티브 테스트는 소프트웨어 개발 중에 발생할 수 있는 예외 상황이나 오류에 대해 테스트하는 절차이다. 이러한 테스트는 코드의 일부분이 의도한 대로 작동하지 않거나 예상치 못한 결과를 초래할 수 있는 부분을 식별하고 수정하기 위해 사용된다.
➡️ 네거티브 테스트는 보통 다음과 같은 시나리오에서 사용된다.
- 예외 처리 테스트
메서드 또는 함수에서 발생할 수 있는 예외 상황을 확인하는 테스트이다. 예를 들어, 유효하지 않은 입력이 주어졌을 때 적절한 예외가 발생하는지 확인하는 등의 테스트를 포함한다.
- 경계 조건 테스트
입력 값의 경계 조건에 대해 테스트한다. 예를 들어, 정수의 경우 0 또는 음수 값, 최대값 또는 최소값 등을 고려하여 코드의 동작을 테스트한다.
- 부정적인 시나리오 테스트
코드가 처리해야 하는 비정상적인 시나리오를 테스트한다. 예를 들어, 파일이 없는 상태에서 파일을 열려고 할 때, 네트워크 연결이 끊긴 상태에서 데이터를 전송하려고 할 때 등을 테스트한다.
👉 숫자를 입력해야 할 곳에 문자를 넣고, 생일에 음수 값을 넣고, 또는 아무것도 입력하지 않고 폼의 저장 버튼을 누르기도 하며, DB에 아무런 데이터가 없는 채로 조회하거나, 엉터리 검색 조건을 넣는 것도 모두 네거티브 테스트이다.
어떤 메소드는 데이터가 없으면 null을 반환하는데, 어떤 메소드는 오류를 던지는 등, 일관성 없이 예외 처리를 하는 것이 아니라, 예외 상황에 대해서 일관성 있는 기준을 만들고 테스트해야 한다.
- UserDaoTest, getAll()
@Test public void getAll() throws ClassNotFoundException, SQLException{ dao.deleteAll(); //== 네거티브 테스트, DB에 데이터가 하나도 없을 경우 ==// dao.add(user1); List<User> users1 = dao.getAll(); assertEquals(users1.size(), 1); checkSameUser(user1, users1.get(0));
앞에서 만들었던 위와 같은 Test가 네거티브 테스트이다. 위 테스트는 DB에 데이터가 하나도 없을 경우 크기가 0인
List
객체를 반환한다. null을 반환할 수도 있고, 오류를 던질 수도 있다. 그것은 개발자의 선택이지만, 비슷한 상황에서 동일한 예외 처리가 이루어져야 한다.
✅ 재사용이 가능한 콜백 분리
템플릿/콜백 패턴을 적용함에 따라 UserDao의 코드가 훨씬 줄어들었다. UserDao는 JdbcTemplate을 호출하고, 인수로 전달할 콜백 객체만 생성해주면 된다. DB를 연결하고, 쿼리를 실행하는 부분은 모두 템플릿이 담당하고, UserDao는 어떤 쿼리를 실행하고, 어떤 결과만 받을지 결정하면 되는 것이다.
- DI 코드 정리
public class UserDao { private JdbcTemplate jdbcTemplate; public void setDataSource(DataSource dataSource) { this.jdbcTemplate = new JdbcTemplate(dataSource); }
DataSource 객체는 더 이상 UserDao가 DB 커넥션을 하지 않기 때문에 필드로 저장할 필요가 없어진다. 필드로 저장했던 DataSource 객체를 지우고,
setDataSource()
메소드에서 매개변수로 받아 jdbcTemplate 객체를 생성하고 저장하는 데만 사용한다.
- 중복 코드 제거, userRowMapper()
//==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; }); }
책에서는 RowMapper 객체를 생성하고 필드로 저장했지만, 여기서는 단지 RowMapper 객체를 리턴하는 메소드로 만들었다. RowMapper 객체는
get()
,getAll()
두 개의 메소드에서 동작한다. 각각의 메소드 안에서 RowMapper 객체를 생성할 수도 있지만 더 많은 기능이 생긴다면 어떻게 될까? 이를 위해 중복을 제거하고자 메소드로 분리한 것이다.
- 최종적으로 만들어진 UserDao
package com.jhcode.spring.ch3.user.dao; import java.sql.SQLException; import java.util.List; import java.util.Optional; import java.util.stream.Stream; import javax.sql.DataSource; import org.springframework.dao.DataAccessException; import org.springframework.dao.support.DataAccessUtils; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.RowMapper; import com.jhcode.spring.ch3.user.domain.User; public class UserDao { private JdbcTemplate jdbcTemplate; public void setDataSource(DataSource dataSource) { this.jdbcTemplate = new JdbcTemplate(dataSource); } //==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; }); } //== DB에 user 추가 ==// 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()); } //== DB에서 id에 해당하는 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(); } } //== 테이블 전체 데이터 삭제 ==// public void deleteAll() throws SQLException { String sql = "DELETE FROM users"; //콜백 객체 생성을 내장 함수가 담당한다. this.jdbcTemplate.update(sql); } //== 테이블 정보 개수 조회 ==// 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); } //== 테이블에 있는 전체 User 정보 가져오기 public List<User> getAll() throws DataAccessException, SQLException { String sql = "SELECT * FROM users ORDER BY id DESC"; return this.jdbcTemplate.query(sql, userRowMapper()); } }
UserDao에는 User 정보를 DB에 넣고, 가져오고, 조작하는 방법에 대한 로직만 담겨 있다. 어떤 정보를 담을지, 어떤 정보를 가져올지만 결정하면 된다. 실행할 쿼리와 가져올 결과가 무엇인지만 결정하면 되는 것이다.
반면 JDBC API를 사용하고, 예외처리, 리소스 반납, DB 연결을 어떻게 가져올지 등은 모두 JdbcTemplate이 담당하고 있다. 따라서 UserDao와 서로의 관심사가 분리되어있기 때문에 변경이 일어나도 UserDao 코드에는 아무런 영향을 주지 않는다.
📖토비 스프링 3.1 -p268~277
🚩jhcode33의 toby-spring-study.git으로 이동하기
Uploaded by N2T