프로그램이 실행중 어떤 원인에 의해서 오작동을 하거나 비정상적으로 종료되는 경우가 있다.
이러한 결과를 초래하는 원인을 프로그램 에러 또는 오류라고 한다.
에러는 종류에 따라 다음과 같이 구분할 수 있다.
- 컴파일 에러 : 컴파일 시에 발생하는 에러
- 런타임 에러 : 실행 시에 발생하는 에러
- 논리적 에러 : 실행은 되지만, 의도와 다르게 동작하는 것
소스 코드를 컴파일하면 컴파일러가 소스코드를 검사하여 오류가 있는지를 알려준다. (컴파일 에러)
컴파일을 에러 없이 성공적으로 마쳤다고 해도 실행 도중에 발생할 수 있는 잠재적인 오류까지 검사할 수 없기 때문에
실행 중에 에러에 의해서 잘못된 결과를 얻거나 프로그램이 비정상적으로 종료될 수 있다. (런타임 에러)
런타임 에러에 의해 프로그램이 비정상적으로 종료되는 것을 방지하기 위해서는
프로그램의 실행도중 발생할 수 있는 모든 경우의 수를 고려하여 이에 대비하는 것이 필요하다.
자바에서는 실행 시 발생할 수 있는 프로그램 오류를 에러(error)와 예외(exception)으로 구분했다.
- 에러 : 프로그램 코드에 의해 수습될 수 없는 심각한 오류 ex) 메모리부족(OutOfMemoryError), 스택오버플로우(StackOverFlowError)
- 예외 : 프로그램 코드에 의해 수습될 수 있는 다소 미약한 오류 (NullPointerException 등)
에러가 발생하면 프로그램의 비정상적 종료를 막을 길이 없지만,
예외는 발생하더라도 프로그래머가 이에 대한 적절한 코드를 미리 작성함으로서 프로그램의 비정상적 종료를 막을 수 있다.
자바에서는 실행 시 발생할 수 있는 오류를 다음과 같이 클래스로 정의하였다.

Throwable 클래스?
Throwable (Java Platform SE 8 )
이름만 보면 인터페이스 같지만 클래스이며 모든 에러의 조상 클래스이다.
Error과 Exception 두가지의 서브 클래스를 가지고 있다.
공식문서에서는 다음과 같이 설명한다.
- Throwable은 발생했을때의 실행 스택의 스냅샷을 포함한다
(JVM은 Throwable 생성 시점에 자동으로 스택 프레임을 StackTraceElement 클래스 배열로 생성한다.) - 또한 에러에 대한 더 자세한 정보를 가지는 메세지와 다른 Throwable을 가질 수 있다.
- 마지막으로 예외 원인을 가지는데 다른 Throwable이 원인의 항목이 될 수 있다.
이런 구조를 이용해 예외 체인을 만들 수 있다.
Throwable 클래스는 다음과 같은 3가지의 public 생성자를 가진다.
- Throwable()
- Throwable(String message)
- Throwable(String message, Throwable cause)
설명에서 이야기한 것처럼 Throwable 클래스는 생성자를 통해 메세지와 다른 Throwable 객체를 cause로 받을 수 있는데,
이는 에러의 정보와 에러를 유발시키는 다른 원인 에러를 담도록 한다.
또한 에러가 발생했을 때의 스택 정보는 생성자에 넣을 필요 없이 StackTraceElement 객체 배열로 자동으로 담아진다.
또한 다음과 같은 메소드들을 가진다.
- StackTraceElement[] getStackTrace()
- Throwable getCause()
- void printStackTrace()
- String getMessage()
- toString()
- ...
public class Main
{
public static void main(String[] args) {
TestClass tc = new TestClass();
System.out.println(tc.toString()); //1
Throwable t = new Throwable("hi");
System.out.println(t.toString()); //2
Throwable tt = new Throwable("hello", t);
System.out.println(tt.toString()); //3
tt.printStackTrace(); //4
}
}
class TestClass{
}
실행 결과

