Posts 학원 #39일차: 예외처리
Post
Cancel

학원 #39일차: 예외처리

인터페이스란 객체 사용 규칙을 정의하는 문법이다. 인터페이스를 마주치면 누가 호출하고 누가 호출 당하는지 빨리 파악해야 한다. 그리고 우리가 호출당하는 프로그램을 짜는 입장인지, 호출 당하는 프로그램을 짜는 입장인지 빨리 파악해야 한다.

IMG_0104

나중에 배울 Servlet도 마찬가지이다. Servlet은 호출 규칙이고, 개발자는 호출 당하는 프로그램을 짜면 된다. 그러면 Tomcat Server와 같은 웹서버가 이를 호출하게 된다.

IMG_0105

mini pms v. 29: 예외처리

현재 프로그램은 다음과 같이 잘못된 값을 입력하면 시스템이 다운된다. 이제 예외 처리 문법으로 시스템이 다운되는 것을 방지할 것이다.

1
2
3
4
5
6
7
8
9
명령> /board/add
번호? okok
Exception in thread "main" java.lang.NumberFormatException: For input string: "okok"
	at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
	at java.lang.Integer.parseInt(Integer.java:580)
	at java.lang.Integer.parseInt(Integer.java:615)
	at com.eomcs.util.Prompt.inputInt(Prompt.java:16)
	at com.eomcs.pms.handler.BoardAddCommand.execute(BoardAddCommand.java:20)
	at com.eomcs.pms.App.main(App.java:107)

무엇 때문에 오류가 나오는지 e.getClass().getName(), e.getMessage()를 출력하도록 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//...
switch (inputStr) {
    //...
  default:
    Command command = commandMap.get(inputStr);
    if (command != null) {
      try {
        command.execute();
      } catch (Exception e) {
        System.out.printf("명령 처리 중 오류 발생: %s\n%s\n",
                          e.getClass().getName(), e.getMessage());
      } else {
        System.out.println("실행할 수 없는 명령입니다.");
      }
    }
    System.out.println();
}
//...

이제 다음과 같이 예외가 발생하더라도 시스템이 멈추지 않고 적절하게 예외 조치를 한 후 계속 실행할 수 있게 한 것이 예외 처리 문법의 목적이다.

1
2
3
4
명령> /board/add
번호? okok
명령 처리 중 오류 발생: java.lang.NumberFormatException
For input string: "okok"

이제 잘못된 형식으로 값을 입력하더라도 시스템을 멈추지 않고 계속 실행하게 한다.

예외 처리

배경

예외처리 문법은 메서드 실행 중에 예외 상황을 만났을 때 리턴 값으로 알려주는 방식의 한계를 극복하기 위한, 그리고 예외가 발생하더라도 시스템을 멈추지 않고 적절한 조치를 취한 후 계속 실행하기 위한 문법이다.

리턴 값으로 오류 여부를 알려줄 때의 한계

예외 처리 문법이 없을 때는 리턴 값으로 오류 여부를 알려 줬다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Calculator {
  public static int compute(String op, int a, int b) {
    switch (op) {
      case "+": return a + b;
      case "-": return a - b;
      case "*": return a * b;
      case "/": return a / b;
      case "%": return a % b;
      default:
        // 만약 유효한 연산자가 아닐 경우 계산 결과는?
        // => 보통 리턴 값으로 알린다.
        return -1;
    }
  }
}

그러나 리턴 값을 검사하여 오류 여부를 파악하는 방식은, 다음과 같이 정상적인 계산 결과도 오류로 취급할 수 있다는 단점이 있다.

1
2
3
4
5
6
7
8
9
10
11
public class Exam0121 {
  public static void main(String[] args) {
    int result = Calculator.compute("-", 6, 7);
    if (result == -1) {
      System.out.println("유효하지 않은 연산자입니다!");
    } else {
      System.out.println(result);
    }
  }
}

