본문 바로가기

Spring

4.2 예외 전환


예외 전환의 목적

  1. 굳이 필요하지 않은 예외 처리를 줄여줌
  2. 로우레벨의 예외를 좀 더 의미있는 예외로 바꿔줌

JdbcTemplate의 DataAccessException

  • SQLException을 런타임 예외로 포장
  • 상세한 예외정보를 의미있고 일관성 있는 예외로 전환해서 추상화

4.2.1 JDBC의 한계

JDBC

  • 자바 표준 JDK에서 가장 많이 사용되는 기능 중 하나
  • DB 접근 방법을 추상화된 API 형태로 정의
  • 각 DB 업체가 JDBC 표준을 따라 만들어진 드라이버를 제공
  • DB에 상관없이 일관된 방법으로 개발 가능

JDBC API로 개발할 경우

  • DB 프로그램 개발 방법을 학습하는 부담을 줄여줌
  • 그러나, DB를 자유롭게 변경해서 사용할 수 있는 유연한 코드를 보장해주지는 않음

비표준 SQL

SQL

  • 어느 정도 표준화된 언어 / 표준 규약 존재
  • 그러나 대부분의 DB는 비표준 문법과 기능을 제공
  • 비표준 SQL이 DAO에 들어가면 해당 DAO에 DB에 종속적인 코드가 됨

해결 방법

  1. 표준 SQL만 사용
    • 현실성이 없음
  2. DB별로 별도의 DAO를 생성
  3. SQL을 외부로 독립

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

SQLException

  • 예외의 원인이 매우 다양함
  • 문제 : DB마다 에러의 종류와 원인이 제각각
  • SQLException의 getErrorCode()를 봐야하는데 DB 에러코드가 DB별로 모두 다름
  • 현재 사용하는 DB의 상태를 보려면 getSqlState()를 사용
  • getSqlState() : SOPEN SQL 스펙에 정의된 SQL 상태코드를 따름
  • 그런데 JDBC 드라이버에서 SQLException에 담을 상태코드를 정확확게 만들어주지 않음

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

DB에 독립적이기 위해서는 비표준 SQL과 에러코드 및 상태정보 문제를 해결해야함

  • 비표준 SQL : 추후 논의
  • SQLException에 담긴 SQL 상태는 신뢰할 수 없으므로 고려하지 않음
  • DB에서 직접 제공해주는 DB 에러 코드는 어느 정도 일관성을 유지함

해결 방법 : DB별 에러코드를 참고해서 발생하는 예외의 원인이 무엇인지 해석해주는 기능

  • 키 값 중복의 경우
    1. MySql : 1062
    2. Oracle : 1
    3. DB2 : -830
  • 이러한 에러 코드 값을 확인할 수 있으면 SQLException 대신 DuplicateKeyException으로 전환 가능
  • DB 종류에 관계없이 동일한 상황에서 일관된 예외를 받을 수 있다면 효과적 대응 가능

스프링의 대처 방법

  • DataAccessException이라는 SQLException을 대체하는 런타임 예외를 정의
  • DataAccessException의 서브클래스인 세분화 된 예외 클래스를 정의
    1. BadSqlGrammarException : SQL 문법 오류
    2. DataAccessResourceFailureException : DB 커넥션을 가져오지 못함
    3. DataIntegrityViolationException : 제약조건 위반
    4. DuplicateKeyException : 중복 키

DAO 메소드나 JdbcTemplate에서 DB 종류별로 에러코드를 매핑하는 것은 부담이 너무 큼

  • 스프링이 정의한 예외 클래스와 매핑해놓은 에러 코드 매핑정보 테이블을 만들어두고 이를 이용
<bean id="Oracle" class="org.springframework.jdbc.support.SQLErrorCodes">
    <property name="badSqlGrammarCodes">
        <value>900, 903, 904, 917, 936, 942, 17006</value>
    </property>
    <property name="duplicateKeyCodes">
        <value>1</value>
    </property>
    <!-- ... -->
</bean>

JdbcTemplate은 SQLException을 단순히 포장하는 것이 아니라 DataAccessException 계층구조의 클래스 중 하나와 매핑해준다.

add() 메소드의 수정

  • JdbcTemplate을 이용하도록 수정
public void add() throws DuplicateKeyException{
    // 예외 처리가 불필요함
    // User를 DB에 add
}

JdbcTemplate을 이용하면 JDBC에서 발생하는 DB 관련 예외는 거의 신경쓰지 않아도 된다.

직접 정의한 예외를 발생시키고 싶은 경우

  • DuplicateKeyException을 DuplicateUserIdException으로 전환하는 코드를 넣으면 된다.
public void add() throws DuplicateUserIdErrorException{
    try {
        // user를 DB에 등록
    } catch (DuplicateKeyException e) {
        throw new DuplicateUserIdException(e);
    }
}

JDK 1.6 이상의 JDBC 4.0부터는 SQLException 좀 더 세분화해서 정의

  • SQLSyntaxErrorException이나 SQLIntegrityConstraintViolationException 등으로 세분화
  • 그러나 여전히 SQLException의 서브클래스이므로 체크 예외임
  • 예외를 세분화하는 기준이 SQL 상태정보를 이용함

