Study with book/토비의 스프링 3.1

4장. 예외


자바 개발자가 가장 신경 쓰기 귀찮아하는 것 중의 하나가 바로 예외처리다. 정상적인 결과와 흐름을 보여주는 코드를 만들기도 버거운데 예외상황까지 처리해야한다는 사실이 부담스러울 수도 있다. JdbcTemplate을 대표로 하는 스프링의 데이터 엑세스 기능에 담겨 있는 예외 처리와 관련된 접근 방법에 대해 알아본다. 

4.1  사라진 SQLException

4.1.1 초난감 예외처리

예외 블랙홀

예외를 잡고는 아무것도 하지 않는다. 예외 발생을 무시해버리고 정상적인 상황인 것처럼 다음 라인으로 넘어가겠다는 분명한 의도가 있는 게 아니라면 연습 중에도 절대 만들어서는 안 되는 코드다. 예외가 발생하면 그것을 catch 블록을 써서 잡아내는 것까지는 좋은데 그리고 아무것도 하지 않고 별문제 없는 것처럼 넘어가 버리는 건 정말 위험한 일이다. 원치 않는 예외가 발생하는 것보다도 훨씬 더 나쁘다. 왜냐하면 프로그램 실행 중에 어디선가 오류가 있어서 예외가 발생했는데 그것을 무시하고 계속 진행해버리기 때문이다. 그리고 예외를 무시하면 그 시스템 오류나 이상한 결과의 원인이 무엇인지 찾아내기가 매우 힘들다.

 

다음은 예외가 발생하면 화면 콘솔에 그 에러 메시지를 찍어준다. 예외가 발생하면 화면에 출력해주는데 뭐가 문제일까? 개발 중에는 IDE 콘솔이나 서버 실행창에 이 메시지가 눈에 확 띄게 보이니 문제가 생겨도 금방 알아차리고 뭔가 조치를 취할 수 있을지 모르겠다. 그러나 다른 로그나 메시지에 금방 묻혀버리면 놓치기 쉽상이다. 콘솔 로그를 누군가가 계속 모니터링하지 않는 한 이 예외 코드는 심각한 폭탄을 남아 있을 것이다. 예외는 처리되어야 한다.

스터디 중에서 왜 system.out.println() 함수와 e.printStackTrace() 함수를 쓰면 안되는지 이야기가 나왔는데 위에서 말한 내용도 그 이유가 될 수 있지만, 성능 차이가 더 큰 이유였다. 나중에 어떤 성능차이가 있는지 알아보도록 하자.

 

무의미하고 무책임한 throws

예외를 흔적도 없이 먹어치우는 예외 블랙홀보다는 조금 낫긴 하지만 이런 무책임한 throws 선언도 심각한 문제점이 있다. 자신이 사용하려고 하는 메소드에 throws Exception이 선언되어 있다고 생각해보면 그런 메소드 선언에서는 의미 있는 정보를 얻을 수 없다. 결국 이런 메소드를 사용하는 메소드에서도 역시 throws Exception을 따라서 붙이는 수밖에 없다. 결과적으로 적절한 처리를 통해서 복구될 수 있는 예외 상황도 제대로 다룰 수 있는 기회를 박탈당한다.

 

4.1.2 예외의 종류와 특징

ERROR

java.lang.Error 클래스의 서브클래스들이다. 에러는 시스템에 뭔가 비정상적인 상황이 발생했을 경우에 사용된다. 그래서 주로 자바 VM에서 발생시키는 것이고 애플리케이션 코드에서 잡으려고 하면 안된다. 대표적으로 OutOfMemoryError나 ThreadDeath 같은 에러는 catch 블록에서 잡아봤자 아무런 대응 방법이 없기 때문이다. 

 

체크 예외

