- 사라진 SQLException
-
JdbcTemplate로 변경 후 throws SQLException이 사라졌다.
// 변경 전 public void deleteAll() throws SQLException { this.jdbcContext.executeSql("delete from users"); } // 변경 후 public void deleteAll() { this.jdbcTemplate.update("delete from users"); }
- 초난감 예외처리
-
SQLException을 알아보기 전에 간단한 초난감 예외처리를 살펴보자.
// 예외를 catch하고 아무것도 하지 않는 코드. // 절대 이렇게 짜서는 안된다. try{ ... } catch(SQLException e){ } // 출력은 해주나, 다른 로그에 묻혀버리기 십상이다. try{ ... } catch(SQLException e){ System.out.println(e); } // 마찬가지로 묻혀버리기 십상이다. 양심에 찔린다.. try{ ... } catch(SQLException e){ e.printStacTrace(); }
-
무의미하고 무책임한 throws. 자신이 예외처리 하기 싫으니 계속 throws를 던지는 코드
public void method1() throws Exception { method2(); } public void method2() throws Exception { method3(); }
-
- 예외의 종류와 특징
- java.lang.Error : 시스템이 비정상적인 상황이 발생했을 경우에 사용되며, 주로 JVM에서 발생시킨다. 따라서 특별한 처리를 할 필요 없다.
- java.lang.Exception : 코드를 실행 중에 예외상황이 발생했을 경우에 사용되며, 체크 예외와 언체크 예외로 구분된다.
- 체크 예외 : RuntimeException은 상속받지 않는 예외. 이 예외는 catch 또는 thorws로 잡아야지 컴파일 에러가 나지 않음.
- 언체크 예외 : RuntimeException을 상속받는 예외. 명시적으로 예외처리를 강제하지 않음.
- 예외처리 방법
-
예외복구 : 예외가 발생해도, 문제를 해결하고 정상상태로 돌려놓는 방식.
// 재시도를 통해 예외 복구 int retry = 3; while(retry-- > 0){ try{ ... } catch(...){ } } // 예외 복구 실패시, 최종적으로는 예외를 던진다. throw new RetryFailedException();
- 예외처리 회피 : 예외를 자신이 처리하지 않고, throw. 예외를 회피하는 것은 의도가 분명해야 하며, 자신을 호출한 곳에서 예외처리 책임을 분명히 지게 해야 한다.
- 예외 전환 : **예외를 다른 예외로 전환.
-
의미를 분명한 예외로 전환
public void add(User user) throws DuplicateUserIdException, SQLException { try{ ... } catch(SQLException e){ // 에러를 파악해서 좀 더 분명한 에러를 자신이 만들어서 던짐 if(e.getErrorCode == MysqlErrorNumber.EP_DUL_ENTRY) throw DuplicateUserIdException(e); // 에러의 근본 원인도 알려주기 위해 이전 예외를 포함시킴. else throw e; } }
-
예외를 간단하게 처리하도록 하기 위해 감싸서 전환. 예를 들어 프레임워크에서 관리하는 예외로 포장함.
-
-
- 예외처리 전략
- 런타임(언체크) 예외의 보편화 : 체크 예외는 예외처리를 강제하는 것 때문에 무책임한 코드가 남발됐다고 비난하는 개발자도 많으며, 요즘 오픈소스들은 체크 예외를 만들지 않는 경향이 있다.
-
따라서 앞 내용인 예외 전환에서 만든 DuplicateUserIdException도 사실 복구 가능한 예외이긴 하지만, 대부분의 SQLException은 복구불가능하므로 체크 예외보다는 언체크 예외로 만드는 편이 좋다.
public class DuplicateUserIdException extends RuntimeException { public DuplicateUserIdException(Throwable cause){ super(cause); } }
-
언체크 예외로 수정한 add 메소드는 다음과 같다.
public void add(User user) throws DuplicateUserIdException { try{ ... } catch(SQLException e){ if(e.getErrorCode == MysqlErrorNumber.EP_DUL_ENTRY) throw new DuplicateUserIdException(e); else throw new RuntimeException(e); // SQLException도 런타임으로 포장 } }
- 애플리케이션 예외 : 시스템 또는 외부의 예외상황이 원인이 아닌 애플리케이션 로직에 의해 발생한 예외를 칭한다. 예외가 발생할 경우 코드에서는 보통 다음과 같이 처리한다.
- 다른 종류의 값을 리턴 (ex: 0, -1). 이러면 개발자들끼리 미리 의사소통으로 정해야 하지만, 잘못될 수 있으며, if문이 많아짐
- 체크예외를 만들어 던져주기. 상대적으로 안전함
- SQLException은 어떻게 됐나?
- SQLException은 복구할 방법이 거의 없으니 기계적인 throws 을 방치않게 Spring에서는 언체크/런타임 예외로 전환하였다.
- Spring은 모든 SQLException을 DataAccessException으로 예외전환 방식을 사용하여 포장해서 던져준다.
- Spring은 대부분의 예외는 런타임 예외
-
- 예외 전환
- JDBC의 한계
- JDBC는 DB에 접근하는 방법을 추상화된 API형태로 제공하므로, 다양한 DB종류를 사용해도, 표준 인터페이스를 사용하여 일관된 방법으로 개발 가능하다.
- 하지만 문제가 되는 부분이 두 가지 존재한다.
- 비표준 SQL : 표준화된 SQL을 사용하긴 하지만 특정한 기능들은 DB 제조사마다 비표준 SQL을 사용한다.
- 호환성 없는 SQLException의 DB 정보 : DB 제조사마다 던지는 에러의 종류와 원인이 다르다. XOPEN SQL 스펙에 정의된 에러에 대한 상태코드가 표준화는 되어있으나, 모든 DB제품이 이를 따르지는 않는다.
- DB 에러 코드 매핑을 통한 전환
- 먼저 호환성 없는 SQLException의 DB 정보 문제를 Spring에서는 어떻게 해결했을까?
- 각각 DB마다 에러코드를 해석하여 동일한 원인에 대하여 같게 처리한다. 예를 들어 키 값이 중복돼서 오류가 발생하는 경우 Mysql은 1062, 오라클은 1, DB2는 -803 이라는 에러 코드를 던진다. Spring은 이 에러코드를 매핑하여 DuplicateKeyException 예외를 던진다.
- 매핑정보는 org.springframework.jdbc.support.sql-error-codes.xml 파일에 작성되어 있다.
- 이처럼 JdbcTemplate를 이용한다면 JDBC에서 발생하는 DB 관련 예외는 거의 신경 쓰지 않아도 된다.
- 그렇다면 이런 방식을 어떻게 생각했는지 DataAccessException이라는 예외를 가지고 아래 설명해본다.
- DAO 인터페이스와 DataAccessException 계층 구조 (이 챕터는 책을 직접 읽고 흐름을 이해하는게 중요)
- DataAccessException : 스프링의 데이터 액세스 기술에 독립적인 추상화된 예외 클래스이다. JDBC 뿐만 아니라, JDO, JAP, iBatis 등에서 발생하는 예외도 이를 이용해서 처리한다.
- 뜬끔없지만, DAO 인터페이스와 구현을 분리하는 이유는 무엇일까? 당연 분리하는 이유는 구현을 여러 방법으로 사용하기 위해서이며, DAO를 사용하는 클라이언트는 구현체가 어떻게 되었는지 상관하지 않아도 되기 때문이다.
-
따라서 DAO를 인터페이스로 만들면 다음과 같다.
public interface UserDao{ public void add(User user); ... }
-
하지만, 실제 구현체가 예외를 만약에 던진다면 throws를 추가해야 하는데, 여러 DAO 기술들(JDBC, JPA, Hibernate 등등)이 던지는 예외가 다르다. 예외가 다르기 때문에 사실 다 다르게 표현돼버린다.
public void add(User user) throws SQLException; // JDBC public void add(User user) throws PersistentException; // JPA public void add(User user) throws HibernateException; // Hibernate // 모든 예외를 받기 위해 이렇게 해도 되기는 하지만, 무책임하다. public void add(User user) throws Exception;
- 그래서 예외를 DataAccessException으로 추상화했다.
- 하지만! 문제점이 있다. 만약 사용자가 DAO 예외를 직접적으로 핸들링하고 싶다면, 추상화를 하게 되면 알 수가 없다.
- (이 부분은 위 의문점에 해결책인 것 같은데, 잘 이해가 되지는 않는다.) 위 의문점을 해결하기 위해, 스프링은 발생하는 예외들을 추상화해서 DataAccessException 계층구조 안에 정리해놓았다.
- DataAccessException 서브클래스중에는 각각 DAO 기술들에게 공통으로 적용되는 예외도 있지만, JPA나 Hibernate에서만 적용되는 서브클래스들도 존재한다.
- 기술에 독립적인 UserDao 만들기
-
UserDao를 인터페이스로 만들어보자.
public interface UserDao { void add(User user); User get(String id); ... }
-
UserDao의 구현체를 만들어보자
public class UserDaoJDbc implements UserDao{ ... }
<bean id="userDao" class="springbook.dao.UserDaoJdbc"> ... </bean>
-
- JDBC의 한계