'Framework/예외처리'에 해당되는 글 6건

  1. 2009.06.08 GUI TEST
  2. 2009.03.26 예외 수집기
  3. 2009.03.10 exception framework
  4. 2009.03.07 checked vs runtime
  5. 2009.02.09 checked exception의 문제
  6. 2009.02.09 예외 처리 격언 2
Framework/예외처리2009. 6. 8. 20:04

흔히 알려진 테스트 하기 어려운 모듈은 네트워크, 데이타 베이스, GUI 인데 특히 Swing으로 제작된 GUI 프로그래밍을 테스트 하는 것은 꽤 어렵다. 

사실 가장 좋은 방법은 그냥 View 모듈을 Model과 Controller에서 최대한 분리하는 방법이 최선이긴 하다. 그걸로 충분하지 않다면 아래와 같인 자동 로봇을 시도해 볼 수 있다.

아래 코드는 특정 시나리오대로 프로그램을 실행하면서 마지막에 해당 GUI 화면을 갭쳐한다.

package com.bleujin.thinlet.sample.robot;

import java.awt.AWTException;
import java.awt.FlowLayout;
import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;
import java.awt.Image;
import java.awt.Rectangle;
import java.awt.Robot;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.awt.image.BufferedImage;

import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JOptionPane;

public class RobotTest {

