본문 바로가기

토비의 스프링 정리

토비의 스프링 - 4.2 예외 전환

4.2.1 JDBC의 한계

  • JDBC 표준 인터페이스를 통해 DB 종류에 상관없이 일관된 방법으로 프로그램을 개발할 수 있음
  • 하지만, DB 종류에 상관없이 사용할 수 있는 데이터 엑세스 코드를 작성하는 일은 쉽지 않음
  • 또한, 유연한 코드를 보장하지 못함
  • 두 가지 걸림돌이 존재
    1. 비표준 SQL
    2. 호환성 없는 SQLException의 DB 에러 정보

4.2.2 비표준 SQL

  • SQL은 어느정도 표준화된 언어이고, 몇가지 표준이 존재
  • 하지만, 비표준 문법은 최적화된 SQL 및 DB의 특별한 기능을 제공하기 위해 사용됨
  • DB는 자주 변경되지 않으므로 대부분의 DB는 비표준 SQL을 지원하고 있음
  • 해결책
    • 호환 가능한 표준 SQL만 사용
      • 표준 SQL만 사용하면 페이징 쿼리에서 부터 문제가 됨. 현실성 없음
    • DB별 별도의 DAO를 만들거나 SQL을 외부에 독립시켜 DB에 따라 변경해 사용
    •  💡 7장에서 자세히 다룰 것임

4.2.3 호환성 없는 SQLException의 DB 에러 정보

  • SQLException 발생 원인은 수백여 가지 존재
  • 또한, DB 마다 에러의 종류와 원인이 제각각임
  • JDBC API는 SQLException 한 가지만 던지도록 되어 있어 안에 담긴 에러 코드상태정보를 참조해야 함
  • 에러코드는 getErrorCode()를 통해 가져올 수 있음
  • 에러코드는 DB 마다 달라지므로 DB마다 처리를 다르게 해 주어야 함
  • 때문에, getSQLState() 상태정보를 가져올 수 있음
  • DB별로 달라지는 에러 코드를 대신할 수 있도록, Open Group의 XOPEN SQL 스펙에 정의된 상태 코드를 따르도록 되어 있음
  • 하지만, JDBC 드라이버에서 SQLException의 상태정보를 정확히 만들어주지 않음
  • 때문에, SQL 상태 코드를 믿고 결과를 파악하는 것은 매우 위험함
// DB마다 다르게 처리
if(e.getErrorCode() == MysqlErrorNumbers.ER_DUP_ENTRY){...}

💡 SQLException만으로 DB에 독립적인 유연한 코드를 작성하는 것은 불가능에 가까움

 

4.2.4 DB 에러 코드 매핑을 통한 전환

  • SQLException의 상태정보는 신뢰할 수 없으므로 고려하지 않음
  • 차라리, DB 전용 에러 코드가 더 정확한 정보
  • DB 종류에 상관없이 동일 상황(중복키 존재 예외, ...)에서 일관된 예외를 얻을 수 있으면 효과적인 대응 가능
  • 스프링은 예외의 효과적 대응을 위해 DataAccessException을 제공하고 있음
  • DataAccessException는 SQLException을 대체할 수 있는 런타임 예외이고, 세분화된 서브 클래스를 정의하고 있음
  • DB마다 에러 코드가 제각각이기 때문에, 스프링은 DB 별로 에러 코드를 분류해서 스프링이 정의한 예외 클래스와 매핑해놓은 에러 코드 매핑정보 테이블을 만들고 이용함
  • JdbcTemplate는 SQLException을 런타임 예외인 DataAccessException로 포장할 뿐만이 아니라, DB의 에러 코드를 DataAccessException 계층구조 클래스 중 하나로 매핑함
  • JdbcTemplate가 던지는 예외는 모두 DataAccessException의 서브클래스 타입임
  • 드라이버나 DB 메타 정보를 참고하여 DB 종류를 확인하고, 매핑 정보를 참고해서 적절한 예외 클래스를 선택하기 때문에 DB에 상관 없이 같은 종류의 에러면 동일한 예외를 받음

