Java/예외처리(Exception Handling)

[Java] 예외처리(exception handling) - 코딩밥상

코딩밥상 2023. 2. 11. 22:29

프로그램 오류

 자바에서는 실행 시(runtime) 발생할 수 있는 프로그램 오류를 '에러(error)'와 '예외(exception)', 두 가지로 구분하였다.

에러는 메모리 부족이나 스택오버플로우와 같이 발생하면 복구할 수 없는 심각한 오류고, 예외는 발생하더라도 수습될 수 있는 비교적 덜 심각한 것이다.

즉 에러가 발생하면, 프로그램의 비정상적인 종료를 막을 길이 없지만, 예외는 발생하더라도 프로그래머가 이에 대한 적절한 코드를 미리 작성해 놓음으로써 프로그램의 비정상적인 종료를 막을 수 있다.

에러(error)         - 프로그램 코드에 의해서 수습될 수 없는 심각한 오류
예외(exception) - 프로그램 코드에 의해서 수습될 수 있는 다소 미약한 오류

 

예외 클래스의 계층구조

자바에서는 실행 시 발생할 수 있는 오류(에러와 예외)를 클래스로 정의하였다.

모든 클래스의 조상은 Object클래스이므로 Exception과 Error클래스 역시 Object클래스의 자손들이다.

그 중 모든 예외의 최고 조상은 Exception클래스이며 두 그룹으로 나눠질 수 있다.

 

-Exception클래스와 그 자손들(Exception클래스들) - 사용자의 실수와 같은 외적인 요인에 의해 발생하는 예외 

=> 컴파일러가 예외처리를 확인함(checked 예외)                         

 

-RuntimeException클래스와 그 자손들(RuntimeException클래스들) - 프로그래머의 실수로 발생하는 예외

=> 컴파일러가 예외처리를 확인하지 않음(unchecked 예외)


예외처리하기 (try-catch문)

예외처리
정의 - 프로그램 실행 시 발생할 수 있는 예외에 대비한 코드를 작성하는 것
목적 - 프로그램의 비정상 종료를 막고, 정상적인 실행상태를 유지하는 것

발생한 예외를 처리하지 못하면, 프로그램은 비정상 종료하며 처리되지 못한 예의는 JVM의 '예외처리기(UncaughtExceptionHandler)'가 받아서 예외의 원인을 화면에 출력한다.

 

예외처리를 위해서 try-catch문을 사용하며, 그 구조는 다음과 같다.

try{
	//예외 상생 가능성 문장
}
catch(Exception1 e1){
	//Exception1이 발생했을 경우, 처리문
}
catch(Exception2 e2){
	//Exception2이 발생했을 경우, 처리문
}
catch(Exception3 e3){
	//Exception3이 발생했을 경우, 처리문
}

발생한 예외의 종류와 일치하는 단 한개의 catch블럭만 수행된다. 일치하는 catch블럭이 없으면 예외는 처리되지 않는다.

if문과 달리, try블럭이나 catch블럭 내에 포함된 문장이 하나뿐이어도 괄호{ }를 생략할 수 없다.

 

try-catch문에서의 흐름

try-catch문에서는 예외의 발생 여부에 따라 흐름이 달라진다.

try블럭 내에서 예외가 발생한 경우
1. 발생한 예외와 일치하는 catch블럭이 있는지 확인
2. 일치하는 catch블럭을 찾으면 블럭 내의 문장을 수행하고 try-catch문을 빠져나감(예외가 발생한 위치 이후에 있는 try블럭의 문장들은 수행되지 않는다.)

try블럭 내에서 예외가 발생하지 않은 경우
1. catch블럭을 거치지 않고 전체 try-catch문을 빠져나가서 수행을 계속함
  • 예외가 발생하면, 발생한 예외에 해당하는 클래스의 인스턴스가 만들어 진다.
  • 맨 위의 catch블럭부터 차례대로 내려가면서 catch블럭의 괄호() 내에 선언된 참조변수의 종류와 생성된 예외클래스의 인스턴스에 instanceof연산자를 이용해서 검사한다.
  • 검사 결과가 true인 catch블럭을 찾게 되면 블럭에 있는 문장들을 모두 수행한 후에 try-catch문을 빠져나가고 예외는 처리된다.

