백엔드

JAVA exception 처리하기

CyberI 2016. 7. 1. 18:11

JAVA exception 처리하기



안녕하세요, cx 사업본부 박토끼입니다. 요즘 일을 하면서 계속 머리속에서 드는 의문이 하나 있었습니다. "예외 처리를 어떻게 해야 잘 처리하는 것일까?" 


사실 Java를 처음 배울 때는 exception에 그다지 민감하지 않았습니다. 단순히 '잘못되어서 에러가 나는구나.' 정도의 생각만 했습니다. 그리고 throw 키워드나 try~catch 블럭을 사용하면서도, IDE 툴에서 빨간색 표시로 잘못되었다고 에러가 뜨고 컴파일 자체가 안되니까 해줘야된다는 생각뿐이었습니다. 


그리고 일을 시작한 후에도 exception 처리가 왜 필요한지 느끼지 못했습니다. '에러가 나는 것 자체가 문제이니, 에러가 안나게 로직을 완벽히 수정하는 것이 옳다. 그러면 exception 처리는 필요없다.' 라고 생각했습니다. 지금 생각해보면 아주 근시안적 생각이었고, exception이 발생하는 이유는 꼭 로직의 오류가 아니라 다른 원인이 있을 수 있다는 것을 전혀 인지하지 못했던 것 같습니다. 또한 exception 처리를 제대로 하지 않으면 서비스 중인 프로그램이 중단되어 사용자에게 매우 불편을 끼칠 수 있다는 생각도 하지 못했던 것 같습니다.


점점 반성문이 되어가는 것 같네요.(!) 반성은 그만하고 이제 java 에서 exception이란 무엇이며, 어떻게 처리해야 하는 지 알아보도록 하겠습니다.


 1. Exception 이란?


Java에서 Exception 이란 프로그램의 실행중에  발생하는 문제입니다. 예외가 발생하면 프로그램의 정상적인 플로우가 중단되며, 프로그램 혹은 애플리케이션이 권장하지 않는, 비정상적인 방법으로 종료됩니다. 그러므로 예외는 처리되어야 합니다.


예외는 매우 다양한 원인으로 인해 발생하는데, 프로그래밍에서는 에러를 세 가지 카테고리로 분류합니다.

  • 컴파일 에러(Compile-time Errors) : 작성한 소스 코드를 빌드할 시 컴파일러에 의해서 파악되는 에러입니다.

  • 런타임 에러(Run-time Errors) : 프로그램 실행중에 발생하는 에러입니다. 종종 프로그램의 충돌을 유발합니다.

  • 로직 에러(Logic Errors) : 프로그램 로직 상의 에러입니다. 이런 종류의 에러는 에러메시지가 나지 않으며, 종종 이런 에러는 무한 루프, 실행해야되는 코드의 지나침, 올바르지 않은 결과를 유발합니다.


그렇다면 실제 상황에서 나타날 수 있는 예외의 시나리오에 대해 예를 들어보겠습니다.
  • 사용자가 유효하지 않은 데이터를 입력하는 경우
  • 열릴 필요가 있는 파일을 찾을 수 없는 경우
  • 커뮤니케이션 중에 네트워크 커넥션을 잃는 경우 혹은 JVM의 메모리가 부족한 경우
