스프링

토비의 스프링 4장 예외

여니여니_ 2021. 11. 22. 23:29

JdbcTemplate 적용 전

1
2
3
public void deleteAll() throws SQLException{
    this.jdbcContext.executeSql("delete from users");
}
cs

 

JdbcTemplate 적용 후

1
2
3
public void deleteAll(){
    this.jdbcTemplate.executeSql("delete from users");
}
cs

 

jdbcTemplate 적용 후, SQLException은 사라졌다.

예외가 발생하면 그것을 catch 블록을 써서 잡아내고 예외 처리를 해주어야한다.

왜냐하면 어떤 기능이 비정상적인 동작을 하거나 메모리/리소스 소진 등의 예상치 못한 문제를 일으킬 수도 있기 때문이다.

예외는 반드시 적절하게 복구되든지 작업을 중단시키고 운영자 또는 개발자에게 통보돼야 한다.

(후반부에 왜 jdbcTemplate을 적용했는지 나온다.)

 

예외를 처리하는 두 가지 나쁜 습관은 다음과 같다.

  • 예외 블랙홀: 예외를 잡고 아무 처리도 하지 않는다.
  • 무의미하고 무책임한 throws: throws Exception처럼 예외를 던지기만 하고 처리하지 않는다.

예외의 종류와 특징

체크 예외

명시적인 처리가 필요한 예외

자바에서 throw를 통해 발생시킬 수 있는 예외

1. Error

  • java.lang.Error 클래스의 서브클래스들
  • 시스템에 뭔가 비정상적인 상황이 발생했을 경우
  • 주로 자바 VM에서 발생
  • 애플리케이션에서는 이런 에러에 대한 처리는 신경 쓰지 않아도 됨

2. Exception과 체크 예외

  • java.lang.Exception 클래스와 그 서브클래스
  • 에러와 달리 개발자들이 만든 애플리케이션 코드의 작업 중 예외상황이 발생했을 경우
  • Exception 클래스는 다시 체크 예외와 언체크 예외로 구분
    • 체크예외: Exception 클래스의 서브클래스이면서 RuntimeException 클래스를 상속하지 않은 것
    • 언체크 예외: RuntimeException 클래스를 상속한 클래스
    • RuntimeException은 Exception의 일종이긴 하지만, 자바는 이 RuntimeException과 그 서브클래스는 특별하게 다룬다.
  • 일반적으로 예외 = 체크 예외
    • 체크 예외가 발생할 수 있는 메소드를 사용할 경우 반드시 예외 처리하는 코드를 함께 작성
    • catch문으로 잡든지, 아니면 다시 trhows를 정의해서 메소드 밖으로 던져야

3.RuntimeException과 언체크/런타임 예외

  • java.lang.RuntimeException 클래스를 상속한 예외들
  • 언체크예외: 명시적인 예외처리를 강제하지 않음.
  • 런타임 예외라고도 함
  • catch문으로 잡거나 throws로 선언하지 않아도 되고, 명시적으로 잡거나 trhow로 선언해줘도 상관없음
  • 예시
    • 오브젝트를 할당하지 않은 레퍼런스 변수를 사용하려고 시도했을 때 발생하는 NullPointerException
    • 허용되지 않는 값을 사용해서 메소드를 호출할 때 발생하는 IllegalArgumentException

 

예외처리 방법

예외 복구

  • 문제를 해결해서 정상 상태로 돌려놓는 것
  • 예외처리 코드를 강제하는 체크 예외들은 예외를 어떤 식으로든 복구할 가능성이 있는 경우에 사용

예외처리 회피

  • 예외처리를 자신이 담당하지 않고 자신을 호출한 쪽으로 던져버리는 것
  • throws 문으로 선언해서 예외가 발생하면 알아서 던져지게 하거나 catch문으로 일단 예외를 잡은 후에 로그를 남기고 다시 예외를 던지는 것
  • 예시
    • JdbcTemplate이 사용하는 콜백 오브젝트는 메소드 선언을 보면 알겠지만 ResultSet이나 PreparedStatement 등을 이용해서 작업하다 발생하는 SQLException을 자신이 처리하지 않고 템플릿으로 던져버린다. 콜백 오브젝트의 메소드는 모두 throws SQLException이 붙어있다. SQLException에 대한 예외를 회피하고 템플릿 레벨에서 처리하도록 던져준다.

