본문 바로가기

Spring/토비의 스프링 3.1

4.1 사라진 SQLException


3장에서 JdbcTemplate을 적용할 때 SQLException이 사라졌다.

  • SQLException은 JDBC API의 메소들이 던지는 것이므로 당연히 있어야 한다.
  • 어디로 간 것인가

4.1.1 초난감 예외처리

예외 블랙홀

초난감 예외처리 예

/*
 * Example 1
 */
try { 
    // do something
} catch(SQLException e) {

}

/*
 * Example 2
 */
try {
    // do something
} catch(SQLException e) {
    System.out.println(e);
}

/*
 * Example 3
 */
try {
    // do something
} catch(SQLException e) {
    e.printStackTrace();
}

예외를 잡고 아무것도 하지 않는 경우

  • 원치 않는 예외가 발생하는 것보다 더 안 좋음
  • 예외가 발생하였는데도 무시하고 계속 실행되며, 예상치 못한 다른 문제를 일으킬 수 있다.
  • 어디서 예외가 발생하였는지 조차 알 수가 없다.

예외를 잡고 화면에 출력해주는 경우

  • 개발 중에는 콘솔이나 서버에 메세지가 뜨면 확인할 수도 있다.
  • 이마저도 다른 로그나 메세지에 묻히기 쉽상이다.
  • 운영서버에 올리고 나면 누군가 콘솔을 계속 바라보고 있지도 않는다.

모든 예외는 적절하게 복구되든지 혹은 작업을 중단시키고 운영자 또는 개발자에게 분명하게 통보되어야 한다.

차라리 다음과 같은 코드가 훨씬 낫다.

try {
    // do something
} catch (SQLException e) {
    e.printStackTrace();
    System.exit(1);
}

실전에서 이렇게 하면 안 된다.
다만, 굳이 예외를 잡아서 뭔가 조치를 취할 방법이 없다면 잡지 말아야한다.

무의미하고 무책임한 throws

예외를 잡아봐야 해결할 방법도 없고, JDK API나 라이브러리가 던지는 각종 예외들을 매번 throws로 선언하기도 귀찮아지면 다음과 같은 코드가 나오기도 한다. (아몰랑)

public void method1() throws Exception {
    method2();
}

public void method2() throws Exception {
    method3();
}

public void method3() throws Exception {
    // do something
}

EJB 시절에는 흔히 볼 수 있던 코드

  • 이 와중에 catch블럭에서 아무것도 안 하는 것 보다는 낫다. (프로그램이 중단되기는 하니까)
  • 메소드 선언에서도 의미 있는 정보를 얻을 수 없으며
  • 예외가 발생하여도 어디서 어떤 예외가 왜 발생했는지 제대로 알 수 없다.

4.1.2 예외의 종류와 특징

  1. Error
    • java.lang.Error 클래스의 서브클래스
    • 시스템에 비정상적인 상황이 발생하였을 경우
    • 주로 JVM에서 발생시키며, 애플리케이션 코드에서 잡으려고 하면 안 됨
    • ex) OutOfMemoryError / ThreadDeath
    • 잡아봤자 할 수 있는 것이 없다.
    • 신경쓰지 말자
  2. Exception과 체크예외
    • java.lang.Exception 클래스와 그 서브클래스로 정의
    • 애플리케이션 코드의 작업 중 예외상황이 발생하였을 경우 사용
    • 체크예외와 언체크예외로 구분
      1. 체크예외
        • Exception의 서브클래스 & RuntimeException을 상속하지 않음
      2. 언체크예외
        • Exception의 서브클래스 & RuntimeException을 상속
    • 일반적으로 예외라고 함은 체크예외를 의미한다.
    • 체크예외가 발생할 수 있는 메소드를 사용할 경우 반드시 예외 처리를 하여야 한다.
    • 하지 않으면 컴파일 에러 발생
  3. RuntimeException과 언체크/런타임 예외
    • java.lang.RuntimeException을 상속
    • 명시적인 예외처리를 강제하지 않음
    • 런타임 예외라고도 함
    • 주로 프로그램의 오류가 있는 경우 발생
    • ex) NullPointerException / IllegalArgumentException
    • 코드에서 주의하면 피할 수 있음
    • 굳이 예외처리를 하지 않아도 됨 (해도 상관은 없음)