  public static void main(String[] args) {

    ButtonFrame frame = new ButtonFrame() ;
    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE;
    frame.setVisible(true;
    
    GraphicsEnvironment environment = GraphicsEnvironment.getLocalGraphicsEnvironment();
    GraphicsDevice screen = environment.getDefaultScreenDevice();

    try {
      Robot robot = new Robot(screen);
      run(robot);
    catch (AWTException ex) {
      ex.printStackTrace();
    }
  }

  private static void run(Robot robot) {
    robot.keyPress(' ');
    robot.keyRelease(' ');

    robot.delay(2000);
    robot.keyPress(KeyEvent.VK_TAB);
    robot.keyRelease(KeyEvent.VK_TAB);
    robot.keyPress(' ');
    robot.keyRelease(' ');

    robot.delay(2000);
    robot.mouseMove(20050);
    robot.mousePress(InputEvent.BUTTON1_MASK);
    robot.mouseRelease(InputEvent.BUTTON1_MASK);

    robot.delay(2000);
    BufferedImage image = robot.createScreenCapture(new Rectangle(00450350));

    ImageFrame frame = new ImageFrame(image);
    frame.setVisible(true);

  }
}

class ButtonFrame extends JFrame {
  private JButton plainJButton; 

  public ButtonFrame() {
    super("Testing Buttons");
    setSize(450350;
    setLayout(new FlowLayout())// set frame layout

    plainJButton = new JButton("Plain Button")
    add(plainJButton)

    ButtonHandler handler = new ButtonHandler();
    plainJButton.addActionListener(handler);
  

  private class ButtonHandler implements ActionListener {
    public void actionPerformed(ActionEvent event) {
      JOptionPane.showMessageDialog(ButtonFrame.this, String.format("You pressed: %s", event.getActionCommand()));
    
  
  
  
}

class ImageFrame extends JFrame {
  public ImageFrame(Image image) {
    setTitle("Capture");
    setSize(450350);
    JLabel label = new JLabel(new ImageIcon(image));
    add(label);
  }

}

'Framework > 예외처리' 카테고리의 다른 글

예외 수집기  (0) 2009.03.26
exception framework  (0) 2009.03.10
checked vs runtime  (0) 2009.03.07
checked exception의 문제  (0) 2009.02.09
예외 처리 격언  (2) 2009.02.09
Posted by bleujin
Framework/예외처리2009. 3. 26. 23:29

이전의 처리를 통해 예외를 한군데로 모을 수 있었으면 이제 다음 단계의 예외 처리 프레임워크로 들어갈 준비가 된 것이다. 프레임워크를 만든다는 것은 어찌보면 왼쪽 그림을 오른쪽 그림으로 만드는 것에 불과하다.



언뜻보기에 클래스의 무작위적인 메시지 교환에서 추상적인 공통점을 찾아내고 이를 단계적인 체인으로 풀어내는 것이다. 이를테면 웹 프레임워크는 브라우저에서 서버 프로그램까지의 과정을 쪼개고 공통점을 묶어서 몇단계의 연결된 과정으로 바꾼다. 이러한 연결된 과정으로 바꾸는 과정에서 중간중간 오른쪽의 세모 그림처럼 request의 접합점을 담당하는 클래스가 생기는데 - 웹프레임워크의 경우 Controller가 이 역활을 한다 - 이 세모 부분의 역할에 따라 프레임워크의 질이 결정된다. 여기서 제 5계를 말할 수 있는데 클래스간의 복잡한 메시지 교환을 횡으로 단계를 가지도록 바꾸면 복잡성이 대폭 감소한다. 이 단계의 과정에서 접합점 역할을 하는 단계에 주의해라.

다시 예외 얘기로 돌아가서 우리는 앞의 글에서 모든 예외가 거쳐가게 되는확장포인트를 만들었다. 중간에 현명한 처리를 할 수 있는 일부 checked exception을 제외하고 마땅한 처리방법이 없는 다른 모든 checked exception 예외와 runtime exception을 하나의 접함점으로 모으면 로그 형식이나 알람 정도 말고도 다양하게 할수 있는게 생긴다.

보통의 프로그램에서는 예외가 생각보다 많이 발생한다. 순간적으로 DB 네트웍이 막혀서 접속이 안될 수도 있고 정기적인 다운타임을 기다리기 귀찮은 누군가 몰래 프로그램을 고칠 수도 있다. "이봐 예외가 발생하지 않는 프로그램을 만들어야지" 라고 주장할 수도 있겠지만 아무리 노력해도 테스트환경에서는 잡히지 않았던 예외들이 사용자의 이상야릇한 PC환경에서는 발생하고 이러한 예외들은 실험실에서 재현하기가 아주 어렵고 사실 사용자는 귀찮기 때문에 웬만한 예외가 발생해도 보고하는 확률은 높지 않다.

보통의 경우 예외 처리 방안은 1)로그에 남긴다. 2) 어드민에게 이메일등을 통해 알린다 정도가 있을 수 있는데 이러한 방법은 모두 후속 처리에 해당한다. 즉 실제 예외가 발생한 후 상당한 시간이 지난후에야 확인이 가능하거나 혹은 지겹게 날라오는 같은 메시지에 질려서 관리자가 무시해 버리기도 해서 대응될때까지 상당한 시간이 흐른다.

두번째로 생각해 봐야 할 점은 수백메가의 예외 로그는 대부분이 중복이라는 사실이다. 넷스케이프 5.0이 처음 나왔을때 버그 리포팅 기능을 생각해보자. 전 세계 1억 2천만명이 매일 날리는 버그 리포팅을 사람이 일일히 확인해서 대응할 수 있을리가 없다.

로그에 남기거나 어드민에게 알리는 방법의 단점은 이렇게 후속처리이며 동시에 대부분의 예외는 아마도 대부분 같은 종류일터인데 사람이 일일이 확인하는 것은 비효율적 이라는 사실이다. 이런 문제를 해결하기 위해 예외 처리 프레임워크는 3가지의 주요 기능을 가져야 한다.

1) 즉시 처리가 되야 한다. 이 말은 수동적인 로그 남기기가 아닌 적극적인 처리를 말한다. 자기 수정같은 거창한 이야기가 아니라 예외가 발생했을때 과거 비슷한 패턴의 예외의 해결방안을 제공하는 시나리오를 제공한다는 말이다. 예외가 발생했을때 "알수 없는 예외가 발생했습니다." 따위는 사용자에게 전혀 도움이 되지 않는다. 사용자가 알고 싶은 것은 첫번째는 "왜 이런게 발생했지"이고 두번째는 "이걸 피하려면 어떻게 해야하나"의 이 두가지이다. 이러기 위해서는 과거 예외 발생 정보와 처리 방안이 어딘가에 저장되어 있어야 한다는 것을 말한다.

2) 예외의 발생 빈도등을 체크하여 예외를 트리아지(Triage) 분류가 가능해야 한다. 보통의 솔류션 제품이 판매되면 1개월의 로그만 모아도 수백메가의 로그 파일이 남는다. 이 로그파일에서 예외 정보는 대부분 중복된 내용일테고 그걸 일일히 살펴보는 것은 시간낭비이기 때문에 사람의 분류의 이전에 프로그램이 예외가 발생한 클래스의 패키지, 예외의 종류, 발생 빈도 등을 고려하여 오류 등급을 미리 분류해 줄 수 있어야 한다.

3) 예외가 발생했을때 자동으로 관련정보를 묶어서 제공해줄 수 있어야 한다. 이는 고객을 위해서가 아니라 이를 수정해야 하는 프로그래머를 위한 정보를 말한다. 해당 예외의 종류가 이전에도 발생한 경우가 있는가? 그에 대한 대처는 어땠는가? 해당 클래스가 이전에도 다른 예외가 발생하지 않았나? 그렇다면 그 클래스의 마지막 수정자는 누구였는가 등의 정보를 같이 제공해 주어야 한다. 또한 현재 수집할수 있는 사용자의 환경과 동작시킨 기능 등도 물론 포함되어야 한다. 사용자의 PC환경은 매우 다양하기 때문에 가능한 많은 정보를 수집해두는게 좋다. 실제로는 어떤 빌어먹을 유틸리티들이 다른 프로세스 영역에 침범함으로 오류를 발생시키기도 하기 때문에 충분하지는 않겠지만 말이다.

이 분야에 대해 조엘의 책에 따르면 자동으로 다음과 같은 자료를 수집하는걸 권장한다.
- 제품의 정확한 버전
- 운영체제 버전과 브라우저 버전
- 예외가 발생한 파일명과 행번호
- 오류 메시지
- 어떤 작업을 하고 있었는지 사용자 설명
- 필요하다면 연락 가능한 사용자 정보


단순한 분류는 사람보다 컴퓨터가 월등히 뛰어나다는 장점을 잘 활용해서 이런 과정은 자동으로 이루어져야 한다. 현재의 대부분의 버그 관리 소프트웨어는 수동으로 이루어지는데 어쩔수 없이 사람이 판단의 개입되어야 하는 일부를 제외하고 자동화 시킬수 있는 부분은 아주 많다. (사실 이 자동 예외 처리기는 AL로 만들 첫번째 소프트웨어로 생각한 것이지만 작업기간이 맞지 않아서 따로 만들고 있다.)

'Framework > 예외처리' 카테고리의 다른 글

GUI TEST  (0) 2009.06.08
exception framework  (0) 2009.03.10
checked vs runtime  (0) 2009.03.07
checked exception의 문제  (0) 2009.02.09
예외 처리 격언  (2) 2009.02.09
Posted by bleujin
Framework/예외처리2009. 3. 10. 02:25


예외 처리에 관해 알아야 할 중요한 사실 중 하나는 예외 처리는 비즈니스 로직이라는 것이다.
if (출금액() > 임금액) throw new 충분한돈이없음예외 와 같은 태생이 비즈니스 로직뿐 아니라 일반적으로 많이 나오는 IOException, SQLEXception등의 java Exception도 어떻게 예외 처리를 하는가는 프로그램마다 다르고 비지니스 로직마다 다르다.

그 전에 프로그래밍에서 예외란 에러와 달리 Alternative Path의 한 종류이고 이도 정상적인 하나의 프로그래밍 로직으로 인정해야 한다.

try {
    ....
} catch(IOException ex){
   log.warn(ex) ;
}

위 구문에서 예외 처리는 중복일까 아닐까? 프레임워크의 시야로 본다면 위 구문이 여러번 나온다면 그건 중복이다. 앞서 다른 글에서 밝힌바와 같이 두 줄이상의 코드가 동일할때만 중복이라고 하는게 아니다. 위와 같은 코드는 구조적인 중복의 2번째 케이스이다. 그런데 단 한줄인데 더 이상 어떻게 하라는 말인가? 한줄을 줄여봤자 한줄인데 이게 왜 중복이고 더 이상 어떻게 줄일 수 있다는 건지 의문을 가질 수 있다.

아마도 객체지향 책을 몇권 읽었다면 구체적인 것에 의지하지 말아라 라는 말을 듯었을 것이다. 구체적인 것에 의존하지 않기 위해 우리는 컨크리트 클래스보다는 추상클래스를 추상클래스보다는 인터페이스를 써야 한다는 것을 이미 알고 있다. 그리고 여기서의 log은 아마도 Apache의 common log같은걸 사용했다면 인터페이스 일테니 충분하지 않은가? 그리고 물론 충분하지 않다. 객체지향에서 구체적인 것이란 단순히 컨크리트 클래스만을 의미하는 것이 아니다. 처음에 언급한대로 예외처리는 비지니스 로직이다라는걸 인정한다면 log를 남긴다는 것 자체가 충분히 구체적이다.

보통의 경우 비지니스 로직과 일반 로직은 수명과 변화의 속도가 다르기 때문에 가능하면 분리해서 구성해야 한다. 잘 알다시피 프로그래머는 CPU가 아니기 때문에 멀티 테스킹의 CPU처럼 유연한 상태전환이 쉽지않고 인터럽트에 취약하기 때문에 로직 중간 catch 코드에 log를 쓸지 warn을 쓸지 info를 쓸지 LogLevel은 몇레벨을 쓸것인지 따위에 고민하게 만들어서는 안된다.

function xxx() throws IOException {
     ,,,,,,,
}

대부분의 코드에서 catch문을 작성하지 않는다면 어느순간 예외들을 던지는 구문이 메소드 내용보다 길어질 것이라고 걱정할지 모르지만 앞서의 조언들을 잘 따라서 인프라를 담당하고 있는 시스템 프레임워크들이 runtime exception을 던지게 하거나 걱정할 정도로 많은 예외를 던져야 한다면 코드를 다시한번 변화의 속도를 기준으로 나눠서 한번정도 Wrapping하는것도 고려한다면 그다지 큰 문제는 아니다.

그마저도 싫다면 혹은 어렵다면
try {
    ....
} catch(IOException ex){
   exManager.resolve(ex) ;
}
와 같이 처리 하는게 낫다 한다. resolve이란 메소드는 log 메소드 보다 더 추상적이고 확장과 구성의 포인트가 많다.

계획대로 모든 예외처리를 미루었다면 FrontController에서
,,,,,
catch (Throwable e) {
     Throwable ex = e ;
     if (e instanceof ServletException && ((ServletException) e).getRootCause() != null) {
        ex = ((ServletException)e).getRootCause() ;
     }
     getExceptionHandler().resolve(new HttpExceptionInfo(request, form, ex));
}
와 같이 한번에 처리해 버릴수 있다.

실제로 예외 처리의 예을 다이어그램으로 그려본다면



와 같이 Chain Of Responsibility 패턴 정도가 나올것이다. Chain Of Responsibility는 어떤 사람에게 요구가 들어왔을때 그 사람이 그것을 처리할 수 있으면 처리하고, 처리할 수 없으면 다음사람에게 넘긴다. 다음 사람이 그것을 처리할 수 있으면 처리하고, 처리할 수 없으면 또 다음 사람에게 넘긴다.

이 패턴은 요구를 하는 사람과 요구를 처리하는 사람을 느슨하게 연결한다. 만약 이 패턴을 사용하지 않으면 이 요구는 이 사람이 처리해야 한다. 라는 지식을 누군가 중앙 집중적으로 가지고 있거나 개별적으로 각각 처리해야한다. 이 지식을 요구를 하는 사람(Client)에게 맡기는 것은 현명하지 않다. 요구를 하는 사람이 처리자들의 역할 분담까지 상세하게 알아야 한다면 부품으로서의 독립성이 손상되고 자신의 일에 집중할 수 없게 되기 때문이다.

Client가 예외를 던지면 예외는 자신의 어떤 종류의 예외인지 예외 코드는 무엇인지 그리고 예외가 발생한 클래스와 메소드의 위치 등 예외와 관련한 모든 정보를 자체적으로 담고 있기 때문에 Handler들은 내가 담당할 예외인지를 판단해서 맞다면 자신이 처리하고 그렇지 않다면 다음 핸들러에 넘겨주는 방식을 취한다. 실제로 변할 가능성이 높은 - 혹은 변화의 속도가 다른 -  구체적으로 파일에 로그를 남길것인지 따라 중복된 예외들을 제외하고 에러데이타베이스를 통해 관리할 것인지 혹은 자고 있을 가엾은 개발자에게 문자를 날릴것인지는 각각의 Handler Class 들이 결정하게 된다. 그리고 컨피그 파일 등에서 아래와 같이 구성파일에서 핸들러의 종류와 순서를 지정할 수 있게 한다면 예외 처리의 변동에 유연하게 대처할 수 있다.