예외 전환

  • 예외를 복구해서 정상적인 상태로 만들 수 없기 때문에 예외를 메소드 밖으로 던지는 것
  • 예외 회피와 달리, 발생한 예외를 그대로 넘기는 게 아니라 적절한 예외로 전환해서 던진다
  • 목적
    • 의미를 분명하게 해줄 수 있는 예외로 바꿔주기 위해서
      • API가 발생하는 기술적인 로우 레벨을 상황에 적합한 의미를 가진 예외로 변경하는 것
      • 보통 전환하는 예외에 원래 발생한 예외를 담아서 중첩예외로 만드는 것이 좋다. 중첩예외는 getCause() 메소드를 이용해서 처음 발생한 예외가 무엇인지 확인할 수 있다.
    • 예외를 처리하기 쉽고 단순하게 만들기 위해 포장하는 것
      • 예외처리를 강제하는 체크 예외를 언체크 예외인 런타임 예외로 바꾸는 경우에 사용
  • 일반적으로 체크 예외를 계속 throws를 사용해 넘기는 건 무의미하다. DAO에서 발생한 SQLException이 웹 컨트롤러 메소드까지 명시적으로 전달된다고 해서 무슨 소용이 있을까? 어차피 복구가 불가능한 예외라면 가능한 한 빨리 런타임 예외로 포장해서 던지게 해서 다른 계층이 메소드를 작성할 때 불필요한 throws 선언이 들어가지 않도록 해줘야 한다.

예외처리 전략

런타임 예외의 보편화

자바의 환경이 서버로 이동하면서 작업을 일시 중지하고 예외 상황을 복구할 수 없게 되어 체크 예외의 활용도와 가치가 떨어지고 있다. 자칫하면 throws Exception으로 흘러들어가 아무런 의미도 없는 메소드를 낳을 뿐이다.

 

그래서 대응이 불가능한 체크 예외라면 빨리 런타임 예외로 전환해서 던지는게 낫다.

 

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

 

애플리케이션 예외

  • 애플리케이션 자체의 로직에 의해 의도적으로 발생시키는 애플리케이션 예외를 가지는 메소드를 설계하는 방법
    1. 정상적인 처리를 했을 경우와 의도적으로 예외를 발생시키고자 하는 경우에 각각 다른 종류의 리턴 값을 돌려주는 것
      1. 단점
        1. 예외 상황에 대한 리턴 값을 명확하게 코드화하고 잘 관리하지 않으면 혼란이 생길 수 있다.
        2. 결과 값을 확인하는 조건문이 자주 등장
    2. 비즈니스적인 의미를 띤 예외를 던지도록 만드는 것
      1. 이때 사용하는 예외는 의도적으로 체크 예외로 만들어 개발자가 잊지 않고 구현하도록 강제한다.

SQLException은 어떻게 됐나?

대부분의 SQLException은 복구가 불가능하다. 더군다나 DAO 밖에서 SQLException을 다룰 수 있는 가능성은 거의 없다. 따라서,

  • 예외처리 전략을 적용하고
  • 가능한한 빨리 언체크/런타임 예외로 전환해줘야

스프링의 JdbcTemplate이 그 예시다.

  • JdbcTemplate 템플릿과 콜백 안에서 발생하는 런타임 예외는 SQLException →DataAccessException으로 포장해 던져준다.
  • 따라서 런타임 예외인 DataAccessException을 잡아서 처리하면 되고 그 외의 경우에는 무시해도 된다.
  • 그 밖에도 스프링의 API 메소드에 정의되어 있는 대부분의 예외는 런타임 예외다. 따라서 발생 가능한 예외가 있다고 하더라도 이를 처리하도록 강제하지 않는다.

예외 전환

JDBC의 한계