이 때 모든 예외 클래스는 Exception클래스의 자손이므로, catch블럭의 괄호()에 Exception클래스 타입의 참조변수를 선언해 놓으면 어떤 종류의 예외가 발생하더라도 이 catch블럭에 의해서 처리된다.

 

finally블럭

finally블럭은 예외의 발생여부에 상관없이 실행되어야할 코드를 포함시킬 목적으로 사용된다. try-catch-finally의 순서로 구성된다.

try{
	//예외가 발생할 가능성이 있는 문장
}
catch(Exception e1){
	//예외처리를 위한 문장
}
finally{
	//예외의 발생여부에 관계없이 항상 수행되어야하는 문장
}

예외가 발생한 경우 : try->catch->finally

예외가 발생하지 않은 경우 : try->finally

(catch블럭의 문장 수행 중 return을 만나도 finally블럭의 문장은 수행된다.)

 

멀티 catch블럭

여러 catch블럭을 ' | '기호를 이용해서, 하나의 catch블럭으로 합칠 수 있다. 이를 '멀티 catch블럭'이라 한다. 이를 이용하면 중복된 코드를 줄일 수 있다. 그리고 연결할 수 있는 예외 클래스의 개수에는 제한이 없다.

단, 연결된 예외클래스가 조상과 자손 관계에 있다면 컴파일 에러가 발생한다. 조상 클래스만 써주는 것과 똑같기 때문이다.

 

멀티 블럭은 하나의 블럭으로 여러 예외를 처리하는 것이기 때문에 실제로 어떤 예외가 발생한 것인지 알 수 없다. 그래서 참조변수로 예외클래스들의 공통 분모인 조상 예외 클래스에 선언된 멤버만 사용할 수 있다.


예외 발생시키기(throw)

키워드 throw를 사용해서 프로그래머가 고의로 예외를 발생시킬 수 있으며, 방법은 아래의 순서를 따른다.

1. new 연산자를 이용해서 발생시키려는 예외 클래스의 객체를 만듦
Exception e = new Exception();

2. 키워드 throw를 이용해서 예외를 발생시킴
throw e;

 => throw new Exception();

 

메서드에 예외 선언하기(throws)

 메서드에 예외를 선언하려면, 메서드의 선언부에 키워드 throws를 사용해서 메서드 내에서 발생할 수 있는 예외를 적어주기만 하면 된다. 예외가 여러 개일 경우에는 쉼표(,)로 구분한다.

void method() throws Exception1, Exception2, ... ExceptionN{
	//메서드 내용
}

 메서드의 선언부에 예외를 선언함으로써 이 메서드를 사용하는 쪽에서는 이에 대한 처리를 하도록 강요하기 때문에, 프로그래머들의 짐을 덜어 주는 것은 물론이고 보다 견고한 프로그램 코드를 작성할 수 있도록 도와준다.

 

 예외를 메서드의 throws에 명시하는 것은 예외를 처리하는 것이 아니라, 자신(예외가 발생할 가능성이 있는 메서드)을 호출한 메서드에게 예외를 전달하여 예외를 떠맡기는 것이다.

 예외를 전달받은 메서드가 또다시 자신을 호출한 메서드에게 전달할 수 있으며, 이런 식으로 계속 호출스택에 있는 메서드들을 따라 전달되다가 제일 마지막에 있는 main메서드에서도 예외가 처리되지 않으면, main메서드마저 종료되어 프로그램이 전체가 종료된다.

 

즉 예외가 발생한 메서드에서 예외처리를 하지 않고 자신을 호출한 메서드에게 예외를 넘겨줄 수는 있지만, 이것으로 예외가 처리된 것은 아니고 예외를 단순히 전달만 하는 것이다.

