Java & Spring/토비의 스프링 3.1

4장) 4.2 예외 전환 ~ 4.3 정리

Zin0_0 2021. 6. 21. 14:29
반응형

4장 예외

4.2 예외 전환

  • 예외 전환의 목적

    • 런타임 예외로 포장
      • 불필요한 catch / throws 없애기
    • 의미있고 추상화된 예외로 바꿔 던지기
  • JdbcTemplate의 DataAccessException이 런타임 예외로 SQLException을 포장

    • 대부분 복구가 불가능한 예외인 SQLException을 Application 레벨에서 신경쓰지 않고, 상세한 예외 정보 전달의 목적
  • JDBC의 한계

    • DB 종류에 상관없이 사용할 수 있는 데이터 엑세스 코드를 작성하는 일이 쉽지 않음
    • 유연한 코드를 보장 못하는 두 가지
      • 비표준 SQL
        • 최적화 기법, 페이지 처리 등 비표준 SQL이 폭넓게 사용됨
        • 비표준 SQL은 DAO에 들어가게되고, 해당 DAO는 특정 DB에 종속적인 코드가 됨
        • 해결책
          • DAO를 DB별로 만들어 사용하거나 SQL을 외부에서 독립시켜서 DB에 따라 변경해 사용할 수 있도록 구성
      • 호환성 없는 SQLException의 DB 에러 정보
        • JDBC는 데이터 처리 중 발생하는 다양한 예외를 모두 SQLException 하나에 담음
        • SQLException에서 getErrorCode()가져오는 에러 코드는 DB별로 에러 코드가 다름
        • 그래서 SQLException은 DB 상태를 담은 SQL 상태정보를 부가적으로 제공하지만, JDBC 드라이버에서 SQLException을 담을 상태코드를 정확하게 만들어주지 않음
          • SQL 상태코드를 믿고 결과를 파악하는 코드는 매우 위험
  • DB 에러 코드 매핑을 통한 전환

    • 스프링은 DataAccessException 이라는 SQLException을 대체할 수 있는 런타임 예외 및 세분화된 다양한 예외 클래스를 제공

      • DataAccessResourceFailureException, BadSqlGrammaException 등
    • DB별 에러코드를 분류해서 스프링이 정의한 예외 클래스와 매핑해놓은 에러 코드 매핑정보를 만들고 이용

      • <bean id="Oracle" class="org.springframwork.jdbc.support.SQLErrorCodes">
            <property name="badSqlGrammarCodes">
                <value>900,903,904,917,936,942,17706</value>
            </property>
            <property name="invalidResultSetAccessCodes">
                <value>17703</value>
            </property>
            ...
        </bean>
        • 오라클 DB의 에러 코드 매핑 파일에 대한 예시
    • 위와 같이 매핑 정보를 참고해서 적절한 예외 클래스를 선택하기 때문에, DB가 달라져도 같은 종류의 에러라면 동일한 예외를 받을 수 있게 코드를 작성할 수 있음

    • 만약, 직접 정의한 예외를 발생시키고 싶다면, try-catch 블록을 이용해서 catch & throw로 자신이 정의한 예외로 던져주는 방법을 이용

    • JDK1.6, JDBC 4.0기준) SQLException의 서브 클래스들은 체크 예외기 때문에, 예외를 세분화하는 기준이 SQL 상태정보를 이용 하는 문제점이 존재

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

    • JDBC이외에 자바 데이터 엑세스 기술에서 발생하는 예외에도 적용
    • DAO 인터페이스와 구현 분리
      • 데이터 엑세스 로직과 다른 코드의 성격 분리 & 분리된 DAO는 전략 패턴을 적용해 구현 방법을 변경해서 사용 가능하게 구성
      • DAO의 사용 기술과 구현 코드는 전략 패턴과 DI를 통해 클라이언트에게 감출 수 있지만, 메소드 선언에 나타나는 예외정보가 문제
      • 인터페이스의 메소드 선언에 없는 예외를 구현 클래스에서 throws할 수 없음
        • 인터페이스에 throws를 추가
        • 하지만, JDBC가 아닌 다른 데이터 액세스 기술로 DAO를 구현하면 사용할 수 없는 코드
        • 가장 단순한 해결 방법은 throws Exception으로 선언하는 것이지만, 상당히 무책임함
        • 따라서, 런타임 예외를 이용해서 인터페이스에서 throws를 선언하지 않도록 해결
    • 데이터 액세스 예외 추상화와 DataAccessException 계층 구조
      • 스프링은 자바의 다양한 데이터 액세스 기술을 사용할 때 발생하는 예외들을 추상화 ~> DataAccessException 계층구조 안에 정리
        • 공통적으로 나타나는 예외를 포함해서 데이터 액세스 기술에서 발생 가능한 대부분의 예외를 계층 구조로 분류
        • 템플릿 메소드나 DAO 메소드에서 직접 활용할 수 있는 예외도 정의되어 있음
          • 기대한 결과가 나오지 않은 예외상황에 대한 예외
            • IncorrectResultSizeDataAccessException, EmptyResultDataAccessException 등
      • 인터페이스 사용, 런타임 예외 전환과 함께 DataAccessException 예외 추상화를 적용하여, 데이터 액세스 기술과 구현 방법에 독립적인 DAO를 만들 수 있음
  • 기술에 독립적인 UserDao 만들기

    • 인터페이스 적용

      • UserDao에서 DAO 기능을 사용하려는 클라이언트들이 필요한 것만 추출해서 인터페이스로 구현

      • public interface UserDao {
            void add(User user);
            User get(String id);
            List<User> getAll();
            void deleteAll();
            int getCount();
        }
        • setDataSource() 메소드 같은 경우에는 구현 방법에 따라 변경될 수 있는 메소드이고, 클라이언트가 알고 있을 필요가 없기 때문에 제외
    • 테스트 보완

      • UserDao 인스턴스 변수를 구현 클래스로 바꿀 필요가 없음
        • @Autowired는 스프링 컨텍스트 내에서 정의된 빈 중 인스턴스 변수 주입이 가능한 타입의 빈을 찾아주기 때문
      • 특정 기술을 사용한 UserDao 구현 내용에 관심을 가지고 테스트한다면, 해당 구현 클래스로 타입을 사용해서 테스트
    • DataAccessException 테스트

      • @Test(expected=DataAccessException.class)
        public void duplicateKey() {
            dao.deleteAll();
        
            dao.add(user1);
            dao.add(user2); // 여기서 예외 발생
        }
    • DataAccessException 활용시 주의사항

      • 스프링을 활용하면 DB 종류와 데이터 액세스 기술에 상관없이 키 값이 중복되는 상황에서 동일한 예외가 발생하기로 기대

        • 하지만, DuplicateKeyException은 JDBC를 이용하는 경우에만 발생
      • DataAccessException이 기술에 상관없이 어느 정도 추상화된 공통 예외로 변환해주지만, 근본적인 한계 때문에 사용에 주의

      • 만약, 기술의 종류와 상관없이 동일한 예외를 얻고 싶다면, 직접 예외를 정의하고 상세한 예외 전환을 이용

        • DuplicateUserIdException이 예시
      • SQLException을 DataAccessException으로 전환하는 보편적인 방법은 DB 에러코드를 이용하는 방법

        • SQLErrorCodeSQLExceptionTranslator를 사용

          • 현재 연결된 DataSource를 필요로 함

          • @Test
            public void sqlExceptionTranslate() {
                dao.deleteAll();
            
                try {
                    dao.add(user1);
                    dao.add(user2);
                } catch (DuplicateKeyException ex) {
                    SQLException sqlException = (SQLException)ex.getRootCause();
                    SQLExceptionTranslator set = new SQLErrorCodeSQLExceptionTranslator(this.datasource);
                    assertThat(set.translate(null, null, sqlException), is(DuplicateKeyException.class));
                }
            }
        • JDBC 이외의 기술을 사용할 때, SQLException을 가져와서 직접 예외 전환하는 방법을 이용할 수 있음

      • SQLException을 그대로 두거나, 의미없는 RuntimeException으로 뭉뜽그려 던지는 대신 스프링의 DataAccessException 계층의 예외로 전환하는 방법을 염두하자