예외의 일부는 사용자에 의해서, 또 일부는 프로그래머에 의해서, 어떤 경우는 물리적인 리소스에 의해서도 발생합니다. Java에서 예외 처리가 어떻게 작동되는지 알기 위해서는 예외의 세 가지 카테고리를 이해해야만 합니다.
이는 위에서 기술한 세 가지 프로그래밍 에러와 동일한 순서로 매칭됩니다.

  • Checked exceptions : 컴파일 시 발생하는 에러입니다. 이는 위에서 컴파일 시 예외라고 불리기도 합니다. 이 예외들은 컴파일 시에 단순히 무시하고 지나갈 수 없으며, 프로그래머는 반드시 이 예외들을 처리야해만 합니다. 예를 들어, 파일에서 데이터를 읽기 위해 FileReader 클래스를 사용하는데, 생성자에 쓰인 파일이 존재하지 않는다면 FileNotFoundException이 발생할 것입니다. 그리고 컴파일러는 프로그래머에게 예외처리를 하라고 알려줍니다.
  • Unchecked exceptions : 실행 시에 발생하는 예외로, 런타임 에러라고도 불립니다. 로직 에러 혹은 API의 부적절한 사용 등의 프로그래밍 버그를 포함합니다. 예를 들어, 사이즈가 5개인 배열을 선언하고 6번째의 요소를 호출한다면, ArrayIndexOutOfBoundsException 예외가 발생합니다. 
  • Errors : 이는 전혀 예외가 아니라, 사용자의 조작 혹은 프로그래머를 넘어서 발생하는 문제입니다. 에러는 코드에서 일반적으로 간과되는데 왜냐하면 에러에 대해서 프로그래머가 할 수 있는 것은 거의 아무것도 없기 때문입니다. 예를 들어, 만약 stack overflow 가 발생하면, 에러가 발생할 것입니다. 이런 에러들은 또한 컴파일 시에도 간과됩니다.

 2. Exception 계층 구조

모든 exception 클래스들은 java.lang.Exception 클래스의 하위유형(subtype)입니다. exception 클래스는 Throwable 클래스의 서브클래스입니다. exception 클래가 아닌 쪽에는 Error라고 불리는 다른 서브클래스가 있으며, 이는 Throwable 클래스에서 파생된 것입니다.


에러는 심각한 실패의 경우에 발생하는 비정상적인 조건입니다. 이는 Java 프로그래밍으로 처리되지 않습니다. 런타임 환경에서 실행되는 에러를 나타내기 위해 Error가 발생됩니다. 예를 들어, JVM이 out of memory되는 경우 입니다. 보통 프로그램은 이런 에러가 발생하면 회복될 수 없습니다.



아마도 Exception의 서브클래들 중에서는 익숙한 것들도 있으실 겁니다. 예를 들어, NullPointerException, 

ArrayIndexOutOfBoundsException, StringIndexOutOfBoundsException, NumberFormatException,

 IllegalArgumentException, ClassNotFoundException, InputMismatchException 등은 프로그래밍을 하면서 많이 접해보셨을 거라 생각합니다.



 3. 예외 처리의 좋은 방법들

예외에 대한 기본적인 개념은 살펴봤는데요, 그렇다면 어떻게 예외를 처리하는 것이 좋은지 10가지 사항을 기술해보도록 하겠습니다.

  • 구체적인 예외를 사용하기
Exception 계층 구조에서 기본이 되는 클래스들은 그다지 유용한 정보를 제공하지 않습니다. 그 때문에 Java는 아주 많은 예외 클래스들을 지원하는 것입니다. 예를 들어서 IOException의 경우 서브클래스로 FileNotFoundException, EOFException 등이 있습니다. 항상 구체적인 예외 클래스를 던지고(throw), catch 블럭에서 사용한다면, 예외의 근본적인 원인을 쉽게 파악할 수 있고 그에 대한 대처를 할 수 있습니다. 이는 디버깅을 쉽게 만드며, 클라이언트에서 적절한 예외 처리를 할 수 있도록 도와줍니다.
  • 예외는 일찍 던지기
가능하면 예외는 빨리 던져야합니다. 아래와 같이 processFile() 메소드가 있다고 할 때, 만약 null 값의 파라미터를 받는다면 메소드는 다음과 같은 예외를 발생시킬 것입니다.

1Exception in thread "main" java.lang.NullPointerException
2    at java.io.FileInputStream.<init>(FileInputStream.java:134)
3    at java.io.FileInputStream.<init>(FileInputStream.java:97)
4    at com.journaldev.exceptions.CustomExceptionExample.processFile
(CustomExceptionExample.java:42)
5    at com.journaldev.exceptions.CustomExceptionExample.main(CustomExceptionExample.java:12)

이런 경우, 디버깅을 하면서 예외의 실제 위치를 알기 위해서 stack trace를 유심히 살펴보아야 합니다. 만약 null 값에 대한 예외를 조금 더 빨리 체크하도록 로직을 바꾸면 훨씬 좋을 것입니다. 아래와 같이 바꾸면 NullPointerException 대신, 커스텀하게 만든 MyException이 던져지면서 '파일 이름은 null 일 수 없습니다.' 라는 로그가 찍힐 것입니다.

