정상혁정상혁

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이 쓰여야 할 때도 있지만 그 것이 과도하게 선호되어 온 것은 지적하고 있습니다.

어쨓든 Exception 부분은 Java로 API 설계와 코딩를 할 때 가장 어려운 부분이라고 느껴집니다. Java 아키텍트와 개발팀의 실력을 측정하는 좋은 방법은 그들이 만든 Exception 처리 코드를 보라는 말까지 있으니까요.

Exception 처리 방식 참고자료

  • 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쪽에도 인용되어 있는 원칙입니다.

  • Barry Ruzek의 EFFECTIVE JAVA EXCEPTIONS

    • Fault Handling(Unplanned condition)에는 RuntimeException, Contingency(Expected condition, alternative method result)에는 return type, exception, error code 등의 전략을 사용

    • Fault를 한곳에서 잡아서 처리하는 Fault barrier pattern 사용 권장. Struts라면 org.apache.struts.action.ExceptionHandler, SpringMVC라면 SimpleMappingExceptionResolver

    • AOP 적용이 도움이 될 수도 있음.

    • 황상철님의 블로그 Effective Java Exceptions 발표자료 페이지에서 이 내용이 요약된 pdf 파일을 받을 수 있습니다. (effective java exceptions.pdf )

  • Jim Cushing Three Rules for Effective Exception Handling

    • Be specific, Throw Early, Catch Late의 3가지 원칙을 제시하고 있습니다.

  • 13 Exceptional Exception Handling Techniques

    • Checked Exception을 RuntimeException으로 감싸기, throws 절에 RuntimeException이라도 선언해주기 등의 기법을 추천하고 있습니다.

  • Gunjan Doshi의 Best Practices for Exception Handling

    • Client code가 할 일이 있을 때는 checked, 없을 때는 unchecked. progamming error에는 unchecked exception.

    • 적절한 캡슐화. 비지니스 layer에서 SqlException을 던지지 말것.

  • Rob Walling의 Exception Handling에 관한 글 The Two Fundamental, No Frills, Square One Rules of Exception Handling

    1. 에러메시지에 도움이 되는 정보를 더할 수 없다면, Exception을 잡지마라, 2. Exception을 잡았으면 기록하라." 두가지 원칙을 말하고제시하고 있습니다.

  • Alan Griffiths의 Exceptional Java

    • 이 주제에 대해 비교적 초기에 나온 글로써, 다른 글에도 많이 인용되고 있습니다.

    • Exception 처리에 대한 전통적인 원칙이 캡슐화 저해, 정보손실, 정보 과적의 문제를 일으킨다고 이야기합니다.

    • public 메소드에서 던지는 Exception은 해당 패키지에 소속된 클래스일것, 다른 패키지에서는 이를 부를 때를 Exception을 전파시키지 말고 그 패키지의 Exception으로 감쌀 것을 추천하고 있습니다.

  • An Exception Handling Framework for J2EE Applications

    • J2EE application에서의 Exception처리 전략에 대해서 설명하고 있습니다.

