✔ 예외
이 포스팅은 토비의 스프링을 읽고 개인적으로 정리하는 글입니다.
사라진 SQLException
- JdbcTemplate를 적용한 코드들에서는 모두
throws SQLExeption
선언이 사라졌다!
초난감 예외처리
예외 블랙홀
- 예외를
catch
하고 아무런 처리도 하지 않는 코드는 연습 중에도 절대 하면 안되는 코드이다.
- 아무처리도 하지 않는다면 발생한 예외로 인해 어떤 기능이 비정상적으로 동작하거나, 메모리나 리소스가 소진되거나, 예상치 못한 다른 문제를 일으킬 수 있다.
- 모든 예외는 적절하게 복구되든지 아니면 작업을 중단시키고 운영자 또는 개발자에게 분명하게 통보돼야 한다.
무의미하고 무책임한 throws
- 예외를 흔적도 없이 먹어치우는 예외 블랙홀보다 조금 낫지만 이런 무책임한 throws 선언도 심각한 문제점이 있다.
- 적절한 처리를 통해 복구될 수 있는 예외상황도 제대로 다룰 수 있는 기회를 박탈당한다.
예외의 종류와 특징
- 가장 큰 이슈는 체크 예외(Checked Exception)이라고 불리는 명시적인 처리가 필요한 예외를 사용하고 다루는 방법
- 자바에서 throw를 통해 발생시킬 수 있는 예외는 크게 세 가지가 있다.
ERROR
- java.lang.Error 클래스의 서브클래스들이다.
- 에러는 시스템에 뭔가 비정상적인 상황이 발생했을 경우에 사용된다.
- 주로 자바 VM에서 발생시키고 어플리케이션 코드에서 잡으려고 하면 안된다.
Exception과 체크 예외
- java.lang.Exception 클래스와 그 서브클래스로 정의되는 예외들은 에러와 달리 개발자들이 만든 애플리케이션 코드의 작업 중에 예외상황이 발생했을 경우에 사용된다.
Checked Exception
과 Unchecked Exception
로 구분된다.
- 전자는
RuntimeException
클래스를 상속하지 않은 것들, 후자는 RuntimeException
을 상속한 클래스들을 말한다.
- 체크 예외가 발생할 수 있는 메소드를 사용할 경우 반드시 예외를 처리하는 코드를 함께 작성해야 한다. 아니면 컴파일 에러가 발생한다.
RuntimeException과 언체크/런타임 예외
- java.lang.RuntimeException 클래스를 상속한 예외들은 명시적인 예외처리를 강제하지 않기 때문에 언체크 예외라고 불린다. 혹은 런타임 예외라고도 한다.
- 이 런타임 예외는
catch
문으로 잡거나 throws
로 선언하지 않아도 된다.
- 주로 프로그램의 오류가 있을 때 발생하도록 의도된 것들이다.
- NPE, IAE등이 있다.
예외처리 방법
예외 복구
- 첫 번째 예외처리 방법은 예외상황을 파악하고 문제를 해결해서 정상 상태로 돌려놓는 것이다.
- 예외로 인해 기본 작업 흐름이 불가능하면 다른 작업 흐름으로 자연스럽게 유도해주는 것이다.
- 단순히 에러 메시지가 사용자에게 그냥 던져지는 것은 예외 복구라고 볼 수 없다.
- 예외처리 코드를 강제하는 체크 예외들은 이렇게 예외를 어떤 식으로든 복구할 가능성이 있는 경우에 사용한다.
예외처리 회피
- 두 번째 방법은 예외처리를 자신이 담당하지 않고 자신을 호출한 쪽으로 던져버리는 것이다.
throws
문으로 선언해서 예외가 발생하면 알아서 던져지게 하거나 catch
문으로 일단 예외를 잡은 후에 로그를 남기고 다시 예외를 던지는 것이다.
- 예외를 자신이 처리하지 않고 회피하는 방법이다.
- 콜백과 템플릿처럼 긴밀하게 역할을 분담하고 있는 관계가 아니라면 자신의 코드에서 발생하는 예외를 그냥 던져버리는 건 무책임한 책임회피일 수 있다.
- 예외를 회피하는 것은 예외를 복구하는 것처럼 의도가 분명해야 한다.
예외 전환
- 마지막으로 예외를 처리하는 방법은 예외 전환을 하는 것이다.
- 예외 회피와 비슷하게 예외를 복구해서 정상적인 상태로는 만들 수 없기 때문에 예외를 메소드 밖으로 던지는 것이다.
- 하지만 예외 회피와 달리, 발생한 예외를 그대로 넘기는 것이 아니라 적절한 예외로 전환해서 던진다는 특징이 있다.
- 보통 두 가지 목적으로 사용된다.
- 첫 째는 내부에서 발생한 예외를 그대로 던지는 것이 그 예외 상황에 대한 적절한 의미를 부여해주지 못하는 경우에, 의미를 분명하게 해줄 수 있는 예외로 바꿔주기 위해서다. API가 발생하는 기술적인 로우레벨을 상황에 적합한 의미를 가진 예외로 변경하는 것이다.
- 두 번째 전환 방법은 예외를 처리하기 쉽고 단순하게 만들기 위해 포장하는 것이다.
- 중첩 예외를 이용해 새로운 예외를 만들고 원인이 되는 예외를 내부에 담아서 던지는 방식은 같다.
- 주로 예외처리를 강제하는 체크 예외를 언체크 예외인 런타임 예외로 바꾸는 경우에 사용한다.
예외처리 전략
런타임 예외의 보편화
- 자바 엔터프라이즈 서버환경은 다르다. 수많은 사용자가 동시에 요청을 보내고 각 요청이 독립적인 작업으로 취급된다.
- 하나의 요청을 처리하는 중에 예외가 발생하면 해당 작업만 중단시키면 그만이다.
- 차라리 애플리케이션 차원에서 예외상황을 미리 파악하고, 예외가 발생하지 않도록 차단하는 게 좋다.
- 최근에 등장하는 표준 스펙 또는 오픈소스 프레임워크에서는 API가 발생시키는 예외를 체크 예외 대신 언체크 예외로 정의하는 것이 일반화되고 있다.
add() 메소드의 예외처리
DuplicatedUserIdException
과 SQLException
두 가지의 체크 예외를 던지게 되어있다.
DuplicatedUserIdException
은 복구 가능한 예외이므로 add() 메소드
를 사용하는 쪽에서 잡아서 대응할 수 있다.
public class DuplicateUserIdExcption extends RuntimeException {
public DuplicateUserIdException(Throwable cause) {
super(cause);
}
}
- 이후
catch
절에서 에러발생의 원인에 따라 SQLException
을 사용할지 DuplicateUserIdException
을 사용할지 결정한다.
애플리케이션 예외
- 런타임 예외 중심의 전략은 굳이 이름을 붙이자면 낙관적인 예외처리 기법이라고 할 수 있다.
- 일단 잡고 보도록 강제하는 체크 예외의 비관적인 접근 방법과 대비된다.
- 반면에 시스템 또는 외부의 예외상황이 원인이 아니라 애프릴케이션 자체의 로직에 의해 의도적으로 발생시키고, 반드시
catch
해서 무엇인가 조치를 취하도록 요구하는 예외도 있다. 이런 예외들을 일반적으로 애플리케이션 예외
라고 한다.
SQLException은 어떻게 됐나?
- 먼저 생각해볼 사항은
SQLException
은 과연 복구가 가능한 예외인가이다. 대부분의 SQLException
은 코드 레벨에서는 복구할 방법이 없다.
- 관리자나 개발자에게 빨리 예외가 발생했다는 사실이 알려지도록 전달하는 방법밖에는 없다.
- 스프링의
JdbcTemplate
는 바로 이 예외처리 전략을 따르고 있다.
JdbcTemplate
템플릿과 콜백 안에서 발생하는 모든 SQLException
을 런타임 예외인 DataAccessException
으로 포장해서 던져준다. 따라서 JdbcTemplate
을 사용하는 UserDao
메소드에선 꼭 필요한 경우에만 런타임 예외인 DataAccessException
을 잡아서 처리하면 되고 그 외의 경우에는 무시해도 된다. 그래서 DAO메소드에서 SQLException
이 모두 사라진 것이다.
- 그 밖에 스프링의 API 메소드에 정의되어 있는 대부분의 예외는 런타임 예외다.
예외 전환
- 예외를 다른 것으로 바꿔서 던지는 예외 전환의 목적은 아래 두 가지이다.
- 런타임 예외로 포장해서 굳이 필요하지 않은
catch/throws
를 줄여주는 것
- 로우레벨의 예외를 좀 더 의미 있고 추상화된 예외로 바꿔서 던져주는 것
- 스프링의
JdbcTemplate
이 던지는 DataAccessException
은 일단 런타임 예외로 SQLException
을 포장해주는 역할을 한다. 그래서 대부분 복구가 불가능한 예외인 SQLException
에 대해 애플리케이션 레벨에서는 신경 쓰지 않도록 해주는 것이다.
- 현실적으로 DB를 자유롭게 바꾸어 사용할 수 있는 DB 프로그램을 작성하는 데는 두 가지 걸림돌이 있다.
비표준 SQL
- 첫 째 문제는 JDBC 코드에서 사용하는 SQL이다.
- 몇 가지 표준 규약이 있긴 하지만, 대부분의 DB는 표준을 따르지 않는 비표준 문법과 기능도 제공한다.
- 이러한 비표준 SQL은 결국 DAO 코드에 들어가고, 해당 DAO는 특정 DB에 대해 종속적인 코드가 되고 만다.
- 이 문제의 해결책은 호환 가능한 표준 SQL만 사용하는 방법과, DB별로 별도의 DAO를 만들거나 SQL을 외부에 독립시켜서 DB에 따라 변경해사용하는 방법이 있다.
호환성 없는 SQLException의 DB 에러 정보
- 두 번째 문제는 바로
SQLException
이다. DB를 사용하다가 발생할 수 있는 예외의 원인은 다양하다.
- 문제는 DB마다 SQL만 다른 것이 아니라 에러의 종류와 원인도 제각각이라는 점이다. 그래서 Jdbc는 데이터 처리 중에 발생하는 다양한 예외를 그냥
SQLException
하나에 모두 담아버린다.
- SQLException이 상태 코드를 제공하는 이유는 DB에 독립적인 에러정보를 얻기 위해서다. 문제는 JDBC 드라이버에서 SQLException을 담을 상태 코드를 정확하게 만들어주지 않는다는 점이다.
DB 에러 코드 매핑을 통한 전환
- DB 종류가 바뀌더라도 DAO를 수정하지 않으려면 이 두 가지 문제를 해결해야 한다.
- DB 별 에러 코드를 참고해서 발생한 예외의 원인이 무엇인지 해석해주는 기능을 만드는 것이다.
- DB 종류에 상관없이 동일한 상황에서 일관된 예외를 전달받을 수 있다면 효과적인 대응이 가능하다.
- 스프링은 데이터 엑세스 작업 중에 발생할 수 있는 예외상황을 수십 가지로 분류하고 이를 추상화해 정의한 다양한 예외 클래스를 제공한다.
- 문제는 DB마다 에러 코드가 제각각이라는 점이다. 대신 스프링은 DB별 에러 코드를 분류해서 스프링이 정의한 예외 클래스와 매핑해놓은 에러 코드 매핑정보 테이블을 만들어두고 이를 이용한다.
- JdbcTemplate를 이용한다면 Jdbc에서 발생하는 DB 관련 예외는 거의 신경 쓰지 않아도 된다.
DAO 인터페이스와 DataAccessException 계층 구조
- DataAccessException은 JDBC의 SQLException을 전환하는 용도로만 만들어진 건 아니다. JDBC 외의 자바 데이터 액세스 기술에서 발생하는 예외에도 적용된다.
- DataAccessException은 의미가 같은 예외라면 데이터 액세스 기술의 종류와 상관없이 일관된 예외가 발생하도록 만들어준다. 데이터 액세스 기술에 독립적인 추상화된 예외를 제공하는 것이다.
DAO 인터페이스와 구현의 분리
- DAO를 굳이 따로 만들어서 사용하는 이유는 무엇인가? 데이터 액새스 로직을 담은 코드를 성격이 다른 코드에서 분리해놓기 위해서다.
- 또한 분리된 DAO는 전략패턴을 적용해 구현 방법을 변경해서 사용할 수 있게 만들기 위해서이기도 하다.
- Jdbc를 사용하는 구현체에서는 throws SQLException을 사용해도 되지만 다른 구현체에서는 이렇게 처리하면 안되므로 DAO 메소드 내에서 런타임 예외로 포장해주는 것으로 할 수 있다.
- 하지만 결국 클라이언트가 DAO의 기술에 의존적이 될 수 밖에 없으므로 이것은 불충분하다.
데이터 액세스 예외 추상화와 DataAccessException 계층구조
- 스프링은 자바의 다양한 데이터 액세스 기술을 사용할 때 발생하는 예외들을 추상화해서 DataAccessException 계층구조 안에 정리해놓았다.
- DataAccessException은 자바의 주요 데이터 액세스 기술에서 발생할 수 있는 대부분의 예외를 추상화하고 있다.
- DataAccessException 계층구조에는 템플릿 메소드나 DAO 메소드에서 직접 활용할 수 있는 예외도 정의되어 있다.
- JdbcTemplate과 같이 스프링의 데이터 액세스 지원 기술을 이용해 DAO를 만들면 사용 기술에 독립적인 일관성 있는 예외를 던질 수 있다.
- 결국 인터페이스 사용, 런타임 예외 전환과 함께 DataAccessException 예외 추상화를 적용하면 데이터 액세스 기술과 구현 방법에 독립적인 이상적인 DAO를 만들 수가 있다.
DataAccessException 활용 시 주의사항
- DuplicateKeyException은 아직까지는 JDBC를 이용하는 경우에만 발생한다. 하이버네이트나 JPA를 사용했을 때도 동일한 예외가 발생할 것으로 기대하지만 실제로 다른 예외가 던져진다.
- 그 이유는 SQLException에 담긴 DB의 에러 코드를 바로 해석하는 JDBC의 경우와 달리 JPA나 하이버네이트, JDO 등에서는 각 기술이 재정의한 예외를 가져와 스프링이 최종적으로 DataAccessException으로 변환하는데, DB의 에러코드와 달리 이런 예외들은 세분화 되어 있지 않기 때문이다.