4.1.3 예외처리 방법

예외 복구

예외상황을 파악하고 문제를 해결해서 정상 상태로 돌려놓는 것

  1. ex1) 파일 IO에서 파일이 없어 IOException이 발생하는 경우
    • 사용자에게 상황을 알려주고 다른 파일을 이용하도록 안내
    • IOException 메세지는 사용자에게 직접 노출되어선 안 된다.
  2. ex2) 네트워크 불안정으로 인한 DB 접속 불가로 SQLException이 발생하는 경우
    • 일정 시간 대기 후 재시도
    • 정해진 횟수를 초과하면 예외 복구를 포기

MAX_RETRY만큼 재시도를 하는 예제

int maxretry = MAX_RETRY;
while (maxretry -- > 0) {
    try {
        // do something
    } catch (SomeExcetion e) {
        // 로그 출력 및 정해진 시간만큼 대기
    } finally {
        // 리소스 반납 및 정리
    }
}
throw new RetryFailedException(); // 최대 재시도 횟수 이후 직접 예외 발생

예외처리 회피

예외처리를 자신이 직접하지 않고 자신을 호출한 쪽으로 던지는 것

  • throws 선언
  • catch로 잡아서 로그를 남긴 후 rethrow
/*
 * Example 1
 */
public void add() throws SQLException {
    // JDBC API
}

/*
 * Example 2
 */
public void add() throws SQLException {
    try {
        // JDBC API
    } catch (SQLException e) {
        // 로그 출력
        throw e;
    }
}

JdbcContext나 JdbcTemplate이 사용하는 콜백 메소드

  • ResultSet이나 PreparedStatement 등에서 발생하는 SQLException을 직접 처리하지 않고 밖으로 던짐
  • SQLException의 처리는 콜백 오브젝트가 할 일이 아니기 때문에 템플릿 레벨에서 처리하도록

이와 같이 역할이 분담되어 있는 관계가 아니라면 막 던져서는 안 된다.

예외 회피는 예외 복구와 마찬가지로 의도가 분명해야한다

예외 전환

예외 회피와 비슷하게 예외를 복구할 수 없어서(또는 자신의 책임이 아니라서) 밖으로 던지지만, 발생한 예외를 그대로 넘기는 것이 아니라 적절한 예외로 전환해서 던진다.

  1. 내부에서 발생한 예외를 그대로 던질 경우, 그 예외 상황에 대한 적절한 의미를 부여하지 못하는 경우
    • ex) 새로운 사용자를 등록하려고 하는데 아이디가 중복되면 JDBC API는 SQLException을 발생시킨다. 이 경우, SQLException을 그대로 던지기 보다는 DuplicateIdException을 정의하여 던지는 것이 보다 명확하다
public void add(User user) throws DuplicateUserIdException, SQLException {
    try {
        // user를 DB에 등록
    } catch (SQLException e) {
        if (e.getErrorCode() == MysqlErrorNumbers.ER_DUP_ENTRY) {
            throw DuplicateUserIdErrorException();
        } else {
            throw e;
        }
    }
}

예외 전환은 원래 발생한 예외를 담아서 중첩 예외(nested exception)로 만드는 것이 좋다.

  • getCause() 메소드를 이용하여 원래의 예외가 무엇인지 확인할 수 있음
  • 예외를 정의할 때 생성자나 initCause() 메소드로 원래의 예외를 넣어주면 됨