 <!--
  Exception Handler
 -->
 <exception-handler>
        <configured-object>
            <class-name>com.bleujin.webapp.exception.DupValOnIndexExceptionHandler</class-name>
            <constructor>
                <constructor-param>
                    <description>contact Message</description>
                    <type>java.lang.String</type>
                    <value> Please, check ID value</value>
                </constructor-param>
            </constructor>
        </configured-object>
        <configured-object>
            <class-name>com.bleujin.webapp.exception.InterMediaExceptionHandler</class-name>
        </configured-object>
        <configured-object>
            <class-name>com.bleujin.webapp.exception.ReferenceExceptionHandler</class-name>
        </configured-object>
        <configured-object>
            <class-name>com.bleujin.webapp.exception.LockExceptionHandler</class-name>
            <constructor>
                <constructor-param>
                    <description>contact Message</description>
                    <type>java.lang.String</type>
                    <value> Please, Confirm Lock Tab Of Admin's Menu</value>
                </constructor-param>
            </constructor>
        </configured-object>
        <configured-object>
            <class-name>com.bleujin.webapp.exception.LastOutExceptionHandler</class-name>
        </configured-object>
 </exception-handler>

'Framework > 예외처리' 카테고리의 다른 글

GUI TEST  (0) 2009.06.08
예외 수집기  (0) 2009.03.26
checked vs runtime  (0) 2009.03.07
checked exception의 문제  (0) 2009.02.09
예외 처리 격언  (2) 2009.02.09
Posted by bleujin
Framework/예외처리2009. 3. 7. 00:36

좀 더 근원적인 질문을 해보자. 예외란 무엇인가?

인터넷을 검색해보니 정보통신용어사전이라는 쓰잘데기 없는 책에는 예외(exception) : 컴퓨터 시스템의 동작 도중 예기치 않았던 이상 상태가 발생하여 수행 중인 프로그램이 영향을 받는 것. 예를 들면, 연산 도중 넘침에 의해 발생한 끼어들기 등이 여기에 해당한다.
라고 등록되어 있다.

Java 프로그래밍에서의 예외는 조금 다르다. 우선 예외란 이상상태가 아니고 예기치 않았던 상황도 아니다. 프로그래밍에서의 이상 상태는 메모리 누수, 왜곡된 데이타 등을 말하며 예외란 프로그래밍에서 발생하는 일반적인 실행의 흐름(일명 Happy Path)을 바꾸는 몇 가지 조건을 처리하도록 설계한 프로그래밍 언어의 개념을 말하며 이는  주 패스가 아니라 대안적인 패스일뿐 이다. 

예외를 가정할수 있는 몇가지 조건의 종류에는 컴퓨터 하드웨어 문제(충분한 공간의 미확보, 할당하지 못하는 기억장치 접근 등), 운영 체제의 설정 실수, 사용자의 입력 실수(없는 파일, 숫자가 기대되는 곳에 문자 입력), 받아들일수 없는 연산(Devide by Zero) 등 아주 많으며 이러한 상황은 이미 예견되는 상황이다.

갑자기 생뚱맞게 정상적인 교육을 받은 프로그래머라면 대부분 알고 있을 예외 정의를 하는 이유는 자바의 공식 정론은 checked는 정상적인 것이며 runtime exception이 프로그래밍 에러를 가르킨다 를 떠올리기 위해서다.

나는 아주 오랫동안 이 논리에 동의해왔다. 그러나 수십만줄짜리 프로그램들을 작성해 보면서 이는 그닥 실용적이지 않다는 생각을 하게 됐다. 그 주제에 대해 Thinking in Java의 Bruce Eckel와 스프링 개발자로 유명한 Rod Jonson도 이 문제에 대해 언급한 바 있다.  

앞의 글에서 checked exception은 먼저 호출자가 예외를 가지고 현명한 무언가를 할수 있다면 체크되는 예외를 사용해야 한다고 했다. 그런데 여기서 중요한건 가능성이 아니라 빈도와 선택 여부이다. 만약 프레임워크 코드를 작성한다면 호출자가 이 예외를 가지고 현명한 처리를 할지 안할지를 예측할 수는 없다. Java API 코드 설계자도 마찬가지이다. 아직 나중에 누군가의 호출자가 예외 처리를 어떻게 할 것인지를 어떻게 미리 할 수 있을까? 따라서 이 경우에는 가능성이 중요한 선택의 기준이 되고 checked exception에 비중을 많이 두게 된다.

하지만 만약 당신이 지금 호출자의 코드를 작성하고 있다면 즉 그렇게 작성된 자바와 프레임워크를 사용하여 비지니스 코드를 작성하고 있는 중이라면 조금 얘기가 다르다. 일반 프로그램을 작성중이라면 가능성의 문제가 아니라 빈도의 정도가 좀 더 중요해진다. 대부분의 비즈니스 코드에서 Database에 접속할때마다 그리고 SQL을 실행할때마다 그리고 rs.getString("")을 할때마다 SQLException을 잡아서 할 수 있는 무언가는 거의 없다. 비록 Framework에서 모든 코드마다 SQLException을 던진다고 모든 코드에서 그걸 모두 잡거나 다시 던지는건 바보같은 짓이다.

Framework에서는 가능성의 문제로 checked exception을 던지지만 일반 클라이언트 프로그램에서 해당 예외를 잡아서 로그나 화면에 출력하는 것 외에 무언가의 다른 처리를 할 빈도가 거의 없다면 그 많은 checked exception은 생산성의 심각한 장애요소로 작용한다. 예외는 기하급수적으로 퍼지기 때문에 함수 하나에서 throw한 예외는 수천줄의 예외 처리를 감당해야 한다. 그래서 일반 클라인언트 코드에서는 잡아봐자 별로 할것도 없는 checked exception보다는 runtime exception이 좀더 적합하다. 대부분이 로그에 기록하는 것 뿐인 예외 처리라면 맨 처음의 프런트코드에서 한번만 잡아주는게 좋다. 빈도는 낮겠지만 현명한 어떤 처리를 해야 한다면 runtime exception도 catch절로 처리 할수 있기 때문에 상관이 없다.


Framework와 일반 코드에서의 예외 처리 원칙이 이와 같이 다르기 때문에 좀 다른 생각을 해야 할 필요가 있다. 실험실과 비실험실은 그래서 직접 만나는 건 좋지 않다. 사실 비실험실에서 실험실 코드를 직접 호출이 된다는 것 자체가 둘의 경계를 희박하게 만들기 때문에 Facade의 중간 Gate를 만든다. Facade가 해야 할일은 실험실의 캡슐화 역할이 가장 중요하지만 예외를 바꾸는 곳으로도 가장 적절한 곳이다.




위 다이어그램에서 Gate의 역할인 DBController의 코드중 일부인 execQuery를 보면

