4.2) 예외 전환
JDBC의 한계
- JDBC는 자바를 이용해 DB에 접근하는 방법을 추상화된 API 형태로 정의해놓고,
각 DB 업체가 JDBC 표준에 따라 만들어진 드라이버를 제공하게 해준다.
- 내부 구현은 DB마다 다르겠지만 JDBC의 Connection, Statement, ResultSet 등의 표준 인터페이스를 통해
그 기능을 제공해주기 때문에 자바 개발자들은 표준화된 JDBC의 API에 익숙해지면
DB의 종류에 상관없이 일관된 방법으로 프로그램을 개발할 수 있다. - 하지만 DB 종류에 상관없이 사용할 수 있는 데이터 액세스 코드를 작성하는 일은 쉽지 않다.
- 내부 구현은 DB마다 다르겠지만 JDBC의 Connection, Statement, ResultSet 등의 표준 인터페이스를 통해
- 현실적으로 DB를 자유롭게 바꾸어 사용할 수 있는 DB 프로그램을 작성하는 데는 두 가지 걸림돌이 있다.
- 첫째 문제는 JDBC 코드에서 사용하는 SQL이다.
SQL은 어느 정도 표준화된 언어이고 몇 가지 표준 제약이 있긴 하지만,
대부분의 DB는 표준을 따르지 않는 비표준 문법과 기능을 제공하여
DB의 특별한 기능을 사용하거나 최적화된 SQL을 만들 때 유용하게 사용한다.
그러므로 다른 DB로 변경하려면 DAO에 담긴 SQL을 적지 않게 수정해야 한다.
이를 위해서는 DAO를 DB 별로 만들어 사용하거나 SQL을 외부에서 독립시켜서 바꿔 쓸 수 있게 해야 한다. - 두 번째 문제는 SQLException이다.
DB마다 SQL만 다른 것이 아니라 에러의 종류와 원인도 제각각이기 때문에
JDBC는 데이터 처리 중에 발생하는 다양한 예외를 그냥 SQLException 하나에 모두 담아버린다.
이때 예외가 발생한 원인은 SQLException의 getErrorCode()로 가져올 수 있는 DB 에러 코드는 DB별로 모두 다르다. - 그러므로 SQLException은 예외가 발생했을 때의 DB 상태를 담은 SQL 상태정보를 부가적으로 제공하며
getSQLState() 메소드로 예외상황에 대한 상태정보를 가져올 수 있으며 DB별로 달라지는 에러 코드를 대신할 수 있도록,
Open Group의 XOPEN SQL 스펙에 정의된 SQL 상태 코드를 따르도록 되어 있다.
- 첫째 문제는 JDBC 코드에서 사용하는 SQL이다.
DB 에러 코드 매핑을 통한 전환
- DB 종류가 바뀌더라도 DAO를 수정하지 않기 위해
SQLException의 비표준 에러 코드와 SQL 상태정보에 대한 해결책을 알아보자.
- SQLException에 담긴 SQL 상태 코드는 신뢰할 만한 게 아니므로 더 이상 고려하지 않는다.
- SQL 상태 코드는 JDBC 드라이버를 만들 때 들어가는 것이므로 같은 DB라도 드라이브를 만들 때마다 달라지기도 하지만,
DB 에러 코드는 DB에서 직접 제공해주는 것이니 버전이 올라가더라도 어느 정도 일관성이 유지된다. - 해결 방법은 DB별 에러 코드를 참고해서 발생한 예외의 원인이 무엇인지 해석해주는 기능을 만드는 것이다.
- 스프링은 DB별 에러 코드를 분류해서
스프링이 정의한 예외 클래스와 매핑해놓은 에러 코드 매핑정보 테이블을 만들어두어 이를 이용한다. - JdbcTemplate은 SQLException을 단지 런타임 예외인 DataAccessException으로 포장하는 것이 아니라
DB의 에러 코드를 DataAccessException 계층구조의 클래스로 매핑해준다.
전환되는 JdbcTemplate에서 던지는 예외는 모두 DataAccessException의 서브클래스 타입이다. - 그러므로 드라이버나 DB 메타정보를 참고해서 DB 종류를 확인하고 DB별로 미리 준비된 매핑정보를 참고해서
적절한 예외 클래스를 선택하기 때문에 DB가 달라져도 같은 종류의 에러라면 동일한 예외를 받을 수 있다. - 만약 개발 정책으로 인해 애플리케이션에서 직접 정의한 예외를 발생시키고 싶을 경우,
이전과 같이 예외를 전환해주는 코드를 넣으면 된다.
DAO 인터페이스와 DataAccessException 계층구조
- DataAccessException은 JDBC의 SQLException을 전환하는 용도로만 만들어진 건 아니다.
JDBC 외의 자바 데이터 액세스 기술(JDO, JPA, ORM, iBatis 등)에서 발생하는 예외에도 적용된다.
- DataAccessException은 의미가 같은 예외라면
데이터 액세스 기술의 종류와 상관없이 일관된 예외가 발생하도록 만들어주어 독립적인 추상화된 예외를 제공한다. - 스프링은 왜 이렇게 DataAccessException 계층구조를 이용해 기술에 독립적인 예외를 정의하고 사용할까?
- DataAccessException은 의미가 같은 예외라면
- DAO는 인터페이스를 사용해 구체적인 클래스 정보와 구현 방법을 감추고, DI를 통해 제공되도록 만드는 것이 바람직하다.
- DAO는 데이터 액세스 로직을 담은 코드를 성격이 다른 코드에서 분리해놓을 수 있다.
- DAO를 사용하는 쪽에서는 DAO가 내부에서 어떤 데이터 액세스 기술을 사용하는지 신경 쓰지 않아도 되며
특정 기술에 독립적인 단순한 오브젝트를 주고받으면서 데이터 액세스 기능을 사용하기만 하면 된다. - 하지만 DAO의 사용 기술과 구현 코드는 전략 패턴과 DI를 통해서 DAO를 사용하는 클라이언트에게 감출 수 있지만,
DAO가 사용하는 데이터 액세스 기술의 API가 예외를 던지기 때문에 메소드 선언에 나타나는 예외정보가 문제가 될 수 있다. - 데이터 액세스 기술의 API는 자신만의 독자적인 예외를 던지기 때문에 인터페이스 메소드를 바꿔주면 모르겠지만,
SQLException을 던지도록 선언한 인터페이스 메소드는 사용할 수 없다. - 결국 인터페이스로 메소드의 구현은 추상화했지만 구현 기술마다 던지는 예외가 다르기 때문에 메소드의 선언이 달라진다.
- 가장 단순한 해결 방법은 모든 예외를 다 받아주는 throws Exception으로 선언하는 것이지만 무책임한 선언이다.
- JDBC보다는 늦게 등장한 JDO, JPA, Hibernate 등의 기술은 SQLException을 체크 예외 대신 런타임 예외를 사용한다.
그러므로 JDBC API를 직접 사용하는 DAO만 메소드 내에서 런타임 예외로 포장해서 던져준다면
DAO에서는 사용하는 기술에 완전히 독립적인 인터페이스 선언이 가능해진다.
- 하지만 데이터 액세스 기술이 달라지면 같은 상황에서도 다른 종류의 예외가 던져진다.
- 따라서 DAO를 사용하는 클라이언트 입장에서는 DAO의 사용 기술에 따라서 예외 처리 방법이 달라져야 한다.
- 단지 인터페이스로 추상화하고, 일부 기술에서 발생하는 체크 예외를 런타임 예외로 전환하는 것만으론 불충분하다.
- 그래서 DataAccessException은 자바의 주요 데이터 액세스 기술에서 발생할 수 있는 대부분의 예외를 추상화하고 있다.
- 스프링의 DataAccessException은 이런 일부 기술에서만 공통적으로 나타나는 예외를 포함해서
데이터 액세스 기술에서 발생 가능한 대부분의 예외를 계층구조로 분류해놓았다. - 그러므로 JDBC, JDO, JPA, 하이버네이트 등 기술의 종류에 상관 없이 데이터 액세스 기술을 부정확하게 사용했을 때는
InvalidDataAccessResourceUsageException 예외가 던져지게 된다.
(이를 다시 세분화하면 JDBC - BadSqlGrammerException, 하이버네이트 - HibernateQueryException 등) - 또한 오브젝트/엔티티 단위로 정보를 업데이트할 때 낙관적인 락킹이 발생할 경우는
ObjectOptimisticLockingFailureException 예외가 던져지게 된다.
(이를 다시 세분화하면 JDBC - JdbcOptimisticLockingFailureException,
하이버네이트 - HibernateOptimisticLockingFailureException 등) - DataAccessException 계층구조에는 템플릿 메소드나 DAO 메소드에서 직접 활용할 수 있는 예외도 정의되어 있다.
- 예를 들어 쿼리 실행 결과가 하나 이상의 로우를 가져올 때 사용하는
IncorrectResultSizeDataAccessException, EmptyResultDataAccessException
- 스프링의 DataAccessException은 이런 일부 기술에서만 공통적으로 나타나는 예외를 포함해서