4.2.5 중복 키로 발생하는 예외

  • JdbcTemplate는 체크 예외인 SQLException을 런타임 예외인 DataAccessException로 변경함
  • 때문에, 별다른 예외 포장을 하지 않아도 됨
  • 또한, 중복 키로 발생하는 예외는 DataAccessException의 서브 클래스인 DuplicateKeyException으로 매핑돼서 던져짐
  • DuplicateKeyException는 DB별로 미리 준비된 에러 코드와 비교 및 매핑 된것임
  • 중복 키는 충분히 대응 가능한 문제이기 때문에 DuplicateUserIdException 체크 예외로 변환해도 됨
public void add() throws DuplicateUserIdException {
    try{
        ... //jdbcTemplate 코드
    } catch(DuplicateKeyException e){
        throw new DuplicateUserIdException(e); 
    }
}
  • JDBC 4.0 부터 SQLException을 DataAccessException처럼 세분화 하고 있음
  • 하지만, 여전히 SQLException은 체크 예외라는 문제가 있음
  • 때문에, DataAccessException를 사용하는 것이 이상적 방법임

4.2.6 DAO 인터페이스

  • DAO를 만들어서 사용하는 이유는 관심사 분리와 클라이언트에게 구체적 방법을 몰라도 사용할 수 있게 하기 위함임
  • JDBC 외에 JPA, JDO 등과 같은 테이터 액세스 기술들이 존재
  • 각 기술들은 서로 다른 예외를 발생함
  • 때문에 메소드 선언에 나타나는 예외정보가 달라 문제가 됨
  • throw Exception을 하는 것은 무책임함
  • DataAccessException는 기술에 관계없이 일관된 예외가 발생하도록 함
  • 또한, 런타임 예외이기 때문에 메소드 선언에 예외 던짐을 하지 않아도 됨
public void add(User user) throws PersistentException; //JPA
public void add(User user) throws HibernateException; //Hibernate
public void add(User user) throws JdoException; //JDO

public void add(User user);
  • 하지만, 단순히 체크 예외를 런타임 예외로 변경하는 것은 DAO를 인터페이스로 만들기 부족함
  • 메소드 선언에 예외가 없다고 하더라도, 예외 발생시 DAO를 호출한 클라이언트가 DB에 맞춰 예외를 처리해야 하기 때문에 DB 기술에 의존적일 수 밖에 없음

4.2.7 DataAccessException 계층구조

  • 스프링은 다양한 데이터 액세스 기술을 사용할 때 발생하는 예외를 추상화해서 DataAccessException 계층구조 안에 정리했음
  • JdbcTemplate는 SQLException의 에러 코드를 DB 별로 매핑해 그에 해당하는 의미 있는 DataAccessException의 서브클래스 중 하나로 전환해서 던짐
  • DataAccessException는 데이터 액세스 기술에서 발생하는 대부분의 예외를 추상화 하고 있음
  • 다른 종류의 예외 및 낙관적인 락킹이 발생을 해도 적절한 예외로 전환해서 던짐
    • 낙관적 락킹(Optimistic Locking)
      • 두 명 이상의 사용자가 동시에 조회하고 순차적으로 업데이트를 할 때, 뒤늦게 업데이트 한 것이 먼저 업데이터를 한 것을 덮어쓰지 않도록 하는 기능
  • JDBC는 낙관적인 락킹이 존재하지 않음
  • 낙관적 락킹을 위해 DataAccessException의 계층 구조에 맞게 예외를 추가할 수 있음
  • 기술마다 낙관적 락킹 예외 발생 이 다르지만, 해당 예외는 OptimisticLockingFailureException를 상속하기 때문에 해당 예외 클래스를 상속받으면 됨