메서드 이름만 들어도 어떤 역할을 하는지 추측할 수 있을 것 같다.
그런데 Throwable의 toString() 메서드는 Object 클래스의 toString()을 오버라이딩한 메소드이다.
기존 Object 클래스의 toString() 메서드는 클래스의 이름과 해시 값을 문자열로 반환하지만,
Throwable의 toString()은 클래스 이름과 저장한 메시지를 문자열로 반환한다.
Exception 클래스
모든 예외의 조상은 Exception 클래스이며 RuntimeException 클래스와 아닌 클래스로 다시 나뉜다.
Exception 클래스 역시 Throwable과 같은 3가지의 퍼블릭 생성자를 가진다.
- Exception()
- Exception(String message)
- Exception(String message, Throwable cause)
또한 위에 언급한 메소드를 포함한 Throwable의 모든 메소드를 상속받는다.
RuntimeException 클래스들은 주로 프로그래머의 실수에 의해 발생될 수 있는 예외들로 구성되어 있으며
ex) NullPointerException, IndexOutOfBoundsException 등등..
그냥 Exception 클래스들은 외부의 영향으로 발생할 수 있는 것들,
사용자의 실수와 같은 외적인 요인에 의해 발생하는 경우가 많다.
ex) ClassNotFoundException, DataFormatException 등등
두 예외의 차이점은 RumtimeException이 아닌 모든 예외는 컴파일 과정에서 예외처리를 강제한다.
(예외 처리문이 존재하지 않을 경우 컴파일 에러를 띄운다.)
이에 따라 두 예외를 다음과 같이 분류하기도 한다.
- RuntimeException이 아닌 Exception : checked Exception
- RuntimeException: unchecked Exception
책에서는
"RuntimeException은 개발자의 부주의로 자주 발생하는 예외들이다. (NullPointer, Index에러 등등)
이러한 에러는 개발자가 조금만 신중해도 예방하기 쉬우며,
참조변수와 배열을 사용하는 대부분의 메소드에서 발생할 가능성을 가지고 있다.
그런데 이러한 예외 모두에 예외처리를 강요하면
참조변수와 배열이 사용되는 모든 곳에 예외처리를 해줘야 해 코드가 지저분해진다." 라고 이야기한다.
그렇다면 예외처리는 어떻게 진행하길래 코드가 지저분해진다고 이야기할까?
예외처리하기 - try, catch 문
예외처리란 프로그램 실행 시 발생할 수 있는 예외에 대비한 코드를 작성하는 것이며
예외의 발생으로 인한 프로그램의 비정상 종료를 막고, 정상적 실행 상태를 유지하기 위해 사용한다.
RuntimeException이 아닌 모든 예외들은 try-catch문을 이용해 예외처리할 것이 강제되어 있다.
class Main {
public static void main(String[] args) {
try{
int a = 1 / 0;
} catch (ArithmeticException e){
System.out.println("0으로 나눌 수 없습니다");
}
}
}
try 블럭은 예외가 발생할 가능성이 있는 문장들을 넣는다.
catch 블럭에서 어떤 종류의 예외를 처리할 것인지 예외의 종류를 넣으면
해당 예외가 발생했을 시 해당 catch 블럭을 실행시킨다.
예외의 종류와 일치하는 catch 블럭이 존재하지 않을 시 예외는 처리되지 않는다.
class Main {
public static void main(String[] args) {
int a = 1 / 0;
}
}

