토비의 스프링 | 4장 예외 (독서메모)

주싱·2022년 10월 2일
0

토비의 스프링

목록 보기
15/30

토비의 스프링 ‘4장 예외’ 장을 읽고 책의 내용을 정리합니다.

1. 사라진 SQLException

1.1 초난감 예외처리

다음과 같은 유형의 난감한 예외처리 코드들이 있다.

  • 예외를 잡고 예외를 삼킨다. 예외가 숨겨지고 이 후에 예외가 후에 드러나면 어디서 문제가 발생했는지 찾기 어렵게 된다.
  • 콘솔 로그만 남기고 삼킨다. 콘솔로그는 다른 로그에 묻혀 버리기 쉽상이다.
  • 애플리케이션을 종료한다
  • Exception으로 퉁친다. 처리할 방법도 없고 긴 예외를 매번 throws 하기도 귀찮아서 throws Exception을 기계적으로 붙이는 경우이다. 메서드 선언에서 아무런 의미있는 정보를 얻을 수 없고 어떤 예외를 발생한다는 건지 혼란만 가져올 것이다. 결국 이런 메서드를 사용하는 메서드에서는 역시 throws Exception을 따라서 붙이는 수밖에 없다. 결과적으로 제대로된 예외 처리를 통해 복구할 수 있는 기회도 읽게 된다.

1.2 예외의 종류와 특징

Error

  • 주로 VM에서 발생시키는 것이고 애플리케이션 코드에서 잡으려고 하면 안 된다. 잡아 봤자 아무런 대응 방법이 없다.
  • java.lang.Error 클래스의 서브클래스 (OutOfMemoryError, ThreadDeath 등)

Exception (Checked)

  • 메서드를 호출한 쪽에서 반드시 예외를 처리하도록 컴파일러가 강제한다.
  • catch 문으로 예외를 잡든지, 메서드 시그니처에 throws를 정의해서 메소드 밖으로 던져지게 하든지 해야한다.
  • RuntimeException이 아닌 모든 java.lang.Exception 서브클래스(IOException, SQLException 등)

RuntimeException (Unchecked)

  • 메서드를 호출한 쪽에 명시적인 예외처리가 강제되지 않는다.
  • catch 문으로 잡거나 throws로 선언하지 않아도 된다. 물론 명시적으로 throws로 선언해줘도 상관없다.
  • java.lang.RuntimeException 서브클래스(NullPointException, IllegalArgumentException 등)

1.3 예외처리 방법

예외 복구

  • 예외상황을 파악하고 문제를 해결해서 정상 상태로 돌려놓는 방법입니다.
  • 예외로 기본 작업 흐름이 불가능하면 다른 작업 흐름으로 자연스럽게 유도해주는 것이다.
  • 예외 복구 예시
    • 파일을 읽으려고 시도하는데 해당 파일이 없는 경우 사용자에게 상황을 알리고 다른 파일응 이용하도록 안내할 수 있다.
    • 네트워크 상황으로 연결 예외가 발생한 경우 일정 간격으로 재시도하다가 최종적으로 실패를 알리는 시도를 할 수 있다.
  • 예외 복구가 아닌 경우
    • 단지 IOException이 발생했다고 사용자에게 알리는 건 예외 복구라고 하기 어렵다.

예외처리 회피

  • 예외처리를 자신이 담당하지 않고 자신을 호출한 쪽으로 던져버리는 것이다.
  • throws 문으로 선언해서 예외가 발생하면 알아서 던져지게 하거나 catch 문으로 예외를 잡아서 로그를 남기고 다시 예외를 던지는(rethrow) 것이다.
  • 그러나 긴밀하게 역할을 분담하고 있는 관계가 아니라면 자신의 코드에서 발생하는 예외를 그냥 던져버리는 건 무책임한 책임회피일 수 있다.
  • 예외를 회피하는 것은 예외를 복구하는 것 처럼 의도가 분명해야 한다.

