PengTory

[Spring] 예외 본문

Spring

[Spring] 예외

펭토리 2022. 11. 1. 19:06

초 난감 예외처리

try{
    // ...
        }
catch(SQLException e){
        } // 예외를 잡고 아무 것도 안함 -> 만들어선 안됨

try catch문을 써서 예외를 잡아낸 것은 좋지만 catch문 안에 아무것도 쓰지 않아 넘겨버리는 것은 위험하다.

왜냐하면 프로그램 실행 중 오류가 있어 예외가 발생했는데 이를 무시하고 계속 진행하기 때문이다.

그렇다면 cath문 안에 e.printStackTrace() 혹은 System.out.println(e)와 같이 메시지를 출력하는 방법은 어떨까?

잠깐 해결된 듯 보이지만 다른 로그나 메시지에 금방 묻혀 찾기 어려울 것이다. 

 

그렇다면 정상적인 예외처리를 하기 위한 방법을 적어보겠다.

 

예외의 종류와 특징

자바에서 throw를 통해 발생시킬 수 있는 예외는 크게 세 가지가 있다.

  • Error

첫째는 java.lang.Error 클래스의 서브클래스들이다. 에러는 시스테에 뭔가 비정상적인 상황이 발생했을 경우 사용된다.

시스템 레벨에서 특별한 작업을 하는 것이 아니라면 애플리케이션에서는 이런 에러에 대한 처리를 신경쓰지 안항도 된다.

 

  • Exception과 체크예외

개발자들이 만든 애플리케이션 코드의 작업 중에 예외상황이 발생했을 경우에 사용된다.

Exception클래스는 체크예외와 언체크 예외로 구분된다.

Exception의 두 가지 종류

 

  • RuntimeException과 언체크/런타임 예외

에러와 마찬가지로 이 런타임 예외는 catch문으로 잡거나 throws로 선언하지 않아도 된다.

런타임 예외는 주로 프로그램의 오류가 있을 때 발생하도록 의도된 것 들이다.

 

예외처리 방법

- 예외 복구

첫 번째 예외처리 방법은 예외 상황을 파악하고 문제를 해결해서 정상 상태로 돌려놓는 것이다.

예외가 처리됐으면 비록 기능적으로는 사용자에게 예외상황으로 비쳐도 애플리케이션에서는 정상적으로 설계된 흐름에 따라 진행해야한다.

 

- 예외처리 회피

두 번째 방법은 예외처리를 자신이 담당하지 않고 자신을 호출한 쪽으로 던져버리는 것이다. throws문으로 선언해서 예외가 발생하면 알아서 던져지게 하거나 catch문으로 일단 예외를 잡은 후 로그를 남기고 다시 예외를 던지는 것이다.

예외처리를 회피하려면 반드시 다른 오브젝트나 메소드가 예외를 대신 처리할 수 있도록 아래 코드처럼 던져줘야 한다.

public void add() throws SQLException{
    //JDBC API
}
public void add() throws SQLException{
    try{
        //JDBC API
    }
    catch(SQLException e){
        // 로그출력
        throw e;
    }
}

 

- 예외 전환

 마지막 방법은 예외 전환이다. 예외 회피와 비슷하게 예외를 복구해서 정상적인 상태로는 만들 수 없기 때문에 예외를 메소드 밖으로 던지는 것이다. 하지만 예외 회피와 달리 발생한 예외를 그대로 넘기는 게 아니라 적절한 예외로 전환해서 던진다는 특징이 있다.

 예외전환은 보통 두 가지 목적으로 사용된다.

 첫 째는 API가 발생하는 기술적인 로우레벨을 상황에 적합한 의미를 가진 예외로 변경하는 것이다. 보통 전환하는 예외에 원래 발생한 예외를 담아서 중첩 예외로 만드는 것이 좋다. 중첩 예외는 getCause() 메소드를 이용해서 처음 발생한 예외가 무엇인지 확인할 수 있다.

 두 번째 전환 방법은 예외를 처리하기 쉽고 단순하게 만들기 위해 포장하는 것이다. 중첩 예외를 이용해 새로운 예외를 만들고 원인이 되는 예외를 내부에 담아서 던지는 방식은 같다. 하지만 의미를 명확하게 하려고 다른 예외로 전환하는 것이 아니다. 주로 예외처리를 강제하는 체크 예외를 언체크 예외인 런타임 예외로 바꾸는 경우에 사용한다.

 

예외처리 전략

예외를 효과적으로 사용하고 코드를 깔끔하게 정리하기 위해 지금까지 배운 것들을 기준으로 일관된 예외처리 전략을 정리해보자.