4.3 정리

  • 엔터프라이즈 애플리케이션에서 사용할 수 있는 바람직한 처리 방법 학습
  • JDBC 예외의 단점과 스프링이 제공하는 효과적인 데이터 액세스 기술의 예외처리 전략과 기능 학습
    • 예외를 잡아서 아무런 조치를 취하지 않거나 의미없는 throws 선언을 남발하는 것은 위험
    • 예외는 복구하거나 의도적으로 전달하거나 적절한 예외로 전환해야함
    • 예외 전환
      • 의미있는 예외로 변경
      • 런타임 예외로 포장
    • 복구할 수 없는 예외는 런타임 예외로 빠르게 전환
      • SQLException이 대표 예시
    • 애플리케이션 로직 예외는 체크 예외
    • SQLException의 에러 코드는 DB에 종속 ~> 독립적인 예외로 전환할 필요 존재
    • 스프링은 DataAccessException이라는 DB에 독립적으로 적용 가능한 추상화된 런타임 예외 계층 제공
    • DAO를 데이터 액세스 기술에서 독립
      • 인터페이스 도입
      • 런타임 예외 전환
      • 기술에 독립적인 추상화된 예외로 전환
  • 낙관적인 락킹(Optimistic Locking)
    • 같은 정보를 두 명 이상의 사용자가 동시에 조회하고 순차적으로 업데이트할 때, 뒤늦게 업데이트한 것이 먼저 업데이트한 것을 덮어쓰지 않도록 막아주는 기능
반응형