이에 오류일 때 리턴하는 값을 -1212121212와 같은 희귀한 값으로 지정할 수 있다. 그러나 아무리 희귀한 값을 리턴한다 하더라도 그 희귀한 값 또한 정상 값일 수 있다. 결국 리턴 값을 검사하여 오류 여부를 처리하는 것으로는 문제를 해결할 수 없다.

예외 발생 시 시스템 멈춤 문제

일반적인 예외의 경우 리턴 값을 검사하여 처리하면 된다. 문제는 0으로 나누는 것처럼 계산할 수 없는 예외 상황이 발생한 경우 JVM은 실행을 종료하게 된다. 0으로 나눌 때처럼 예외가 발생하더라도 JVM을 멈추지 않고 계속 정상적으로 실행되게 만드는 문법이 “예외처리”이다.

next(), nextInt()와 같은 메서드는 공백을 기준으로 나눠진다. 공백이 몇 개이든 상관 없다.

1
2
3
4
5
입력> /
1 0
Exception in thread "main" java.lang.ArithmeticException: / by zero
	at com.eomcs.exception.ex1.Calculator2.compute(Calculator2.java:10)
	at com.eomcs.exception.ex1.Exam0210.main(Exam0210.java:22)

예외처리

예외를 호출자에게 알려주는 문법

유효하지 않은 연산자인 경우 throw 명령을 이용하여 호출자에게 오류 상황을 알린다. 리턴하지 않는다. 던진 순간 메서드는 실행이 끝난다.

택배에서 물건을 담을 때 사용되는 박스 규격이 있는 것처럼, throw로는 Throwable 객체만 던질 수 있다. 즉 Throwable의 서브 클래스만을 던질 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Calculator3 {

  public static int compute(String op, int a, int b) {
    switch (op) {
      case "+": return a + b;
      case "-": return a - b;
      case "*": return a * b;
      case "/": return a / b;
      case "%": return a % b;
      default:
        // 유효하지 않은 연산자인 경우 throw 명령을 이용하여 호출자에게 
        // 오류 상황을 알린다.
        throw new RuntimeException("해당 연산자를 지원하지 않습니다.");
    }
  }
}

예외를 받는 문법

예외를 던질 수도 있는 메서드를 호출할 때는 try 블록 안에서 호출하고 메서드가 던지는 예외를 받을 수 있는 catch 블록도 만들어야 한다. 즉 try 블록 안에서 메서드를 호출하다가 예외가 발생하면 catch 블록에서 파라미터로 받는다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class Exam0120 {
  public static void main(String[] args) {
    Scanner keyScan = new Scanner(System.in);
    while (true) {
      System.out.print("입력> ");
      String op = keyScan.next();
      if (op.equalsIgnoreCase("quit"))
        break;

      try {
        int v1 = keyScan.nextInt();
        int v2 = keyScan.nextInt();

        int result = Calculator3.compute(op, v1, v2);
        System.out.println(result);

      } catch (InputMismatchException e) {
        System.out.println("입력 값이 유효하지 않습니다.");
        keyScan.nextLine(); // 입력이 잘못되었을 경우, 나머지 입력을 무시한다.

      } catch (RuntimeException e) {
        System.out.println(e.getMessage());
      } 
    }
    keyScan.close();
  }
}

개념

예외를 호출자에게 알려주는 문법

  • throw [Throwable 객체];

    예: Throwable <- Exception <- RuntimeException

    서브 클래스는 직계자식 및 밑의 자식들을 퉁쳐서 말하는 것

예외를 받았을 때 처리하는 문법

  • 다형적 변수에 입각해서 부모 클래스의 참조 변수로 받을 수 있다.

  • 예외가 발생하면 catch 블록이 실행된다. 코드에서 던진 예외 객체는 catch의 파라미터가 받는다.