예외 전환

  • 예외처리 회피와 같이 예외를 메서드 밖으로 던지지만 적절한 예외로 전환해서 넘긴다.
  • 예외 전환의 두 가지 목적
    • 예외의 의미를 사용자 코드가 분명히 알 수 있도록 하기 위해
      • 기술적인 의미를 가지는 로우레벨 예외를 사용자 상황에 적합한 의미를 가진 예외로 변경한다.
      • 보통 전환하는 예외에 원래 발생한 예외를 담아서 중첩 예외(nested exception)로 만드는 것이 좋다.
    • 예외를 처리하기 쉽고 단순하게 만들기 위해 포장(wrap)하는 경우
      • 체크 예외를 언체크 예외인 런타임 예외로 바꾸는 경우가 있다.
      • 복구가 불가능한 경우라면 빨리 런타임 예외로 포장해 던지게 해서 다른 계층의 메서드를 작성할 때 불필요한 throws 선언이 들어가지 않도록 해줘야 한다.
      • 대부부의 서버환경에서는 애플리케이션 코드에서 처리하지 않고 전달한 예외들을 일괄적으로 다룰 수 있는 기능을 제공한다.
      • 런타임 예외로 포장해서 던져버리고, 예외처리 서비스 등을 이용해 자세한 로그를 남기고, 관리자에게는 메일 등으로 통보해주고, 사용자에게는 친절한 안내 메시지를 보여주는 식으로 처리하는게 바람직하다.

1.4 예외처리 전략

런타임 예외의 보편화

  • 자바의 환경이 서버로 이동하면서 체크 예외의 활용도와 가치는 점점 떨어지고 있다. 자칫하면 throws Exception으로 점철된 아무런 의미도 없는 메서드들을 낳을 뿐이다. 그래서 대응이 불가능한 체크 예외라면 빨리 런타임 예외로 전환해서 던지는게 낫다.
  • 예전에는 복구할 가능성이 조금이라도 있으면 체크 예외로 만든다고 생각했는데, 지금은 항상 복구할 수 있는 예외가 아니면 일단 언체크 예외로 만드는 경향이 있다.
  • 런타임 예외를 발생시키는 경우 API 문서나 레퍼런스 문서 등을 통해, 메서드를 사용할 때 발생할 수 있는 예외의 종류와 원인, 활용 방법을 자세히 설명해두자.

낙관적 vs 비관적 기법

  • 낙관적 예외처리 기법 - 일단 복구할 수 있는 예외는 없다고 가정하고 예외가 생겨도 어차피 런타임 예외임으로 시스템 레벨에서 알아서 처리해줄 것이고, 꼭 필요한 경우는 런타임 예외라고 잡아서 복구하거나 대응해줄 수 있으니 문제될 것이 없다는 낙관적인 태도에 기반
  • 비관적인 접근 방법 - 직접 처리할 수 없는 예외가 대부분이고 하더라도 혹시 놓치는 예외가 있을 수 있으니 일단 잡고 보도록 강제하는 비관적인 접근 방법

애플리케이션 예외

  • 시스템 또는 외부 예외상황이 원인이 아니라 애플리케이션 자체의 로직에 의해 의도적으로 발생시키고, 반드시 catch 해서 무엇인가 조치하도록 요구하는 예외
  • 처리해야 할 예외 상황을 가진 메서드 설계 두 가지 방법
    • 반환 값을 통한 처리
      • 예외 상황에 대한 반환 값을 코드화하고 잘 관리하지 않으면 큰 혼란
      • 메서드를 호출할 때마다 if 블록이 범벅된 코드가 될 가능성
    • 예외를 던지기
      • 정상적인 흐름을 따르는 코드와 예외 처리 코드를 분리할 수 있다.
  • 애플리케이션 예외는 의도적으로 체크 예외로 만든다. 그래서 개발자가 잊지 않고 발생할 수 있는 예외를 처리하는 코드를 구현하도록 강제하는 것이 좋다.

스프링의 예외처리

  • DAO 밖에서 SQLException을 다룰 수 있을 가능성은 거의 없다. 필요도 없는 기계적인 throws 선언이 등장하도록 방치하지 말고 가능한 빨리 언체크/런타임 예외로 전환해줘야 한다.
  • 스프링의 JdbcTemplate은 바로 이 예외 처리 전략을 따르고 있다. JdbcTemplate 템플릿과 콜백 안에서 발생하는 모든 SQLException을 런타임 예외인 DataAccessException으로 포장해서 던져준다.
  • 스프링의 API 메서드에 정의되어 있는 대부분의 예외는 런타임 예외다. 따라서 발생 가능한 예외가 있다고 하더라도 이를 처리하도록 강제하지 않는다.

2 예외 전환

다시 예외 전환 두 가지 케이스

  • 런타임 예외로 포장해서 굳이 필요하지 않은 catch/throws를 줄여주는 것
  • 로우레벨의 예외를 좀 더 의미 있고 추상화된 예외로 바꿔서 던져주는 것