4.2.8 UserDao에 인터페이스 적용

  • 클라이언트에서 필요한 것을 추출
  • setDataSource()는 UserDao 구현 방법(JDBC)에 따라 변경될 수 있는 메소드이고, 클라이언트가 알 필요가 없기 때문에 추출하지 않음
  • @Autowired는 스프링 컨텍스트 내에서 정의된 빈 중에서 인스턴스 변수에 주입 가능한 타입을 찾아 빈을 찾음
  • UserDaoJdbc는 UserDao의 상속을 받기 때문에 굳이 변경하지 않아도 됨
  • 만약 테스트가 UserDao의 구현 내용에 관심이 있으면 변경을 해야 함
  • 인터페이스와 구현을 분리함으로써 DI가 적용 되었음

public interface UserDao {
    void add(User user);
    User get(String id);
    List<User> getAll();
    void deleteAll();
    int getCount();
}

public class UserDaoJdbc implements UserDao{
    ...
}

public class UserDaoTest{
    @Autowired
    private UserDao dao;
    ...
}
<bean id="userDao" class="com.ksb.spring.UserDaoJdbc">
    <property name="dataSource" ref="dataSource"/>
</bean>

4.2.9 중복키 예외 테스트

  • 데이터 액세스 예외 학습 테스트임
  • 같은 사용자를 연속으로 등록 했을 때 발생하는 예외를 확인하는 테스트
  • DataAccessException의 예외가 던저져야 함
  • 구체적으로 DuplicatekeyException이 던저질 것임
public class UserDaoTest {
    ...
    @Test(expected = DataAccessException.class)
//  @Test(expected = DuplicatekeyException.class)
    public void duplicateKey(){
        dao.deleteAll();

        dao.add(user1);
        dao.add(user1);
    }
}

4.2.10 DataAccessException 활용 시 주의사항

  • DuplicatekeyException는 JDBC에서만 발생함
  • 나머지 기술들에서 예외가 발생하면 스프링이 최종적으로 DataAccessException로 변환함
  • 이유는 JDBC를 제외한 나머지 기술들은 세분화가 되지 않았기 때문임
  • 예를들어, 하이버네이트에서 중복 키 예외 시 ConstraintViolationException 발생 함
  • 해당 예외를 스프링이 포괄적인 DataIntegrityViolationException로 변환함
  • 물론 DuplicateKeyException도 DataIntegrityViolationException의 한 종류임
  • 하지만, DataIntegrityViolationException는 제약조건 위반 예외 상황에서도 발생하기 때문에 DuplicateKeyException보다 효용성이 떨어짐
  • DuplicatekeyException를 잡아서 처리하는 코드를 만드려면 미리 학습 테스트를 만들어 실제 전환되는 예외를 확인해서 적절히 처리해 줘야 함

4.2.11 스프링에서 SQLException을 DataAcceessException으로 전환하는 방법

  • 여러 방법이 있음
  • DB 에러 코드를 이용하는 것이 효과적임
  • SQLException을 코드에서 직접 전환하고 싶으면 SQLExceptionTranslator 인터페이스를 구현한 SQLErrorCodeSQLExceptionTranslator를 사용하면 됨
  • SQLErrorCodeSQLExceptionTranslator는 DataSource를 필요로 함
  • translate()는 SQLException을 DataAccessException 타입의 예외로 변환함
//왜 안되지?
public class UserDaoTest {
    @Autowired
    UserDao dao;
    @Autowired
    DataSource dataSource;
    ...

    @Test
    public void sqlExceptionTranslate() {
        dao.deleteAll();

        dao.add(user1);
        dao.add(user1);
        try {
            dao.add(user1);
            dao.add(user1);
        } catch (DuplicateKeyException ex) {
            SQLException sqlEx = (SQLException) ex.getRootCause();
            SQLExceptionTranslator set =
                    new SQLErrorCodeSQLExceptionTranslator(this.dataSource);
            assertThat(set.translate(null, null, sqlEx),
                    is(DuplicateKeyException.class));
        }
    }
}