예외를 받지 않으면

  • 즉시 현재 메서드의 실행을 멈추고 호출자에게 예외 처리를 위임한다. 만약에 그 호출자가 JVM이라면 프로그램 실행을 종료한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Exam0110 {
  static void m() {
    // 예외를 호출자에게 알려주는 문법
    throw new RuntimeException("예외가 발생했습니다!");
  }
  
  public static void main(String[] args) {
    // 예외를 받았을 때 처리하는 문법
    try {
      m();
    } catch (RuntimeException e) {
      System.out.println(e.getMessage());
    }
    System.out.println("시스템을 종료합니다.");
    
    // 예외를 받지 않으면?
    m();
    System.out.println("시스템을 종료합니다.");
  }
}

예외 던지기: 예외 상황을 호출자에게 알려주기

Throwable 객체

throw 명령어를 사용하여 예외 정보를 호출자에게 던질 때, Throwable 타입의 객체를 던져야 한다(throw [java.lang.Throwable 타입의 객체]). 이 Throwable에는 java.lang.Errorjava.lang.Exception 두 부류의 서브 클래스가 있다.

java.lang.Error(시스템 오류)

  • JVM에서 발생된 오류이다.
  • 개발자가 사용하는 클래스가 아니다.
  • 이 오류가 발생하면 현재의 시스템 상태를 즉시 백업하고, 실행을 멈춰야 한다. JVM에서 오류가 발생한 경우에는 계속 실행해봐야 근본적으로 문제를 해결할 수 없어 소용이 없기 때문이다.
  • ex) 스택 오버플로우 오류, VM 관련 오류, AWT 윈도우 관련 오류, 스레드 종료 오류 등

java.lang.Exception(애플리케이션 오류)

  • 애플리케이션에서 발생시킨 오류이다.
  • 개발자가 사용하는 클래스이다.
  • 적절한 조치를 취한 후 계속 시스템을 실행하게 만들 수 있다.
  • ex) 배열의 인덱스가 무효한 오류, I/O 오류, SQL 오류, Parse 오류, 데이터 포맷 오류 등

문법

오류를 던진다면 반드시 메서드 선언부에 어떤 오류를 던지는지 선언해야 한다. 어떤 오류를 던지는지 선언함으로써 메서드 호출자에게 이런 오류가 발생할 수 있다고 알려줄 수 있다.

throw로 던질 수 있는 객체는 오직 java.lang.Throwable 타입만 가능하다. 메서드의 throws에 선언할 수 있는 클래스도 마찬가지로 Throwable 타입만 가능하다.

1
2
3
static void m3() throws String { // 컴파일 오류!
  //throw new String(); // 컴파일 오류!
}

예외를 던질 때는 Throwable 클래스를 직접 사용할 수 있지만, 그 하위 클래스, 특히 Exception 클래스를 사용하는 것이 권장된다.

1
2
3
static void m1() throws Throwable {
  throw new Throwable(); // ok!
}

여러 개의 오류를 던지는 경우에도 메서드 선언부에 그대로 나열해야 한다.

1
2
3
4
5
6
7
8
9
static void m2() throws FileNotFoundException, RuntimeException {
    int a = 100;
    if (a < 0)
      throw new FileNotFoundException();
    else
      throw new RuntimeException();
  }
  public static void main(String[] args){}
}

Error 계열의 예외를 던질 경우, 메서드 선언부에 표시하는 것은 선택사항이다. 그러나 Error 계열의 클래스는 JVM 관련 오류일 때 사용하는 클래스이기 때문에 사용하지 않는 것이 권장된다.

1
2
3
static void m1() /*throws Error*/ {
  throw new Error();
}

Exception 계열의 예외를 던질 경우, 반드시 메서드 선언부에 어떤 예외를 던지는지 지정해야 한다. 보통 개발자가 애플리케이션을 작성ㄷ하면서 예외를 던질 경우 이 클래스 및 하위 클래스를 사용한다.

1
2
3
static void m1() throws Exception {
  throw new Exception();
}

RuntimeException 예외를 던질 경우, 메서드 선언부에 예외를 던진다고 표시하지 않아도 된다. 스텔스 모드를 지원하기 위해 만든 예외이기 때문이다.

1
2
3
static void m() throws /*RuntimeException*/ {
  throw new RuntimeException(); // ok!
}

