Exception 에 대해 다루게 된 계기

어떤 회사의 면접 시험에서 RuntimeException, IOException 를 던지는 메소드를
각각 상속받는 클래스에 대한 코드를 보여주고 어떤 결과를 예상하는 지 묻는 문제가 나왔었다.

부끄럽지만, 당시 RuntimeException, IOException 에 대해
‘아! Java가 강제로 제어하는 에러와 개발자가 제어 가능한 RuntimeException 에 대해 묻는 문제구나’
라는 생각으로 안일하게 접근했다가 낭패를 본 경험에서 부터 출발한다.

Exception 이 뭔데?

어떤 프로그램이라도 구동시 어떤 오류(예외)가 발생할 수도 발생 안할 수도 있다.
이 때 발생하는 오류(예외)를 Java에서는 계층구조로 정의하였다.

그리고 오류(예외)가 발생 했을 때

  • 적절히 제어해서 회생가능? –> 예외
  • 발생하면 더 이상 회생 불가능? –> 오류

로 구분지을 수 있다.

Java Exception 계층구조를 살펴 보자.

구조를 보면 크게 Error, Exceptions 로 나눌 수 있고,
Exception 은 각각 Checked Exceptions 와 Unchecked Exceptions 로 나뉠 수 있다.

Error vs Exception

그렇다면 오류가 발생 했을 때, 이 오류를 해결하고 계속 구동이 불가능한가? 적절한 처리 이후에 가능한가?
이 질문의 답으로 Error 와 Exception 이 나뉠 수 있다.

  • Error(오류) : 발생하면 회생 불가
    • ThreadDeath
    • VirtualMachineError
    • AssertionError
  • Exception(예외) : 적절한 처리 후에 계속 구동 가능
    • 예외는 또 하위로 Checked, Unchecked 예외로 나눌 수 있다.
    • IOException
    • RuntimeException

Checked vs Unchecked Exception

Unchecked Exception

Java 명세서에서 정의하는 Unchecked Exception 은 다음과 같다.

Runtime Exception 클래스와 Error 클래스를 포함한다.
그 외 모든 Exception 은 Checked Exception 이다.

Unchecked Exception 의 대표적인 종류

  • NullPointerException
  • ClassCastException

Unchecked Exception 을 한 문장으로 정의해보면
‘Java Program : 에이 설마 이걸 이렇게 쓸 리가 있나?’ 이다.
효용가치가 있는 코드를 작성하는 사람이라면, 아래와 같은 코드를 작성하진 않을 것이다.

1
2
Object nullObj = null;
nullObj.toString(); // NullPointerException 발생!!

거진 대부분의 Unchecked Exception 은 ‘실수’로 발생할 수 있는 상황에 대해
예외로 정의해 놓은 것이라고 이해할 수 있다. 따라서 개발자는 Unchecked Exception 이 발생할 수도 있는 부분에는 적절한 조치를 취할 수도, 취하지 않을 수도 있다.

단어에서부터 Unchecked(확인되지 않은 = 확인 할 수 없는) 예외이다.
프로그램 구동 시 배열 index 에 -1 이 들어올지, 나누려는 수가 0 인지 아닌 지는 실제 값이 들어오는 순간에야 알 수 있는 것들이다.
하지만, Java 에서는 이런 상황에서도 예외를 발생 시켜서 오류 상황에 대해 개발자가 알아챌 수 있도록 알려주고 있다.

Checked Exception

반면, 컴파일 이전에 발생할 수도 있는 오류(예외) 케이스에 대해 정의해놓은 예외가 바로 Checked Exception 이다.

Checked Exception 의 대표적인 종류

  • IOException
  • ClassCastException

아래는 FileInputStream 생성자 내부 코드이다.
file 이 유효하지 않은 경우에 대비하여 Checked Exception 인 FileNotFoundException을 발생시켜,
개발자에게 적절한 예외 처리를 요구하고 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public FileInputStream(File file) throws FileNotFoundException {
String name = (file != null ? file.getPath() : null);
SecurityManager security = System.getSecurityManager();
if (security != null) {
security.checkRead(name);
}
if (name == null) {
throw new NullPointerException();
}
if (file.isInvalid()) {
throw new FileNotFoundException("Invalid file path");
}
fd = new FileDescriptor();
fd.attach(this);
path = name;
open(name);
}

Checked Exception 을 발생시킬 수 있는 클래스 / 메소드를 다루는 개발자는 반드시 적절한 대처를 해주어야 한다

Exception 왜 씀?

Java 의 기본적인 철학은 ‘잘못 구성된 코드는 실행되지 않는다’ 이다.

특히, Java 에서는 오류와 예외로 구분지어 놓고, 그 하위를 계층 구조로 촘촘하게 나누어 놨다는 것은
위 기본 철학에서 반영하고 있는 ‘잘못 구성된 코드는 실행되지 않는다’를 매우 적절하게 반영하고 있다.

Exception 이 존재하는 이유에 대해서는 ‘만약 Exception 이란 클래스가 정의되어 있지 않다면?’ 으로 반문하면서 찾을 수 있다.

