자바를 공부했다고 한다면 몰라선 안되는 Exception에 대해 훑어보겠다. 자바는 클래스에 미친 언어인 만큼(내 의견이며 오라클과는 관련X), 오류와 예외까지 클래스로 관리한다.
1. Error VS Exception
자바의 malfunction은 크게 두 가지, Error(오류)와 Exception(예외)로 나뉜다.

# Error : 프로그램 외적으로 발생하는 오류
OOM(Out Of Memory) 났는데 램 할당량 늘려야할듯요?
스택 오버플로 났는데 무한재귀 없나 확인해봐!
자바 프로그램 시작이 안되는데요.. 포트 문제인지 뭔지 감이 안잡혀.
앞선 내용들은 자바 프로그램이 아닌 하드웨어와 관련해서 문제가 생긴 예시들이다. (잘못 쓰여져 무한루프 도는 프로그램으로 인해 발생하는 경우도 있지만 어쨌든 하드웨어가 영향을 받아 에러를 일으킴) 자바에선 이런 문제들을 "Error"라고 칭한다. 이러한 에러와 관련해선 소프트웨어 개발자가 딱히 할 수 있는 일이 없다.
# Exception : 프로그램 내부에서 발생하는 예외
ArrayIndexOutOfBoundsException은 개발자로 인생을 살아간다면 한 번 쯤은 맞딱뜨렸을 예외일 것이다. NullPointerException은 최선의 주의를 기울여 대처해야하는 예외이고, 자바를 이용해 IO작업을 할 때 IOException을 처리하지 않았습니다! 하는 IDE의 경고문 역시 본 적이 있을 것이다.
이처럼 자바 소스코드와 관련해서, 프로그램 내부에서 발생하는 malfunction을 "예외(Exception)"라고 한다. 에러와는 다르게 예외는 개발자가 손 닿을 수 있는 범위라면 모두 고려하여 처리하는게 좋다. 예외를 가볍게 생각했다가 프로그램의 어떤 부분에서 대참사가 날지 모르기 때문이다.
2. Checked Exception , Unchecked Exception
CheckedException과 UncheckedException의 가장 큰 차이점은, 해당 예외를 처리하지 않았을 때 컴파일이 되냐 안되냐의 차이이다. 이는 각 예외의 용어적 뜻을 생각해보면 쉽게 유추할 수 있다. 사전에 체크되었기 때문에 Checked Exception, 사전에 체크되지 않았기 때문에 Unchecked Exception인 것이다. 컴파일 때 체크되는 Checked Exception은 그 자체로 Exception이지만, 컴파일때 체크되지 않고 JVM의 동작 과정중에 일어나는 예외는 JVM의 런타임에 발생하므로 Runtime Exception이라고 한다.
실제로 자바의 예외들은 모두 Exception 클래스를 상속받는데, 그 중에서도 RuntimeException 클래스를 상속받은 예외들을 Unchecked Exception이라고 부른다.

정말 checked exception은 먼저 체크되어야 할까? checked exception 상황을 만들어서 확인해보자. 존재하지 않는 파일을 읽는 fileReader 객체를 만들어봤다.
public class CheckedException {
public static void main(String[] args) {
FileReader fileReader = new FileReader(new File("존재하지않는파일.txt"));
}
}
위 코드를 javac로 컴파일 하려고 하면, 컴파일 에러가 난다.
error: unreported exception FileNotFoundException; must be caught or declared to be thrown
FileReader fileReader = new FileReader(new File("buchu.txt"));
"FileNotFoundException"이 처리되지 않았다는 뜻이다. FileNotFoundException은 IOException을 상속받은 클래스이고, IOException은 대표적인 checked exception이다.