catch(SQLException e) {
    // ...
    throw DuplicateUserIdException(e);
}
catch(SQLException e) {
    // ...
    throw DuplicateUserIdException().initCause(e);
}

주로 예외처리를 강제하는 체크예외를 런타임 예외를 바꾸는 경우에 사용

대표적인 예 : EJBException

  • EJB 컴포넌트 코드에서 발생하는 대부분의 체크 예외는 의미있거나 복구 가능한 것이 아니다.
  • 이러한 경우 런타임 예외인 EJBException으로 포장해서 던지는 편이 낫다.
try {
    OrderHome orderHome = HJBHomeFactory.getInstance().getOrderHome();
    Order order = orderHome.findByPrimaryKey(Integer id);
} catch (NamingException ne) {
    throw new EJBException(ne);
} catch (SQLException se) {
    throw new EJBException(se);
} catch (RemoteException re) {
    throw new EJBException (re);
}

EJBException은 RuntimeException을 상속한 런타임 예외

  • 런타임 예외로 전환해서 던져주면 EJB는 시스템 예외로 인식하고 트랜잭션을 롤백해준다.
  • 런타임 예외이므로 다른 클래스에서 이를 처리할 필요도 없고, 처리할 수도 없다.

애플리케이션 로직상에서 예외조건이 발생하거나 예외상황이 발생하는 경우

  • 애플리케이션 코드에서 의도적으로 던지는 예외
  • 체크 예외를 사용하는 것이 적절
  • 체크 예외는 계속 throws를 사용해 넘기는 것은 무의미하다.
  • 복구 불가능한 예외라면 가능한 한 빨리 런타임 예외로 포장해서 던지게 해줘야 한다.

4.1.4 예외처리 전략

런타임 예외의 보편화

체크 예외 : 복구할 가능성이 있으므로 자바에서 예외처리를 강제함

  • 개발자의 실수를 방지해주지만, 귀찮은 것도 사실
  • 초창기 자바에서 애플릿, AWT, 스윙 등을 사용하는 독립 어플리케이션의 경우 애플리케이션 중단되지 않도록 반드시 복구했어야만 했고, 합당했다.
  • 그러나 엔터프라이즈 환경에서는 수많은 사용자가 동시에 요청을 보내고 각 요청은 독립적인 작업으로 취급된다. 따라서 서버의 특정 계층에서 예외가 발생했을 경우, 작업을 일시중지하고 사용자와 커뮤니케이션하면서 예외상황을 복구할 수 있는 방법이 없다.
  • 애플리케이션 차원에서 예외상황을 미리 파악하고, 예외가 발생하지 않도록 차단하는 것이 더 좋다.
  • 프로그램 오류 또는 외부 환경으로 인한 예외의 경우, 해당 요청의 작업을 취소하고 개발자에게 통보해주는 것이 더 좋다.

이러한 이유로 최근 등장하는 API는 체크예외 대신 언체크 예외를 던지도록 만드는 경우가 많다.

add() 메소드의 예외처리

add() 메소드에서 던지는 예외

  1. DuplicateUserIdException
    • 복구 가능한 예외
  2. SQLException
    • 대부분 복구 불가능 예외

DuplicateUserIdException과 같이 의미가 있는 예외의 경우 add() 메소드 대신 add()를 호출하는 오브젝트에서 다룰 수도 있다.

  • 어디에서든 Exception을 잡아서 처리할 수 있다면 체크 예외보다는 런타임 예외로 만드는 것이 낫다.
  • 대신 add() 메소드는 명시적으로 DuplicateUserIdException을 던진다고 선언해야 한다.

DuplicateUserIdException의 정의

  • RuntimeException을 상속한 런타임 예외
  • 중첩 예외를 위해 생성자 추가
public class DuplicateUserIdException extends RuntimeException {
    public DuplicateUserIdException(Throwable cause) {
        super(cause);
    }
}

