✅ 인터페이스 적용
지금까지 만들어서 써왔던 userDao 클래스를 이제 인터페이스와 구현으로 분리해보자. 인터페이스의 이름은 가장 단순하게 하고, 구현 클래스는 각각의 특징을 따르는 이름을 붙이는 경우로 만들어 보자.
- UserDao interface
package com.jhcode.spring.ch4.user.dao; import java.util.List; import java.util.Optional; import com.jhcode.spring.ch4.user.domain.User; public interface UserDao { void add(User user); Optional<User> get(String id); List<User> getAll(); void deleteAll(); int getCount(); }
UserDao interface는 이를 구현한 클래스는 DB마다 설정이 달라질 수 있지만, 기본적으로 사용되는 메소드들을 다형성을 통해서 interface 타입으로 받아서 사용할 수 있다.
- UserDaoJdbc
package com.jhcode.spring.ch4.user.dao; 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.ch4.user.domain.User; public class UserDaoJdbc implements 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 DuplicateUserIdException { 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) { 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() { String sql = "DELETE FROM users"; //콜백 객체 생성을 내장 함수가 담당한다. this.jdbcTemplate.update(sql); } //== 테이블 정보 개수 조회 ==// public int getCount(){ 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() { String sql = "SELECT * FROM users ORDER BY id DESC"; return this.jdbcTemplate.query(sql, userRowMapper()); } }
UserDaoJdbc는 UserDao의 추상 메소드를 구현했다.
- applicationContext.xml
<bean id="userDao" class="com.jhcode.spring.ch4.user.dao.UserDaoJdbc"> <property name="dataSource" ref="dataSource"/> <!-- 아직 dataSource를 사용하는 메소드가 있어 제거하지 않았다. --> </bean>
Bean으로 등록될 클래스의 이름이 바뀌었기 때문에 applicationContext에 Bean으로 등록했던 기존의 UserDao를 UserDaoJdbc로 변경한다.
✅ 테스트 보완
테스트 코드에서 기존에 있던 UserDao를 DI 받던 코드도 UserDaoJdbc로 변경해야할까?
public class UserDaoTest {
@Autowired
private Userdao dao; // UserDaoJdbc로 변경해야 할까?
//...생략
굳이 그럴 필요는 없다. UserDaoJdbc는 UserDao를 구현했으므로 다형성을 통해 부모 타입의 참조 변수로 참조할 수 있다. 스프링에 등록한 빈 객체인 UserDaoJdbc는 DI할 때 주입 가능한 변수에 적절히 알맞은 빈으로 주입될 것이다.
경우에 따라서 의도적으로 UserDaoJdbc 구현체를 받아야할 때도 있다. UserDaoJdbc의 구현 기능을 테스트하고자 할 때이다. 구현이 제대로 되었는지 확인하기 위해서는 구현체를 직접적으로 DI하는 것이 좋다. 하지만 구현 내용에 관심 없이 DAO의 기능이 제대로 동작하는지만 확인하고 싶다면 UserDao 인터페이스 타입으로 받는 것이 좋다. 즉, 어떤 관심사를 테스트하는가에 따라 DI 받는 부분도 달라지는 것이다.
- UserDaoTest, DataAccessException Test
@Test public void sqlExceptionTranslate() { // 데이터베이스의 모든 데이터 삭제 dao.deleteAll(); try { // 동일한 사용자(user1)를 두 번 추가하여 중복 키 예외 발생 dao.add(user1); dao.add(user1); } catch (DuplicateKeyException ex) { // DuplicateKeyException의 원인인 SQLException 객체를 가져옴 SQLException sqlEx = (SQLException) ex.getCause(); // SQLErrorCodeSQLExceptionTranslator를 사용하여 SQLException 번역 SQLExceptionTranslator set = new SQLErrorCodeSQLExceptionTranslator(this.dataSource); DataAccessException transEx = set.translate(null, null, sqlEx); // 번역된 예외의 클래스가 DuplicateKeyException인지 확인 assertEquals(DuplicateKeyException.class, transEx.getClass()); } }
➡️ 위 테스트 코드가 실행되는 순서와 목적은 아래와 같다.
dao.deleteAll()
메소드를 호출하여 데이터베이스에서 모든 데이터를 삭제합니다.
dao.add(user1)
메소드를 호출하여user1
객체를 데이터베이스에 추가합니다.
dao.add(user1)
메소드를 호출하여 다시user1
객체를 데이터베이스에 추가합니다. 이 작업은 중복 키(Duplicate Key) 예외를 발생시킬 수 있습니다.
DuplicateKeyException
예외가 발생하는지 확인하기 위해try-catch
블록을 사용합니다.
ex.getCause()
를 통해DuplicateKeyException
예외의 원인인SQLException
객체를 가져옵니다.
SQLErrorCodeSQLExceptionTranslator
객체를 생성하고, 해당 데이터소스(dataSource
)를 사용하여SQLException
을 번역합니다.
- 번역된 예외(
transEx
)의 클래스를 확인하여DuplicateKeyException
와 일치하는지(assertEquals
) 검증합니다.
📖 토비 스프링 3.1 -p308~317
🚩jhcode33의 toby-spring-study.git으로 이동하기
🏷️이미지 출처 및 참고한 사이트
Uploaded by N2T