    public Rows execQuery(IQueryable query, int limit) {
        try {
            long start = System.currentTimeMillis();
            Rows result = query.execQuery(dbm, limit);
            long end = System.currentTimeMillis();

            handleServant(start, end, query, IQueryable.EXEC_QUERY);
            return result;
        } catch(SQLException ex) {
            throw RepositoryException.throwIt(ex, query) ;
        }
    }

실험실 코드에서 던지는 checked exception인 SQLException을 rumtime exception으로 바꾸어 버린다. 대부분의 SQLException이 별도의 Path를 가지지 않는다면 클라이언트는 굳이 처리해야 할 부담을 갖지 않는 것이다. 적은 확률이지만 무언가를 해야 한다면 runtime exception인 RepositoryException을 잡는다. 만약 대부분의 코드가 SQL Exception을 처리해야 한다면 - Framework를 이용하는 새로운 Framework를 만들어야 한다면 - 상속등을 이용해 새로운 Gate를 만들면 그만이다.


이제 예외 처리에 대한 문제를 정리해 보자.

모든 호출자들이 이 문제에 대해 알고 싶어하고 처리해야 하는가 ?
   ex) processInvoice() 메소드에서 소비 한도를 초과했다.
   -> checked를 정의하고 사용하여 자바의 컴파일시 지원의 장점을 사용하자.

