[코드리뷰] Exception 처리

2015-03-24 09:48

권고 사안

Exception 무시하지 않기

아래와 같이 catch절에서 아무 것도 하지 않는 코드는 바람직하지 않습니다.

try {
    process();
} catch (IOException e) {

}

정말 할일이 없다면 //ignore 로 의도를 주석으로라도 달아줍니다. 다만 Connection.close() 등, 워낙 관례적으로 catch절을 무시하는 코드는 주석이 없어도 의도를 파악하는데 어려움이 없기는합니다.

있으나마나한 catch 절 쓰지 않기

아래와 같이 catch절에서 아무 작업도 없이 바로 throw 를 하는 코드는 있나마나한 코드입니다.

try {
    process();
} catch (IOException e) {
    throw e
}

아래 코드와 그냥 똑같습니다.

process();

Exception을 무시하는것보다는 위험은 적지만, 그래도 굳이 불필요한 코드만 추가한 것입니다. catch절에는 예외 흐름에 적합한 구현코드가 있어야 합니다. 로깅이나 Layer에 적합한 Exception 변환 등도 그 예입니다.

e.printStackTrace() 보다는 로거 사용

Exception을 기록으로 남기고 끝낼 경우에라도 로깅 프레임워크를 사용하는 편이 좋습니다.

try {
    process();
} catch (IOException e) {
    e.printStackTrace()
}

->

try {
    process();
} catch (IOException e) {
    log.error("fail to process file", e);
}

Tomcat에서 e.printStackTrace()로 콘솔에 찍힌 값은 {TOMCAT_HOME}/logs/catalina.out 에만 남습니다. 로깅 프레임워크를 이용하면 파일을 쪼개는 정책을 설정할 수 있고, 여러 서버의 로그를 한곳에서 모아서 보는 시스템을 활요할 수도 있습니다.

log.error()메서드에 Exception객체를 직접 넘기는 e.printStackTrace()처럼 Exception의 스택도 모두 남겨줍니다. 에러의 추적성을 높이기 위해서는 e.toString()이나 e.getMessage()로 마지막 메시지만 남기기보다는 전체 에러 스택을 다 넘기는 편이 좋습니다.

Layer에 맞는 Exception 던지기

DAO에서 ServletException은 던진다거나 Servlet에서 SQLException을 처리하는것은 Layer별 역할에 맞지 않습니다. 적절한 수준으로 추상화된 Exception을 정의하거나 IllegalArgumentException 같은 java의 표준 Exception을 활용할 수도 있습니다. Service layer에서는 Business 로직의 수준에 맞는 custom exception을 정의하는 것도 고려할만 합니다. 이 때 cause exception을 상위 Exception의 생성자에 넘기는 exception chaning기법도 확용할만 합니다.

try {
    process(url);
} catch (IOException e) {
    throw new BankAccountExeption("fail to call " + url, e);
}

java.lang.Exception 남용하지 않기

메서드 시그니처에서 java.lang.Exception은 세부 Exception을 가리게 됩니다.

public void updateUser throws Exception {

    ....
}

정말 다른 Exception을 지정할것이 없을때 최후의 수단으로 씁니다. 프레임워크에서는 checked exception에 대한 처리를 미루는 목적으로 사용하기도 하지만, Business 코드에서는 습관적으로 java.lang.Exception을 쓴다면 정교한 예외처리를 할 수 없습니다.

Unchecked exception 활용 검토

Java 의 Exception 처리는 C++로부터 도입되었지만 checked exception은 Java만의 독특한 특징입니다. 아시다시비, 컴파일러가 exception을 꼭 처리해라고 강요하는 것이죠. 이것은 java 이후에 설계된 언어인 C#이나 루비에도 채택되지 않았습니다. 즉 Java 이외의 다른 언어들의 Exception 처리 방식은 Java의 unchecked exception과 동일한 방식입니다.

Java의 초기에는 checked Exception을 사용하라고 권장했지만, 지금은 많은 반론이 제기되고 있습니다. 극단적으로 Java언어에서checked Exception 도입 자체가 실패라고 주장하는 사람도 많습니다. Thinking in Java의 저자인 Bruce Eckel도 그 중 한 사람입니다. Spring framework의 아버지 Rod Johnson도 Checked Exception이 쓰여야 할 때도 있지만 그 것이 과도하게 선호되어 온 것은 지적하고 있습니다.

JDBC API에서도 Checked Exception을 남발한 문제가 보입니다. catch 절에서 아무 것도 하지 않는 것은 바람직하지 않은 코딩이지만 JDBC API에서는 정말 할 것이 없습니다. 그래서 이런 문제점을 알고서 그 후에 나온 JDBC를 활용한 API들, Spring의 JdbcTemplet, Hibernate의 Query 인터페이스, JPA의 Query 인터페이스, JDO의 Query 인터페이스에서는 Checked Exception인 SQLException을 볼 수 없게 설계되어 있습니다.

현 시점에서는 unchecked exception을 디폴트로, 특별한 이유가 있는 것만 checked exception을 활용하는 방식이 더 보편적입니다.

에러 페이지 적절한 활용

웹어플리케이션에서는 초기에 과도하게 모든 예외를 catch할 필요는 없이, RuntimeException과 에러페이지를 적절히 활용하는 방식이 보통입니다. DBMS의 다운 같은 정상적이지 않는 상황은 에러페이지를 이용하고 어플리케이션에서는 공통적으로 catch를 하지는 않습니다. Spring의 ExceptionResolver등을 이용해서 예외처리를 한곳으로 모을수도 있습니다. 물론 정교한 에러메시지나 그에 따른 별도의 화면 Flow가 필요할 때는 섬세하게 catch를 할 수도 있습니다. 에러 페이지도 Business 언어 수준의 예외를 정의하고 거기에 맞는 페이지를 따로 준비하기도 합니다.

