본문 바로가기
JAVA

[JAVA] 예외 처리 (Exception Handling)

by Amy IT 2022. 5. 31.

▶ 예외 처리(Exception Handling)란?

 

일반적으로 에러(error)라고 일컫는 예외(Exception)는 프로그램 실행 중 발생되는 의도치 않은 문제를 의미합니다. 예외가 발생되면 프로그램이 비정상 종료되는데, 예외가 발생되었을 때 프로그램이 비정상 종료되지 않고 정상 종료되도록 처리하는 것을 예외 처리(Exception Handling)라고 합니다. 예외 처리의 목적은 이미 발생된 예외를 수정하는 것이 아닌, 예외가 발생된 이유를 메세지를 통해 사용자에게 알려주고, 프로그램이 끝까지 수행되어 정상 종료되도록 하는 것입니다. 

 

다음은 자바에서 제공하는 예외처리 클래스들간의 상속관계를 계층구조로 표현한 것입니다.

 

 

예외 클래스의 최상위 클래스는 Throwable 클래스이고, 하위로 Error 클래스와 Exception 클래스로 나뉩니다. Error 클래스는 일반 어플리케이션에서 처리할 수 없는 심각한 에러와 관련된 부분이고, Exception 클래스는 예외 처리가 가능한 부분입니다. Exception 클래스에서 RuntimeException 클래스 계열은 컴파일시 예외 처리 여부를 검사하지 않는 계열로서, 컴파일시 예외 처리를 하지 않아도 에러가 발생하지 않지만 실행 시점에 에러가 발생하게 됩니다. IOException과 SQLException 클래스 계열은 컴파일시 예외 처리 여부를 검사하는 계열로서, 예외 처리 코드를 구현하지 않으면 컴파일시 에러가 발생하므로 반드시 명시적으로 예외 처리를 해야 합니다. 

 

 

 

 

▶ 예외 처리를 하는 두 가지 방법

 

1. try~catch~finally 문을 이용한 직접 처리

 

(1) try~catch 문

예외가 발생하면 try~catch 블록을 이용해 직접 처리할 수 있습니다. 

try {
      //예외발생코드
} catch (예외클래스명 변수명) {
      //예외처리코드
}
public class ExceptionTest {
	public static void main(String[] args) {
		System.out.println("프로그램 시작");
		try {
			int num = 10;
			int result = num/0; //ArithmeticException
			System.out.println("연산된 값: "+result);
		} catch (ArithmeticException e) {
			System.out.println("예외 발생됨");
			e.printStackTrace();
			System.out.println(e.getMessage());
		}
		System.out.println("프로그램 종료");
	}
}

정수를 0으로 나누어 예외가 발생되었습니다. 예외 처리의 메커니즘은 다음과 같습니다.

 

  1. 프로그램이 실행되며 "프로그램 시작"이 출력됩니다.
  2. try 블록 내 int result = num/0; 코드에서 예외가 발생합니다.
  3. 예외가 발생하면 시스템에서 예외 처리 가능한 클래스(ex.ArithmeticException)를 검색하고 예외 클래스를 사용하기 위해 자동으로 객체 생성하여 참조값을 코드로 던집니다.
  4. 시스템이 던진 참조값을 catch 블록에서 잡아 참조변수에 저장하는데, 이때 클래스 타입이 일치하거나 다형성 적용이 가능한 타입의 catch 블록이 실행됩니다. 
  5. 참조변수 e에 저장된 예외 클래스를 이용하여 예외 정보를 출력할 수 있습니다. printStackTrace() 메소드는 예외가 발생되기까지의 모든 과정과 예외 코드 라인을 출력하고, getMessage() 메소드는 간략하게 예외 정보를 출력합니다.
  6. 발생된 예외를 처리했기 때문에 catch 블록 이후의 실행문이 수행되고 프로그램이 정상 종료됩니다. 

위 과정에 따라 발생된 예외를 처리합니다. 예외가 발생한 시점에서 일치하는 catch 블록을 수행하기 때문에, 예외가 발생한 이후의 코드("연산된 값" 출력)는 수행되지 않았습니다. 만일 try 블록 내에서 예외가 발생하지 않으면 catch 블록을 거치지 않고 전체 try~catch 블록을 빠져나가서 수행을 계속하게 됩니다. 

 

 