결국 어느 한 곳에서는 만드시 try-catch문으로 예외처리를 해주어야 한다.

 

예외 되던지기(exception re-throwing)

 한 메서드에서 발생할 수 있는 예외가 여럿인 경우, 몇 개는 try-catch문을 이용해 메서드 내에서 자체적으로 처리하고, 그 나머지는 선언부에 지정하여 호출한 메서드에서 처리하도록 함으로써, 양쪽에서 나눠서 처리되도록 할 수 있다. 심지어 단 하나의 예외에 대해서도 예외가 발생한 메서드와 호출한 메서드 양쪽에서 처리하도록 할 수 있다.

이것은 예외를 처리한 후에 인위적으로 다시 발생시키는 방법을 통해서 가능한데, 이를 '예외 되던지기'라고 한다.

 

 예외가 발생할 가능성이 있는 메서드에서 try-catch문을 이용해서 예외를 처리해주고 catch문에서 필요한 작업을 수행한 후에 throw문을 사용해서 예외를 다시 발생시킨다. 다시 발생한 예외는 이 메서드를 호출한 메서드에게 전달되고 호출한 메서드의 try-catch문에서 예외를 또다시 처리한다.

이 때 주의할 점은 예외가 발생할 메서드에서는 try-catch문을 사용해서 예외처리를 해줌과 동시에 메서드의 선언부에 발생할 예외를 throws에 지정해줘야 한다는 것이다.

static void method1() throws Exception{	//예시
	try{
    	throw new Exception();
    }
    catch(Exception e){
    	//작업 수행
        throw e;
    }
}

 

사용자 정의 예외 만들기

 기존의 정의된 예외 클래스 외에 필요에 따라 프로그래머가 새로운 예외 클래스를 정의하여 사용할 수 있다. 보통 Exception클래스 또는 RuntimeException클래스로부터 상속받아 클래스를 만들지만, 필요에 따라서 알맞은 예외 클래스를 선택할 수 있다.

class MyException extends Exception{	//예시
	private final int ERR_CODE;
    
    MyException(String msg){
    	super(msg);
    }
    
    MyException(String msg){
    	this(msg,100);
    }
    
    public int getErrCode(){
    	return ERR_CODE;
    }
}

 

연결된 예외(chained exception)

 한 예외가 다른 예외를 발생시킬 수도 있다. 예를 들어 예외 A가 예외 B를 발생시켰다면, A를 B의 '원인 예외(cause exception)'라고 한다.

그 방법으로는 initCause()로 원인 예외를 등록하는 방법이 있다. 'initCause()'는 Exception클래스의 조상인 Throwable클래스에 정의되어 있기 때문에 모든 예외에서 사용가능하다.

Throwable initCause(Throwable cause) - 지정한 예외를 원인 예외로 등록
Throwable getCause( ) - 원인 예외를 반환 

발생한 예외를 그냥 처리하면 될 텐데, 원인 예외로 등록해서 다시 예외를 발생시키는 이유는

여러가지 예외를 하나의 큰 분류의 예외로 묶어서 다루기 위해서이다.

 

 조상 클래스로 catch블럭을 작성하면, 실제로 발생한 예외가 어떤 것인지 알 수 없다는 문제가 생긴다. 그렇다고 상속 관계를 변경하는 것도 부담이다.

때문에 생각한 것이 예외가 원인 예외를 포함할 수 있게 한 것이다. 이렇게 하면, 두 예외는 상속관계가 아니어도 상관없다.

 

또 다른 이유는 checked예외를 unchecked예외로 바꿀 수 있도록 하기 위해서이다.

checked예외로 예외처리를 강제한 이유는 견고한 프로그램을 작성할 수 있도록 유도하기 위한 것이었는데, 환경이 바뀌며 checked예외가 발생해도 예외를 처리할 수 없는 상황이 하나 둘 발생하기 시작했다. 따라서 checked예외를 unchecked예외로 바꾸면 예외처리가 선택적이 되므로 억지로 예외처리를 하지 않아도 된다.