예외 처리

Tina Jeong·2021년 2월 9일
0

Re-자바

목록 보기
13/16

Exception vs Error

Exception과 Error는 둘다 Throwable을 상속 받고, 이례적인 상황이 발생했음을 의미한다. 그러나 Error은 절대 발생하면 안되는 것, Exception은 모종의 이유로 사용자에게 상황을 notify하고 프로그램이 추가적인 처리가 요구되는 것을 의미한다. Error는 OutOfMemoryError와 같이 프로그램 실행에 치명적인 결함이 발견됐을 때 발생한다. Error를 예외처리하려는 시도가 있는데, 이는 전혀 근본적인 해결책이 될 수 없으므로 추천하지 않는다.

다시 말해 Exception에는 checked exception과, unchecked exception 두 종류가 있다. checked exception은 코드에 exception이 명시되어야 하는 exception을 의미한다. checked exception에 대한 예외 처리가 안되면 컴파일 에러가 발생한다. unchecked exception은 코드 명시를 강제하지 않는 exception이다. RuntimeException나 Error를 상속받은 경우이다. 즉, 코드 상에서 exception에 대한 처리가 가능하다면 checked exception이고, 그게 어렵다면 unchecked exception이다.

결론적으로 다음과 같은 포함관계에 놓인다.

그러나, 자바 언어에서는 위의 개념적 포함관계와 다르게 Error가 Exception 클래스를 상속 받는 다거나 포함된다거나 하지 않으니 유의한다. Error와 Exception이 각각 Throwable을 상속 받고 있는 구조이다.

예외처리 방법

checked exception들을 처리하는 방법이다.

try-catch-finally

첫번째는 try/catch/finally 키워드를 이용해 블록을 형성시키는 방법이다.

try {
...
}
catch (SomeException e1) {
...
}
catch (AnotherException | YetAnotherException e2) {
...
}
finally {
...
}

try 블록 안에는 프로그램 로직을 작성한다. try 블록을 실행하던 중 Throwable의 자식 클래스 중 하나인 SomeException이 발생하면, try 블록의 실행이 중단되고 catch 블록으로 인터프리터가 넘어간다. 그리고 e1이라는 이름으로 해당 Exception 객체가 받아진다. 프로그래머는 e1 객체를 참조하여 정보를 얻어내고, catch 블록 안에서 일련의 exception 발생 후 처리 로직을 처리할 수 있다.

try 블록을 실행하던 중에 SomeException이 아니라 AnotherException이나 YetAnotherException이 발생해도 catch 블록으로 들어가는 메커니즘은 동일하다. Exception 종류에 따라 catch 블록을 나눠서 처리할 수 있고, catch하는 Exception이 여러개일 경우 기호 |로 구분하는 것이 구조적으로 가능하다는 것을 보여주기 위해 명시한 것이다.

catch 블록은 optional한 블록이다. 프로그래머가 의도적인 exception 처리를 할 필요가 없다고 여겨지는 경우에는 그 처리를 자바 인터프리터로 넘겨서 에러 메세지를 출력하도록 한다.

finally 블록은 catch같이 optional한 블록이지만, 일단 finally를 포함시키면 해당 블록 안의 코드들은 Exception이 발생하든, 발생하지 않든 항상 실행된다. 보통 try 블록에 포함된 BufferedReader를 close()하거나 Network connection을 close()하는 등의 후처리 클린업 코드를 명시하는 편이다.

try-with-resources(TWR)

앞서 언급한 try-catch-finally 블록을 이용한 방법은 finally 블록에서 클린업 처리를 해줘야 하는 불편함이 있었다. file, 문자열 등 resource를 다룰 때 TWR 방식으로 코딩하면 프로그래머의 부담이 줄어든다.

try (InputStream is = new FileInputStream("/Users/ben/details.txt")) {
...
}

위의 파일을 읽어들이는 is 객체는 try 블록 안에서의 scope을 가지며, 블록을 벗어나면 자동으로 메모리 할당이 해제 되어 편리하다. 주의 할 점은, close() 같은 클린업만 자동화해주는 것이지, IOException 같은 Exception들은 별도로 catch로 묶어주어야 한다.