Checked Exception에 대한 부정적 견해를 정리한 글

  • Brian Goetz의 글: Java theory and practice: The exceptions debate

    • Bruce Eckel, Rod Johnson, Joshua Bloch등의 주장을 정리해 놓은 글입니다. Checked Exception이 상세한 구현을 부적절하게 노출함,불안정한 메소드 시그너처,읽기힘든 코드,Exception 삼키기, 너무 많은 Exception wrapping의 문제점 있는 것을 나열하고 있습니다. unchecked exception은 Documentation이 더욱 중요하다고 강조하고 있습니다.

  • Rod Waldhoff의 글 : Java’s checked exceptions were a mistake

    • Java의 Exception Handling은 실패한 실험이라고 주장하는 내용입니다. Checked Exception방식은 일부 low level에서만 의미가 있다고 말합니다.

  • Bruce Eckel의 견해(Thinking in Java의 저자): Does Java need Checked Exceptions?

    • Error report 방식을 통일했다는 점에서는 의미가 있지만, 오히려 개발자들이 Exception을 그냥 삼키는 코드를 많이 짜게 하는 결과가 생겼다고 지적합니다.

  • Bill Venners(C#의 아키텍트)와 Bruce Eckel의 인터뷰 : The Trouble with Checked Exceptions

    • versioning, scalability의 문제 때문에 C#에 Checked Exception이 도입되지 않았다고 밝히고 있습니다.

Checked Exception에 대한 논의가 진행된 페이지

이클립스 Exception처리 코드 템플릿 관련 자료

정상혁정상혁

이미 많이 알려진 내용이지만, 아직도 문제를 많이 일으키는 주제입니다. 그래서 보다 이 주제를 검색엔진에서 쉽게 찾을 수 있었으면 하는 마음에서 이 글을 정리해봤습니다.

  Connection conn = null;
  PreparedStatement pstmt = null;
  ResultSet rs = null; // <---- !!!
  try{
     conn = ...<getConnection()>...;
     pstmt = conn.prepareStatement("select .....");
     rs = pstmt.executeQuery(); // <----- !!!
     while(rs.next()){
       ......
     }
  }  catch(Exception e){
     ....
  }  finally {
     if ( rs != null ) try{rs.close();}catch(Exception e){}
     if ( pstmt != null ) try{pstmt.close();}catch(Exception e){}
     if ( conn != null ) try{conn.close();}catch(Exception e){}

 }

이것이 JDBC API 사용시에 권장되는 코딩방식입니다. 코드는 참조자료에 있는 이원영님의 글에서 인용했습니다.

JDBC 스펙을 찾아보면 Statement가 닫힐 때 ResultSet은 닫히고, Connection이 닫히면 Statement도 닫힌다고 되어 있습니다. 하지만 Staement close 시에 Exception이 발생한다면 이것이 따로 Exception을 catch되지 않고서는 뒤에 Connection을 닫는 코드가 실행되지 않습니다. 그리고 Connection pool에서 얻어온 Connection객체는 connection.close()로 처리하는 것이 pool로의 반환을 의미하는 것이지 실제로 connetion을 close하는 것이 아니기 때문에 Statement까지 닫아준다고 장담할 수 없습니다. ResultSet의 경우도 WAS에도 제공하는 Statement cache 기능 때문에 명시적으로 close해주는 것이 확실한 자원해제를 보장할 수 있습니다.

DBMS에서 "maximum open cursor exceed !" 나 "Limit on number of statements exceeded " 에러를 내고 있다면 위와 같이 코딩했는지 한번 확인해보시기 바랍니다.

각 벤더별 드라이버의 구현이나 WAS의 Connection Pool의 구현등에 따라서 저 정도까지 안 해도 문제가 안 생길 수도 있습니다. 그리고 독립적으로 돌아가는 배치프로그램이나 커넥션풀을 쓰지 않는 경우에는 보다 덜 엄격해도 될 때도 있습니다. 그래도 어떠한 경우에도 안심하고 있을만한 코드는 위와 같은 구조입니다.

javaservice.net에서 이원영님이 처음에 이 문제에 대한 글을 쓰신것이 2000년 9월입니다. 그래서 많은 분들이 알고 계시지만 그래도 정말 반복적으로 만나게 되는 문제입니다. 저의 경험이 편향된지도 모르겠지만, 지금까지 제가 만났던 JDBC AP를 그대로 쓰는 개발팀은 세 팀이였었는데, 모두 이렇게 코딩하지 않을 경우 문제가 생길 가능성이 있다는 것을 모르고 있었습니다. 결국 그 중 한 팀은 시스템 전체를 몇 일동안 매시간마다 재부팅시키게 만들게 했었습니다.

미국의 모 대형항공사의 예약시스템을 3시간동안 멈춘 코드도 위와 같은 방식을 따르지 않았었습니다. finally절이 다음과 같았다고 합니다.

} finally{
    if (stmt!=null) stmt.close();
    if (conn!=null) conn.close();
}