Exception 클래스의 서브클래스이면서 RuntimeException 클래스를 상속하지 않은 것들이다. 체크 예외가 발생할 수 있는 메소드를 사용할 경우 반드시 예외를 처리하는 코드를 함께 작성해야 한다. 사용할 메소드가 체크 예외를 던진다면 이를 catch 문으로 잡든지, 아니면 다시 throws를 정의해서 메소드 밖으로 던져야 한다. 그렇지 않으면 컴파일 에러가 발생한다. 대표적으로 IOException, SQLException 등이 있다. 

 

언체크 예외

Exception 클래스의 서브클래스이면서 RuntimeException 클래스를 상속한 것들이다. 이러한 예외들은 명시적인 예외처리를 강제하지 않기 때문에 언체크 예외라고 불린다. 에러와 마찬가지로 이 런타임 예외는 catch문으로 잡거나 throws로 선언하지 않아도 된다. 물론 명시적으로 잡거나 throws로 선언해줘도 상관없다. 런타임 예외는 주로 프로그램의 오류가 있을 때 발생하도록 의도된 것들이다. 개발자가 부주의해서 발생할 수 있는 경우에 발생하도록 만든 것이 런타임 예외이다. 런타임 예외는 예상하지 못했던 예외상황에서 발생하는 게 아니기 때문에 굳이 catch나 throws를 사용하지 않아도 되도록 만든 것이다. 대표적으로 NullpointException, IllegalArgumentException 등이 있다.

 

4.1.3 예외처리 방법

예외 복구

첫 번째 예외처리 방법은 예외 상황을 파악하고 문제를 해결해서 정상 상태로 돌려놓는 것이다. 네트워크가 불안해서 가끔 서버에 접속이 잘 안되는 열약한 환경에 있는 시스템이라면 원격 DB서버에 접속하다가 실패해서 SQLException이 발생하는 경우에 재시도를 해볼 수 있다. 혹은 일정 시간 대기했다가 다시 접속을 시도해보는 방법을 사용해서 예외상황으로부터 복구를 시도할 수 있다. 이렇듯 예외처리 코드를 강제하는 체크 예외들은 이렇게 예외를 어떤 식으로든 복구할 가능성이 있는 경우에 사용한다.

 

예외처리 회피

예외처리를 자기가 담당하지 않고 호출한 쪽으로 던지는 방법이다. 콜백 오브젝트의 메소드는 모두 throws SQLException이 붙어있다. SQLException을 처리하는 일은 콜백 오브젝트의 역할이 아니라고 보기 때문이다. 예외를 회피하는 것은 예외를 복구하는 것처럼 의도가 분명해야 한다. 콜백/템플릿처럼 긴밀한 관계에 있는 다른 오브젝트에게 예외처리 책임을 분명히 지게 하거나, 자신을 사용하는 쪽에서 예외를 다루는 게 최선의 방법이라는 확신이 있어야 한다.

 

예외 전환

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

예외 전환을 하는 이유는 두 가지가 있는데 아래와 같다.

1) 내부에서 발생한 예외를 그대로 던지는 것이 그 예외 상황에 대한 적절한 의미를 부여해주지 못하는 경우에, 의미를 분명하게 해줄 수 있는 예외로 바꿔주기 위해서다.

2) 예외를 처리하기 쉽고 단순하게 만들기 위해 포장하는 것이다. 주로 예외처리를 강제하는 체크 예외를 언체크 예외인 런타임 예외로 바꾸는 경우에 사용한다. 대표적으로 EJBException이 있다. EJBException은 RuntimeException 클래스를 상속한 런타임 예외다. 런타임 예외이기 때문에 EJB 컴포넌트를 사용하는 다른 EJB나 클라이언트에서 일일이 예외를 잡거나 다시 던지는 수고를 할 필요가 없다. 이런 예외는 잡아도 복구할 만한 방법이 없기 때문이다. 반대로 언체크 예외를 체크 예외로 바꾸는 경우가 있다. 비지니스적인 의미가 있는 예외는 이에 대한 적절한 대응이나 복구 작업이 필요하기 때문이다.

 

4.1.4 예외처리 전략

실습 코드 링크 : https://github.com/vvshinevv/toby-spring/tree/feature/4.1.4