던지는 예외를 메서드에 선언하기

메서드에서 발생되는 예외는 메서드 선언부에 모두 나열해야 한다.

1
2
3
4
5
6
static void m(int i) throws Exception, RuntimeException, SQLException, IOException {
  if (i == 0) throw new Exception();
  else if (if == 1) throw new RuntimeException();
  else if (if == 2) throw new SQLException();
  else if (if == 3) throw new IOException();
}

공통분모를 사용하여 퉁치는 방법

메서드에서 발생하는 예외의 공통 수퍼클래스를 지정하여 여러 개를 나열하지 않을 수 있다.

1
2
3
4
5
6
static void m(int i) throws Exception {
  if (i == 0) throw new Exception();
  else if (if == 1) throw new RuntimeException();
  else if (if == 2) throw new SQLException();
  else if (if == 3) throw new IOException();
}

그러나 호출자에게 어떤 오류가 발생하는지 정확하게 알려주는 것이 유지 보수에 도움이 된다. 따라서 가능한 해당 메서드에서 발생하는 예외는 모두 나열하는 것이 좋다!

던지는 예외 받기

다음과 같이 예외를 던지는 메서드가 있다고 하자.

1
2
3
4
5
6
7
8
static void m(int i) throws Exception, RuntimeException, SQLException, IOException {
  if (i == 0) throw new Exception();
  else if (if == 1) throw new RuntimeException();
  else if (if == 2) throw new SQLException();
  else if (if == 3) throw new IOException();
  else if (i < 0)
      throw new Error(); // 시스템 오류가 발생하다고 가정하자!
}

예외를 던질 수 있다고 선언된 메서드를 호출할 때, 예외 처리를 하지 않으면 컴파일 오류가 발생한다.

1
2
3
public static void main(String[] args) {
  //m(1); => 컴파일 오류
}

방법1: 예외 처리 책임을 상위 호출자에게 위임

예외를 처리하고 싶지 않다면 예외 처리 책임을 상위 호출자에게 위임할 수 있다.

위 코드에서 컴파일 오류는 발생하지 않지만, main()의 상위 호출자에게 예외 처리 책임을 위임하고 있다. main()의 호출자는 JVM이고, JVM은 main()에서 던지는 예외를 받는 순간 즉시 실행을 멈춘다. 그래서 main()의 호출자에게 책임을 넘기는 것은 바람직하지 않다. main()은 예외 처리의 마지막 보루라고 할 수 있다.

1
2
3
public static void main(String[] args) throws Exception{
  m(1);
}

방법2: try ~ catch

try ~ catch를 사용하여 코드 실행 중에 발생된 예외를 중간에 가로챈다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static void main(String[] args) {
  try {
    // try 블록에서 예외가 발생할 수 있는 메서드를 호출한다.
    m(1);
  } catch (IOException e) {
    System.out.println("IOException 발생");
  } catch (SQLException e) {
    System.out.println("SQLException 발생");
  } catch (RuntimeException e) {
    System.out.println("RuntimeException 발생");
  } catch (Exception e) {
    System.out.println("기타 Exception 발생");
  }
}

catch 블록을 여러 개 사용할 때는 순서에 주의한다. 여러 개의 예외를 받을 때 수퍼 클래스의 변수로 먼저 받지 말아야 한다. 만약 받는다면 그 클래스의 모든 서브 클래스 객체도 다 받게 된다. 즉 서브 클래스의 변수에서 받을 기회조차 없다. 예외 객체를 정확하게 받으려면 위의 코드처럼 서브 클래스 예외부터 받아야 한다.

1
2
3
4
5
6
7
8
9
public static void main(String[] args) {
  try {
    m(1);
  } catch (Exception e) {    
  } catch (IOException e) { // 컴파일 오류! 
  } catch (SQLException e) {
  } catch (RuntimeException e) {
  } 
}

다형적 변수의 특징을 이용하여 여러 예외를 한 catch에서 받을 수 있다.