프로그램은 구동시에 반드시 오류(예외) 상황이 발생하게 된다. Exception 이 정의되어 있지 않다면,
프로그램 운영을 담당하는 개발자는 24시간 5분 대기조로 언제 발생할지 모르는 오류를 항상 대비하고 있어야만 할 것 이다.

Exception 를 어떻게 제어하나요?

try - catch 로 제어 (예외 처리, 복구, 전환)

1
2
3
4
5
6
7
8
9
10
11
12
13
try {
// 예외가 발생할 수도 있는 코드 위치
} catch (Exception e) { // 만약 예외가 발생했을 때, 던져진 오류(예외)를 잡는다(catch)
// 잡아서 어떻게 할건데?
// 1. 예외 처리
logger.error(e.getMessage()); // 로깅
e.getStackTrace(); // 어쩌다가 이지경까지 왔니?
// 2. 예외 복구 : 다른 제 3의 로직 전개
...
// 3. 예외 전환 :
throw new RuntimeException();
// 예를 들어, checked Excpetion에 대해 개발자가 '이 예외는 별거 아니니까 신경꺼' 라고 RuntimeException 으로 전환
}

throws 로 제어권을 메소드 호출자에게 이관(예외 처리 회피)

개발자가 작성중인 코드에서 예외가 발생할 수도 있는 경우,
이에 대한 처리를 메소드 호출자에게 책임을 전가 시킬 수 있다.

1
2
3
void fileRead() throws FileNotFoundException {
...
}

이렇게 되면 이 메소드를 사용하는 입장에서는
Checked Exception 의 경우

  • 다시 책임을 전가(throws)
  • 적절한 처리(try - catch)

로 처리할 수 있다. 그리고, 이는 강제사항이다.

반면, Unchecked Exception 의 경우, throws로 명시 해줘도
이를 호출한 메소드에서 처리를 해도 되고 안해도 된다.

생각해보자

Exception 에 대해 다시 정리해보면서, ‘이제껏 나는 적절한 Exception Handling 을 하고 있었나?’ 다시 반문하게 되었다.
생각해보니 ‘IDE 에서 제안하는 방법으로 처리’하거나, ‘빌드가 될 수준까지만 예외 처리’를 하고 있었다.

생각없이 코드를 짜고 있었다는 것을 반성 하게 되며 여러가지 생각을 갖게 되었다.

Exception 의 계층구조에 대하여 드는 생각

Exception 에 대한 Java 의 철학(잘못 구성된 코드는 실행되지 않는다)을 다시 곱씹어 보면서,
Exception 이 가진 계층구조에 대해 생각해보는 계기가 되었다.

Java의 특징 중 하나인 상속을 통해 계층구조로 Exception 을 설계했다.
계층구조로 인해, 계층구조의 상위 Exception 으로 갈수록 추상화 정도가 높아지고,
반면 하위 계층으로 갈 수록 구체화 정도가 높아지게 된다.

catch 상위 exception 에 대해 범용적으로 처리하던지
vs
특정 exception 과 그 하위에 대해서만 처리하던지

범위를 개발자가 직접 지정할 수 있게 된다.

따라서, ‘잘못 구성된 코드는 실행되지 않는다.’ 라는 철학의 전반엔 계층구조를 통해,
개발자에게 최소한의 예외 상황에서의 자유도를 보장해주는 것이다.

RuntimeException 을 처리 하지 않아도 될까?

RuntimeException 은 위에서 정의한 것 처럼 ‘실수’로 인해 발생할 수 있는 예외이다.
이 대목에서 숙련된 자바 개발자와 초보 개발자의 차이를 발견할 수 있는 부분인 것 같다.

RuntimeException 에 대해 어떻게 처리할 수 있는가? 더 나아가서 RuntimeException 이 발생하지 않도록 어디까지 해봤니?
라는 질문을 던졌을 때, 대답하는 것에 따라 숙련된 개발자인지 아닌지 구별이 가능하다는 것이다.
면접에서도 이런 저의로 RuntimeException vs IOException 에 대한 질문이 나왔던 것 같다.

예를 들면, NullPointerException 을 발생시키지 않는 간단한 방법 중에서

1
2
3
4
5
6
7
public boolean isHighLevel(String developer) {
if (developer.equals("숙련된개발자")) {
// ...
return true;
}
return false;
}

아주 간단한 코드지만, developer 라는 변수에 만약 null 값이 들어온다면?
바로 NullPointerException 이 발생하게 된다.

아래의 코드로 이런 상황 자체를 발생시키지 않게 할 수 있다.

1
2
3
4
5
6
7
public boolean isHighLevel(String developer) {
if ("숙련된개발자".equals(developer)) {
// ...
return true;
}
return false;
}

동일한 로직을 갖고있는 코드지만, 이렇게 RuntimeException 에 대해

  • 그 종류를 얼마나 알고 있는 지
  • 어떻게 대비할 수 있는 지
    숙련도를 어렴풋이 알 수 있을 것 같다.

참고자료