스프링 JdbcTemplate 예외

  • JdbcTemplate은 DataAccessException이라는 런타임 예외를 던진다.
  • DataAccessException은 SQLException을 포장해주는 역할을 한다.
  • 대부분 복구가 불가능한 예외인 SQLException에 대해 애플리케이션 레벨에서는 신경 쓰지 않도록 해준다.
  • SQLException에 담기 힘든 상세한 예외정보를 의미 있고 일관성 있는 예외로 전환해서 추상화해주려는 용도로 쓰이기도 한다.

2.1 JDBC의 한계

  • JDBC는 자바를 이용해 DB에 접근하는 방법을 추상화된 API형태로 제공해놓고, 각 DB업체가 JDBC 표준을 따라 만들어진 드라이버를 제공해 준다.
  • 표준화된 JDBC의 API에만 익숙해지면 DB의 종류에 상관없이 일관된 방법으로 프로그램을 개발할 수 있다.
  • 하지만 DB 종류에 상관없이 사용할 수 있는 데이터 엑세스 코드를 작성하는 일은 쉽지 않다.

비표준 SQL

  • SQL은 어느 정도 표준화된 언어이고 몇 가지 표준 규약이 있긴 하지만, 대부분의 DB는 표준을 따르지 않고 비표준 문법과 기능도 제공한다.
  • 이 문제의 해결책을 생각해보면 호환 가능한 표준 SQL만 사용하는 방법과 DB별로 별도의 DAO를 만들거나 SQL을 외부에 독립시켜서 DB에 따라 변경해 사용하는 방법이 있다.

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

  • 진짜 문제는 DB마다 SQL만 다른 것이 아니라 에러의 종류와 원인도 제각각이라는 점이다. 그래서 JDBC는 데이터 처리 중 발생한 예외를 그냥 SQLException 하나에 모두 담아버린다. JDBC API는 이 SQLException 한 가지만 던지도로 설계되어 있다.
  • 예외가 발생한 원인은 SQLException 안에 담긴 에러 코드와 SQL 상태정보를 참조해봐야 한다. 그런데 SQLException의 getErrorCode()로 가져올 수 있는 DB 에러 코드는 DB별로 다르다. DB벤더가 정의한 고유한 에러 코드를 사용하기 떄문이다.

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

(SQL과 관련된 부분은 다른 장에서 살펴보고 SQLException의 비표준 에러코드의 해결책에 대해 알아보자)

  • DB 벤더에 따라 달라지는 호환성 없는 SQLException 문제를 해결하는 방법은 DB 벤더 별 에러 코드를 참고해서 발생한 예외의 원인이 무엇인지 해석해주는 기능을 만드는 것이다.
  • JdbcTemplate은 SQLException을 단지 런타임 예외인 DataAccessException으로 포장하는 것이 아니라 DB의 에러코드를 DataAccessException 계층구조의 클래스 중 하나로 매핑해 준다.

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

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

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

  • DAO의 사용 기술과 구현 코드는 전략 패턴과 DI를 통해서 DAO를 사용하는 클라이언트에게 감출 수 있지만, 메서드 선언에 나타나는 DB 접근 기술에 따라 다른 예외정보가 문제가 된다. DAO의 인터페이스를 분리해서 기술에 독립적인 인터페이스로 만들려면 각 구현에 따라 예외가 일치하지 않는 문제 역시 해결해야한다.

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

  • 그래서 스프링은 자바의 다양한 데이터 엑세스 기술을 사용할 때 발생하는 예외들을 추상화해서 DataAccessException 계층구조 안에 정리해놓았다.
  • 스프링의 JdbcTemplate은 DB 벤더 별로 다른 SQLException의 에러 코드를 DB별로 매핑해서 그에 해당하는 의미있는 DataAccessException의 서브클래스 중 하나로 전환해서 던져준다. 뿐만아니라 DataAccessException은 자바의 주요 데이터 엑세스 기술에서 발생할 수 있는 내부의 예외를 추상화한다.

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

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

테스트 보완

  • 테스트의 관심사 역시 중요하다. 구현 기술에 상관없이 DAO의 기능이 동작하는 데만 관심이 있다면 UserDao 인터페이스로 받아서 테스트하는 편이 낫다. 바면에 특정 기술을 사용한 UserDao 구현 애용에 관심을 가지고 테스트하려면 테스틍서 @Autowired로 DI받을 때 구체 클래스를 주입 받으면 된다.
profile
소프트웨어 엔지니어, 일상

0개의 댓글