1
2
3
4
5
6
7
8
public static void main(String[] args) {
  try {
    m1();
  } catch (Exception e) {
    //RuntimeException, SQLException, IOException 모두
    //Exception의 서브 클래스이기 때문에 이 블록에서 모두 처리할 수 있다.
  }
}

OR 연산자를 사용하여 여러 개의 예외를 묶어 받을 수 있다.

1
2
3
4
5
6
7
8
9
public static void main(String[] args) {
  try {
    m(1);
  } catch (RuntimeException | SQLException | IOException e) {
    
  } catch (Exception e) {
    
  }
}

가능한 Error 계열의 시스템 예외를 받지 말자. 받더라도 현재의 프로그램을 종료하기 전에 필수적으로 수행해야 하는 마무리 작업만 수행하도록 하자. 시스템 예외는 당장 프로그램을 정상적으로 실행할 수 없는 상태일 때 발생하기 때문이다. 정상적인 복구가 안 되는 예외임으로 이 예외를 처리해서는 안 된다. 시스템을 멈출 수밖에 없다.

1
2
3
4
5
6
7
8
9
public static void main(String[] args) {
  try {
    m(-1);
  } catch (Exception e) {
    System.out.println("애플리케이션 예외 발생!");
  } catch (Error e) {
    System.out.println("시스템 예외 발생!");
  }
}

Throwable 변수로 예외를 받지 않도록 주의하자. catch문을 작성할 때 Throwable 변수로 선언하면 시스템 오류인 Error까지 받기 때문에 JVM에서 발생된 오류에 대해서도 예외 처리를 하는 문제가 발생한다. 시스템 오류는 애플리케이션에서 처리할 수 없으므로 실무에서는 예외를 받을 때 Throwable 변수를 사용하지 않는다.

1
2
3
4
5
6
7
public static void main(String[] args) {
  try {
    m(-1);
  } catch (Throwable e) {
    System.out.println("애플리케이션 예외 발생!");
  }
}

다음과 같이 Exception 변수를 사용하면 애플리케이션 예외를 처리하고, 시스템 예외는 main() 호출자인 JVM에게 위임하게 된다. 이렇게 Exception 계열의 애플리케이션 예외만 처리하자.

1
2
3
4
5
6
7
public static void main(String[] args) {
  try {
    m(-1);
  } catch (Exception e) {
    System.out.println("애플리케이션 예외 발생!");
  }
}

예외 처리 후 마무리 작업

finally 블록

정상적으로 실행하든, 아니면 예외가 발생하여 catch 블록을 실행하든 finally 블록은 무조건 실행한다. 즉 try ~ catch ~ 블록을 나가기 전에 반드시 실행한다. 그래서 이 블록에는 try에서 사용한 자원을 해제시키는 코드를 주로 둔다. 자원에는 파일, DB 커넥션, 소켓 커넥션, 대량의 메모리 등이 있다.

1
2
3
4
5
6
7
8
9
10
11
12
public static void main(String[] args) {
  try {
    m(0);
    System.out.println("try");
  } catch (RuntimeException | SQLException | IOException e) {
    System.out.println("catch 1");
  } catch (Exception e) {
    System.out.println("catch 2");
  } finally {
    System.out.println("finally");
  }
}

try 블록을 나가기 전에 무조건 실행해야 할 작업이 있다면 catch 블록이 없어도 finally 블록만 사용할 수 있다. 만약 여기서 try 블록이 없이 m(1); System.out.println("마무리 작업 실행!");이라고 정의되어 있다면, m(1)에서 예외가 발생했을 경우 그 다음 코드는 실행되지 않을 것이다. 예외가 발생할 수 있는 메서드를 사용할 때 반드시 실행되어야 하는 작업이 있다면 finally 블록을 사용할 수밖에 없다.

다음 코드에서 m()에서 발생된 예외는 try 블록에서 받지 않고 main() 호출자에게 위임한다. main() 선언부에 위임할 예외의 종류를 표시해야 한다.