런타임 예외의 보편화

자바가 처음 만들어질 때 많이 사용되던 애플릿이나 AWT, 스윙을 사용한 독립형 애플리케이션에서는 통제 불가능한 시스템 예외라고 할지라도 애플리케이션의 작업이 중단되지 않게 해주고 상황을 복구해야 했다. 예를 들어 워드의 파일 열기 기능에서 사용자가 입력한 이름에 해당하는 파일을 찾을 수 없다고 애플리케이션이 종료되어버리게 할 수는 없다.

그러나 자바 엔터프라이즈 서버 환경은 다르다. 수 많은 사용자가 동시에 요청을 보내고 각 요청이 독립적인 작업으로 취급된다. 하나의 요청을 처리하는 중에 예외가 발생하면 해당 작업만 중단시키면 그만이다. 차라리 예외상황을 미리 파악하고, 예외가 발생하지 않도록 차단하는게 좋다. 혹은 개발자에게 통보해서 빠르게 대응하는 편이 낫다.

 

4.1.5 SQLException은 어떻게 됐나?

스프링의 JdbcTemplate은 언체크/런타임 예외로 전환하는 정책을 따르고 있다. 모든 SQLException을 런타임 예외인 DataAccessException으로 포장해서 던져준다. 그래서 메소드에서 사라지게 되었다.

4.2 예외 전환

4.2.1 JDBC의 한계

1) 비표준 SQL

SQL은 어느정도 표준화된 언어이고 몇 가지 표준 규약이 있지만 대부분의 DB는 표준을 따르지 않는 비표준 문법과 기능을 제공한다. 결국 비표준 SQL은 DAO 코드에 들어가게 되고, 해당 DAO는 특정 DB에 종속적인 코드가 되고 만다.

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

 

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

SQLException이 문제이다. DB를 사용하다가 발생할 수 있는 예외의 원인은 다양하다. 문제는 DB마다 SQL만 다른 것이 아니라 에러의 종류와 원인도 제각각이라는 점이다. 그래서 JDBC는 데이터 처리 중에 발생하는 다양한 예외를 그냥 SQLException 하나에 모두 담아버린다. 결국 호환성 없는 에러 코드와 표준을 잘 따르지 않는 상태 코드를 가진 SQLException만으로 DB에 독립적인 유연한 코드를 작성하는 건 불가능에 가깝다.

 

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

SQLException의 비표준 에러 코드와 SQL 상태정보에 대한 해결책을 알아보자. 스프링은 DataAccessException이라는 SQLException을 대체할 수 있는 런타임 예외를 정의하고 있을 뿐 아니라 DataAccessException의 서브클래스로 세분화된 예외 클래스들을 정의하고 있다. 문제는 DB마다 에러 코드가 제각각이라는 점이다. 스프링은 DB별 에러 코드를 분류해서 스프링이 정의한 예외 클래스와 매핑해놓은 에러 코드 매핑정보 테이블을 만들어두고 이를 이용한다. 전환되는 JdbcTemplate에서 던지는 예외는 모두 DataAccessException의 서브클래스 타입이다. 

 

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

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

DAO를 인터페이스로 분리해서 기술에 독립하려면 인터페이스로 분리해야 한다. 그러나 데이터 액세스 기술의 API는 자신만의 독자적인 예외를 던지기 때문에 인터페이스 분리에 한계가 있다. 

 

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

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

 

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

실습 코드 링크 : https://github.com/vvshinevv/toby-spring/tree/feature/4.2.4

인터페이스 적용

테스트 보완

DataAccessException 활용 시 주의사항


[참고자료]

토비 스프링 vol.1 - 이일민

'Study with book > 토비의 스프링 3.1' 카테고리의 다른 글

6장. AOP1  (0) 2020.04.12
5장. 서비스 추상화  (0) 2020.04.05
3장. 템플릿2  (0) 2020.03.22
3장. 템플릿1  (0) 2020.03.22
2장. 테스트  (0) 2020.03.15