아직은 DataAccessException을 사용하는 것이 이상적

  • 시간이 많이 지나서 JDK 6.0 이상을 사용하며, JDBC 4.0의 스펙을 충실히 따라 정확한 상태정보를 가지고 일관성있는 예외를 만들어주는 JDBC 드라이버가 충분히 보급된다면 모르겠다.
  • 시간은 충분히 많이 지났고, JDK 11이 출시된 현재는 어떠한가?

4.2.3 DAO 인터페이스와 DataAccessException 계층구조

DataAccessException

  • JDBC 외에도 자바 데이터 액세스 기술에서 발생하는 예외에도 적용
  • JDO, JPA, IBatis 등
  • 의미가 같은 예외라면 데이터 액세스 기술의 종류와 상관없이 일관된 예외가 발생하도록

DAO 인터페이스와 구현의 분리

DAO를 굳이 분리해서 사용하는 이유

  • 데이터 액세스 로직을 담은 코드를 성격이 다른 코드에서 분리해놓기 위함
  • 전략 패턴을 적용해 구현 방법을 변경해서 사용할 수 있게 만들기 위해

DAO의 사용 기술과 구현 코드는 전략 패턴과 DI를 사용하는 클라이언트에게 감출 수 있지만, 메소드 선언에 나타나는 예외정보가 문제될 수 있다.

이상적인 DAO 인터페이스

public interface UserDao {
    public void add(User user);
    // ...
}

예외 정보를 포함하여 선언하는 add()

public interface UserDao {
    public void add(User user) throws SQLException;
    // ...
}

JDBC가 아닌 데이터 액세스 기술(JPA, iBatis)로 전환하면 사용할 수 없다.

  • 인터페이스로 메소드의 구현은 추상화했지만 구현 기술마다 던지는 예외가 다르므로 기술에 종속된다.
  • 가장 단순한 해결방법 : throws Exception (WTF)

다행히 JDBC 이후의 JDO, Hibernate, JPA 등은 체크 예외 대신 런타임 예외를 사용하므로 throws 선언을 하지 않아도 된다.

  • 즉, 인터페이스에서 예외 정보를 포함하지 않아도 된다.
  • JDBC API를 직접 사용하는 DAO에서만 SQLException을 런타임 예외로 포장해주면 된다.

대부분의 데이터 액세스 예외는 애플리케이션에서 복구 불가능하지만, 모두 다 그런 것은 아니다.

  • 중복 키 에러
  • 시스템 레벨에서 예외를 의미있게 분류해야 하는 경우도 있다.
  • 그러나 같은 상황에서도 기술에 따라 다른 예외가 던져진다.
  • 결국, DAO를 사용하는 클라이언트 입장에서는 DAO의 사용기술에 따라 예외 처리 방법이 달라져야 한다.

데이터 액세스 예외 추상화와 DataAccessException 계층 구조

스프링의 대처방법

  • 다양한 데이터 액세스 기술을 사용할 떄 발생하는 예외들을 추상화하여 DataAccessException 계층구조 내에 정리

DataAccessException

  • JdbcTemplate의 SQLException의 에러 코드를 DB 별로 매핑하여 의미 있는 서브클래스로 전환해서 던져 줌
  • JDBC 뿐만 아니라 자바의 주요 데이터 액세스 기술에서 발생할 수 있는 대부분의 예외를 추상화
  • InvalidDataAccessResourceUsageException : 데이터 액세스 기술을 부정확하게 사용한 경우
    1. JDBC의 BadSqlGrammarException
    2. Hibernate의 HibernateQueryException
  • InvalidDataAccessResourceUsageException는 프로그램을 잘못 작성해서 발생하는 오류이며, 기술의 종류에 상관없이 같은 타입의 예외를 던져주므로 시스템 레벨의 예외처리 작업을 통해 개발자에게 빠르게 통보 가능
  • ObjectOptimisticLockingFailureException : 낙관적 락킹(optimistic locking) 발생

스프링의 데이터 액세스 지원 기술을 이용해 DAO를 만들면 사용 기술에 독립적인 이상적인 DAO를 만들 수 있다.

4.2.4 기술에 독립적인 UserDao 만들기

인터페이스 적용

UserDao 클래스를 인터페이스와 구현으로 분리

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

setDataSource()는 추출하면 안 됨

  • UserDao 구현 방법에 따라 변경될 수 있는 메소드
  • UserDao를 사용하는 클라이언트가 알 필요 없음

UserDaoJdbc 클래스에 implements 선언

public class UserDaoJdbc implements UserDao {
    // ...
}

빈 설정 변경

  • 빈의 이름은 클래스 이름이 아니라 구현 인터페이스의 이름을 따르는 경우가 일반적 (구현 클래스를 바꿔도 혼란이 생기지 않도록 하기 위함)
<bean id="userDao" class="com.david.tobysspring.user.dao.UserDaoJdbc">
    <property name="dataSource" ref="dataSource" />
</bean>

테스트 보완