그 예약 시스템은 이중화된 DB로 구성되어 있었고, 그 DB들은 가상IP주소로 어플리케이션과 연결되어 있었습니다. 정기 점검을 위해 DB중 하나를 수동 fail-over 시키는 순간 내려간 DB의 JDBC연결에서 나온 statement객체의 close문장은 Exception을 일으켰습니다. 이 문장은 별도로 catch 되지 않았기 때문에 그 다음의 conn.close()는 실행되지 않았습니다. 결국 이 때문에 반환되지 않은 Connection 자원들로 인해 리소스 풀은 곧 바닥이 났습니다. 그 후에 새로 Connection을 얻고자 하는 다른 프로그램들은 블록되어서 전체 시스템을 멈추었습니다.

아마도 JDBC API를 쓰는 곳에는 언제나 생길 수 있는 문제일 것입니다. 좋은 API는 문서를 안 보고 자연스럽게 써도 사용하기 쉽고 문제를 안 일으키는 것일텐데, JDBC는 제대로 사용하기가 오히려 더 어려운 API입니다. 위의 항공사 사건 같이 전 세계에서 JDBC로 인해 야기된 장애,생산성 저하를 다 따져본다면, 가히 이 API가 인류에게 끼친 해악이 엄청나다는 생각까지도 듭니다. 요즘은 Framework 기반 개발로 JDBC를 직접 안 쓰는 것이 이런 점에서는 다행입니다.

JDBC API에서 대표적으로 지적받는 문제점은 Checked Exception을 남발했다는 것입니다. catch 절에서 아무 것도 하지 않는 것은 바람직하지 않은 코딩이지만 JDBC API에서는 정말 할 것이 없습니다. 그래서 이런 문제점을 알고서 그 후에 나온 JDBC를 활용한 API들, Spring의 JdbcTemplet, HibernateQuery 인터페이스, JPAQuery 인터페이스, JDOQuery 인터페이스에서는 Checked Exception인 SqlException을 볼 수 없게 설계되어 있습니다.

그리고 Java6 이전의 JDBC에서는 접속에러, 쿼리에러, 제약조건 에러 등 다양한 원인으로 생기는 Exception을 SqlException 1개로 다 때우는 문제도 있었습니다. Spring에서는 이것을 더 섬세하게 구분한 Exception들을 정의를 하고 있습니다. DataAccessException의 하위 클래스를 보면 CleanupFailureDataAccessException, DataIntegrityViolationException, DataRetrievalFailureException 등이 보입니다. Java6에 포함된 JDBC 4.0에서는 SQLNonTransientException, SQLRecoverableException, SQLTransientException 등의 하위 클래스가 생겼고, ,Spring에서는 이런 클래스도 잘 인식해서 적절한 DataAccessException의 하위 클래스로 변환해줍니다.

참고자료

미국 항공사 장애 사건 관련

정상혁정상혁

네이버 Open API Cafe에서 검색 API의 Java Client 모듈을 보게 되었습니다.

위의 모듈을 참고해서 같은 역할을 하는 모듈을 다르게 구현해봤습니다.