호출자들 중의 소수만이 이 문제를 처리하길 원하는가?
   ex) getString() throws SQLException
   -> runtime - 이것은 호출자에게 예외를 잡을지에 대한 선택권을 넘겨주면서도 모든 호출자들에게 그것을 잡도록 강요하지 않는다.

먼가 끔직히 잘못되었는가? 그 문제가 치료 불가능한가?
   ex) DB 접속오류
   -> runtime - 오출자가 그 에러에 관한 정보를 통보 받는것 외에 아무것도 할 수가 없다.

아직 잘 모르겠는가?
   -> runtime 고려 - 가능한 runtime을 고려해본다. 두개의 클래스를 제공해 호출자가 잡을지 말지에 대한 선택권을 줘라.

'Framework > 예외처리' 카테고리의 다른 글

GUI TEST  (0) 2009.06.08
예외 수집기  (0) 2009.03.26
exception framework  (0) 2009.03.10
checked exception의 문제  (0) 2009.02.09
예외 처리 격언  (2) 2009.02.09
Posted by bleujin
Framework/예외처리2009. 2. 9. 08:31

밑의 예외 처리에 이어지는 글.

자바의 정설 이론은 checked exception는 정상적인 것이며 runtime exception은 처리할 수 없는 프로그래밍 에러를 가리킨다. 그리고 아래의 글중 관례 2는 자바의 정설에 기반한 관례이다.

사실 checked exception의 간단한 사용 예를 설명하는 예제들을 보면 이는 분명 멋진 생각처럼 보인다. 문제는 간단한 사용 예제가 아닐때이다.
// example
public interface User {
    public boolean login(String userId, String pwd) throws SQLException ;
    .....
}
이를테면 위의 예제와 같이 사용자 정보를 가지고 허가 여부를 확인하는 매소드를 작성하였다고 하자. 그러나 위의 인터페이스는 아주  깨어지기 쉽다. 첫번째로 사용자 정보가 DB에 있다는 가정을 하였고 두번째로 DB라고 해서 SQLException만 리턴하는 것은 아니다.(IOException등도 throw 할 수 있다.) 세번째로 그러함에도 불구하고 SQLException은 충분히 추상적이지도 충분히 세세하지도 못한 예외라른 사실이다.

물론 그런 저런 책들에서 언급한대로
// example
public interface User {
    public boolean login(String userId, String pwd) throws NoUserIdException ;
    .....
}
와 같은 새로운 고수준 사용자 예외로 처리할 방법을 생각했을 수도 있다. 사실 책에서의 글대로 이 아이디어는 아주 멋져보인다. 다만 이생각은 무균의 연구실을 벗어난 순간 아래와 같은 사실들로 오염되어 버린다.


1. 너무 많은 코드

직접 login 메소드의 구현을 담당한다고 생각해 보자.

public boolean login(String userId, String pwd) throws NoUserIdException {
      try {
           // connection DB 
           // exectue sql 
          .........
          // disconnect DB
      } catch(IOException e) {
             throw new NoUserIdException(.....) ;
      } catch(SQLException e){
             throw new NoUserIdException(.....) ;
      }
}