이렇듯 checked exception은 컴파일 전에 예외 처리를 해줘야 한다.
그럼 대표적인 unchecked exception인 NPE를 발생시켜보자.
public class UncheckedException {
public static void main(String[] args) {
String myString = null;
System.out.println(myString.length());
}
}
이렇게 뻔하게 예외가 발생할게 보이는 코드인데도 컴파일이 잘 된다! 그리고 run하면?
java.lang.NullPointerException: Cannot invoke "String.length()" because "myString" is null
역시 NullPointerException이 발생했다.
그렇다면 왜? Checked Exception과 Unchecked Exception을 굳이 이런 식으로 나눠놓은 것일까? 그것은 효율성을 위해서이다.
말했듯, 예상 가능한 예외는 처리할 수 있을만큼 사전에 처리하는 것이 베스트이다. 그러나 참조 타입의 포인터, 배열의 인덱스, 숫자간 계산 등 프로그램에서 빈번히 일어나는 작업 모두에 예외 처리를 강제한다면 프로그램은 프로그램 자체 코드보다 예외 처리 코드가 훨씬 길어질 것이다. 때문에 예외 처리가 critical한 상황이 아니라면 대부분의 예외들을 RuntimeException으로 빼서 개발자가 편한 방식대로 예외를 처리할 수 있도록 한 것이다.
3. try-catch로 예외 핸들링
예외를 핸들링하는 것은 기본적으로 try-catch 블록으로 할 수 있다. try 안에 예외가 발생할 가능성이 있는 로직의 코드를 넣어두고, catch 조건문 안에 처리할 예외 클래스를 집어넣어 해당 예외 클래스가 발생했을 때 실행할 코드를 작성하는 방식이다.
앞선 CheckedException의 코드를 다음과 같이 try-catch 블락으로 묶을 수 있다.
public class CheckedException {
public static void main(String[] args) {
try {
FileReader fileReader = new FileReader(new File("존재하지않는파일.txt"));
} catch (IOException e) {
// IOException 발생됐을 때 실행될 블록
e.printStackTrace();
}
}
}
printStackTrace() 메소드를 호출해 발생한 예외와 관련 정보들을 콘솔창에 쫙 출력하도록 했다. 실행해보면 아까완 다르게 컴파일이 잘 된다! 그리고 결과는.
java.io.FileNotFoundException: 존재하지않는파일.txt (No such file or directory)
at java.base/java.io.FileInputStream.open0(Native Method)
at java.base/java.io.FileInputStream.open(FileInputStream.java:216)
at java.base/java.io.FileInputStream.<init>(FileInputStream.java:157)
at java.base/java.io.FileReader.<init>(FileReader.java:75)
at org.example.exception.basic.CheckedException.main(CheckedException.java:11)
"존재하지않는파일.txt"는 존재하지 않는다고 잘 말해주고 있다!
if-else if처럼, catch 블록을 연달아서 쓸 수 있다. 위쪽의 catch엔 조금 더 특수한 예외로, 밑으로 갈수록 일반적인 예외로 구성한다. 그리고 가장 마지막에 finally 블록을 추가할 수 있는데, 이는 catch블록의 실행 여부와 상관없이 무조건 실행되는 코드가 작성되는 블락이다. try-with-resource가 없었을 시절 열린 파일이나 소켓 객체를 닫는 용도로 사용했다.
public class CheckedException {
public static void main(String[] args) {
try {
FileReader fileReader = new FileReader(new File("존재하지않는파일.txt"));
} catch (IOException e) {
// IOException 발생됐을 때 실행될 블록
e.printStackTrace();
} catch (Exception e) {
// IOException 외, 일반적인 예외가 발생했을 때 실행될 블록
e.printStackTrace();
} finally {
// 무조건 실행될 블록
System.out.println("finally executed");
}
}
}
이제 조금 재밌는 실험을 해보자. Checked 클래스와 Unchecked 클래스를 만들건데, Checked 클래스는 Exception 클래스를 상속받고 Unchecked 클래스는 RuntimeException 클래스를 상속받도록 했다.
public class Checked extends Exception {
public Checked(String message) {
super(message);
}
}
public class Unchecked extends RuntimeException {
public Unchecked(String message) {
super(message);
}
}
상속 구조는 아래 그림과 같다.

글의 초반에 나왔던 상속 지도가 맞다면 클래스 이름대로 Checked는 checked excepton, unchecked는 unchecked exception이 될 것이다. 정말 각각의 예외를 throw 했을 때 컴파일 성공의 차이가 있을까?
자바에서 예외를 던지기 위해선 throw 키워드 뒤에 예외 객체를 넘기면 된다.
public class ThrowingExceptions {
public static void main(String[] args) {
// 됨
throw new Unchecked("unchecked exception");
// 안됨
throw new Checked("checked exception");
}
}
Unchecked만이 던져질 땐 컴파일이 되었지만, Checked가 추가된 순간 컴파일 에러를 뱉는다. 이제 각 예외를 try-catch 블락으로 묶어 처리해보겠다.
public class ThrowingExceptions {
public static void main(String[] args) {
try {
throw new Unchecked("unchecked exception");
} catch (Unchecked unchecked) {
unchecked.printStackTrace();
}
try {
throw new Checked("checked exception");
} catch (Checked checked) {
checked.printStackTrace();
}
}
}
이제 컴파일도 되고 실행도 된다.
org.example.exception.throwing.Unchecked: unchecked exception
at org.example.exception.throwing.ThrowingExceptions.main(ThrowingExceptions.java:7)
org.example.exception.throwing.Checked: checked exception
at org.example.exception.throwing.ThrowingExceptions.main(ThrowingExceptions.java:14)
실제로 RuntimeException을 상속받았는지, Exception을 상속받았는지에 따라 컴파일이 되는지 여부까지 살펴봤다!
REFERENCE
자바를 공부했다고 한다면 몰라선 안되는 Exception에 대해 훑어보겠다. 자바는 클래스에 미친 언어인 만큼(내 의견이며 오라클과는 관련X), 오류와 예외까지 클래스로 관리한다.
1. Error VS Exception
자바의 malfunction은 크게 두 가지, Error(오류)와 Exception(예외)로 나뉜다.