발생한 예외를 처리하지 못하면 프로그램은 비정상적 종료되며
처리하지 못한 예외는 JVM의 예외처리기가 받아서 원인을 화면에 출력한다.
- 예외가 발생했을 경우 JVM은 Exception 클래스를 생성한다. (스택 정보, 메세지 등을 포함)
- 발생한 Exception 클래스를 처리할 수 있는 try-catch문을 탐색하고 없을 시 해당 메소드를 호출한 이전 메소드로 넘겨진다.
- 계속 반복하다가 main 메소드에서도 처리할 수 없다면 default exception handler가 실행된다
- 해당 exception 정보를 노출시키며 해당 쓰레드를 중지시킨다.
catch 블럭은 해당 예외 뿐만 아니라 해당 예외를 상속하는 모든 예외를 검사해낸다.
따라서 catch 블럭에 (Exception e) 를 넣을 시 모든 종류의 예외를 처리할 수 있다.
그러나 위에서부터 순서대로 예외를 검사하므로 Exception 클래스를 넣어 보통 맨 마지막 catch 블럭에서 모든 예외를 검사하고도 거르지 못한 예외를 처리하는데 사용한다.
멀티 캐치블럭
JDK 1.7 부터 여러 catch 블럭을 | 기호를 사용해 하나의 catch 블럭으로 합칠 수 있게 되었다.
연결할 수 있는 예외 클래스의 개수에는 제한이 없으나
- ( 부모 클래스 | 자식 클래스 ) 시에는 컴파일 에러가 발생한다. (부모 클래스만 사용해도 검출이 가능하다)
- 발생한 예외를 하나의 문장으로 처리해야 하므로 두 예외의 공통 조상 클래스에서 선언한 멤버만 사용할 수 있다.
예외 발생시키기
throw 메소드를 이용해 고의로 예외를 발생시킬 수 있다.
Exception e = new Exception("고의 발생 예외");
throw e;
throw new Exception("고의 발생 예외");
메서드에 예외 선언하기
예외를 처리하는 방법에는 지금까지 배운 try-catch 문을 사용하는것 이외에, 예외를 메서드에 선언하는 방법이 있다.
void method() throws Exception1, Exception2, ..... ExceptionN{
}
메소드 뒤에 throws를 사용해서 메서드 내에서 발생할 수 있는 예외를 쉼표로 구분해 적어주면 된다.
이렇게 예외를 선언하면, catch 블록과 마찬가지로 예외뿐 아니라 자손타입의 예외도 발생할 수 있다는 점에
주의하자.
class Main {
public static void main(String[] args) {
//예외처리 구현 없이 호출 시 컴파일 에러 발생!!
//throwException();
try{
throwException();
} catch (Exception e){
System.out.println("hi");
}
}
public static void throwException() throws Exception{
throw new Exception("exception");
}
}
이렇게 예외를 선언하면 사실 예외를 처리하는 것이 아닌 해당 메서드에서 발생한 예외를 호출한 메서드에게 넘기겠다
(throws 던지겠다) 라고 이야기하는 것이다.
예외 처리를 호출한 메서드에게 넘기는 것인데,
이렇게 예외를 선언하면 해당 메소드를 사용하는 메소드에서 예외를 처리하는 부분을 구현한다.
또 이렇게 예외를 선언할때는 일반적으로 RuntimeException 클래스들은 적지 않는다.
보통 반드시 처리해줘야 하는 예외들만을 선언한다.
메서드의 선언부에 RuntimeException이 아닌 예외를 선언하면
메서드를 사용하려는 사람이 메서드의 선언부를 봤을때 어떤 예외들이 처리되어야 하는지 쉽게 알 수 있다.
또한 해당 예외를 처리하는 구문이 구현되었는지 컴파일러가 확인해주기도 한다.
예외를 전달받은 메서드가 또 다시 자신을 호출한 메서드에게 계속해서 전달할 수 있으며,
제일 마지막에 있는 main 메서드에서도 예외가 처리되지 않으면 main 메서드마저 종료되며 프로그램 전체가 종료된다.
이처럼 throw를 통해 예외가 발생한 메서드에서 예외처리를 하지 않고 자신을 호출한 메서드에게 예외를 넘겨줄 수는 있지만
이것으로 예외를 처리한 것은 아니고, 단순히 전달한 것이다.
결국 어느 한 곳에서는 반드시 예외처리를 해주어야한다.
finally 블럭
예외의 발생여부에 관계없이 반드시 실행되는 문장을 넣을 목적으로 사용한다.
try-cath문의 끝에 선택적으로 덧붙일 수 있다.
try 문에서 return을 만나 메소드를 종료하는 경우에도 finally 블럭이 먼저 실행된 후 메서드를 종료한다.
try-with-resources 문
JDK1.7부터 추가되었는데, 주로 입출력과 관련된 클래스를 사용할때 사용한다.
입출력에 사용되는 클래스는 사용 후 close() 메소드를 통해 꼭 닫아줘야 하는 것들이 있다.
이 경우 finally 블록에 close() 메소드를 넣어 처리하는데, close() 역시 예외를 발생시킬수 있다는 점이 문제가 된다.
그러면 finally 블록 안에 try-catch 문을 한 번 더 넣어줘야 하는데 코드가 복잡해지며,
finally문에서 예외가 발생할 경우 기존 try 블럭의 예외가 무시된다!
try-with-resources 문은 try () 괄호 안에서 객체를 생성하는 문장을 넣으면
따로 close()를 호출하지 않아도 try 블럭을 벗어나는 순간 자동으로 close()를 호출해준다.
만약 try 블록과 close() 모두 예외가 발생할 경우 먼저 발생한 예외에 대한 내용을 출력하고,
close에서 발생한 예외는 suppressed(억제된) 이라는 의미의 머리말과 함께 출력한다.
두 예외가 동시에 발생할 수 없기 때문에 실제 발생한 예외안에 억제된 예외를 넣어 저장한다.
Throwable 안에는
- addSuppressed(Throwable exception)
- Throwagble[] getSuppressed()
메서드가 정의되어 있다.
사용자 정의 예외 만들기
기존의 정의된 예외 클래스 외에 필요에 따라 프로그래머는 새로운 예외 클래스를 정의해서 사용할 수 있다.
보통은 RuntimeException 클래스를 상속받는 예외를 만들지만 필요에 따라 예외 클래스를 선택할 수 있다.
class MyException extends RuntimeException{
public MyException(String msg){
super(msg);
}
}
예외 안에 메세지를 넣고 싶다면 다음과 같이 RuntimeException의 생성자를 사용하면 된다.
class MyException extends RuntimeException{
private int code;
public MyException(String msg){
super(msg);
}
public MyException(String msg, int code){
super(msg);
this.code = code;
}
public String toString(){
return this.getMessage() + code;
}
}
또한 예외 안에 숫자로 된 예외 코드를 넣고 싶다면 다음과 같이 멤버 변수를 추가하면 된다.
또한 ExceptionClass의 toString() 메소드는 예외가 처리되지 못해 로그를 띄울때 사용되는데
이 함수를 오버라이딩 하면 예외 로그를 변경할 수 있다.
기존의 예외 클래스는 주로 Exception을 상속받는 checked 예외로 작성하는 경우가 많았지만,
요즘은 예외처리를 선택적으로 할 수 있는 RuntimeException을 상속받아 작성하는 쪽으로 바뀌고 있다고 책에선 이야기한다.
checked 예외는 예외처리를 강제하기 때문에 불필요한 try-catch 문을 넣어 코드를 복잡하게 만들기 때문이다.
때문에 예외처리의 여부를 선택할 수 있는 unchecked 예외가 더 환영받고 있다.
예외 되던지기
한 메서드에서 발생할 수 있는 예외가 여럿일 경우,
몇개는 try-catch 문을 통해서 메서드에서 처리하고, 나머지는 선언부에 지정하여 호출한 메서드에게 넘기도록 처리할 수 있다.
심지어 하나의 예외를 발생한 메서드, 호출한 메서드 양쪽에서 처리하도록 할 수도 있다.
class Main {
public static void main(String[] args) {
try{
throwException();
} catch(RuntimeException e){
System.out.println("메인 메소드 예외 처리");
}
}
public static void throwException() throws RuntimeException{
try{
throw new RuntimeException("고의 발생 예외");
} catch(RuntimeException e){
System.out.println("메소드 예외 처리");
throw e; //예외 되던지기
}
}
}