- 그러므로 스프링의 데이터 액세스 지원 기술을 이용해 DAO를 만들면 사용 기술에 독립적인 일관성 있는 예외를 던질 수 있다.
- 결국 인터페이스 사용, 런타임 예외 전환과 함께 DataAccessException 예외 추상화를 적용하면
데이터 액세스 기술과 구현 방법에 독립적인 이상적인 DAO를 만들 수 있다.
기술에 독립적인 UserDao 만들기
- UserDao 클래스를 인터페이스와 구현으로 분리해보자.
- 인터페이스와 구현 클래스의 이름을 정하기 위해서는 인터페이스 이름 앞에는 I라는 접두어를 붙이는 방법도 있고,
인터페이스 이름은 가장 단순하게 하고 구현 클래스는 각각의 특징을 따르는 이름을 붙이는 경우도 있다. - 사용자 처리 DAO의 이름은 UserDao라고 하고, JDBC를 이용해 구현한 클래스의 이름을 UserDaoJdbc라고 하자.
- UserDao 인터페이스에는 UserDao 클래스에서 DAO의 기능을 사용하려는 클라이언트들이 필요한 것만 추출해내면 된다.
- 이때 UserDao의 구현 방법에 따라 변경될 수 있으며 UserDao를 사용하는 클라이언트가 알 필요가 없으므로 제외한다.
- 기존의 UserDao 클래스는 UserDaoJdbc로 변경하고 UserDao 인터페이스를 구현하도록 implements로 선언한다.
- 그리고 스프링 설정파일에서 userDao 빈 클래스의 이름은 UserDao에서 UserDaoJdbc로 바꿔준다.
- 인터페이스와 구현 클래스의 이름을 정하기 위해서는 인터페이스 이름 앞에는 I라는 접두어를 붙이는 방법도 있고,