1private static void processFile(String file) throws MyException {
2        if(file == nullthrow new MyException("파일 이름은 null 일 수 없습니다.","NULL_FILE_NAME");
3//이후 처리
4}
  • 캐치는 늦게
Java는 checked exception(컴파일 에러)를 처리하거나 메소드 시그니처에 선언하도록 강요하기 때문에, 종종 개발자들은 exception을 잡고 에러를 로그로 남기곤 합니다. 그러나 이 방법은 좋지 않습니다. 적절히 예외를 처리할 수 있을 때 exception을 catch 해야 합니다. 같은 메소드는 다른 애플리케이션에서 사용될 수 있으며 다른 방식으로 예외를 처리하기를 원할 수 있습니다. 호출한 곳에서 예외를 처리하는 방법을 결정하도록 하기 위해서 항상 호출한 곳으로 예외를 던져주어야 합니다.
  • 리소스 닫기
예외는 프로그램의 프로세싱을 중단시키기 때문에, fanally 블럭에서 사용한 모든 리소스를 닫아주거나 혹은 Java 7의 try-with-resources 를 사용하면 Java 런타임이 리소스를 닫아줄 것입니다. 
ex) try-with-resources
try(FileReader fr=new FileReader("E://file.txt")){
         char [] a = new char[50];
         fr.read(a); // reads the contentto the array
         for(char c : a)
         System.out.print(c); //prints the characters one by one
      }catch(IOException e){
          e.printStackTrace();
       }   
  • 예외를 로그로 남기기
항상 예외 메시지는 로그를 남겨야 하며, 예외를 던질 경우에는 명확한 메시지를 제공하여 호출한 쪽에서 왜 예외가 발생했는지 쉽게 알 수있도록 해야합니다. catch 블럭을 빈 상태로 두는 것은 반드시 피해야 하며 디버깅을 위해 예외의 의미있는 상세정보를 주도록 해야합니다.
  • 다양한 예외들을 위한 하나의 캐치 블럭
대체로 예외 상세를 로그로 남기고 메시지를 사용자에게 제공하는데, 이런 경우 단일 캐치 블럭에서 복수의 예외를 처리할 수 있는 Java 7 의 특징을 사용할 수 있습니다. 이는 코드 사이즈를 줄여주며 깔끔하게 보이도록 해줍니다.
  • 커스텀한 익셉션을 사용하기
설계 당시에 예외 처리 전력을 명확히 하여 여러개의 예외를 던지고 받는 대신에 커스텀 exception을 만들고 호출하는 프로그램이 에러를 처리할 수 있도록 하는 것이 훨씬 낫습니다. 
  • 네이밍 관습과 패키징
커스텀 exception을 만들 때, 클래스명을 'Exception'으로 끝나도록 만들어야 그 자체로 예외인 것을 명확하게 알 수 있습니다.  또한 JDK에 되어 있는 것처럼 커스텀 exception 들을 패키지화 하는 것이 좋습니다. 
  • 분별력 있게 예외 사용하기
예외 처리는 많은 노력이 필요하며, 가끔은 예외를 발생시켜야되는 필요가 전혀 없는 때가 있습니다. 그리고 단지 작업이 성공했는지 실패했는지 호출한 프로그램이 판단할 수 있도록 boolean 변수만 return 해줄 수도 있습니다. 이런 방법은 작업이 부가적인 경우이거나 작업에 실패했다고 해서 프로그램이 멈추기를 원하지 않는 경우에 도움이 될 수 있습니다. 
  • 예외를 발생 문서화하기
메소드에 의해서 발생되는 예외를 정확히 명시하기 위해서 javadoc의 @throws를 사용하세요. 이는 다른 애플리케이션에서 사용할 인터페이스를 제공할 때 매우 유용합니다.






참고


http://www.tutorialspoint.com/java/java_exceptions.htm

http://www-acad.sheridanc.on.ca/~jollymor/prog24178/oop2.html