(2) 다중 catch 문

try 블록 내에서 발생되는 여러 예외를 처리하기 위해 다중 catch 문을 사용할 수 있습니다. 

public class ExceptionTest2 {
	public static void main(String[] args) {
		System.out.println("프로그램 시작");
		try {
			int num = 10;
			int result = num/10;
			System.out.println("연산된 값: "+result);
			String name = null;
			System.out.println(name.length()); //NullPointerException
			System.out.println("이름: "+name);
		} catch (ArithmeticException e) {
			System.out.println("예외처리 1번");
			System.out.println(e.getMessage());
		} catch (NullPointerException e) { //catch블록 수행 
			System.out.println("예외처리 2번");
			System.out.println(e.getMessage());
		} catch (Exception e) { 
			System.out.println("예외처리 3번");
			System.out.println(e.getMessage());
		}
		System.out.println("프로그램 종료");
	}
}

이번에는 값이 null인 참조변수로 메소드를 호출하여 예외가 발생되었습니다. 여러 catch 문 중 첫 번째 catch 문부터 실행되는데, 생성된 예외 클래스와 변수의 데이터 타입이 같거나 다형성 적용이 가능한 catch 블록이 수행됩니다. 여기서는 NullPointerException이 발생하였으므로 두 번째 catch 블록이 수행되어 "예외처리 2번"이 출력되었습니다. 예외가 처리되었으므로 try~catch 블록을 빠져나가 "프로그램 종료"를 출력하고 있습니다. 주의할 점은, 이렇게 다중 catch 문을 사용할 때는 상속 구조에서 하위인 예외 클래스부터 작성해야 한다는 것입니다. 상위 예외 클래스부터 작성하면 첫 번째 catch 블록에서 모든 예외가 처리되어 컴파일 에러가 발생합니다. 이렇게 다중으로 catch 블록을 사용하면 각각의 예외 상황에 대해 개별적으로 처리가 가능합니다.

 

이와 달리 다형성을 적용하여 Exception 클래스로 모든 예외를 받으면 하나의 catch 블록으로 모든 예외를 처리할 수도 있습니다. 

public class ExceptionTest3 {
	public static void main(String[] args) {
		try {
			int [] arr = {10,20};
			System.out.println(arr[3]); //ArrayIndexOutOfBoundsException
			int num = 10;
			int result = num/0; //ArithmeticException
			System.out.println(result);
			String name = null;
			System.out.println(name.length()); //NullPointerException
		} catch (Exception e) {
			System.out.println("모든 예외 처리");
			System.out.println(e.getMessage());
		}
		System.out.println("프로그램 종료");
	}
}

이번에는 arr 배열의 없는 방의 데이터를 가져오려고 하여 예외가 발생했습니다. catch 블록에서 Exception 타입으로 모든 예외를 받을 수 있기 때문에 예외가 처리되었습니다. 예외 발생 이후의 코드들은 수행되지 않았지만, 다른 예외가 발생되어도 하나의 catch 블록으로 모든 예외를 처리할 수 있게 됩니다. 

 

 

(3) 다중 try~catch 문

위의 예시에서 예외가 처음 발생되면 예외 발생 시점에 catch 블록이 수행되기 때문에, 예외가 발생한 이후의 코드들은 실행되지 않았습니다. 코드가 전부 실행되도록 하기 위해 try~catch문을 다음과 같이 각각 작성할 수 있습니다. 

public class ExceptionTest4 {
	public static void main(String[] args) {
		System.out.println("프로그램 시작");
		try {
			int num = 10;
			int result = num/0;
			System.out.println(result);
		} catch (ArithmeticException e) {
			System.out.println("예외처리 1번 "+e.getMessage());
		} //예외 처리 후 코드 실행 
		try {
			String name = null;
			System.out.println(name.length());
		} catch (NullPointerException e) {
			System.out.println("예외처리 2번 "+e.getMessage());
		}
		System.out.println("프로그램 종료");
	}
}

 

 

(4) finally 문