1
2
3
4
5
6
7
public static void main(String[] args) throws Exception {
  try {
    m(1);
  } finally {
    System.out.println("마무리 작업 실행!");
  }
}

자원 해제

1
2
3
4
5
6
7
8
9
10
public static void main(String[] args) {
  // 키보드 입력을 읽어들이는 도구 준비
  Scanner keyScan = new Scanner(System.in);
  
  // 스캐너 객체를 사요애하여 키보드 입력을 읽어들인다.
  System.out.print("입력> ");
  int value = keyScan.nextInt();
  System.out.println(value * value);
  keyScan.close();
}

프로그램을 즉시 종료한다면 Scanner를 다 사용하고 난 다음에 굳이 스캐너에 연결된 키보드와 연결을 끊을 필요는 없다. JVM이 종료되면 OS는 JVM이 사용한 모든 자원을 자동으로 회수하기 때문에 굳이 close()를 호출하지 않아도 된다.

그러나 365일 24시간 내내 실행되는 시스템이라면, 키보드 입력을 사용하지 않는 동안에는 다른 프로그램에서 사용할 수 있도록 스캐너와 연결된 키보드를 풀어줘야 한다. 이것을 자원 해제라고 한다. 보통 자원해제시키는 메서드의 이름은 close()이다. 지금까지 짠 프로그램은 바로 종료되는 프로그램이기 때문에 close()를 사용하여 자원을 해제하지 않아도 괜찮았지만, 앞으로 자바로 프로그램을 짤 영역은 서버 쪽이다. 즉 365일 24시간 내내 실행되는 서버쪽 프로그램을 개발하는 것이기 때문에, 항상 자원을 사용한 후 해제시키는 것을 습관적으로 해야 한다.

문제는 close()를 호출하기 전에 예외가 발생한다면 제대로 자원을 해제시키지도 못한다는 것이다. 그러나 정상적으로 실행되든 예외가 발생하든 상관 없이 자원해제 같은 일은 반드시 실행해야 한다. finally 블록을 사용하면 이 문제를 해결할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
public static void main(String[] args) {
  Scanner keyScan = null;
  try {
    keyScan = new Scanner(System.in);
    
    System.out.print("입력> ");
    int value = keyScan.nextInt();
    System.out.println(value * value);
  } finally {
    keyScan.close();
    System.out.println("스캐너 자원 해제!");
  }
}

try-with-resources

자원해제시키는 코드를 매번 작성하기 귀찮다면 try-with-resources 문법을 사용하자. 자동으로 자원해제하기 때문에 굳이 finally 블록에서 close()를 직접 호출할 필요가 없다. 단, java.lang.AutoCloseable 구현체에 대해서만 가능하다. AutoCloseable 인터페이스 구현체는 close() 메서드를 가지고 있음을 보증하고 있다.

문법: try (java.lang.AutoCloseable 구현체) {...}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static void m() throws Exception {
  try (Scanner keyScan = new Scanner(System.in);
      FileReader in = new FileReader("Hello.java");
 
      // 반드시 AutoCloseable 구현체여야 한다.
      //String s = "Hello"
      
      // 변수 선언만 올 수 있다.
      //if (true) {}
      ) {
    System.out.println("입력> ");
    int value = keyScan.nextInt();
    System.out.println(value * value);
  }
}