그러나 Android에서는 사소한 catch 누락이 앱크래쉬로 이어지기 때문에 더욱 유의해야 합니다.

참고자료

  • Effective Java 2nd Edition Chapter 9 (Item 57 ~ 65)
    • Item 57 : 예외는 예외상황에서만 써야 한다. 예외를 생성하고 던지고 잡는 것은 비용이 많이 드는 작업이고 JVM의 최적화 대상에서 빠질 수 있다. 프로그램 흐름을 예외로 제어하려 하면 안된다. 좋은 API는 클라이언트가 프로그램 흐름을 제어할 때 예외를 쓸 수 밖에 없도록 만들지 않는다.
    • Item 58: 복구 가능한 조건에 Checked Exception을, 프로그래밍 에러에 RuntimeException을써라. Error의 하위 클래스는 만들지 말고 처리하지 않는 예외는 모두 RuntimeException의 하위클래스이어야 한다.
    • Item 59 : Checked Exception은 꼭 필요할 때만 던져라. catch절에서 특별한 할 일이 없이 없는 API를 checked exception으로 처리하는 것은 프로그램만 더 복잡하게 만들 뿐이다.
    • Item 60 : 표준예외를 선호하라. IllegalArgumentException, IllegalStatementException, UnsupportedOperationException, ConcurrentModificationException 등 Java의 표준예외를 활용해라.
    • Item 61 : 예외를 적절하게 추상화 하라. 높은 계층에서 낮은 계층의 예외를 잡아서 높은 계층의 추상화 수준에 맞게 변환해서 던져야 한다. 예외변환(Exception translation) 패턴은 하위 레벨에 영향받지 않는 Exception 전파에 유리하지만 너무 남용하지는 마라. 가능하면 low-level exception이 없이 성공하도록 유도하는 것이 바람직하다. Exception chaining은 적절한 변환을 하면서도 세부 원인을 보존하는 장점이 있다.
    • Item 62 : 메소드가 던지는 모든 예외를 명세문서에 기술하라 Checked exception은 메소드 선언부에 하나씩 선언하고, @throws 태그를 써서 모든 예외가 발생하느 상황을 정확하게 문서화하라. 단지 귀찮다는 이유만으로, 공통 상위타입으로 예외를 던지려 하지 마라. unchecked exception은 @throws 태그를 써서 명세문서에 기술하지만, 메소드 선언의 throws 절에는 나타나지 말아야 한다.
    • Item 63 : 실패에 대한 자세한 정보를 상세 메시지에 담아라 실패원인을 포착하려면, 예외의 문자열 표현에 반드시 예외 발생에 영향을 준 모든 필드와 인자의 값이 들어 있어야 한다.
    • Item 64 : 실패 원자성을 얻기위해 노력하라. 메소드 호출이 실패하더라도 객체상태는 메소드 호출 전과 같아야 한다. 오류(error)는 예외(exception)와 달리 보통 복구할수 없기 때문에 오류가 발생했을 때 실패 원자성을달성하기 위해 애쓸 필요가 없다.
    • Item 65 : 예외를 잡아서 버리지 마라. 빈 catch block은 "예외 사항을 처리하라"라고 알려주는 예외의 존재 이유 자체를 짓발는 것이다. catch block 안에서 정말 아무 것도 할 것이 없다면, 최소한 왜 예외를 잡아서 처리하지 않고 버리는지 그 이유라도 주석으로 달아 놓아야 한다.
  • Expert One-on-One J2EE Design and Development 중 Chapter 4 Design Techniques and Coding Standards for J2EE Projects, Exception Handling 부분
    • alternative return value가 있는 경우에는 Checked exception
    • data connection 생성 실패와 같이 뭔가 크게 잘못 되고 있어서 호출한 쪽에서 아무도 이를 처리할 수 없을 때는 Runtime exception.
    • 소수의 호출자만이 Exception을 받아서 처리해야 할 때도 Runtime exception.
    • 불명확하면 Runtime exception.
    • checked Exception과 Runtime exception 페이지에서 일부 내용이 번역되어 있습니다.
    • Spring 프레임웍크 워크북 (박재성 저) 88쪽에도 인용되어 있는 원칙입니다.
  • 그외 다양한 참고자료는 http://blog.benelog.net/1901121 참조

3개의 의견 from SLiPP

2015-04-09 10:45

sonarqube에서 exception 관련한 메시지 공유해 본다.

다음과 같이 구현하는 경우 Exception 상태가 사라지게 된다.

// Noncompliant - exception is lost
try { /* ... */ } catch (Exception e) { LOGGER.info("context"); }   


// Noncompliant - exception is lost (only message is preserved)       
try { /* ... */ } catch (Exception e) { LOGGER.info(e.getMessage()); }   


// Noncompliant - exception is lost 
try { /* ... */ } catch (Exception e) { throw new RuntimeException("context"); }

그래서 추천하는 방법은 다음과 같이 처리해야 한다고 설명하고 있다.

try { /* ... */ } catch (Exception e) { LOGGER.info(e); }   


try { /* ... */ } catch (Exception e) { throw new RuntimeException(e); }


try {
  /* ... */
} catch (RuntimeException e) {
  doSomething();
  throw e;
} catch (Exception e) {
  // Conversion into unchecked exception is also allowed
  throw new RuntimeException(e);
}

예외적으로 다음과 같이 구현하는 경우도 있다.

int myInteger;
try {
  myInteger = Integer.parseInt(myString);
} catch (NumberFormatException e) {
  // It is perfectly acceptable to not handle "e" here
  myInteger = 0;
}
의견 추가하기