add() 메소드의 수정

  • SQLException을 런타임 예외인 DuplicateUserIdException으로 전환해서 던짐
  • 처리 불가능한 예외인 SQLException은 RuntimeException으로 전환
public void add() throws DuplicateUserIdException{
    try {
        // user를 DB에 등록
    } catch (SQLException e) {
        if (e.getErrorCode() == MysqlErrorNumbers.ER_DUP_ENTRY) {
            throw new DuplicateUserIdException(e);
        } else {
            throw new RuntimeException(e);
        }
    }
}

add()를 사용하는 오브젝트

  • SQLException을 처리하지 않아도 됨
  • 필요한 경우 DuplicateUserIdException을 이용할 수 있음

런타임 예외를 일반화해서 사용하는 방법은 장점이 많음

  • 단, 런타임 예외이므로 좀 더 주의해야함
  • 컴파일러가 체크를 해주지 않음
  • API 문서나 레퍼런스 등을 통해 예외에 대해 자세히 설명해두어야 한다.

애플리케이션 예외

애플리케이션 예외

  • 애플리케이션 로직 자체의 로직에 의해 의도적으로 발생시키고, 반드시 catch하여 무엇인가 조치를 취하도록 요구하는 예외
  • ex) 은행 출금 기능 메소드에서 잔고 이상의 금액을 출금하려고 할 때 발생시키는 예외

처리 방법

  1. 정상 처리와 예외 발생 시 다른 리턴 값을 돌려주는 방법
    • ex) 정상처리 : 1, 예외발생 : 0, -1
    • 리턴 값을 명확하게 코드화하고, 잘 관리하여야 한다.
    • 결과값을 반드시 확인해야하므로 이를 위한 if블록이 많아짐
  2. 정상은 그대로 두고 예외 상황에서 비지니스적 의미를 가진 예외를 던지게 함
    • ex) InsufficientBalanceException
    • 이 때 사용하는 예외는 의도적으로 체크 예외로 만듬

잔고 출금 메서드

try {
    BigDecimal balance = account.withdraw(amount);
    // ... 정상 처리 결과 출력
} catch (InsufficientBalanceException e) {
    BigDecimal availFunds = e.getAvailFunds();
    // ... 잔고 부족 안내 메세지 출력
}

4.1.5 SQLException은 어떻게 됐나?

JdbcTemplate을 적용하던 중 SQLException이 왜 사라졌는가에 대한 답변

  • SQLException은 99% 복구 불가능한 예외이다. (코드 레벨에서 복구할 방법이 없다)
  • ex) SQL 문법 오류, 제약 조건 위반, DB 서버 다운, 네트워크 불안정 등
  • 시스템 예외는 당연히 복구할 방법이 없으므로 개발자에게 빠르게 알리는 것이 최선
  • INSERT문에 넣을 파라미터 값의 검증 역시 코드레벨에서는 할 수 있는 것이 없다.

스프링의 JdbcTemplate은 이러한 예외처리 전략을 따르고 있다.

  • JdbcTemplate 템플릿과 콜백안에서 발생하는 모든 SQLException을 런타임 예외인 DataAccessException으로 포장해서 던짐
  • UserDao는 필요한 경우에만 DataAccessException을 잡아서 처리하면 됨
  • JdbcTemplate의 모든 메소드에서 DataAccessException이 throws로 선언되어 있음
  • 이것은 명시적인 선언일 뿐 런타임 에러이므로, 호출한 오브젝트에서는 신경쓰지 않아도 된다.

UserDao 메소드에서 SQLException을 던지고 있는 메소드가 있다면 DataAccessException으로 변경해주고, UserDaoTest에서는 더 이상 SQLException을 잡아주지 않아도 된다.


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

5장. 서비스 추상화  (0) 2019.02.06
4.3 정리  (0) 2019.01.25
4장. 예외  (0) 2019.01.25
3.7 정리  (0) 2019.01.23
3.6 스프링의 JdbcTemplate  (0) 2019.01.23