public static void main(String[] args) throws Exception {
  m();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
static class A {}
static class B {
  public void close() throws Exception {
    System.out.println("B 클래스의 자원을 해제하였습니다.")
  }
}
static class C implements AutoCloseable {
  @Override
  public void close() throws Exception {
    System.out.println("C 클래스의 자원을 해제하였습니다.");
  }
}

public static void main(String[] args) {
  // A클래스는 AutoCloseable 구현체가 아니기 때문에 여기에 선언할 수 없다.
  // B클래스에 close() 메서드가 있어도 
  // AutoCloseable 구현체가 아니기 때문에 선언할 수 없다.
  try (C obj = new C()) {
    System.out.println("try 블록 실행...");
  }
  // finally 블록에서 C의 close()를 호출하지 않아도 자동으로 호출한다.
}

/*
try 블록 실행...
B 클래스의 자원을 해제하였습니다.
*/

예외가 발생하면 try 블록을 나가기 전에 close()가 먼저 호출된다. 그 후 catch 블록이 실행된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
static class B implements AutoCloseable {
  public void m(int value) throws Exception {
    if (value < 0)
      throw new Exception("음수입니다.");
    System.out.println("m() 호출!");
  }
  
  @Override
  public void close() throws Exception {
    System.out.println("close() 호출!");
  }
  
  public static void main(String[] args) {
    try (B obj = new B()) {
      System.out.println("try 블록 실행.. 시작");
      obj.m(-100);
      System.out.println("try 블록 실행... 종료");
    } catch (Exception e) {
      System.out.println("예외 발생!: " + e.getMessage());
    }
  }
}
/*
try 블록 실행.. 시작
close() 호출!
예외 발생!: 음수입니다.
*/

변수 선언은 반드시 괄호 안에 해야 한다.

1
2
3
4
5
public static void main(String[] args) {
  B obj = null;
  try (obj = new B()) { // 컴파일 오류!
  }
}

Exception 예외 던지고 받기

메서드에서 발생된 예외를 상위 호출자에게 던지려면 상위 호출자 또한 메서드 선언부에 해당 예외를 기술해야 한다. 해당 메서드를 사용할 개발자에게 “이 메서드 안에서 예외가 발생합니다”라고 알려주는 역할을 한다. 이처럼 예외를 상위 호출자에게 전달하려면, 그 호출 경로에 있는 모든 메서드에 throws 문장을 선언해야 하는 번거로움이 있다. 다른 방법이 없다. 무조건 선언해야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Exam0120 {
  static void m1 throws Exception { m2(); }
  static void m2 throws Exception { m3(); }
  static void m3 throws Exception { m4(); }
  static void m4 throws Exception {
    throw new Exception("m4()에서 예외 발생!");
  }
  
  public static void main(String[] args) {
    try {
      m1();
    } catch (Exception e) {
      System.out.println(e.getMessage());
    }
  }
}

### RuntimeException

RuntimeException을 상위 호출자에게 전달할 때는 굳이 메서드 선언부에 지정하지 않아도 된다. 상위 호출자도 RuntimeException 예외를 받을 경우, try ~ catch 예외를 처리하지 않으면 자동으로 그 상위 호출자에게 예외를 던질 것이다. 즉 RuntimeException 계열의 예외를 던지는 메서드를 사용할 때는 그 호출 경로에 있는 모든 메서드에 굳이 throws 문장을 선언할 필요가 없다. 예외를 처리하고 싶은 곳에서 catch 블록으로 받아 처리하면 된다. 이러면 중간에 끼어 있는 메서드를 만들 때 throws 문장을 선언하지 않아도 되어서 편하다. 스텔스처럼 조용히 예외를 전달한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Exam0120 {
  static void m1 { m2(); }
  static void m2 { m3(); }
  static void m3 { m4(); }
  static void m4 {
    throw new RuntimeException("m4()에서 예외 발생!");
  }
  
  public static void main(String[] args) {
    try {
      m1();
    } catch (RuntimeException e) {
      System.out.println(e.getMessage());
    }
  }
}

사용자 정의 예외 만들기: 예외에 의미 부여

RuntimeException 계열의 예외는 굳이 throws 문장을 선언하지 않아도 되지만, 실행 예외가 발생할 가능성이 있는 메서드를 호출하는 개발자에게 어떤 예외가 발생할 수 있는지 명확하게 제시해주는 것이 유지보수에 도움이 되기 때문에 메서드 선언부에 발생되는 예외를 명시하는 것이 좋다.

그러나 다음 코드에서 read() 메서드를 사용하는 개발자가 이 메서드에서 RuntimeException을 던진다는 의미에 대해 직관적으로 이해하기는 힘들다. 예외를 던지는 것은 이해하지만, 그 예외가 의미하는 바를 즉시 알아보기 힘들다는 말이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class Exam0120 {
  static Board read() throws RuntimeException {
    try (Scanner keyScan = new Scanner(System.in)) {
      Board board = new Board();

      System.out.print("번호> ");
      board.setNo(Integer.parseInt(keyScan.nextLine()));
      System.out.print("제목> ");
      board.setTitle(keyScan.nextLine());
      System.out.print("내용> ");
      board.setContent(keyScan.nextLine());
      System.out.print("등록일> ");
      board.setCreatedDate(Date.valueOf(keyScan.nextLine()));

      return board;
    }
  }

  public static void main(String[] args) {
    try {
      Board board = read();
      System.out.println("---------------------");
      System.out.printf("번호: %d\n", board.getNo());
      System.out.printf("제목: %s\n", board.getTitle());
      System.out.printf("내용: %s\n", board.getContent());
      System.out.printf("등록일: %s\n", board.getCreatedDate());

    } catch (RuntimeException e) {
      System.out.println(e.getMessage());
      System.out.println("게시물 입력 중에 오류 발생!");
      // e.printStackTrace();
    }
  }
}

실무에서는 개발자에게 예외의 의미를 직관적으로 알 수 있도록 RuntimeException같은 평범한, 의미가 모호한 이름의 클래스를 사용하지 않는다. 대신에 기존 예외를 상속받아 의미있는 이름으로 서브 클래스를 정의한 다음 그 예외 클래스를 사용한다. 즉 사용자 정의 예외를 만들어 사용한다. 원본 예외 대신 BoardException이 발생하면 게시물 관리 작업을 하다가 오류가 발생했음을 직관적으로 알게 될 것이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public class Exam0130 {
  static Board read() throws BoardException {
    try (Scanner keyScan = new Scanner(System.in)) {
      Board board = new Board();

      System.out.print("번호> ");
      board.setNo(Integer.parseInt(keyScan.nextLine()));

      System.out.print("제목> ");
      board.setTitle(keyScan.nextLine());

      System.out.print("내용> ");
      board.setContent(keyScan.nextLine());

      System.out.print("등록일> ");
      board.setCreatedDate(Date.valueOf(keyScan.nextLine()));

      return board;
    } catch (Exception 원본오류) {

      throw new BoardException("게시물 입력 도중 오류 발생!", 원본오류);
    }
  }

  public static void main(String[] args) {
    try {
      Board board = read();
      System.out.println("---------------------");
      System.out.printf("번호: %d\n", board.getNo());
      System.out.printf("제목: %s\n", board.getTitle());
      System.out.printf("내용: %s\n", board.getContent());
      System.out.printf("등록일: %s\n", board.getCreatedDate());

    } catch (BoardException e) {
      System.out.println(e.getMessage());
      e.printStackTrace();
    }

  }
}

다음 클래스는 생성자가 호출될 때 그와 대응하는 수퍼 클래스의 생성자를 호출하는 일 외에는 다른 작업을 수행하지 않는다. 이 클래스는 예외 클래스 기능을 확장하기 위함이 아니라, 의미 있는 이름을 가진 예외 클래스를 만드는 것이 목적이다. 즉, 예외가 발생했을 때 클래스 이름으로 어떤 예외인지 쉽게 추측하기 위함이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package com.eomcs.exception.ex5;

public class BoardException extends RuntimeException {

  public BoardException() {
    super();
  }

  public BoardException(String message, Throwable cause, boolean enableSuppression,
      boolean writableStackTrace) {
    super(message, cause, enableSuppression, writableStackTrace);
  }

  public BoardException(String message, Throwable cause) {
    super(message, cause);
  }

  public BoardException(String message) {
    super(message);
  }

  public BoardException(Throwable cause) {
    super(cause);
  }
}
This post is licensed under CC BY 4.0 by the author.

💻 HTTP #5: HTTP의 진화

학원 #40일차: 파일 입출력

Loading comments from Disqus ...