- 런타임 예외의 보편화

자바는 예외처리에서 catch 블록이나 throws선언을 강제하고 있다. 이렇게 예외처리를 강제하는 것은 예외가 발생할 가능성이 있는 API 메소드를 사용하는 개발자의 실수를 방지할 수 있지만 귀찮기도 하다.

따라서 애플리케이션 차원에서 예외상황을 미리 파악하고 예외가 발생하지 않도록 차단하는 것이 좋다. 또는 프로그램 오류나 외부 환경으로 인해 예외가 발생하는 경우라면 빨리 해당 요청의 작업을 취소하고 서버 관리자나 개발자에게 통보해주는 것이 좋다.

자바 초기부터 있었던 JDK의 API와 달리 최근에 등장하는 표준 스펙 또는 오픈 소스 프레임워크에서는 API가 발생시키는 예외를 체크 예외 대신 언체크 예외로 정의하는 것이 일반화 되고 있다.

 

- add() 메소드의 예외처리

지금까지 작성한 add() 메소드는 DuplicatedUserldExceptionSQLException, 두 가지의 체크 예외던지게 되어 있다.

DuplicatedUserldException충분히 복구 기능한 예외이므로 add() 메소드 를 사용히는 쪽에서 잡아서 대응할 수 있다. 하지만 SQLException은 대부분 복구 불가 능한 예외이므로 잡아봤자 처리할 것도 없고 결국 throws타고 계속 앞으로 전달되 다가 애플리케이션 밖으로 던져질 것이다. 그럴 바에는 그냥 런타임 예외로 포장해 던 져버려서 그 밖의 메소드들이 신경 쓰지 않게 해주는 편이 낫다.

DuplicatedUserldException도 굳이 체크 예외로 둬야 하는 것은 아니 다. DuplicatedUserldException처럼 의미 있는 예외는 add( ) 메소드를 바 로 호출한 오브젝트 대신 앞단의 오브젝트에서 다룰 수도 있다.

위 방법들을 이용해 add() 메소드를 아래처럼 수정해보았다.

public void add() throws DuplicateUserIdException{
    try{
        //JDBC를 이용해 user 정보를 DB에 추가하는 코드
        // or 그런 기능이 있는 다른 SQLException을 던지는 메소드르 호출하는 코드
    }
    catch(SQLException e){
        if (e.getErrorCode() == MysqlErrorNumbers.ER_DUP_ENTRY)
            throws new DuplicateUserIdException(e); // 예외전환
        else
            throw new RuntimeException(e); // 예외 포장
    }
}

- 애플리케이션 예외

런타임 예외 중심의 전략은 낙관적인 예외처리 기법이라고 할 수 있다.

일단 복구할 수 있는 예외는 없다고 가정하고 예외가 생겨도 어차피 런타임 예외 이므로 시스랩 레벨에서 알아서 처리해줄 것이고, 꼭 필요한 경우는 런타임 예외라도 잡아서 복구하거나 대응해줄수 있으니 문제 될 것이 없다는낙관적인 태도를 기반으로 하는 것이다.

반면에 시스템 또는 외부의 예외상황이 원인이 아니라 애플리케이션 자체의 로직에 의해 의도적으로 발생시키고 반드시 catch 해서 무엇인가 조치를 취하도록 요구히는 예외도 있다. 이런 예외들을 일반적으로 애플리케이션 예외라고 한다.

try{
    BigDecimal balance = account.withdraw(amount);
        ...
        //정상적인 처리 결과를 출력하도록 진행
}
catch(InsufficientBalanceException e){ //체크예외
    //InsufficientBalanceException에 담긴 인출 가능한 잔고금액 정보를 가져옴
        BigDecimal availFunds = e.getAvailFunds();
        ...
        // 잔고 부족 안내 메시지를 준비하고 이를 출력
}

 


예외전환

예외를 다른 것으로 바꿔서 던지는 예외 전환의 목적은 두 가지라고 설명했다. 하나는 앞에서 적용해본 것처럼 런타임 예외로 포장해서 굳이 필요하지 않은 catch/throws를 줄여주는 것이고, 다른 하나는 로우레벨의 예외를 의미 있고 추상화된 예외로 꿔서 던져주는것이다.

* JDBC의 한계

JDBC자바를 이용해 DB접근히는 방법을 추상화된 API 형태로 정의해놓고, DB 업체가 JDBC 표준을 따라 만들어진 드라이버를 제공하게 해준다. 하지만 DB 종류에 상관없이 사용할 수 있는 데이터 액세스 코드를 작성하는 일은 쉽지 않다. 현실적으로 DB자유롭게 바꾸어 사용할 있는 DB 프로그램을 작성히는 데는 두 가지 걸림돌이 있다.