throw

throw exception;
method() throws exception ...

throw는 statement로써, checked나 unchecked exception 객체와 함께 쓰인다. if문이나 case와 함께 throw가 단일 statement로 쓰이거나, method 단위에서 쓰일 수도 있다.

throw를 통해 예외가 던져지면, 자바 인터프리터는 해당 블록에 try-catch 형태의 예외 처리 코드가 있는지 확인한다. 해당 블록에 없다면, 해당 메소드에 있는지 확인하고, 해당 메소드에 없다면, 해당 메소드를 호출한 블록에 있는 지 확인한다. 이런식으로 계속 연어마냥 거슬러 올라가 main() 메소드까지 이른다. main()에도 없다면 메세지를 출력하고, 프로그램 실행을 중단한다. 그래서 예외처리코드를 찾는 방식이 call-stack 형태와 같은 것을 알 수 있다.

try {
	throw new ExceptionA();
}
catch (ExceptionA | ExceptionB e) {
... // ExceptionA가 잡힘
}

자바의 예외 계층 구조

도입부에 예외 계층 구조를 간단히 언급했다. 아래 그림을 보면 좀더 명확히 이해할 수 있을 것이다. 번개표시가 unchecked exception에 속한다.

RuntimeException

runtime exception은 이름처럼 프로그램 실행 중에, JVM 작동 중에 발생하는 예외를 의미한다. runtime exception은 checked exception처럼 예외 처리를 강제하지 않는다. 대표적인 runtime exception에는 ArithmeticException, NullPointerException ,IndexOutOfBoundException 등이 있다.

ArithmeticException은 보통 나눗셈의 분모가 0인 경우에 발생하고, 다음과 같은 메세지를 출력하면서 프로그램 실행이 종료된다. 물론 예외 처리 코드를 넣어서 종료되지 않게 하고 후처리를 할 수도 있다.

Exception in thread "main" java.lang.ArithmeticException: / by zero

NullPointerException은 객체의 멤버를 이용하거나 메소드 인자로 넘길 때 해당 객체가 null일 경우에 발생한다. 해당 객체를 이용할 수 없기 때문이다.

IndexOutOfBoundException은 할당된 크기 이상의 index에 대한 접근을 시도할 때 발생하며, ArrayIndexOutOfBoundsException,StringIndexOutOfBoundsException등이 해당 예외를 상속받는다. ArrayIndexOutOfBoundsException은 size 이상의 index에 접근하거나, 음수 인덱스에 접근을 시도할 경우 발생한다. StringIndexOutOfBoundsException은 할당된 문자열 객체 크기-문자열은 상수이다-에서 벗어난 index에 접근을 시도할 때 발생한다.

예외 커스텀

자바는 일반적인 예외는 거의 제공하지만, 커스텀된 예외의 필요가 생길 때가 있다. 비즈니스 로직에 있어서 예외를 프로그래머와 사용자에게 이해시키기 위해 새로 구현이 필요한 경우, 또는 이미 존재하는 예외의 일부만 이용할 필요가 있는 경우이다. 자바에 원래 checked, unchecked 예외가 둘다 존재하는 것처럼, 커스텀 예외도 두 종류 모두 존재할 수 있다.

📌 예외 커스텀 시 root cause를 인자로 넘겨 chaining 하는 습관을 들이자.

custom checked exception

예를 들어 파일을 읽는 다음과 같은 코드가 있다고 하자.

try (Scanner file = new Scanner(new File(fileName))) {
    if (file.hasNextLine()) return file.nextLine();
} catch(FileNotFoundException e) {
    // Logging, etc 
}

해당 경우에 파일 이름의 포맷 등 validation check해서 걸러내는 과정이 필요하다면, 코드에 명시해서 처리하는 것이 좋을 것이다.

public IncorrectFileNameException(String errorMessage, Throwable err) {
    super(errorMessage, err);
}