# Error : 프로그램 외적으로 발생하는 오류
OOM(Out Of Memory) 났는데 램 할당량 늘려야할듯요?
스택 오버플로 났는데 무한재귀 없나 확인해봐!
자바 프로그램 시작이 안되는데요.. 포트 문제인지 뭔지 감이 안잡혀.
앞선 내용들은 자바 프로그램이 아닌 하드웨어와 관련해서 문제가 생긴 예시들이다. (잘못 쓰여져 무한루프 도는 프로그램으로 인해 발생하는 경우도 있지만 어쨌든 하드웨어가 영향을 받아 에러를 일으킴) 자바에선 이런 문제들을 "Error"라고 칭한다. 이러한 에러와 관련해선 소프트웨어 개발자가 딱히 할 수 있는 일이 없다.
# Exception : 프로그램 내부에서 발생하는 예외
ArrayIndexOutOfBoundsException은 개발자로 인생을 살아간다면 한 번 쯤은 맞딱뜨렸을 예외일 것이다. NullPointerException은 최선의 주의를 기울여 대처해야하는 예외이고, 자바를 이용해 IO작업을 할 때 IOException을 처리하지 않았습니다! 하는 IDE의 경고문 역시 본 적이 있을 것이다.
이처럼 자바 소스코드와 관련해서, 프로그램 내부에서 발생하는 malfunction을 "예외(Exception)"라고 한다. 에러와는 다르게 예외는 개발자가 손 닿을 수 있는 범위라면 모두 고려하여 처리하는게 좋다. 예외를 가볍게 생각했다가 프로그램의 어떤 부분에서 대참사가 날지 모르기 때문이다.
2. Checked Exception , Unchecked Exception
CheckedException과 UncheckedException의 가장 큰 차이점은, 해당 예외를 처리하지 않았을 때 컴파일이 되냐 안되냐의 차이이다. 이는 각 예외의 용어적 뜻을 생각해보면 쉽게 유추할 수 있다. 사전에 체크되었기 때문에 Checked Exception, 사전에 체크되지 않았기 때문에 Unchecked Exception인 것이다. 컴파일 때 체크되는 Checked Exception은 그 자체로 Exception이지만, 컴파일때 체크되지 않고 JVM의 동작 과정중에 일어나는 예외는 JVM의 런타임에 발생하므로 Runtime Exception이라고 한다.
실제로 자바의 예외들은 모두 Exception 클래스를 상속받는데, 그 중에서도 RuntimeException 클래스를 상속받은 예외들을 Unchecked Exception이라고 부른다.

정말 checked exception은 먼저 체크되어야 할까? checked exception 상황을 만들어서 확인해보자. 존재하지 않는 파일을 읽는 fileReader 객체를 만들어봤다.
public class CheckedException {
public static void main(String[] args) {
FileReader fileReader = new FileReader(new File("존재하지않는파일.txt"));
}
}
위 코드를 javac로 컴파일 하려고 하면, 컴파일 에러가 난다.
error: unreported exception FileNotFoundException; must be caught or declared to be thrown
FileReader fileReader = new FileReader(new File("buchu.txt"));
"FileNotFoundException"이 처리되지 않았다는 뜻이다. FileNotFoundException은 IOException을 상속받은 클래스이고, IOException은 대표적인 checked exception이다.