- 비표준 SQL

첫째 문제는 JDBC 코드에서 사용하는 SQL이다. SQL은 어느정도 표준화된 언어이고 몇 가지 표준 규약도 있지만 대부분의 DB는 표준을 따르지 않는 비표준 문법과 기능도 제공한다. DB의 변경 가능성을 고려해서 유연하게 만들어야 한다면 SQL은 큰 걸림돌이 된다.

이 문제의 해결책은 호환 가능한 표준 SQL만 사용하는 방법과 DB별로 별도의 DAO를 만들거나 SQL을 외부에 독립시켜 DB에 따라 변경해 사용하는 방법이 있다.

 

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

두 번째 문제는 SQLException이다. DB를 사용하다가 발생할 수 있는 예외의 원인은 다양하다. 또한 DB마다 SQL만 다른 것이 아니라 에러의 종류와 원인도 모두 제각각이다. 따라서 SQLException은 예외가 발생했을 때의 DB 상태를 담은 SQL 상태 정보를 부가적으로 제공한다. 이러한 상태코드를 제공하는 이유는 DB에 독립적인 에러정보를 얻기 위해서다. 그런데 문제는 DB의 JDBC 드라이버에서 SQLException을 담을 상태 코드를 정확하게 만들어주지 않는다.

결국 호환성 없는 에러 코드와 표준을 잘 따르지 않는 상태 코드를 가진 SQLException만으로 DB에 독립적인 유연한 코드를 작성하는 건 불가능에 가깝다.

 

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

DB 종류가 바뀌더라도 DAO수정하지 않으려면 가지 문제를 해결해야 한다.

해결 방법은 DB에러 코드를 참고해서 발생한 예외의 원인이 무엇인지 해석해 주는 기능을 만드는 것이다.

문제는 DB마다 에러 코드가 제각각이라는 점이다. 스프링은 DB별 에러 코드분류해서 스프링이 정의한 예외 클래스 와 매핑해놓은 에러 코드 매핑정보 테이블을 만들어두고 이를 이용한다.

 

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

스프링이 왜 DataAccessException 계충구조를 이용해 기술에 독립적인 예외를 정의하고 사용하게 하는지 알아보자.

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

데이터 엑세스 로직을 담은 코드를 성격이 다른 코드에 분리해 놓기 위해 DAO를 따로 만들어 사용한다. 따라서 DAO는 인터페이스를 사용해 구체적인 클래스 정보와 구현 방법을 감추고, DI를 통해 제공되도록 만드는 것이 바람직하다.

그런데 DAO의 사용 기술과 구현 코드는 전략 패턴과 DI를 통해 DAO를 사용하는 클라이언트에게 감출 수 있지만, 메소드 선언에 나타나는 예외정보가 문제가 될 수 있다. 따라서 아래와 같이 정의해야 한다.

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

이제 DAO에서 사용하는 기술에 완전히 독립적인 인터페이스 선언이 가능해졌다.

하지만 DAO를 사용하는 클라이언트 입장에서는 DAO의 사용 기술에 따라서 예외 처 리 방법이 달라져야한다. 결국클라이언트가 DAO의 기술에 의존적이 수밖에 없다. 단지 인터페이스로 추상화하고 일부 기술에서 발생하는 체크 예외를 런타임 예외로 전환하는 것만으론 불충분하다.

 

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

그래서 스프링은 자바의 다양한 데이터 엑세스 기술을 사용할 때 발생하는 예외들을 추상화 해서 DataAccessException 계층구조 안에 정리해두었다.

JDBC를 이용한 낙관적인 락킹 예외 클래스의 적용
DataAccessException 계층구조(일부)

JdbcTempate같이 스프링의 데이터 액세스 지원 기술을 이용해 DAO만들면 사용 기술에 독립적인 일관성 있는 예외를 던질 있다.

결국 인터페이스 사용, 런타임 예외 전환과 함께 DataAccessException 예외 추상화를 적용하면 데이터 액세스 기술과 구현 방법에 독립적인 이상적인 DAO를 만들 수가 있다.

 

 

[참고자료] 토비의 스프링 3.1 Vol.1 스프링의 이해와 원리

'Spring' 카테고리의 다른 글

스프링 시큐리티(Spring Security)의 개념  (0) 2023.01.02
JDBC와 JPA 그리고 Hibernate  (0) 2022.12.22
[Spring] 템플릿  (0) 2022.10.21
[Spring] 테스트  (0) 2022.10.14
[Spring] 오브젝트와 의존관계 (5) _ XML  (0) 2022.10.14