테스트 코드의 인스턴스 변수는 변경해야 할까?

  • 굳이 하지 않아도 된다.
  • Autowired는 스프링 컨텍스트 내에서 정의된 빈 중에서 인스턴스 변수에 주입 가능한 타입의 빈을 찾아준다.

변경할 수도 있다.

  • 테스트의 관심이 Jdbc 기술 구현에 있는 경우

테스트의 관심이 구현 기술에 관계없이 DAO 기능 동작이라면 인터페이스를 받는 것이 낫다.

  • 구현 기술이 바뀌더라도 테스트가 여전히 유효하기 때문

setUp()에서 setDataSource를 부르는 부분은 캐스팅을 해주어야 한다.

((UserDaoJdbc) dao).setDataSource(dataSource);

중복된 키를 가진 정보를 등록하였을 때 어떤 예외가 발생하는지를 확인하는 테스트 추가

  • 스프링의 데이터 액세스 예외를 다루는 기능을 알아보기 위한 일종의 학습 테스트
  • 어떤 예외가 나올 지 확인하기 위해서는 테스트가 실패하도록 하여야 한다.
  • 같은 키 값을 넣을 경우, 예외가 발생할 것으로 예측되므로 expected를 사용하지 않고 테스트를 만든다.
@Test
public void duplicateKey() {
    dao.deleteAll();

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

테스트를 실행하면 다음과 같은 메세지가 출력된다.

org.springframework.dao.DuplicateKeyException: PreparedStatementCallback; SQL [INSERT INTO users(id, name, password) VALUES (?, ?, ?)]; ORA-00001: unique constraint (SPRINGBOOK_TEST.SYS_C007021) violated
; nested exception is java.sql.SQLIntegrityConstraintViolationException: ORA-00001: unique constraint (SPRINGBOOK_TEST.SYS_C007021) violated
...

DuplicateKeyException이 발생하였음을 확인할 수 있다.

  • DuplicateKeyException은 DataAccessException의 서브클래스로 DataIntegrityViolationException의 한 종류이다.

expected 항목을 DuplicateKeyException으로 바꾸고 실행하면 성공한다.

  • 좀 더 정확한 예외 발생을 확인하는 테스트가 됐다.

DataAccessException 활용 시 주의사항

스프링을 활용할 경우 DB 종류나 데이터 액세스 기술에 상관없이 키 값이 중복되는 상황에서 동일한 예외가 발생하리라 기대 가능

  • 다만, DuplicateKeyException은 JDBC를 사용하는 경우에만 발생한다.
  • Hibernate나 JPA를 이용할 경우 다른 예외가 던져짐
  • DataIntegrityViolationException를 사용할 수는 있지만 이 경우 DuplicateKeyException보다는 이용가치가 떨어진다.
  • 따라서 사용에 주의를 기울여야한다.

DAO에서 기술의 종류와 관계없이 동일한 예외를 얻고 싶다면 DuplicateUserIdException처럼 직접 예외를 정의해두고, 각 DAO의 add() 메소드에서 좀 더 상세한 예외 전환을 해 줄 필요가 있다.

SQLException을 직접 해석해 DataAccessException으로 전환하는 테스트

  • SQLException을 DataAccessException으로 전환하는 다양한 방법 제공
  • DB 에러 코드를 이용하는 것이 가장 보편적이고 효과적
  • SQLExceptionTranslator 인터페이스의 SQLErrorCodeSQLExceptionTranslator 사용
  • 현재 사용하는 DB의 종류를 파악하기 위해 DataSource가 필요함

UserDaoTest

public class UserDaoTest {
    @Autowired
    private UserDao dao;

    @Autowired
    private DataSource dataSource;

    // ...
}

DuplicateSqlException으로 전환하는 테스트

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

    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), isA(DuplicateException.class));
    }
}

원본 코드가 작동하지 않아 다음과 같이 변경

@Test(expected=DuplicateKeyException.class)
public void sqlExceptionTranslate() {
    dao.deleteAll();

    try {
        dao.add(user1);
        dao.add(user1);
    } catch (DuplicateKeyException ex) {
        SQLException sqlEx = (SQLException)ex.getRootCause();
        SQLExceptionTranslator set = new SQLErrorCodeSQLExceptionTranslator(this.dataSource);

        throw set.translate(null, null, sqlEx);
    }
}

테스트 프로세스

  1. JdbcTemplate을 이용하는 UserDao를 이용해 강제로 DuplicateKeyException을 발생시킴
  2. DuplicateKeyException : 중첩된 예외로 내부에 SQLException을 가지고 있음
  3. getRootCause() 메소드를 이용해 꺼내옴
  4. 주입받은 DataSource를 이용하여 SQLErrorCodeSQLExceptionTranslator의 오브젝트를 생성
  5. SQLException을 파라미터로 넣어서 translate() 메소드를 호출하면 SQLException을 DataAccessException 타입의 예외로 변환해 줌

JDBC 외의 기술을 사용할 때나 JDBC를 사용하고 있지만 JdbcTemplate과 같은 자동 예외 전환 기능을 사용할 수 없을 때 유용


' Spring' 카테고리의 다른 글

스프링 초기 설정  (0) 2018.12.07