/**
* UserDao 인터페이스
*/
public interface UserDao {
void add(User user);
User get(String id);
List<User> getAll();
void deleteAll();
int getCount();
}
<?xml version="1.0" encoding="UTF-8" ?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">
<bean id="userDao" class="com.gaga.springtoby.user.dao.UserDaoJdbc">
<property name="dataSource" ref="dataSource" />
</bean>
<bean id="dataSource" class="org.springframework.jdbc.datasource.SimpleDriverDataSource">
<property name="driverClass" value="com.mysql.jdbc.Driver" />
<property name="url" value="jdbc:mysql://localhost/toby" />
<property name="username" value="toby" />
<property name="password" value="gaga" />
</bean>
</beans>
/**
* JdbcTemplate를 적용한 UserDao 구현 클래스
*/
public class UserDaoJdbc implements UserDao {
private JdbcTemplate jdbcTemplate;
// JdbcTemplate 초기화
public void setDataSource(DataSource dataSource) {
// DataSource 오브젝트는 JdbcTemplate을 만든 후에는 사용하지 않으니 저장해두지 않아도 된다.
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
// 재사용 가능하도록 독립시킨 RowMapper
private RowMapper<User> userMapper = new RowMapper<User>() {
// ResultSet한 로우의 결과를 오브젝트에 매핑해주는 RowMapper
@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;
}
};
/* 새로운 사용자를 생성 */
// JdbcTemplate의 update() 메소드를 적용
// 스프링의 JdbcTemplate은 예외처리 전략을 따르고 있으므로
// JdbcTemplate 템플릿과 콜백 안에서 발생하는 모든 SQLException을 런타임 예외인 DataAccessException으로 포장해서 던져준다.
public void add(final User user) {
this.jdbcTemplate.update("insert into users(id, name, password) values(?,?,?)", user.getId(), user.getName(), user.getPassword());
}
/* 아이디를 가지고 사용자 정보 읽어오기 */
// JdbcTemplate의 queryForObject() 메소드를 적용
public User get(String id) {
return this.jdbcTemplate.queryForObject("select * from users where id = ?", new Object[] {id}, this.userMapper);
}
/* 모든 사용자 정보 읽어오기 */
// JdbcTemplate의 query() 메소드를 적용
public List<User> getAll() {
return this.jdbcTemplate.query("select * from users order by id", this.userMapper);
}
/* 모든 사용자 삭제하기 */
// JdbcTemplate의 update() 메소드를 적용
public void deleteAll() {
this.jdbcTemplate.update("delete from users");
}
/* 사용자 테이블의 레코드 개수 읽어오기 */
// JdbcTemplate의 queryForInt() 메소드를 적용 (Deprecated)
// JdbcTemplate의 queryForObject() 메소드를 적용
public int getCount() {
// return this.jdbcTemplate.queryForInt("select count(*) from users");
return this.jdbcTemplate.queryForObject("select count(*) from users", Integer.class);
}
}
- 기존 UserDao 클래스의 테스트 코드에서 UserDao 인스턴스 변수 선언도 UserDaoJdbc로 변경해야 할까?
- @Autowired는 스프링의 컨텍스트 내에서 정의된 빈 중에서 인스턴스 변수에 주입 가능한 타입의 빈을 찾아준다.
- UserDao는 UserDaoJdbc가 구현한 인터페이스이므로 UserDaoJdbc 클래스로 정의된 빈을 넣어도 문제가 없다.
- 만약 테스트의 관심이 구현 기술에 상관없이 DAO의 기능이 동작하는 데만 관심이 있다면,
UserDao 인터페이스로 받아서 테스트하는 편이 낫다.
반면에 특정 기술을 사용한 UserDao의 구현 내용에 관심을 가지고 테스트하려면
테스트에서 @Autowired로 DI 받을 때 UserDaoJdbc나 UserDaoHibernate 같이 특정 타입을 사용해야 한다. - 다음으로 스프링이 데이터 액세스 예외를 다루는 기능을 직접 확인해보기 위해
중복된 키를 가진 정보를 등록했을 때 예외가 발생하도록 테스트 항목을 DataAccessException로 테스트를 추가해본다. - 테스트를 실행해보면 성공한 것으로 보아 DataAccessException 타입의 예외가 던져졌음을 알 수 있으므로
구체적으로 어떤 예외인지 확인해보기 위해 테스트를 실패하게 만들어본다. - 그러면 DuplicateKeyException은 DataAccessException의 서브 클래스로
DataIntegrityViolatationException의 한 종류임을 알 수 있다.
그러므로 DuplicateKeyException로 테스트의 항목으로 바꾸고 실행하면 더 정확한 예외 발생을 확인하는 테스트가 된다.
(JDBC에 따라 SQLException이 세분화해서 정의되어 SQLIntegrityConstraintViolationException이 발생할 수 있음)
/**
* 애플리케이션 컨텍스트가 없는 UserDaoTest
*/
public class UserDaoTest {
// 특정 기술을 사용한 UserDao의 구현 클래스
@Autowired
private UserDaoJdbc userDaoJdbc;
private User user1;
private User user2;
private User user3;
@Before // 매번 테스트 메소드를 실행하기 전에 먼저 실행시켜준다.
public void setUp() {
// 직접 UserDao의 오브젝트를 생성하고, 테스트용 DataSource 오브젝트를 만들어 직접 DI 한다.
userDaoJdbc = new UserDaoJdbc();
DataSource dataSource = new SingleConnectionDataSource("jdbc:mysql://localhost:/toby", "toby", "gaga", true);
userDaoJdbc.setDataSource(dataSource); // 수동 DI
// 3개의 사용자 정보를 하나씩 추가한다.
this.user1 = new User("gyumee", "박성철", "springno1");
this.user2 = new User("leegw700", "이길원", "springno2");
this.user3 = new User("bumjin", "박범진", "springno3");
}
@Test
public void addAndGet() {
// deleteAll() 메소드를 이용해 DB의 모든 내용을 삭제한다.
userDaoJdbc.deleteAll();
// 레코드의 개수가 0인지 확인한다.
assertThat(userDaoJdbc.getCount(), is(0));
// add() 메소드를 이용해 DB에 등록해본다.
userDaoJdbc.add(user1);
userDaoJdbc.add(user2);
// 첫 번째 User의 id로 get()을 실행하면 첫 번째 User의 값을 가진 오브젝트를 돌려주는지 확인한다.
User userget1 = userDaoJdbc.get(user1.getId());
assertThat(userget1.getName(), is(user1.getName()));
assertThat(userget1.getPassword(), is(user1.getPassword()));
// 두 번째 User의 id로 get()을 실행하면 두 번째 User의 값을 가진 오브젝트를 돌려주는지 확인한다.
User userget2 = userDaoJdbc.get(user2.getId());
assertThat(userget2.getName(), is(user2.getName()));
assertThat(userget2.getPassword(), is(user2.getPassword()));
}
// 레코드를 add() 했을 때 getCount()에 대한 좀 더 꼼꼼한 테스트
@Test
public void count() {
userDaoJdbc.deleteAll();
assertThat(userDaoJdbc.getCount(), is(0));
// 3개의 사용자 정보를 하나씩 추가하면서 매번 getCount()의 결과가 하나씩 증가하는지 확인한다.
userDaoJdbc.add(user1);
assertThat(userDaoJdbc.getCount(), is(1));
userDaoJdbc.add(user2);
assertThat(userDaoJdbc.getCount(), is(2));
userDaoJdbc.add(user3);
assertThat(userDaoJdbc.getCount(), is(3));
}
// 사용자 정보가 없을 때 get() 테스트
@Test(expected = EmptyResultDataAccessException.class) // 발생할 것으로 기대되는 예외 클래스를 지정해준다.
public void getUserFailure() {
userDaoJdbc.deleteAll();
assertThat(userDaoJdbc.getCount(), is(0));
// 존재하지 않는 id로 get()을 호출한다.
userDaoJdbc.get("unknown_id"); // 예외가 발생한다.
}
// 모든 사용자 정보를 가져오는 getAll() 테스트
@Test
public void getAll() {
userDaoJdbc.deleteAll();
// 데이터가 없을 때는 크기가 0인 리스트 오브젝트가 리턴돼야 한다.
List<User> users0 = userDaoJdbc.getAll();
assertThat(users0.size(), is(0));
userDaoJdbc.add(user1); // id : gyumee
List<User> users1 = userDaoJdbc.getAll();
assertThat(users1.size(), is(1));
checkSameUser(user1, users1.get(0));
userDaoJdbc.add(user2); // id : leegw700
List<User> users2 = userDaoJdbc.getAll();
assertThat(users2.size(), is(2));
checkSameUser(user1, users2.get(0));
checkSameUser(user2, users2.get(1));
userDaoJdbc.add(user3); // id : bumjin
List<User> users3 = userDaoJdbc.getAll();
assertThat(users3.size(), is(3));
checkSameUser(user3, users3.get(0)); // user3의 id 값이 알파벳순으로 가장 빠르믈 첫 번째로 가도록 한다.
checkSameUser(user1, users3.get(1));
checkSameUser(user2, users3.get(2));
}
// DataAccessException에 대한 테스트
// @Test(expected = DataAccessException.class)
@Test(expected = DuplicateKeyException.class)
public void duplicateKey() {
userDaoJdbc.deleteAll();
// 강제로 같은 사용자를 두 번 등록한다.
userDaoJdbc.add(user1);
// 여기서 예외가 발생해야 한다.
userDaoJdbc.add(user1);
}
// User 오브젝트의 내용을 비교하는 검증 코드에서 반복적으로 사용되므로 분리해놓았다.
private void checkSameUser(User user1, User user2) {
assertThat(user1.getId(), is(user2.getId()));
assertThat(user1.getName(), is(user2.getName()));
assertThat(user1.getPassword(), is(user2.getPassword()));
}
}
- 이렇게 스프링을 활용하면
DB 종류나 데이터 액세스 기술에 상관없이 키 값이 중복이 되는 상황에서는 동일한 예외가 발생하리라 기대한다.
- 하지만 DuplicateKeyException은 JDBC를 이용하는 경우만 발생한다.
- SQLException에 담긴 DB의 에러 코드를 바로 해석하는 JDBC의 경우와 달리 JPA나 하이버네이트, JDO 등에서는
각 기술이 재정의한 예외를 가져와 스프링이 최종적으로 DataAccessException으로 변환하는데,
DB의 에러 코드와 달리 이런 예외들은 세분화되어 있지 않기 때문이다. - DataAccessException이 기술에 상관없이 어느 정도 추상화된 공통 예외로 변환해주긴 하지만 근본적인 한계가 존재한다.
- 그러므로 DataAccessException을 잡아서 처리하는 코드를 만들려고 한다면
미리 학습 테스트를 만들어서 실제로 전환되는 예외의 종류를 확인해둘 필요가 있다.
- 만약 DAO에서 사용하는 기술의 종류와 상관없이 동일한 예외를 얻고 싶다면
직접 예외를 정의해두고, 각 DAO의 메소드에서 좀 더 상세한 예외 전환을 해줄 필요가 있다.
- 하이버네이트 예외의 경우라면 중첩된 예외로 SQLException이 전달되기 때문에
이를 다시 스프링의 JDBC 예외 전환 클래스의 도움을 받아서 처리할 수 있다. - 학습 테스트를 하나 더 만들어 SQLException을 직접 해석해 DataAccessException으로 변환하는 코드를 추가해본다.
- SQLErrorCodeSqlExceptionTranslator를 사용하면 에러 코드 변환에 필요한 DB의 종류를
현재 연결된 DataSource를 사용해 알아내게 된 후, DB 에러 코드를 이용해 SQLException을 코드로 직접 변환하게 된다. - 테스트를 위해 강제로 DuplicateKeyException을 발생시키면
중첩된 예외로 JDBC API에서 처음 발생한 SQLException을 내부에 갖고 있다.
이때 SQLErrorCodeSqlExceptionTranslator의 오브젝트를 만들어 SQLException을 파라미터로 넣어서
translate() 메소드를 호출해주면 SQLException을 DataAccessException 타입의 예외로 변환해준다.
변환된 DataAccessException 타입의 예외는 DuplicateKeyException 타입임을 확인하여
코드에 의한 예외 전환이 잘 됐음을 확인할 수 있다.
(JDBC에 따라 SQLException이 세분화해서 정의되어 SQLIntegrityConstraintViolationException이 발생할 수 있음) - 이제 DB에 상관없이 항상 DuplicateKeyException 예외로 전환되게 된다.
- JDBC 외의 기술을 사용할 때도 DuplicateKeyException을 발생시키려면
SQLException을 가져와서 직접 예외 전환을 하는 방법을 생각해볼 수 있다.
또는 JDBC를 사용하지만 자동으로 예외를 전환해주는 스프링의 기능을 사용할 수 없는 경우라면
스프링의 DataAccessException계층의 예외로 전환하게 할 수 있다.
- 하이버네이트 예외의 경우라면 중첩된 예외로 SQLException이 전달되기 때문에
<?xml version="1.0" encoding="UTF-8" ?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">
<bean id="userDao" class="com.gaga.springtoby.user.dao.UserDaoJdbc">
<property name="dataSource" ref="dataSource" />
</bean>
<bean id="dataSource" class="org.springframework.jdbc.datasource.SimpleDriverDataSource">
<property name="driverClass" value="com.mysql.jdbc.Driver" />
<property name="url" value="jdbc:mysql://localhost/toby" />
<property name="username" value="toby" />
<property name="password" value="gaga" />
</bean>
</beans>
/**
* 스프링 테스트 컨텍스트를 적용한 UserDaoTest
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations="/applicationContext.xml")
public class UserDaoTest {
// UserDao 인터페이스
@Autowired
private UserDao userDao;
// 에러 코드 변환에 필요한 DB의 종류를 현재 연결된 DataSource를 사용해 알아낸다.
@Autowired
private DataSource dataSource;
private User user1;
private User user2;
private User user3;
@Before // 매번 테스트 메소드를 실행하기 전에 먼저 실행시켜준다.
public void setUp() {
// 3개의 사용자 정보를 하나씩 추가한다.
this.user1 = new User("gyumee", "박성철", "springno1");
this.user2 = new User("leegw700", "이길원", "springno2");
this.user3 = new User("bumjin", "박범진", "springno3");
}
@Test
public void addAndGet() {
// deleteAll() 메소드를 이용해 DB의 모든 내용을 삭제한다.
userDao.deleteAll();
// 레코드의 개수가 0인지 확인한다.
assertThat(userDao.getCount(), is(0));
// add() 메소드를 이용해 DB에 등록해본다.
userDao.add(user1);
userDao.add(user2);
// 첫 번째 User의 id로 get()을 실행하면 첫 번째 User의 값을 가진 오브젝트를 돌려주는지 확인한다.
User userget1 = userDao.get(user1.getId());
assertThat(userget1.getName(), is(user1.getName()));
assertThat(userget1.getPassword(), is(user1.getPassword()));
// 두 번째 User의 id로 get()을 실행하면 두 번째 User의 값을 가진 오브젝트를 돌려주는지 확인한다.
User userget2 = userDao.get(user2.getId());
assertThat(userget2.getName(), is(user2.getName()));
assertThat(userget2.getPassword(), is(user2.getPassword()));
}
// 레코드를 add() 했을 때 getCount()에 대한 좀 더 꼼꼼한 테스트
@Test
public void count() {
userDao.deleteAll();
assertThat(userDao.getCount(), is(0));
// 3개의 사용자 정보를 하나씩 추가하면서 매번 getCount()의 결과가 하나씩 증가하는지 확인한다.
userDao.add(user1);
assertThat(userDao.getCount(), is(1));
userDao.add(user2);
assertThat(userDao.getCount(), is(2));
userDao.add(user3);
assertThat(userDao.getCount(), is(3));
}
// 사용자 정보가 없을 때 get() 테스트
@Test(expected = EmptyResultDataAccessException.class) // 발생할 것으로 기대되는 예외 클래스를 지정해준다.
public void getUserFailure() {
userDao.deleteAll();
assertThat(userDao.getCount(), is(0));
// 존재하지 않는 id로 get()을 호출한다.
userDao.get("unknown_id"); // 예외가 발생한다.
}
// 모든 사용자 정보를 가져오는 getAll() 테스트
@Test
public void getAll() {
userDao.deleteAll();
// 데이터가 없을 때는 크기가 0인 리스트 오브젝트가 리턴돼야 한다.
List<User> users0 = userDao.getAll();
assertThat(users0.size(), is(0));
userDao.add(user1); // id : gyumee
List<User> users1 = userDao.getAll();
assertThat(users1.size(), is(1));
checkSameUser(user1, users1.get(0));
userDao.add(user2); // id : leegw700
List<User> users2 = userDao.getAll();
assertThat(users2.size(), is(2));
checkSameUser(user1, users2.get(0));
checkSameUser(user2, users2.get(1));
userDao.add(user3); // id : bumjin
List<User> users3 = userDao.getAll();
assertThat(users3.size(), is(3));
checkSameUser(user3, users3.get(0)); // user3의 id 값이 알파벳순으로 가장 빠르믈 첫 번째로 가도록 한다.
checkSameUser(user1, users3.get(1));
checkSameUser(user2, users3.get(2));
}
// DataAccessException에 대한 테스트
// @Test(expected = DataAccessException.class)
@Test(expected = DuplicateKeyException.class)
public void duplicateKey() {
userDao.deleteAll();
// 강제로 같은 사용자를 두 번 등록한다.
userDao.add(user1);
// 여기서 예외가 발생해야 한다.
userDao.add(user1);
}
// SQLException 젼환 기능의 학습 테스트
// DataSource를 사용해 SQLException에서 직접 DuplicateKeyException으로 전환
// JDBC에 따라 SQLException이 세분화해서 정의되어 SQLIntegrityConstraintViolationException이 발생할 수 있음
@Test
public void sqlExceptionTranslate() {
userDao.deleteAll();
try {
userDao.add(user1);
userDao.add(user1);
} catch(DuplicateKeyException ex) {
SQLException sqlEx = (SQLException) ex.getRootCause();
// 코드를 이용한 SQLException을 DataAccessException 타입의 예외로 변환
SQLExceptionTranslator set = new SQLErrorCodeSQLExceptionTranslator(this.dataSource);
// 변환된 DataAccessException 타입의 예외는 DuplicateKeyException 타입임을 확인
assertThat(set.translate(null, null, sqlEx), is(DuplicateKeyException.class));
}
}
// User 오브젝트의 내용을 비교하는 검증 코드에서 반복적으로 사용되므로 분리해놓았다.
private void checkSameUser(User user1, User user2) {
assertThat(user1.getId(), is(user2.getId()));
assertThat(user1.getName(), is(user2.getName()));
assertThat(user1.getPassword(), is(user2.getPassword()));
}
}
'Java-Spring > 토비의 스프링 3.1' 카테고리의 다른 글
[토비의 스프링 3.1] Vol.1 스프링의 이해와 원리 - 서비스 추상화 (1) (0) | 2023.12.01 |
---|---|
[토비의 스프링 3.1] Vol.1 스프링의 이해와 원리 - 서비스 추상화 (0) (0) | 2023.11.30 |
[토비의 스프링 3.1] Vol.1 스프링의 이해와 원리 - 예외 (1) (0) | 2023.09.30 |
[토비의 스프링 3.1] Vol.1 스프링의 이해와 원리 - 예외 (0) (0) | 2023.09.30 |
[토비의 스프링 3.1] Vol.1 스프링의 이해와 원리 - 템플릿 (6) (0) | 2023.09.29 |
4.2) 예외 전환
JDBC의 한계
- JDBC는 자바를 이용해 DB에 접근하는 방법을 추상화된 API 형태로 정의해놓고,
각 DB 업체가 JDBC 표준에 따라 만들어진 드라이버를 제공하게 해준다.
- 내부 구현은 DB마다 다르겠지만 JDBC의 Connection, Statement, ResultSet 등의 표준 인터페이스를 통해
그 기능을 제공해주기 때문에 자바 개발자들은 표준화된 JDBC의 API에 익숙해지면
DB의 종류에 상관없이 일관된 방법으로 프로그램을 개발할 수 있다. - 하지만 DB 종류에 상관없이 사용할 수 있는 데이터 액세스 코드를 작성하는 일은 쉽지 않다.
- 내부 구현은 DB마다 다르겠지만 JDBC의 Connection, Statement, ResultSet 등의 표준 인터페이스를 통해
- 현실적으로 DB를 자유롭게 바꾸어 사용할 수 있는 DB 프로그램을 작성하는 데는 두 가지 걸림돌이 있다.
- 첫째 문제는 JDBC 코드에서 사용하는 SQL이다.
SQL은 어느 정도 표준화된 언어이고 몇 가지 표준 제약이 있긴 하지만,
대부분의 DB는 표준을 따르지 않는 비표준 문법과 기능을 제공하여
DB의 특별한 기능을 사용하거나 최적화된 SQL을 만들 때 유용하게 사용한다.
그러므로 다른 DB로 변경하려면 DAO에 담긴 SQL을 적지 않게 수정해야 한다.
이를 위해서는 DAO를 DB 별로 만들어 사용하거나 SQL을 외부에서 독립시켜서 바꿔 쓸 수 있게 해야 한다. - 두 번째 문제는 SQLException이다.
DB마다 SQL만 다른 것이 아니라 에러의 종류와 원인도 제각각이기 때문에
JDBC는 데이터 처리 중에 발생하는 다양한 예외를 그냥 SQLException 하나에 모두 담아버린다.
이때 예외가 발생한 원인은 SQLException의 getErrorCode()로 가져올 수 있는 DB 에러 코드는 DB별로 모두 다르다. - 그러므로 SQLException은 예외가 발생했을 때의 DB 상태를 담은 SQL 상태정보를 부가적으로 제공하며
getSQLState() 메소드로 예외상황에 대한 상태정보를 가져올 수 있으며 DB별로 달라지는 에러 코드를 대신할 수 있도록,
Open Group의 XOPEN SQL 스펙에 정의된 SQL 상태 코드를 따르도록 되어 있다.
- 첫째 문제는 JDBC 코드에서 사용하는 SQL이다.
DB 에러 코드 매핑을 통한 전환
- DB 종류가 바뀌더라도 DAO를 수정하지 않기 위해
SQLException의 비표준 에러 코드와 SQL 상태정보에 대한 해결책을 알아보자.
- SQLException에 담긴 SQL 상태 코드는 신뢰할 만한 게 아니므로 더 이상 고려하지 않는다.
- SQL 상태 코드는 JDBC 드라이버를 만들 때 들어가는 것이므로 같은 DB라도 드라이브를 만들 때마다 달라지기도 하지만,
DB 에러 코드는 DB에서 직접 제공해주는 것이니 버전이 올라가더라도 어느 정도 일관성이 유지된다. - 해결 방법은 DB별 에러 코드를 참고해서 발생한 예외의 원인이 무엇인지 해석해주는 기능을 만드는 것이다.
- 스프링은 DB별 에러 코드를 분류해서
스프링이 정의한 예외 클래스와 매핑해놓은 에러 코드 매핑정보 테이블을 만들어두어 이를 이용한다. - JdbcTemplate은 SQLException을 단지 런타임 예외인 DataAccessException으로 포장하는 것이 아니라
DB의 에러 코드를 DataAccessException 계층구조의 클래스로 매핑해준다.
전환되는 JdbcTemplate에서 던지는 예외는 모두 DataAccessException의 서브클래스 타입이다. - 그러므로 드라이버나 DB 메타정보를 참고해서 DB 종류를 확인하고 DB별로 미리 준비된 매핑정보를 참고해서
적절한 예외 클래스를 선택하기 때문에 DB가 달라져도 같은 종류의 에러라면 동일한 예외를 받을 수 있다. - 만약 개발 정책으로 인해 애플리케이션에서 직접 정의한 예외를 발생시키고 싶을 경우,
이전과 같이 예외를 전환해주는 코드를 넣으면 된다.
DAO 인터페이스와 DataAccessException 계층구조
- DataAccessException은 JDBC의 SQLException을 전환하는 용도로만 만들어진 건 아니다.
JDBC 외의 자바 데이터 액세스 기술(JDO, JPA, ORM, iBatis 등)에서 발생하는 예외에도 적용된다.
- DataAccessException은 의미가 같은 예외라면
데이터 액세스 기술의 종류와 상관없이 일관된 예외가 발생하도록 만들어주어 독립적인 추상화된 예외를 제공한다. - 스프링은 왜 이렇게 DataAccessException 계층구조를 이용해 기술에 독립적인 예외를 정의하고 사용할까?
- DataAccessException은 의미가 같은 예외라면
- DAO는 인터페이스를 사용해 구체적인 클래스 정보와 구현 방법을 감추고, DI를 통해 제공되도록 만드는 것이 바람직하다.
- DAO는 데이터 액세스 로직을 담은 코드를 성격이 다른 코드에서 분리해놓을 수 있다.
- DAO를 사용하는 쪽에서는 DAO가 내부에서 어떤 데이터 액세스 기술을 사용하는지 신경 쓰지 않아도 되며
특정 기술에 독립적인 단순한 오브젝트를 주고받으면서 데이터 액세스 기능을 사용하기만 하면 된다. - 하지만 DAO의 사용 기술과 구현 코드는 전략 패턴과 DI를 통해서 DAO를 사용하는 클라이언트에게 감출 수 있지만,
DAO가 사용하는 데이터 액세스 기술의 API가 예외를 던지기 때문에 메소드 선언에 나타나는 예외정보가 문제가 될 수 있다. - 데이터 액세스 기술의 API는 자신만의 독자적인 예외를 던지기 때문에 인터페이스 메소드를 바꿔주면 모르겠지만,
SQLException을 던지도록 선언한 인터페이스 메소드는 사용할 수 없다. - 결국 인터페이스로 메소드의 구현은 추상화했지만 구현 기술마다 던지는 예외가 다르기 때문에 메소드의 선언이 달라진다.
- 가장 단순한 해결 방법은 모든 예외를 다 받아주는 throws Exception으로 선언하는 것이지만 무책임한 선언이다.
- JDBC보다는 늦게 등장한 JDO, JPA, Hibernate 등의 기술은 SQLException을 체크 예외 대신 런타임 예외를 사용한다.
그러므로 JDBC API를 직접 사용하는 DAO만 메소드 내에서 런타임 예외로 포장해서 던져준다면
DAO에서는 사용하는 기술에 완전히 독립적인 인터페이스 선언이 가능해진다.
- 하지만 데이터 액세스 기술이 달라지면 같은 상황에서도 다른 종류의 예외가 던져진다.
- 따라서 DAO를 사용하는 클라이언트 입장에서는 DAO의 사용 기술에 따라서 예외 처리 방법이 달라져야 한다.
- 단지 인터페이스로 추상화하고, 일부 기술에서 발생하는 체크 예외를 런타임 예외로 전환하는 것만으론 불충분하다.
- 그래서 DataAccessException은 자바의 주요 데이터 액세스 기술에서 발생할 수 있는 대부분의 예외를 추상화하고 있다.
- 스프링의 DataAccessException은 이런 일부 기술에서만 공통적으로 나타나는 예외를 포함해서
데이터 액세스 기술에서 발생 가능한 대부분의 예외를 계층구조로 분류해놓았다. - 그러므로 JDBC, JDO, JPA, 하이버네이트 등 기술의 종류에 상관 없이 데이터 액세스 기술을 부정확하게 사용했을 때는
InvalidDataAccessResourceUsageException 예외가 던져지게 된다.
(이를 다시 세분화하면 JDBC - BadSqlGrammerException, 하이버네이트 - HibernateQueryException 등) - 또한 오브젝트/엔티티 단위로 정보를 업데이트할 때 낙관적인 락킹이 발생할 경우는
ObjectOptimisticLockingFailureException 예외가 던져지게 된다.
(이를 다시 세분화하면 JDBC - JdbcOptimisticLockingFailureException,
하이버네이트 - HibernateOptimisticLockingFailureException 등) - DataAccessException 계층구조에는 템플릿 메소드나 DAO 메소드에서 직접 활용할 수 있는 예외도 정의되어 있다.
- 예를 들어 쿼리 실행 결과가 하나 이상의 로우를 가져올 때 사용하는
IncorrectResultSizeDataAccessException, EmptyResultDataAccessException
- 스프링의 DataAccessException은 이런 일부 기술에서만 공통적으로 나타나는 예외를 포함해서

- 그러므로 스프링의 데이터 액세스 지원 기술을 이용해 DAO를 만들면 사용 기술에 독립적인 일관성 있는 예외를 던질 수 있다.
- 결국 인터페이스 사용, 런타임 예외 전환과 함께 DataAccessException 예외 추상화를 적용하면
데이터 액세스 기술과 구현 방법에 독립적인 이상적인 DAO를 만들 수 있다.
기술에 독립적인 UserDao 만들기
- UserDao 클래스를 인터페이스와 구현으로 분리해보자.
- 인터페이스와 구현 클래스의 이름을 정하기 위해서는 인터페이스 이름 앞에는 I라는 접두어를 붙이는 방법도 있고,
인터페이스 이름은 가장 단순하게 하고 구현 클래스는 각각의 특징을 따르는 이름을 붙이는 경우도 있다. - 사용자 처리 DAO의 이름은 UserDao라고 하고, JDBC를 이용해 구현한 클래스의 이름을 UserDaoJdbc라고 하자.
- UserDao 인터페이스에는 UserDao 클래스에서 DAO의 기능을 사용하려는 클라이언트들이 필요한 것만 추출해내면 된다.
- 이때 UserDao의 구현 방법에 따라 변경될 수 있으며 UserDao를 사용하는 클라이언트가 알 필요가 없으므로 제외한다.
- 기존의 UserDao 클래스는 UserDaoJdbc로 변경하고 UserDao 인터페이스를 구현하도록 implements로 선언한다.
- 그리고 스프링 설정파일에서 userDao 빈 클래스의 이름은 UserDao에서 UserDaoJdbc로 바꿔준다.
- 인터페이스와 구현 클래스의 이름을 정하기 위해서는 인터페이스 이름 앞에는 I라는 접두어를 붙이는 방법도 있고,

/**
* UserDao 인터페이스
*/
public interface UserDao {
void add(User user);
User get(String id);
List<User> getAll();
void deleteAll();
int getCount();
}
<?xml version="1.0" encoding="UTF-8" ?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">
<bean id="userDao" class="com.gaga.springtoby.user.dao.UserDaoJdbc">
<property name="dataSource" ref="dataSource" />
</bean>
<bean id="dataSource" class="org.springframework.jdbc.datasource.SimpleDriverDataSource">
<property name="driverClass" value="com.mysql.jdbc.Driver" />
<property name="url" value="jdbc:mysql://localhost/toby" />
<property name="username" value="toby" />
<property name="password" value="gaga" />
</bean>
</beans>
/**
* JdbcTemplate를 적용한 UserDao 구현 클래스
*/
public class UserDaoJdbc implements UserDao {
private JdbcTemplate jdbcTemplate;
// JdbcTemplate 초기화
public void setDataSource(DataSource dataSource) {
// DataSource 오브젝트는 JdbcTemplate을 만든 후에는 사용하지 않으니 저장해두지 않아도 된다.
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
// 재사용 가능하도록 독립시킨 RowMapper
private RowMapper<User> userMapper = new RowMapper<User>() {
// ResultSet한 로우의 결과를 오브젝트에 매핑해주는 RowMapper
@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;
}
};
/* 새로운 사용자를 생성 */
// JdbcTemplate의 update() 메소드를 적용
// 스프링의 JdbcTemplate은 예외처리 전략을 따르고 있으므로
// JdbcTemplate 템플릿과 콜백 안에서 발생하는 모든 SQLException을 런타임 예외인 DataAccessException으로 포장해서 던져준다.
public void add(final User user) {
this.jdbcTemplate.update("insert into users(id, name, password) values(?,?,?)", user.getId(), user.getName(), user.getPassword());
}
/* 아이디를 가지고 사용자 정보 읽어오기 */
// JdbcTemplate의 queryForObject() 메소드를 적용
public User get(String id) {
return this.jdbcTemplate.queryForObject("select * from users where id = ?", new Object[] {id}, this.userMapper);
}
/* 모든 사용자 정보 읽어오기 */
// JdbcTemplate의 query() 메소드를 적용
public List<User> getAll() {
return this.jdbcTemplate.query("select * from users order by id", this.userMapper);
}
/* 모든 사용자 삭제하기 */
// JdbcTemplate의 update() 메소드를 적용
public void deleteAll() {
this.jdbcTemplate.update("delete from users");
}
/* 사용자 테이블의 레코드 개수 읽어오기 */
// JdbcTemplate의 queryForInt() 메소드를 적용 (Deprecated)
// JdbcTemplate의 queryForObject() 메소드를 적용
public int getCount() {
// return this.jdbcTemplate.queryForInt("select count(*) from users");
return this.jdbcTemplate.queryForObject("select count(*) from users", Integer.class);
}
}
- 기존 UserDao 클래스의 테스트 코드에서 UserDao 인스턴스 변수 선언도 UserDaoJdbc로 변경해야 할까?
- @Autowired는 스프링의 컨텍스트 내에서 정의된 빈 중에서 인스턴스 변수에 주입 가능한 타입의 빈을 찾아준다.
- UserDao는 UserDaoJdbc가 구현한 인터페이스이므로 UserDaoJdbc 클래스로 정의된 빈을 넣어도 문제가 없다.
- 만약 테스트의 관심이 구현 기술에 상관없이 DAO의 기능이 동작하는 데만 관심이 있다면,
UserDao 인터페이스로 받아서 테스트하는 편이 낫다.
반면에 특정 기술을 사용한 UserDao의 구현 내용에 관심을 가지고 테스트하려면
테스트에서 @Autowired로 DI 받을 때 UserDaoJdbc나 UserDaoHibernate 같이 특정 타입을 사용해야 한다. - 다음으로 스프링이 데이터 액세스 예외를 다루는 기능을 직접 확인해보기 위해
중복된 키를 가진 정보를 등록했을 때 예외가 발생하도록 테스트 항목을 DataAccessException로 테스트를 추가해본다. - 테스트를 실행해보면 성공한 것으로 보아 DataAccessException 타입의 예외가 던져졌음을 알 수 있으므로
구체적으로 어떤 예외인지 확인해보기 위해 테스트를 실패하게 만들어본다. - 그러면 DuplicateKeyException은 DataAccessException의 서브 클래스로
DataIntegrityViolatationException의 한 종류임을 알 수 있다.
그러므로 DuplicateKeyException로 테스트의 항목으로 바꾸고 실행하면 더 정확한 예외 발생을 확인하는 테스트가 된다.
(JDBC에 따라 SQLException이 세분화해서 정의되어 SQLIntegrityConstraintViolationException이 발생할 수 있음)
/**
* 애플리케이션 컨텍스트가 없는 UserDaoTest
*/
public class UserDaoTest {
// 특정 기술을 사용한 UserDao의 구현 클래스
@Autowired
private UserDaoJdbc userDaoJdbc;
private User user1;
private User user2;
private User user3;
@Before // 매번 테스트 메소드를 실행하기 전에 먼저 실행시켜준다.
public void setUp() {
// 직접 UserDao의 오브젝트를 생성하고, 테스트용 DataSource 오브젝트를 만들어 직접 DI 한다.
userDaoJdbc = new UserDaoJdbc();
DataSource dataSource = new SingleConnectionDataSource("jdbc:mysql://localhost:/toby", "toby", "gaga", true);
userDaoJdbc.setDataSource(dataSource); // 수동 DI
// 3개의 사용자 정보를 하나씩 추가한다.
this.user1 = new User("gyumee", "박성철", "springno1");
this.user2 = new User("leegw700", "이길원", "springno2");
this.user3 = new User("bumjin", "박범진", "springno3");
}
@Test
public void addAndGet() {
// deleteAll() 메소드를 이용해 DB의 모든 내용을 삭제한다.
userDaoJdbc.deleteAll();
// 레코드의 개수가 0인지 확인한다.
assertThat(userDaoJdbc.getCount(), is(0));
// add() 메소드를 이용해 DB에 등록해본다.
userDaoJdbc.add(user1);
userDaoJdbc.add(user2);
// 첫 번째 User의 id로 get()을 실행하면 첫 번째 User의 값을 가진 오브젝트를 돌려주는지 확인한다.
User userget1 = userDaoJdbc.get(user1.getId());
assertThat(userget1.getName(), is(user1.getName()));
assertThat(userget1.getPassword(), is(user1.getPassword()));
// 두 번째 User의 id로 get()을 실행하면 두 번째 User의 값을 가진 오브젝트를 돌려주는지 확인한다.
User userget2 = userDaoJdbc.get(user2.getId());
assertThat(userget2.getName(), is(user2.getName()));
assertThat(userget2.getPassword(), is(user2.getPassword()));
}
// 레코드를 add() 했을 때 getCount()에 대한 좀 더 꼼꼼한 테스트
@Test
public void count() {
userDaoJdbc.deleteAll();
assertThat(userDaoJdbc.getCount(), is(0));
// 3개의 사용자 정보를 하나씩 추가하면서 매번 getCount()의 결과가 하나씩 증가하는지 확인한다.
userDaoJdbc.add(user1);
assertThat(userDaoJdbc.getCount(), is(1));
userDaoJdbc.add(user2);
assertThat(userDaoJdbc.getCount(), is(2));
userDaoJdbc.add(user3);
assertThat(userDaoJdbc.getCount(), is(3));
}
// 사용자 정보가 없을 때 get() 테스트
@Test(expected = EmptyResultDataAccessException.class) // 발생할 것으로 기대되는 예외 클래스를 지정해준다.
public void getUserFailure() {
userDaoJdbc.deleteAll();
assertThat(userDaoJdbc.getCount(), is(0));
// 존재하지 않는 id로 get()을 호출한다.
userDaoJdbc.get("unknown_id"); // 예외가 발생한다.
}
// 모든 사용자 정보를 가져오는 getAll() 테스트
@Test
public void getAll() {
userDaoJdbc.deleteAll();
// 데이터가 없을 때는 크기가 0인 리스트 오브젝트가 리턴돼야 한다.
List<User> users0 = userDaoJdbc.getAll();
assertThat(users0.size(), is(0));
userDaoJdbc.add(user1); // id : gyumee
List<User> users1 = userDaoJdbc.getAll();
assertThat(users1.size(), is(1));
checkSameUser(user1, users1.get(0));
userDaoJdbc.add(user2); // id : leegw700
List<User> users2 = userDaoJdbc.getAll();
assertThat(users2.size(), is(2));
checkSameUser(user1, users2.get(0));
checkSameUser(user2, users2.get(1));
userDaoJdbc.add(user3); // id : bumjin
List<User> users3 = userDaoJdbc.getAll();
assertThat(users3.size(), is(3));
checkSameUser(user3, users3.get(0)); // user3의 id 값이 알파벳순으로 가장 빠르믈 첫 번째로 가도록 한다.
checkSameUser(user1, users3.get(1));
checkSameUser(user2, users3.get(2));
}
// DataAccessException에 대한 테스트
// @Test(expected = DataAccessException.class)
@Test(expected = DuplicateKeyException.class)
public void duplicateKey() {
userDaoJdbc.deleteAll();
// 강제로 같은 사용자를 두 번 등록한다.
userDaoJdbc.add(user1);
// 여기서 예외가 발생해야 한다.
userDaoJdbc.add(user1);
}
// User 오브젝트의 내용을 비교하는 검증 코드에서 반복적으로 사용되므로 분리해놓았다.
private void checkSameUser(User user1, User user2) {
assertThat(user1.getId(), is(user2.getId()));
assertThat(user1.getName(), is(user2.getName()));
assertThat(user1.getPassword(), is(user2.getPassword()));
}
}
- 이렇게 스프링을 활용하면
DB 종류나 데이터 액세스 기술에 상관없이 키 값이 중복이 되는 상황에서는 동일한 예외가 발생하리라 기대한다.
- 하지만 DuplicateKeyException은 JDBC를 이용하는 경우만 발생한다.
- SQLException에 담긴 DB의 에러 코드를 바로 해석하는 JDBC의 경우와 달리 JPA나 하이버네이트, JDO 등에서는
각 기술이 재정의한 예외를 가져와 스프링이 최종적으로 DataAccessException으로 변환하는데,
DB의 에러 코드와 달리 이런 예외들은 세분화되어 있지 않기 때문이다. - DataAccessException이 기술에 상관없이 어느 정도 추상화된 공통 예외로 변환해주긴 하지만 근본적인 한계가 존재한다.
- 그러므로 DataAccessException을 잡아서 처리하는 코드를 만들려고 한다면
미리 학습 테스트를 만들어서 실제로 전환되는 예외의 종류를 확인해둘 필요가 있다.
- 만약 DAO에서 사용하는 기술의 종류와 상관없이 동일한 예외를 얻고 싶다면
직접 예외를 정의해두고, 각 DAO의 메소드에서 좀 더 상세한 예외 전환을 해줄 필요가 있다.
- 하이버네이트 예외의 경우라면 중첩된 예외로 SQLException이 전달되기 때문에
이를 다시 스프링의 JDBC 예외 전환 클래스의 도움을 받아서 처리할 수 있다. - 학습 테스트를 하나 더 만들어 SQLException을 직접 해석해 DataAccessException으로 변환하는 코드를 추가해본다.
- SQLErrorCodeSqlExceptionTranslator를 사용하면 에러 코드 변환에 필요한 DB의 종류를
현재 연결된 DataSource를 사용해 알아내게 된 후, DB 에러 코드를 이용해 SQLException을 코드로 직접 변환하게 된다. - 테스트를 위해 강제로 DuplicateKeyException을 발생시키면
중첩된 예외로 JDBC API에서 처음 발생한 SQLException을 내부에 갖고 있다.
이때 SQLErrorCodeSqlExceptionTranslator의 오브젝트를 만들어 SQLException을 파라미터로 넣어서
translate() 메소드를 호출해주면 SQLException을 DataAccessException 타입의 예외로 변환해준다.
변환된 DataAccessException 타입의 예외는 DuplicateKeyException 타입임을 확인하여
코드에 의한 예외 전환이 잘 됐음을 확인할 수 있다.
(JDBC에 따라 SQLException이 세분화해서 정의되어 SQLIntegrityConstraintViolationException이 발생할 수 있음) - 이제 DB에 상관없이 항상 DuplicateKeyException 예외로 전환되게 된다.
- JDBC 외의 기술을 사용할 때도 DuplicateKeyException을 발생시키려면
SQLException을 가져와서 직접 예외 전환을 하는 방법을 생각해볼 수 있다.
또는 JDBC를 사용하지만 자동으로 예외를 전환해주는 스프링의 기능을 사용할 수 없는 경우라면
스프링의 DataAccessException계층의 예외로 전환하게 할 수 있다.
- 하이버네이트 예외의 경우라면 중첩된 예외로 SQLException이 전달되기 때문에
<?xml version="1.0" encoding="UTF-8" ?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">
<bean id="userDao" class="com.gaga.springtoby.user.dao.UserDaoJdbc">
<property name="dataSource" ref="dataSource" />
</bean>
<bean id="dataSource" class="org.springframework.jdbc.datasource.SimpleDriverDataSource">
<property name="driverClass" value="com.mysql.jdbc.Driver" />
<property name="url" value="jdbc:mysql://localhost/toby" />
<property name="username" value="toby" />
<property name="password" value="gaga" />
</bean>
</beans>
/**
* 스프링 테스트 컨텍스트를 적용한 UserDaoTest
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations="/applicationContext.xml")
public class UserDaoTest {
// UserDao 인터페이스
@Autowired
private UserDao userDao;
// 에러 코드 변환에 필요한 DB의 종류를 현재 연결된 DataSource를 사용해 알아낸다.
@Autowired
private DataSource dataSource;
private User user1;
private User user2;
private User user3;
@Before // 매번 테스트 메소드를 실행하기 전에 먼저 실행시켜준다.
public void setUp() {
// 3개의 사용자 정보를 하나씩 추가한다.
this.user1 = new User("gyumee", "박성철", "springno1");
this.user2 = new User("leegw700", "이길원", "springno2");
this.user3 = new User("bumjin", "박범진", "springno3");
}
@Test
public void addAndGet() {
// deleteAll() 메소드를 이용해 DB의 모든 내용을 삭제한다.
userDao.deleteAll();
// 레코드의 개수가 0인지 확인한다.
assertThat(userDao.getCount(), is(0));
// add() 메소드를 이용해 DB에 등록해본다.
userDao.add(user1);
userDao.add(user2);
// 첫 번째 User의 id로 get()을 실행하면 첫 번째 User의 값을 가진 오브젝트를 돌려주는지 확인한다.
User userget1 = userDao.get(user1.getId());
assertThat(userget1.getName(), is(user1.getName()));
assertThat(userget1.getPassword(), is(user1.getPassword()));
// 두 번째 User의 id로 get()을 실행하면 두 번째 User의 값을 가진 오브젝트를 돌려주는지 확인한다.
User userget2 = userDao.get(user2.getId());
assertThat(userget2.getName(), is(user2.getName()));
assertThat(userget2.getPassword(), is(user2.getPassword()));
}
// 레코드를 add() 했을 때 getCount()에 대한 좀 더 꼼꼼한 테스트
@Test
public void count() {
userDao.deleteAll();
assertThat(userDao.getCount(), is(0));
// 3개의 사용자 정보를 하나씩 추가하면서 매번 getCount()의 결과가 하나씩 증가하는지 확인한다.
userDao.add(user1);
assertThat(userDao.getCount(), is(1));
userDao.add(user2);
assertThat(userDao.getCount(), is(2));
userDao.add(user3);
assertThat(userDao.getCount(), is(3));
}
// 사용자 정보가 없을 때 get() 테스트
@Test(expected = EmptyResultDataAccessException.class) // 발생할 것으로 기대되는 예외 클래스를 지정해준다.
public void getUserFailure() {
userDao.deleteAll();
assertThat(userDao.getCount(), is(0));
// 존재하지 않는 id로 get()을 호출한다.
userDao.get("unknown_id"); // 예외가 발생한다.
}
// 모든 사용자 정보를 가져오는 getAll() 테스트
@Test
public void getAll() {
userDao.deleteAll();
// 데이터가 없을 때는 크기가 0인 리스트 오브젝트가 리턴돼야 한다.
List<User> users0 = userDao.getAll();
assertThat(users0.size(), is(0));
userDao.add(user1); // id : gyumee
List<User> users1 = userDao.getAll();
assertThat(users1.size(), is(1));
checkSameUser(user1, users1.get(0));
userDao.add(user2); // id : leegw700
List<User> users2 = userDao.getAll();
assertThat(users2.size(), is(2));
checkSameUser(user1, users2.get(0));
checkSameUser(user2, users2.get(1));
userDao.add(user3); // id : bumjin
List<User> users3 = userDao.getAll();
assertThat(users3.size(), is(3));
checkSameUser(user3, users3.get(0)); // user3의 id 값이 알파벳순으로 가장 빠르믈 첫 번째로 가도록 한다.
checkSameUser(user1, users3.get(1));
checkSameUser(user2, users3.get(2));
}
// DataAccessException에 대한 테스트
// @Test(expected = DataAccessException.class)
@Test(expected = DuplicateKeyException.class)
public void duplicateKey() {
userDao.deleteAll();
// 강제로 같은 사용자를 두 번 등록한다.
userDao.add(user1);
// 여기서 예외가 발생해야 한다.
userDao.add(user1);
}
// SQLException 젼환 기능의 학습 테스트
// DataSource를 사용해 SQLException에서 직접 DuplicateKeyException으로 전환
// JDBC에 따라 SQLException이 세분화해서 정의되어 SQLIntegrityConstraintViolationException이 발생할 수 있음
@Test
public void sqlExceptionTranslate() {
userDao.deleteAll();
try {
userDao.add(user1);
userDao.add(user1);
} catch(DuplicateKeyException ex) {
SQLException sqlEx = (SQLException) ex.getRootCause();
// 코드를 이용한 SQLException을 DataAccessException 타입의 예외로 변환
SQLExceptionTranslator set = new SQLErrorCodeSQLExceptionTranslator(this.dataSource);
// 변환된 DataAccessException 타입의 예외는 DuplicateKeyException 타입임을 확인
assertThat(set.translate(null, null, sqlEx), is(DuplicateKeyException.class));
}
}
// User 오브젝트의 내용을 비교하는 검증 코드에서 반복적으로 사용되므로 분리해놓았다.
private void checkSameUser(User user1, User user2) {
assertThat(user1.getId(), is(user2.getId()));
assertThat(user1.getName(), is(user2.getName()));
assertThat(user1.getPassword(), is(user2.getPassword()));
}
}
'Java-Spring > 토비의 스프링 3.1' 카테고리의 다른 글
[토비의 스프링 3.1] Vol.1 스프링의 이해와 원리 - 서비스 추상화 (1) (0) | 2023.12.01 |
---|---|
[토비의 스프링 3.1] Vol.1 스프링의 이해와 원리 - 서비스 추상화 (0) (0) | 2023.11.30 |
[토비의 스프링 3.1] Vol.1 스프링의 이해와 원리 - 예외 (1) (0) | 2023.09.30 |
[토비의 스프링 3.1] Vol.1 스프링의 이해와 원리 - 예외 (0) (0) | 2023.09.30 |
[토비의 스프링 3.1] Vol.1 스프링의 이해와 원리 - 템플릿 (6) (0) | 2023.09.29 |