finally 블록은 예외 발생 여부와 상관없이 항상 실행됩니다. 예를 들어 파일이나 데이터베이스와 같은 외부 자원을 사용하면, 예외가 발생하든 안 하든 항상 사용했던 자원을 해제하는 작업이 필요합니다. 이러한 경우 finally 블록을 사용할 수 있습니다. 

public class ExceptionTest5 {
	public static void main(String[] args) {
		try {
			int num = 10;
			int result = num/1;
			System.out.println(result);
		} catch (Exception e) {
			System.out.println("예외처리 "+e.getMessage());
		} finally {
			System.out.println("finally 블록 실행됨");
		}
		System.out.println("프로그램 종료");
	}
}

프로그램이 정상적으로 수행되는 경우, catch 블록을 거치지 않고 finally 블록이 실행된 것을 확인할 수 있습니다. 예외가 발생하면 catch 문이 실행되고 예외 처리 후 finally 블록을 실행하게 됩니다. catch 블록 없이 try~finally 문만으로도 코드 작성이 가능한데, 이렇게 하면 예외가 발생되어도 finally 문장까지 수행한 후 예외가 발생하게 됩니다. 

 

 

 

 

2. throws 키워드를 이용한 예외 처리 위임 

 

예외 처리를 하는 두 번째 방법입니다. 메소드를 호출하고 호출된 메소드가 실행되면서 예외가 발생할 수 있습니다. 이때 throws 키워드를 이용하면 예외가 발생된 곳에서 예외를 직접 처리하지 않고 호출한 곳으로 예외 처리를 위임할 수 있습니다. 호출한 곳에서 try~catch 문으로 예외를 직접 처리하지 않고 계속하여 위임하면 최종적으로 main() 메소드까지 위임하게 되며, main() 메소드에서 try~catch 문으로 예외 처리를 할 수 있습니다. main() 메소드에서도 throws 하게 되면 프로그램이 비정상 종료됩니다. 

지정자 리턴타입 메소드명([파라미터]) throws 예외클래스, 예외클래스2 { }

 

main() 메소드에서 a() 메소드를 호출하고, a() 메소드에서 다시 b() 메소드를 호출하는데, b() 메소드에서 예외가 발생한 경우, 예외를 처리하는 몇 가지 경우에 대해 try~catch 문과 throws 키워드를 비교하며 알아보도록 하겠습니다. 실습을 위해 없는 클래스를 호출하는 상황을 가정해 보겠습니다. Class 클래스의 메소드인 forName() 메소드는 클래스의 이름을 매개변수로 받아서 해당 클래스를 반환해 줍니다. 일치하는 클래스가 없을 경우 ClassNotFoundException이 발생하는데, 이는 컴파일시 예외 처리 여부를 검사하는 계열로서 예외 처리를 어떻게 해야하는지 쉽게 확인할 수 있습니다.

 

 

(1) b() 에서 try~catch 문으로 직접 처리

public class ExceptionTest6 {
	public static void a(){
		b();
		System.out.println("a()함수 종료됨");
	}
	public static void b(){ 
		try {
			Class.forName("TestClass");
		} catch (ClassNotFoundException e) {
			e.printStackTrace();
		}
	}
	public static void main(String[] args) {
		System.out.println("main 프로그램 시작");
		a();
		System.out.println("main 프로그램 종료");
	}
}

프로그램이 실행되며 main 함수 내 첫 번째 문장이 수행된 후 a() 메소드를 호출합니다. a() 메소드 내에서 다시 b() 메소드를 호출하고, 호출된 b() 메소드에서 예외가 발생됩니다. 발생된 예외를 직접 처리하였으므로 b() 메소드 종료 후 다시 호출한 a() 메소드로 돌아가 나머지 코드를 수행하고, main 으로 돌아가 마지막 문장을 출력합니다. 

 

 

(2) b() 에서 throws로 위임 => a() 에서 try~catch 문으로 직접 처리

public class ExceptionTest6_2 {
	public static void a(){
		try {
			b();
		} catch (ClassNotFoundException e) {
			e.printStackTrace();
		}
		System.out.println("a()함수 종료됨");
	}
	public static void b() throws ClassNotFoundException{ 
		Class.forName("TestClass"); 
	}
	public static void main(String[] args) {
		System.out.println("main 프로그램 시작");
		a();
		System.out.println("main 프로그램 종료");
	}
}