커스텀 예외는 Exception을 상속받아 구현하며, 에러 메세지를 생성자의 인자로 받아 exception으로 넘겨 에러 메세지를 출력할 수 있도록 한다. IncorrectFileNameExceptionFileNotFoundException의 일종이므로 root cause인 그와 연결되도록 생성자 인자를 추가로 받아 처리한다.

...
catch (FileNotFoundException fe) {
    if (!isCorrectFileName(fileName)) {
        throw new IncorrectFileNameException(
          "Incorrect filename : " + fileName , fe);
    }
    // ...
}

custom unchecked exception

unchecked exception은 RuntimeException을 상속받아 처리하며, 아래의 코드는 파일의 확장자가 잘못된 경우에 exception 처리를 하기 위한 것이다. 마찬가지로 IncorrectFileExtensionExceptionIllegalArgumentExceptionroot로 가지므로 chaining을 위해 생성자에 인자로 넘겨주었다.

public class IncorrectFileExtensionException 
  extends RuntimeException {
    public IncorrectFileExtensionException(String errorMessage, Throwable err) {
        super(errorMessage, err);
    }
}

최종 코드의 모습이다.

 catch (FileNotFoundException err) {
    if (!isCorrectFileName(fileName)) {
        throw new IncorrectFileNameException(
          "Incorrect filename : " + fileName , err);
    }
    
    //...
} catch(IllegalArgumentException err) {
    if(!containsExtension(fileName)) {
        throw new IncorrectFileExtensionException(
          "Filename does not contain extension : " + fileName, err);
    }
    
    //...
}

※ assert

assert는 exception의 용도와 다르다. exception은 프로그래머가 사용자에게 특정한 상황을 catch시키려는 의도로 사용하지만, assert는 프로그램 실행 조건을 체크함으로서, 프로그램이 정상적으로 실행된다는 신뢰성을 높이기 위해 사용한다.

assert
v. If someone asserts a fact or belief, they state it firmly
출처: https://www.collinsdictionary.com/dictionary/english/assert

assert는 뒤의 조건이 반드시 참일 것이라 가정한다. 프로그래머가 이 조건이 참일거야! 단언하는 것이다. 만약 조건이 false라면, 예상하지 못한 값이나 상황이 발생했다는 의미이다.

assert assertion;
assert assertion : errorcode;

assert는 boolean으로 결과가 나오는 표현과 함께 쓰이며, 해당 표현이 true일 경우에는 아무런 처리를 하지않고 넘어간다. false인 경우에는 AssertionError를 발생시킨다. 즉, assert 키워드 뒤의 표현은 반드시 true여야 프로그래머가 제시한 조건에 만족한다는 뜻이다.
또,assertion이 false인 경우 optional하게 errorcode 부분을 넣어서 메세지를 프린트 하는 등의 처리를 할 수 있다.

assert문은 true를 강제한다는 측면에서 프로그래머의 예상조건을 체크하는 디버깅 용도로 사용할 수 있다. 그러나, 어플리케이션을 최종 테스트하기엔 flexible하지 않고, 실제 실행 환경에서는 assert문이 제외된다고 하니(JVM에서 assert를 사용하려면 -ea 인자를 추가해주어야 한다) 테스트를 하려면 JUnit 등의 프레임워크 사용을 추천한다고.

참고
Java in a Nutsell, 7th Edition
https://docs.oracle.com/en/java/javase/15/docs/api/java.base/java/lang/Throwable.html
https://docs.oracle.com/en/java/javase/15/docs/api/java.base/java/lang/Exception.html
https://docs.oracle.com/en/java/javase/15/docs/api/java.base/java/lang/RuntimeException.html
https://docs.oracle.com/en/java/javase/15/docs/api/java.base/java/lang/Error.html
https://docs.oracle.com/en/java/javase/15/docs/api/java.base/java/lang/IndexOutOfBoundsException.html
https://offbyone.tistory.com/294
https://www.baeldung.com/java-new-custom-exception
https://www.baeldung.com/java-assert

계속해서 문서를 업데이트하고 있습니다. 언제든지 댓글피드백 남겨주세요. 😉

profile
Keep exploring, 계속 탐색하세요.

0개의 댓글