............... 와 같은 식의 메소드를 작성하게 된다. 별 문제 없어 보인다? What Problem ?...

문제는 이와 비슷한 식의 커스텀 예외를 던지는 메소드를 앞으로 수천개는 더 만들어야 한다는 것이다. 그 말은 수천개의 메소드들을 모두 자바의 표준 exception을 잡아서 사용자 예외로 wrapping해서 던지는 구문들만 수만줄은 된다는 얘기다.

단지 코드가 길다는게 좋은 프로그래밍이 아니다라고 할수는 없겠지만 이는 그 효과에 비해서 노력은 지나치게 많이 소모된다. 먼 미래 코드 어딘가에서 위 메소드를 사용했을때 NOUserIdException이라는 그럴듯한 이름을 가진 예외 처리를 할수 있다는 장점에 비해 위 관례를 지키기 위해 소요되는 노력은 지나치게 많다.



2. 깨지기 쉬운 메소드 원형

다시 위의 코드를 보자. login 메소드는 과연 NoUserIdException 만으로 충분한가? 위의 인터페이스로 좀더 코드를 작성하다보면 NotPasswordMatchedException이나 NotPermittedException 등등이 필요해질 것이다.
그러다 보면
public boolean login(String userId, String pwd) throws NoUserIdException, NotPasswordMatchedException, NotPermittedException, etc....
....와 같이 인터페이스를 수정해야 한다.

문제는 아직 더 있다. DB가 연결이 되지않을때도 엉뚱하게 그런 사용자가 없다느니 패스워드가 일치하지 않는다는 둥의 엉뚱한 예외가 튀어나올 것이다.

구현 메소드를 수정해서 connect절에 SQLException이 발생하면 다시 coonect DB 구문에 SQL 예외가 발생하면 다시 NotConnecttedUserInfo같은 사용자 예외를 던져야 할까?

그러다 보면