이번에는 b() 메소드에서 발생한 예외를 직접 처리하지 않고 b() 메소드를 호출한 a() 메소드로 예외 처리를 위임하고 있습니다. a() 메소드 내 catch 블록에서 예외를 처리한 후 나머지 코드가 실행됩니다. 

 

 

(3) b() 에서 throws로 위임 => a() 에서 throws로 위임 => main() 에서 try~catch 문으로 직접 처리

public class ExceptionTest6_3 {
	public static void a() throws ClassNotFoundException{
		b();
		System.out.println("a()함수 종료됨");
	}
	public static void b() throws ClassNotFoundException{ 
		Class.forName("TestClass");
	}
	public static void main(String[] args) {
		System.out.println("main 프로그램 시작");
		try {
			a();
		} catch (ClassNotFoundException e) {
			e.printStackTrace();
		}
		System.out.println("main 프로그램 종료");
	}
}

이번에는 b() 메소드에서 발생한 예외를 b() 메소드를 호출한 a() 메소드로 위임하고, a() 메소드는 다시 a() 메소드를 호출한 main() 메소드로 위임하는 경우입니다. a() 메소드에서 예외를 처리하지 않고 넘기고 있으므로 예외 발생 코드 이후의 코드("a()함수 종료됨" 출력)는 수행되지 않았음을 확인할 수 있습니다. 

 

 

(4) b() 에서 throws로 위임 => a() 에서 throws로 위임 => main() 에서 throws로 위임

public class ExceptionTest6_4 {
	public static void a() throws ClassNotFoundException{
		b();
		System.out.println("a()함수 종료됨");
	}
	public static void b() throws ClassNotFoundException{ 
		Class.forName("TestClass"); 
	}
	public static void main(String[] args) throws ClassNotFoundException {
		System.out.println("main 프로그램 시작");
		a();
		System.out.println("main 프로그램 종료");
	}
}

main() 메소드에서도 try~catch 문으로 예외를 직접 처리하지 않고 다시 넘기게 되면 프로그램이 비정상 종료되어 "main 프로그램 종료"가 출력되지 않는 것을 확인할 수 있습니다. 

 

 

(5) 다중 throws 문

public class ExceptionTest7 {
	public static void a() throws ArithmeticException, NullPointerException{
		b();
	}
	public static void b() throws ArithmeticException, NullPointerException{
		int num = 10;
		int result = num/0;
		System.out.println(result);
	}
	public static void main(String[] args) {
		System.out.println("프로그램 시작");
		try {
			a();
		} catch (Exception e) {
			System.out.println(e.getMessage());
		}
		System.out.println("프로그램 종료");
	}
}

필요시 다중 catch 문이 가능하듯, 다중 throws 문도 가능합니다. 그런데 catch 블록에서 모든 예외를 Exception 클래스 하나로 받고 있는 것처럼, 모든 예외를 Exception 하나로 던지는 것도 가능합니다. 

 

 

(6) throws 이용한 메소드를 오버라이딩 하는 경우

class SuperClass {
	public void a() throws NullPointerException {}
	public void b() throws Exception {}
	public void c() throws ArithmeticException {}
}
class SubClass extends SuperClass {
	@Override
	public void a() {}
	@Override
	public void b() throws RuntimeException {}
//	@Override
//	public void c() throws Exception {} //확대 불가 
}
public class TestMain {
	public static void main(String[] args) {}
}

부모 클래스에 throws 키워드를 이용한 메소드가 있을 때, 이를 상속받는 자식 클래스에서는 메소드 오버라이딩 적용시 throws 예외 처리를 하지 않거나, 부모에서 사용한 예외 클래스와 동일하거나 또는 하위 예외 클래스여야 합니다. 

 

 

이번 글에서는 예외 처리의 기본 개념과 목적, 예외 처리하는 두 가지 방법에 대해 알아보았습니다. 다음 글에서 이어서 명시적으로 예외를 발생시키는 방법과 사용자 정의 예외 클래스를 사용하는 방법을 정리해 보도록 하겠습니다. 

 

 

댓글