특징은 아래와 같습니다.

  • RSS를 파싱하는 부분을 XML parsing API로 널리 알려진 Digester(http://commons.apache.org/digester/)를 사용했습니다. Digester의 예제 코드로 제공되는 RSS파싱모듈을 그대로 써서 짧은 코드로 파싱이 가능했습니다.

  • 요청 파라미터를 담는 클래스를 따로 뺐습니다. (RequestParameter.java) 이중 Target 값은 (blog, news 등 검색할 컨텐츠 유형을 선택하는 파라미터입니다.) enum으로 해서 정해진 값이 아닐 경우 compile이 안 되게 했습니다.

  • open API key값은 필수값이므로 OpenApiClient클래스의 생성자의 파라미터로 받았습니다. 대신 키 값이 없이 이 객체가 생성될 수 없도록 default 생성자는 private으로 돌려놨습니다.

첨부한 파일은 이클립스에서 Dynamic Web Project로 생성한 폴더를 압축한 것입니다. 테스트 실행 서버는 Tomcat 5.5를 사용했습니다. Eclipse WTP가 설치되어 있는 환경이면 실행이 가능합니다. 그리고 enum을 썼기에 Java5이상이어야 합니다.

Open API에 대한 자세한 사용법은 http://openapi.naver.com/index.nhn 를 참조하시면 됩니다.

소스코드

NaverSearchClient.java
package openapiclient;

import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLEncoder;

import org.apache.commons.digester.rss.Channel;
import org.apache.commons.digester.rss.RSSDigester;

public class NaverSearchClient {

    private static final String OPEN_API_URL = "http://openapi.naver.com/search";
    private String key;

    @SuppressWarnings("unused")
    private NaverSearchClient(){};

    public NaverSearchClient(String key){
    this.key = key;
    }
    public Channel search(RequestParameter param) throws Exception{
        RSSDigester digester = new RSSDigester();
        URL requestUrl = getRequestUrl(param);
        InputStream is = requestUrl.openConnection().getInputStream();
     return (Channel) digester.parse(is);
    }

    private URL getRequestUrl(RequestParameter param) throws UnsupportedEncodingException, MalformedURLException {
        StringBuffer serverUrl = new StringBuffer(OPEN_API_URL);
        serverUrl.append("?target=" + param.getTarget());
        serverUrl.append("&key=" + key);
        serverUrl.append("&start=" + param.getStart());
        serverUrl.append("&display=" + param.getDisplay());
        serverUrl.append("&query=" + URLEncoder.encode(param.getQuery(), "UTF-8"));
        if(param.getSort()!=null) serverUrl.append("&sort=" + param.getSort());
        return new URL(serverUrl.toString());
    }
}
RequestParameter.java
package openapiclient;

public class RequestParameter {

 public enum Category{
  KIN,BLOG,CAFE,DOC,WEBKR,BOOK, SHOP, ENCYC,
  KRDIC, JPDIC, ENDIC, NEWS, LOCAL, VIDEO,IMAGE;
  public String toString(){
   return super.toString().toLowerCase();
  }
 }

 private Category target;
 private String sort;
 private int start;
 private int display;
 private String query;

 // getter and setters 생략
 }

JSP에서 사용한 예제

JSTL을 함께 사용해서 찍어본 예제입니다. http://openapi.naver.com/index.nhn 에 가셔서 API key를 발급 받으시고 소스 중간에 밑줄로 표시된 부분에 그 값을 넣으시고 돌려주시면 됩니다.

<%@ page language="java" contentType="text/html; charset=EUC-KR"   pageEncoding="EUC-KR"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ page import="openapiclient.RequestParameter" %>
<%@ page import="openapiclient.NaverSearchClient" %>
<%@ page import="org.apache.commons.digester.rss.Channel" %>
<%
 String KEY = "????"; // Open API key값을 넣으세요
 NaverSearchClient client = new NaverSearchClient(KEY);
 RequestParameter param = new RequestParameter();
 param.setDisplay(10);
 param.setStart(1);
 param.setQuery("미역국");
 param.setTarget(RequestParameter.Category.NEWS);
 Channel result = client.search(param);
 result.render(System.out); // 콘솔에 받아온 내용을 확인삼아 찍어봄
 request.setAttribute("result", result);
%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=EUC-KR">
<title>Naver Open API를 이용한 검색</title>
</head>
<body>
<c:forEach var='item' items='${result.items}'>
  <p>
  <a href="${item.link}"> ${item.title} </a> <br/>
  ${item.description}
  </p>
</c:forEach>
</body>
</html>

실행결과화면

openApiClient.JPG