public boolean login(String userId, String pwd) throws NoUserIdException, NotPasswordMatchedException, NotPermittedException, etc....
 {
          // 변수 선언
           try {
                // connection DB 
           } catch(SQLException e){
               throw new NotConnecttedUserInfo(...) ;
          }
          // exectue sql 
         try {
           if (rs.next()) {
               if (pwd.equals(rs.getString("pwd") {
                    // 권한을 확인한다
                    try {
                       if ( isPermitted(rs)) {
                          ...
                      } else {
                         .....
                      }
                  } catch (SQLException ex) {
                   }
               } else {
                   throw new NotMatchedException(....) ;
               }
           } else {
              throw new NoUserIdException(.......) ;
           }
         } catch(SQLException e){
              ....               // outer SQLException
         } catch(IOException e) {
              .....             // outer IOException
         }

}

.........
공공장소에서 쓰레기를 버릴때 드는 죄책감이 들게된다. 첫번째 문제인 원래의 본 코드보다 예외 처리하는 코드가 더 복잡해 보인다는 건 일단 넘어가더라도 구현 메소드를 작성하면서 인터페이스를 수정해야 되는 웃기는 상황이 자주 발생하게 된다. 왜냐하면 새로운 Exception 종류가 앞으로도 계속 생겨날 것이기 때문이다. 그리고 그만큼 코드는 예외처리가 더욱 복잡해질 것이다.



3. 예외들의 끝없는 wrapping

바로 위의 예제의 // OuterSQLException 에 있는 예외 처리구문을 보자. 여기다가는 어떤 사용자 예외를 넣어야 할까? 기본적을 RecordSet인 rs의 모든 메소드는 SQLException을 던지기 때문에 안 잡을 수도 없다. 그럼 UnknownedException 같은 웃기는 이름의 exception을 새로 만들어야 할까 ? 아니면 그냥 원래의 SQLException을 던질까?

이 상황을 타계하기 위해 NotLoginnedException이라는 상위 수준으로 추상화된 Exception으로 Wrapping  했다고 하자. 

즉 이와 같이 고쳤다고 해보자.
         } catch(SQLException e){
             throw new NotLoginnedException(.... ) ;               // outer SQLException
         } catch(IOException e) {
             throw new NotLoginnedException(.... ) ;           // outer IOException
         }


그럼 메소드 원형은 public boolean login(String userId, String pwd) throws NoUserIdException, NotPasswordMatchedException, NotPermittedException, NotLogginedUserException etc.... 와 같이 될것이고 이 지저분한 쓰레기들을 보다가 문득 깨닫게 된다.

다른 사용자 예외들의 부모클래스로 NotLogginedUserException를 지정하면
public boolean login(String userId, String pwd) throws NotLogginedUserException ;
와 같이 간편하게 바꿀 수 있다고 ....

그럼 코드는 아래와 같이 수정할 수 있다.
public boolean login(String userId, String pwd) throws NotLogginedUserException {
      try {
           // connection DB 
           // exectue sql 
          .........
          // disconnect DB
      } catch(IOException e) {
             throw new NotLogginedUserException (.....) ;
      } catch(SQLException e){
             throw new NotLogginedUserException (.....) ;
      }
}

사실 지저분한 코드를 잔뜩 썼다가 지웠지만 그 결과문은 맨 처음 예제에서 에외 이름만 바뀐거에 불과하다. 한걸음만 더 생각해보면 우리는 닭질을 하고 있다. 앞의 예에서 우리가 사용자 예외로 처리함으로서 얻을 수 있는 작은 이득은 NoUserIdException이라는 예외 이름 그 자체에 있다고 했다.

그런데 login이라는 메소드에 NotLogginedException이라는 예외 이름이 과연 직관적인가? 그럴바엔 애초에 SQLException이나 별 차이도 없지 않은가 말이다

이렇게 변명할 수도 있다. 물론 메소드 원형에서 던지는 NotLogginedException은 좀 그렇지만 아래와 같이 

void report(UserInfo userInfo) {
       try {
            boolean isPermitted = login(userInfo.getUser(), userInfo.getPwd()) ;
            .....
       } catch(NotPermittedException ex) {
           // print notPermitted message 
           ....
      } catch(NotLoggingedException ex ) {
      }
      ........
}

와 같이 쓸수 있을거라고 ......

라는 생각은 한번도 상업 프로그램을 짜보지 않은 학자들이나 교생들 혹은 공기가 통하지 않는 곳에서 연구를 하고 있는 연구생들이나 할만한 생각이다.

첫번째로 login이라는 원형 메소드에 있는 NotLoggingedException 도 신경쓰기 귀찮은 판에 그 하위 exception 까지 문서화된 정보를(그런게 있다면 말이다..) 보면서 신경쓰고 싶지 않고

두번째로 신경쓰고 싶더라도 report라는 메소드를 작성하고 있는 프로그래머는 login 메소드 말고도 그 안에서 호출하고 있는 수많은 메소들마다 던지는 사용자 Exception에 아주 질려버릴 것이기 때문이다.

그래서 report 함수의 프로그래머는
void report(UserInfo userInfo) throw Exception{
       boolean isPermitted = login(userInfo.getUser(), userInfo.getPwd()) ;
       .....
       // 기타 등등의 코드....

}

와 같이 최상위 Exception으로 처리해야 하는 귀찮음을 대충 보자기 Exception 같은걸로 둘둘 싸서 다른 프로그래머에게 던져 버린다.

물론 쓸데없이 부지런한 프로그래머는
void report(UserInfo userInfo) throw ReportException{
       try {
          boolean isPermitted = login(userInfo.getUser(), userInfo.getPwd()) ;
          .....
          // 기타 등등의 코드....
       } catch(Exception ex){
           throw new ReportException(....) ;
       }

}
와 같이 자신의 쓰레기도 쓸쩍 넣는 치밀함을 보이기도 한다 -ㅅ-;

이를 단순히 개발자의 게으름으로 치부할 수는 없다. 엔트로피는 상승하게 마련이고 사실 작은 코드에서는 멋져보이는 해결책이 실제로는 엄청난 엔트로피의 증가를 가져오는 씨앗이 됐기 때문이다.

작은 메소드에서는 합당해 보이지만 그 메소드들을 호출하여 만드는 다른 프로그래머는 checked exception에 드는 과도한 노력을 피하기 마련이고 이는 결국 속이 보이지 않는 검은색 봉투로 exception을 모두 담아서 wrapping을 해버린다. 물론 이러한 과정에서 유용한 정보를 더한다면 나름 그럴듯한 선택일수도 있다. 그러나 애초에 복구가 가능하지 않는 NotLogginedException같은 경우 그것을 감싸는 것은 아무것도 성취하지 못하고 새로운 쓰레기만 더하게 된다. 그리고 그 쓰레기 만큼이나 stack rewind 하는 비용이 더 들게 된다.


4. 위의 이유들로 checked Exception이 인터페이스에서 잘 동작하지 않는다. 

결국 인터페이스를 작성할때 UnKnownException같은 불분명한 중립적 사용자 예외를 작성하거나 최상위 Exception을 던지는 인터페이스들을 만들다 보면 상황은 더욱 더 심각해진다.

마치 늪처럼 발버둥을 칠수록 더욱 더 깊이 빠져들게 되는 것이다.
이 문제들은 처리할 수 없는 예외들을 잡고, 감싸진 예외들을 다시 던지도록 강요받는 checked Exception의 문제로 속성지어질 수 있다. 이것은 성가시고, 그 자체가 에러를 낳기 쉬우며, 어떤 유용한 다른 목적도 제공하지 않는다.


그럼 다음에는 계속....


'Framework > 예외처리' 카테고리의 다른 글

GUI TEST  (0) 2009.06.08
예외 수집기  (0) 2009.03.26
exception framework  (0) 2009.03.10
checked vs runtime  (0) 2009.03.07
예외 처리 격언  (2) 2009.02.09
Posted by bleujin
Framework/예외처리2009. 2. 9. 05:15


exception란 말 그대로 exception이 필요한 상황은 예외적이라고 생각해서일까?
언어에 대한 기본 문법책은 아주 많이 있지만 exceptional 하게도 exception 분야는 그리 많이 다루지 않는다.

좋은 클래스와 좋은 메소드 디자인과 마찬가지로 좋은 예외처리 디자인은 중요하다. 


관례 1 예외는 예외 상황에만 써야 한다.

// Bad
try {
   int i = 0 ;
   while(true)
       a[i++].fn() ;
} catch(ArrayIndexOutOfBoundsException e) {
}


// Good
for (int i =0 ; i < a.length ; i++)
   a[i].fn() ;

예외 기반의 구현 패턴은 코드의 목적을 애매하게 만들고 성능도 떨어뜨린다. 게다가 제대로 동작하리란 보장조차 없다. 예외 기반의 구현패턴을 쓰면, 다른 버그가 있어도 프로그램은 이 버그를 감춘채 조용히 수행된다. 이런 버그는 정말 잡아내기 어렵다. 예외는 말 그대로 예외상황에서만 써야 한다.프로그램 흐름을 예외로 제어하려 하면 안된다. 


관례 2 checked exception과 runtime exception을 구분해서 던져라

처리해야 하는 예외(checked exception)는 호출자가 예외 상항을 복구 할수 있다가 기대할 수 있을때 던진다. 보통의 경우 런타임 예외는 프로그래밍 오류(precondition violation)가 발생했을때만 써야 한다. 에러는 JVM의 자원이 부족하거나, 불변규칙이 변하는 것과 같이 더 이상 프로그램을 진행할 수 없을때 JVMㅡ에서만 던지는 것이 관례이다.

그러나 관례란 관례일 뿐이다. 관례 2는 많은 예외상항이 있는데 그것에 관해서는 따로 다른 글로 포스팅하겠다.



관례 3 예외를 던질때는 신중해야 한다.

# Bad
} catch(TheCheckedException e) {
   throw new Error("Assertion error") ;
}