이렇듯 checked exception은 컴파일 전에 예외 처리를 해줘야 한다.
그럼 대표적인 unchecked exception인 NPE를 발생시켜보자.
public class UncheckedException {
public static void main(String[] args) {
String myString = null;
System.out.println(myString.length());
}
}
이렇게 뻔하게 예외가 발생할게 보이는 코드인데도 컴파일이 잘 된다! 그리고 run하면?
java.lang.NullPointerException: Cannot invoke "String.length()" because "myString" is null
역시 NullPointerException이 발생했다.
그렇다면 왜? Checked Exception과 Unchecked Exception을 굳이 이런 식으로 나눠놓은 것일까? 그것은 효율성을 위해서이다.
말했듯, 예상 가능한 예외는 처리할 수 있을만큼 사전에 처리하는 것이 베스트이다. 그러나 참조 타입의 포인터, 배열의 인덱스, 숫자간 계산 등 프로그램에서 빈번히 일어나는 작업 모두에 예외 처리를 강제한다면 프로그램은 프로그램 자체 코드보다 예외 처리 코드가 훨씬 길어질 것이다. 때문에 예외 처리가 critical한 상황이 아니라면 대부분의 예외들을 RuntimeException으로 빼서 개발자가 편한 방식대로 예외를 처리할 수 있도록 한 것이다.
3. try-catch로 예외 핸들링
예외를 핸들링하는 것은 기본적으로 try-catch 블록으로 할 수 있다. try 안에 예외가 발생할 가능성이 있는 로직의 코드를 넣어두고, catch 조건문 안에 처리할 예외 클래스를 집어넣어 해당 예외 클래스가 발생했을 때 실행할 코드를 작성하는 방식이다.
앞선 CheckedException의 코드를 다음과 같이 try-catch 블락으로 묶을 수 있다.
public class CheckedException {
public static void main(String[] args) {
try {
FileReader fileReader = new FileReader(new File("존재하지않는파일.txt"));
} catch (IOException e) {
// IOException 발생됐을 때 실행될 블록
e.printStackTrace();
}
}
}
printStackTrace() 메소드를 호출해 발생한 예외와 관련 정보들을 콘솔창에 쫙 출력하도록 했다. 실행해보면 아까완 다르게 컴파일이 잘 된다! 그리고 결과는.
java.io.FileNotFoundException: 존재하지않는파일.txt (No such file or directory)
at java.base/java.io.FileInputStream.open0(Native Method)
at java.base/java.io.FileInputStream.open(FileInputStream.java:216)
at java.base/java.io.FileInputStream.<init>(FileInputStream.java:157)
at java.base/java.io.FileReader.<init>(FileReader.java:75)
at org.example.exception.basic.CheckedException.main(CheckedException.java:11)
"존재하지않는파일.txt"는 존재하지 않는다고 잘 말해주고 있다!
if-else if처럼, catch 블록을 연달아서 쓸 수 있다. 위쪽의 catch엔 조금 더 특수한 예외로, 밑으로 갈수록 일반적인 예외로 구성한다. 그리고 가장 마지막에 finally 블록을 추가할 수 있는데, 이는 catch블록의 실행 여부와 상관없이 무조건 실행되는 코드가 작성되는 블락이다. try-with-resource가 없었을 시절 열린 파일이나 소켓 객체를 닫는 용도로 사용했다.
public class CheckedException {
public static void main(String[] args) {
try {
FileReader fileReader = new FileReader(new File("존재하지않는파일.txt"));
} catch (IOException e) {
// IOException 발생됐을 때 실행될 블록
e.printStackTrace();
} catch (Exception e) {
// IOException 외, 일반적인 예외가 발생했을 때 실행될 블록
e.printStackTrace();
} finally {
// 무조건 실행될 블록
System.out.println("finally executed");
}
}
}
이제 조금 재밌는 실험을 해보자. Checked 클래스와 Unchecked 클래스를 만들건데, Checked 클래스는 Exception 클래스를 상속받고 Unchecked 클래스는 RuntimeException 클래스를 상속받도록 했다.
public class Checked extends Exception {
public Checked(String message) {
super(message);
}
}
public class Unchecked extends RuntimeException {
public Unchecked(String message) {
super(message);
}
}
상속 구조는 아래 그림과 같다.

글의 초반에 나왔던 상속 지도가 맞다면 클래스 이름대로 Checked는 checked excepton, unchecked는 unchecked exception이 될 것이다. 정말 각각의 예외를 throw 했을 때 컴파일 성공의 차이가 있을까?
자바에서 예외를 던지기 위해선 throw 키워드 뒤에 예외 객체를 넘기면 된다.
public class ThrowingExceptions {
public static void main(String[] args) {
// 됨
throw new Unchecked("unchecked exception");
// 안됨
throw new Checked("checked exception");
}
}
Unchecked만이 던져질 땐 컴파일이 되었지만, Checked가 추가된 순간 컴파일 에러를 뱉는다. 이제 각 예외를 try-catch 블락으로 묶어 처리해보겠다.
public class ThrowingExceptions {
public static void main(String[] args) {
try {
throw new Unchecked("unchecked exception");
} catch (Unchecked unchecked) {
unchecked.printStackTrace();
}
try {
throw new Checked("checked exception");
} catch (Checked checked) {
checked.printStackTrace();
}
}
}
이제 컴파일도 되고 실행도 된다.
org.example.exception.throwing.Unchecked: unchecked exception
at org.example.exception.throwing.ThrowingExceptions.main(ThrowingExceptions.java:7)
org.example.exception.throwing.Checked: checked exception
at org.example.exception.throwing.ThrowingExceptions.main(ThrowingExceptions.java:14)
실제로 RuntimeException을 상속받았는지, Exception을 상속받았는지에 따라 컴파일이 되는지 여부까지 살펴봤다!