JDBC는 자바 표준 JDK에서도 가장 많이 사용되는 기능 중의 하나다.

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

  • DB마다 SQL 뿐만 아니라 에러의 종류와 원인도 제각각이다. 그래서 JDBC는 데이터 처리 중에 발생하는 다양한 예외를 그냥 SQLException 하나에 모두 담아버린다. SQLException은 예외가 발생했을 때의 DB 상태를 담은 SQL 상태정보를 부가적으로 제공한다. getSQLState() 메소드로 예외상황에 대한 상태정보를 가져올 수 있다. 이 상태정보는 DB별로 달라지는 에러 코드를 대신할 수 있도록, 스펙에 정의된 SQL 상태 코드를 따르도록 되어있다. 그러나 결국 호환성 없는 에러 코드와 표준을 잘 따르지 않는 상태 코드를 가진 SQLException 만으로 DB에 독립적인 유연한 코드를 작성하는 건 불가능에 가깝다.
  • 한마디로 이건 아니고,

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

  • 스프링은 DataAccessException이라는 SQLException을 대체할 수 있는 런타임 예외를 정의하고 있을 뿐 아니라 DataAccessException의 서브클래스로 세분화된 예외 클래스들을 정의하고 있다. 데이터 엑세스 작업 중에 발생 할 수 있는 예외 상황을 수십 가지 예외로 분류하고 이를 추상화해 정의한 다양한 예외 클래스를 제공한다.
  • 스프링에 구현되어 있다. 이 말씀,

 

 

JdbcTemplate은 DataAccessException 계층구조의 클래스(서브 클래스) 중 하나로 매핑해준다. DB별로 미리 준비된 매핑정보를 참고해서 적절한 예외 클래스를 선택하기 때문이 DB가 달라져도 같은 종류의 에러라면 동일한 예외를 받을 수 있다. 데이터 엑세스 기술에 독립적인 추상화된 예외를 제공하는 것이다.

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

DataAccessException은 JDBC의 SQLException을 전환하는 용도로만 만들어진 건 아니다. JDBC 외의 자바 데이터 엑세스 기술에서 발생하는 예외에도 적용된다.

DataAccessException은 의미가 같은 예외라면 데이터 액세스 기술의 종류와 상관없이 일관된 예외가 발생하도록 만들어준다. 데이터 액세스 기술에 독립적인 추상화된 예외를 제공하는 것이다.

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

DAO를 굳이 따로 만들어서 사용하는 이유

  • 데이터 액세스 로직을 담은 코드를 성격이 다른 코드에서 분리해놓기 위해서
  • DAO를 사용하는 클라이언트 입장에서는 DAO의 사용 기술에 따라서 예외 처리 방법이 달라져야 한다. 결국 클라이언트가 DAO의 기술에 의존적이 될 수 밖에 없다.

데이터 액세스 예외 추상화

  • 스프링의 DataAccessException은 자바의 주요 데이터 액세스 기술에서 발생할 수 있는 대부분의 예외를 추상화하고 있음
  • 데이터 엑세스 기술에서 발생 가능한 대부분의 예외를 계층구조로 분류해놓음

DataAccessException 활용시 주의사항

스프링에서 DuplicateKeyException은 아직까지는 JDBC를 이용하는 경우에만 발생한다.

데이터 액세스 기술을 하이버네이트나 JPA를 사용했을 때도 실제로 다른 예외가 던져진다.

그 이유는 SQLException에 담긴 DB의 에러 코드를 바로 해석하는 JDBC의 경우와 달리 JPA나 하이버네이트, JDO 등에서는 각 기술이 재정의한 예외를 가져와 스프링이 최종적으로 DataAccessException으로 변환하는데,

DB의 에러 코드와 달리 이런 예외들은 세분화되어 있지 않기 때문이다.

스프링은 SQLException을 DataAccessException으로 전환하는 방법

  • DB 에러 코드를 이용
    • SQLException을 코드에서 직접 전환하고 싶다면 SQLExceptionTranslator 인터페이스를 구현한 클래스 중에서 SQLErrorCodeSQLExceptionTranslator를 사용하면 됨