# BadBad
} catch(TheCheckedException e){
   e.printStackTrace() ;
   System.exit(1) ;
}

# Not Bad
if(obj.actionPermitted(args)) {
   obj.action(args) ;
} else {
   // 예외 상황을 처리한다.
}


처리해야 하는 예외를 던지는 메소드가 너무 많은 API는 아주 쓰기 번거롭다. 처리해야 하는 예외를 던지는 메소드를 호출하는 메소드는 catch 블록을 써서 이 예외를 잡거나, 같은 예외를 던진다고 선언하여 이 예외를 외부로 전파해야 한다. 사실 이런 작업은 프로그래머에게는 만만치 않은 부담을 준다. API를 정확히 쓰더라도 이런 예외가 발생할 수 있고, 프로그래머가 이 예외를 적절하게 처리할 수 있을때만 처리할 수 있는 예외를 던져야 한다. 위의 2조건을 모두 만족하지 못한다면 처리하지 않는 예외를 던지는 것이 좋다. 이 메소드를 사용하는 프로그래머들이 어떻게 처리할까 라는 질문을 한번 더 던져보자....

사실 관례 2의 예외는 바로 이 관례 3에 기인한다. 지나치기 많은 예외를 던지는것은 부작용을 너무 많이 가져온다. 



관례 4 표준 예외를 써라

클래스의 이름이 중요한만큼이나 예외의 이름도 중요하다. 따라서 가능하면 존재하는 예외라면 존재하는 표준 예외를 써주는 것이 좋다.

IllegalArgumentException : 인자값이 적절하지 못할때
IllegalStateExcetpion : 호출을 받은 객체 상태가 적절하지 못할때
NullPointerException : null을 금지한 인자값이 null일때
IndexOutOfBoundsException : 인덱스 값이 범위를 벗어났을때
ConcurrentModificationException : 동시 수정을 금지한 객체를 동시에 수정할때.
UnsupportedOperationException : 객체가 메소드를 지원하지 않을때.


관례 5 예외를 적절하게 추상화하라

// exception translation
try {

   ......
} catch(LowerLevelException e){
   throw new HigherLevelException(...) ;
}

낮은 계층의 세부 구현사항이 외부에 드러나게 되어 추상화 수준이 높은 계층의 API를 오염시킬수 있다. 인터페이스 디자인할때 어려운 점중 하나는 어느 수준의 예외를 던져야 하는가이다. 메소드 구현이 되지 않는 상태에서 어떤한 예외가 나올것이라고 추측하는 것은 미래를 예측하는 것 만큼이나 매우 매우 어렵기 때문이다. 그래서 인터페이스에서 아무 예외든 다 받아주겠어 식의 throw Exception은 다시 관례 2의 예외로 나타난다. 



관례 6 실패에 대한 자세한 정보를 상세 메시지 문자열에 담아라

예외의 문자열 표현과 최종 사용자가 쉽게 이해할 수 있어야 하는 "사용자에게 제공하는 오류 메시지"와 혼동하면 안된다. 사용자에게 제공하는 오류 메시지와 달리, 예외의 문자열 표현은 프로그래머나 유지보수 요원이 실패를 분석할때 쓰는 것이다. 따라서 예외ㅡ 문자열 표현은 이해하기 쉬운 정보를 담기보다는 자세한 정보를 담는 것이 훨씬 더 중요하다.


# Bad
try {
    ....
} catch(IOException ex) {
   throw new UserException("예외 발생.") ;
}


# Good
try {
    ....
} catch(IOException ex) {
   throw new UserException( ex.getMessage() + "예외와 관계있을수 있는 프로퍼티 정보 ") ;



관례 7 실퍠 원자성을 얻기 위해 노력하라

# Good
public Object pop(){
   if (size == 0) throw new EmptyStackException() ;

   Object result = element[--size] ;
   elements[size] = null ;
   return result ;
}



관례 8 예외를 잡아서 버리지 마라

예외를 잡아서 버리지 마라, 너무나 당연한 진리를 많은 프로그래머가 무시하고 있기 때문에 다시 한번 이야기한다. 어떤 메소드가 예외를 던질 수 있다는 것은, 이 메소드의 설계자가 여러분께 뭔가 알리고 싶어한다는 것이다.

catch 블럭 안에서 정말 아무것도 할 것이 없다면, 최소한 왜 예외를 잡어서 처리하지 않고 버리는지 그 이유라도 주석으로 달아 놓아야 한다.

그러나 이런 관례는 오히려 프로그래머의 게으름을 촉진시킬수 있다.




예외의 예외는 다음 글에서 =ㅅ=;




'Framework > 예외처리' 카테고리의 다른 글

GUI TEST  (0) 2009.06.08
예외 수집기  (0) 2009.03.26
exception framework  (0) 2009.03.10
checked vs runtime  (0) 2009.03.07
checked exception의 문제  (0) 2009.02.09
Posted by bleujin