하나의 예외에 대해 예외가 발생한 메서드, 호출한 메서드 양쪽 모두에서 처리해야할 작없이 있을 때 사용된다.
연결된 예외(chained - exception)
한 예외가 다른 예외를 발생시킬 수도 있다.
이 경우 throwable 클래스에서 봤듯 생성자에 cause 매개변수가 있는데
이렇게 생성할 경우 예외 안에 그 예외를 발생시킨 원인 예외를 집어넣을 수 있다.
이렇게 예외를 연결하면 다음과 같은 장점을 가지며 예외를 관리하기 쉽다.
- 디버깅시 예외 발생의 흐름을 파악하기 쉽다.
- 여러 예외를 하나의 큰 분류의 예외로 다룰 수 있다.
- checked 예외를 unchecked 예외로 바꿀 수 있다.
잘 작성된 예외와 처리문은 프로그램의 안정성을 높이는 좋은 방법이지만,
막무가내로 try-catch 문을 사용해버린다면 가독성과 재활용성을 낮추는 원인이 되기도 한다.
https://hbase.tistory.com/157
[Java] 예외처리 코드 잘 작성하는 9가지 방법
자바 프로그래밍에서 '예외처리(Exception Handling)'는 다소 까다로운 주제다. 개발 조직들은 자신들만의 예외처리 규칙을 만들고 사용하는 경우가 많다. 일반적으로 자바 프로젝트에서 따르면 좋은
hbase.tistory.com
https://gksdudrb922.tistory.com/183
[Clean Code] 7. 오류 처리
오류 코드보다 예외를 사용하라 예외를 사용하지 않고 오류 플래그를 설정하는 등의 방식은 코드를 복잡하게 만든다. 오류가 발생하면 예외를 던지는 편이 낫다. 그래야 코드가 깔끔해진다. 논
gksdudrb922.tistory.com
예외 처리문을 잘 사용하는 방법에 대해서도 공부를 더 해보고 정리해봐야겠다.
'JAVA' 카테고리의 다른 글
JAVA의 정석 정독하기 #10 - 날짜와 시간 (0) | 2023.11.15 |
---|---|
JAVA의 정석 정독하기 #9 - java.lang 패키지와 유용한 클래스 (1) | 2023.11.14 |
JAVA의 정석 정독하기 #7 - 상속과 다형성 (1) | 2023.11.12 |
JAVA의 정석 정독하기 #6 - Class (1) | 2023.11.11 |
JAVA의 정석 정독하기 #5 - 배열(Array) (0) | 2023.10.29 |