예외 블랙홀
try { ... }catch(SQLException e){ } |
예외를 잡고 아무것도 하지 않는다.. 예외 발생을 무시해버리고 정상적인 상황인 것 처럼 다음 라인으로 넘어가겠다는 분명한 의도가 있는게 아니라면 연습 중에도 절대 만들어서는 안되는 코드다.
}catch(SQLException e){ System.out.println(e); } }catch(SQLException e){ e.printStrackTrace(); } |
화면에 메시지를 출력한 것은 예외를 처리한 게 아니다.
예외를 처리할 때 반드시 지켜야 할 핵심 원칙은 한 가지다.
모든 예외는 적절하게 복구되즌지 아니면 작업을 중단시키고 운영자 또는 개발자에게 분명하게 통보돼야 한다.
}catch(SQLException e){ e.printStrackTrace(); System.exit(1); } |
차라리 위와 같이 만드는 것이 백 배 낫다. 물론 실전에서 이렇게 만들라는 건 아니다. 예외를 무시하거나 잡아먹어 버리는 코드는
만들지 말라는 뜻이다.
무의미하고 무책임한 throws
EJB가 한창 쓰이던 시절에 흔히 볼 수 있던 코드다.
public void method1() throws Exception { method2(); } public void method2() throws Exception { method3(); } public void method3() throws Exception { } |
예외를 흔적도 없이 먹어버리는 예외 블랙홀보다는 조금 낫긴 하지만 이런 무책임한 throws 선언도 심각한 문제점이 있다.
결국 이런 메소드를 사용하는 메소드에서도 역시 throws Exception 을 따라서 붙이는 수밖에 없다. 결과적으로 적절한 처리르 ㄹ통해 복구될 수 있는 예외상황도 제대로 다룰 수 있는 기회를 박탈당한다.
위의 두가지 나쁜 습관은 어떤 경우에도 용납하지 않아야 한다.
그렇다면 예외를 어떻게 다뤄야 할까?
자바에서 throw 를 통해 발생시킬 수 있는 예외는 크게 세가지가 있다.
□ Error
첫번째는 java.lang.Error 클래스의 서브클래스들이다.
주로 Java VM 에서 발생시키는 에러이므로 Application 에서 잡으려고 하면 안된다.
OutOfMemoryError 나 ThreadDeath 같은 에러는 catch 블록으로 잡아봤자 아무런 대응 방법이 없다.
따라서 system level 에서 특별한 작업을 하는게 아니라면 application 에서는 이런 에러에 대한 처리는 신경쓰지 않아도 된다.
□ Exception 과 체크 예외
java.lang.Exception 클래스와 그 서브클래스로 정의되는 예외들은 개발자들이 만든 application code 작업 중에 예외상황이 발생했을 경우에 사용된다.
Exception 은 check exception 과 unchecked exception 으로 구분된다.
일반적으로 예외라고 하면 Exception 클래스의 서브클래스 중에서 RuntimeExceptino을 상속하지 않는 것만을 말하는 check exception 이라고 생각해도 된다. 사용할 메소드가 체크 예외를 던진다면 catch 문으로 잡든지, throws 를 정의해서 메소드 밖으로 던져야 한다.
그렇지 않으면 컴파일 에러가 발생한다.
□ RuntimeException 과 언체크/런타임 예외
java.lang.RuntimeException 클래스를 상속한 예외들은 명시적인 예외처리를 강제하지 않기 때문에 언체크 예외라고 불린다.
또는 런타임 예외라고도 한다. 대표적으로 NullPointerException 이나 IllegalArgumentException 등이 있다.
피할 수 있지만 개발자가 부주의해서 발생할 수 있는 경우에 발생하도록 만든 것이 런타임 예외다.
따라서 런타임 예외는 예상하지 못했던 예외상황에서 발생하는 게 아니기 때문에 굳이 catch나 throws 사용하지 않아도 되도록 만든 것이다.
그런데 자바 언어를 설계하고 JDK 를 개발한 사람들의 이런 설계의도와는 현실과 잘 맞지 않았고 비난의 대상이 되기도 했다.
특히 체크 예외가 예외처리를 강제하는 것 때문에 예외 블랙홀이나 무책임한 throws 같은 코드가 남발됐다.
최근에 새로 등장하는 자바 표준 스펙의 API들은 예상 가능한 예외상황을 다루는 예외를 체크 예외로 만들지 않는 경향이 있기도 하다.
예외 처리 방법
예외 복구
1) 사용자가 요청한 파일을 읽으려고 하는데 파일이 없을 경우 IOException 이 발생, 이런 경우 상황을 알리고 다른 파일을 이용하도록 안내. IOException 에러 메시지가 사용자에게 그냥 던져지는 것은 예외 복구라고 볼 수 없다.
2) 네트워크 접속 시 실패할 경우 MAX_RETRY 만큼 재시도
예외처리 회피
public void add() throws SQLException { // JDBC API } public void add() throws SQLException { try { // JDBC API } catch ( SQLException e ) { // 로그 출력 throw e; } } |
JdbcContext 나 JdbcTemplate 이 사용하는 콜백 오브젝트는 ResultSet 이나 PreparedStatement 등을 이용해서 작업하다 발생하는 SQLException을 자신이 처리하지 않고 템플릿으로 던져버린다.
만약 SDAO가 SQLException 을 생각없이 던져버리면 어떻게 될까?
DAO 를 사용하는 서비스 계층이나 웹 컨트롤러에서 과연 SQLException 을 제대로 처리할 수 있을까?
예외를 회피하는 것은 예외를 복구하는 것처럼 의도가 분명해야 한다.
예외 전환
발생한 예외를 그대로 넘기는 게 아니라 적절한 예외로 전환해서 던진다는 특징이 있다.
public void add(User user) throws DuplicateUserIdException, SQLException { try { } catch (SQLException e) { if ( e.getErrorCode == MysqlErrorNumbers.ER_DUP_ENTRY ) { throw DuplicateUserIdException(); } else { throw e; } } } |
예외 처리 전략
런타임 예외의 보편화
독립형 애플리케이션과 달리 서버의 특정 계층에서 예외가 발생했을 때 작업을 일시 중지하고 사용자와 바로 커뮤니케이션하면서 예외상황을 복구할 수 있는 방법이 없다.
차라리 채르리케이션 차원에서 예외상황을 미리 파악하고, 예외가 발생하지 않도록 차단하는게 좋다. 또는 프로그램의 오류나 외부 환경으로 인해 예외가 발생하는 경우라면 빨리 해당 요청의 작업을 취소하고 서버 관리자나 개발자에게 통보해주는 편이 낫다.
자바 초기부터 있었던 JDK의 API와 달리 최근에 등장하는 표준 스펙 또는 오픈소스 프레임워크에서는 API가 발생시키는 예외를 체크 예외 대신 언체크 예외로 정의하는 것이 일반화되고 있다.
런타임 예외를 일반화해서 사용하는 방법은 여러모로 장점이 많다.
애플리케이션 예외
시스템 또는 외부의 예외상황이 원인이 아니라 애플리케이션 자체의 로직에 의해 의도적으로 발생시키고, 반드시 catch 해서 무엇인가 조치를 취하도록 요구하는 예외도 있다. 이런 예외들을 일반적으로 애플리케이션 예외라고 한다.
예외 상황에 대한 리턴 값을 명확하게 코드화하고 잘 관리하지 않으면 혼란이 생길 수 있다. 어떤 개발자는 0, -1, -999를 돌려줄 수도 있다. 사전에 상수로 정의해둔 표준코드를 사용하지 않는다면 자칫 개발자 사이의 의사소통 문제로 인해 제대로 동작하지 않을 위험이 있다.
또 한가지 문제는 결과 값을 확인하는 조건문이 자주 등장한다는 점이다 이런 식으로 결과를 돌려주는 메소드를 연이어 사용하는 경우라면 if 블록이 범벅된 코드가 이어질지 모른다. 코드는 지저분해지고 흐름을 파악하고 이해하기가 힘들어 질 것이다.
try 블록 안에 깔끔하게 정리해 두고 예외상황에 대한 처리는 catch 블록에 모아둘 수 있기 때문에 코드를 이해하기도 편하다. 번거로운 if 문을 남발하지 않아도 된다.
try { BigDecimal balance = account.withdraw(amount); // 정상적인 처리결과를 출력하도록 진행 } catch (InsufficientBalanceException e) { // InsufficientBalanceException에 담긴 인출 가능한 잔고금액 정보를 가져옴. BigDecimal availFunds = e.getAvilFunds(); ... // 잔고 부족 안내 메시지를 준비하고 이를 출력하도록 진행 } |
SQLException은 어떻게 됐나?
먼저 생각해볼 사항은 SQLException은 과연 복구가 가능한 예외인가인다. 99%의 SQLException은 코드 레벨에서는 복구할 방법이 없다.
프로그램의 오류 또는 개발자의 부주의 때문에 발생하는 경우이거나, 통제할 수 없는 외부상황 때문에 발생하는 것이다.
예를 들어 SQL 문법이 틀렸거나, 제약조건을 위반했거나, DB 서버가 다운됐거나, 네트워크가 불안정하거나, DB 커넥션 풀이 꽉 차 DB 커넥션을 가져올 수 없는 경우 등이다. 시스템 예외라면 당연히 애플리케이션 레벨에서 복구할 방법이 없다. 관리자가 개발자에게 빨리 예외가 발생했다는 사실이 알려지도록 전달하는 방버밖에 없다.
Spring의 JdbcTemplate 은 바로 이 예외 처리 전략을 따르고 있다. JdbcTemplate 템플릿과 콜백 안에서 발생하는 모든 SQLExceptino을 런타임 예외인 DataAccessExceptino으로 포장해서 던져준다.
예외 전환
스프링의 JdbcTemplate이 던지는 DataAccessException은 일단 런타임 예외로 SQLException을 포장해주는 역할을 한다.
그래서 대부분 복구가 불가능한 예외인 SQLExceptino에 대해 애플리케이션 레벨에서는 신경 쓰지 않도록 해주는 것이다.
또한 DataAccessException은 SQLException에 담긴 다루기 힘든 상세한 예외정보를 의미 있고 일관성 있는 예외로 전환해서 추상화해주려는 용도로 쓰이기도 한다.
DB 에러 코드 매핑을 통한 전환
DB 종류가 바뀌더라도 DAO 를 수정하지 않으려면 두가지 문제 (비표준 SQL, 호환성 없는 SQLException의 DB 에러 정보)를 해결해야 한다. SQL 상태 코드는 JDBC 드라이버를 만들 때 들어가는 것이므로 같은 DB라고 하더라도 드라이버를 만들 때마다 달라지기도 하지만, DB 에러코드는 DB에서 직접 제공해주는 것이니 버전이 올라가더라도 어느 정도 일관성이 유지된다.
키 값이 중복돼서 중복 오류가 발생하는 경우 MySQL이라면 1062. 오라클은 1, DB2 라면 0803이라는 에러 코드를 받게 된다.
Spring의 DataAccessException 은 서브클래스로 BadSqlGrammarException (SQL 문법 오류), DataAccessResourceFailureException(DB 커넥션 가져오지 못한 경우), DataIntegrityViolationException (데이터 제약조건 위배),
DuplicateKeyException (중복 키 발생)등을 사용할 수 있다.
문제는 DB 마다 에러 코드가 제각각이라는 점이다.
스프링은 DB별 에러코드를 분류해서 스프링이 정의한 예외 클래스와 매핑해놓은 에러 코드 매핑정보 테이블을 만들어두고 이를 이용한다.
<bean id="Oracle" class="org.springframework.jdbc.support.SQLErrorCodes"> <property name="badSqlGrammarCodes"> // 예외 클래스 종류 <value>900,903,904,917,936,942,17006</value> // 매핑되는 DB 에러코드, 에러코드가 세분화된 경우에는 여러개 </property> <property name="invalidResultSetAccessCode"> <value>17003</value> </property> ... ... </bean> |
중복 키 예외의 전환
public void add() throws DuplicateUserIdExceptikon { try { // jdbcTemplate 을 이용해 user 를 add } catch ( DuplicateKeyException e ) { throw new DuplicateUserIdException(e); // 예외를 전환할 때는 원인이 되는 예외를 중첩하는 것이 좋다. } } |
스프링의 에러 코드 매핑을 통한 DataAccessException 방식을 사용하는 것이 이상적이다.
DAO 인터페이스와 DataAccessException 계층 구조
DAO 인터페이스와 구현의 분리
DAO 를 굳이 따로 만들어서 사용하는 이유는 무엇일까? 가장 중요한 이유는 데이터 엑세스 로직을 담은 코드를 성격이 다른 코드에서 분리해놓기 위해서다. 또한 분리된 DAO 는 전략 패턴을 적용해 구현 방법을 변경해서 사용할 수 있게 만들기 위해서이기도 하다. DAO 를 사용하는 쪽에서는 DAO가 내부에서 어떤 데이터 엑세스 기술을 사용하는지 신경 쓰지 않아도 된다. DAO 는 인터페이스를 사용해 구체적인 클래스 정보와 구현방법을 감추고, DI를 통해 제공되도록 만드는 것이 바람직하다.
데이터 액세스 예외 추상화와 DataAccessException 계층 구조
스프링은 자바의 다양한 데이터 엑세스 기술을 사용할 때 발생하는 예외들을 추상화해서 DataAccessExceptino 계층구조 안에 정리해 놓았다. 스프링이 기술의 종류에 상관없이 비슷한 성격의 예외를 특정 타입의 예외로 던져주므로 시스템 레벨의 예외처리 작업을 통해 개발자에게 빠르게 통보해주도록 만들 수 있다.
JdbcTempate과 같이 스프링의 데이터 액세스 지원 기술을 이용해 DAO 를 만들면 사용 기술에 독립적인 일관성 있는 예외를 던질 수 있다.
결국 인터페이스 사용, 런타임 예외 전환과 함께 DataAccessException 에외 추상화를 적용하면 데이터 액세스 기술과 구현 방법에 독립적인 이상적인 DAO 를 만들 수 있다.
정리
· 예외를 잡아서 아무런 조치를 취하지 않거나 의미 없는 throws 선언을 남발하는 것은 위험하다.
· 예외는 복구하거나 예외처리 오브젝트로 의도적으로 전달하거나 적절한 에외로 전환해야 한다.
· 좀 더 의미 있는 예외로 변경하거나, 불필요한 catch/throws 를 피하기 위해 런타임 에외로 포장하는 두가지 방법의 예외 전환이 있다.
· 복구할 수 없는 예외는 가능한 빨리 런타임 예외로 전환하는 것이 바람직하다.
· 애플리케이션의 로직을 담기 위한 예외는 체크 예외로 만든다.
· JDBC 의 SQLException은 대부분 복구할 수 없는 예외이므로 런타임 예외로 포장해야 한다.
· SQLException의 에러 코드는 DB에 종속되기 때문에 DB에 독립적인 예외로 전환될 필요가 있다.
· 스프링은 DataAccessException을 통해 DB에 독립적으로 적용 가능한 추상화된 런타임 예외 게층을 제공한다.
· DAO를 데이터 액세스 기술에서 독립시키려면 인터페이스 도입과 런타임 예외 전환, 기술에 독립적인 추상화된 예외로 전환이 필요하다.
'IT > Spring' 카테고리의 다른 글
Spring Boot - MyBatis 연동 (0) | 2018.08.29 |
---|---|
Spring Security - 시작하기 (0) | 2018.08.23 |
JUnit (0) | 2014.05.03 |
quartz cron 값 변경 (0) | 2014.01.07 |
Spring 이란? (0) | 2013.12.24 |