[Java] 8. 예외처리(exception handling)

2022. 6. 8. 17:02JAVA/Language

1. 프로그램 오류

에러의 종류

  • 컴파일 에러(compile-time error) : 컴파일 시에 발생하는 에러
  • 런타임 에러(runtime error) : 실행 시에 발생하는 에러
  • 논리적 에러(logical error) : 실행은 되지만, 의도와 다르게 동작하는 것

에러와 예외의 비교

  • 에러(error) : 프로그램 코드에 의해서 수슬될 수 없는 심각한 오류, 예를 들어 메모리 부족(OutOfMemoryError) 또는 스택오버플로우(StackOverflowError)가 존재합니다.
  • 예외(exception) : 프로그램 코드에 의해서 수습 될 수 있는 다소 미약한 오류

 

2. 예외 클래스의 계층 구조

예외 클래스의 분류

  • Exception 클래스와 그 자손들 : 사용자의 실수와 같은 외적인 요인에 의해 발생하는 예외
  • RuntimeException 클래스와 그 자손들 : 프로그래머의 실수로 발생하는 예외

Exception 클래스와 RuntimeException 클래스를 분류하는 이유는 무엇인가?

RuntimeException 클래스를 제외한 Exception 클래스 포함 자식 클래스들은 Checked Exception으로써 반드시 예외 처리를 해야합니다. 반면 RuntimeException 클래스는 Unchecked Exception으로써 명시적인 예외처리를 하지 않아도 되기 때문입니다.

 

Checked Exception과 UnChecked Exception 비교

 

 

3. 예외처리하기 : try-catch문

예외처리 정의

  • 프로그램 실행 시 발생할 수 있는 예외에 대비한 코드를 작성하는 것

예외처리 목적

  • 프로그램의 비정상 종료를 막고, 정상적인 실행상태를 유지하는 것

예외 처리하기 위한 try-catch문 구조

try{
	// 예외가 발생할 가능성 있는 문장을 넣음
}catch(Exception1 e1){
	// Exception1이 발생했을 경우, 이를 처리하기 위한 문장을 적음
}catch(ExceptionN en){
	// ExceptionN이 발생했을 경우, 이를 처리하기 위한 문장을 적음
}

try 구문에서 예외가 발생시 예외에 맞는 catch 블럭으로 이동하여 예외 처리를 수행합니다.

 

다음은 정수 100을 0~9 사이의 임의의 정수로 나눈 결과를 출력하는 예제입니다. 그리고 랜덤으로 나온 수 중 0이 나와서 정수 100을 0으로 나누었을때 예외가 발생하고 예외 처리로 0을 출력하고 반복문을 계속 반복합니다.

public class ExceptionEx3 {
	public static void main(String[] args) {
		int number = 100;
		int result = 0;
		
		for(int i = 0; i < 10; i++) {
			try {
				result = number / (int) (Math.random() * 10);	// 0으로 나뉠수 있음
				System.out.println(result);
			}catch (ArithmeticException e) {
				System.out.println("0");
			}

		}
	}
}
11
16
100
33
12
16
0
11
14
50

위 결과와 같이 0~9사이의 임의의 정수로 나눈 결과에서 0으로 나누는 예외가 발생했음에도 예외 처리로 0을 출력하고 반복문을 진행한 것을 볼 수 있습니다.

 

4. try-catch문에서의 흐름

try 블럭 내에서 예외가 발생한 경우

  1. 발생한 예외와 일치하는 catch 블럭이 있는지 확인
  2. 일치하는 catch 블럭을 탐색하면, 그 catch 블럭 내의 문장들을 수행하고 전체 try-catch문을 탈출하여 그 다음 문장을 계속해서 수행합니다. 만약 일치하는 catch 블럭을 탐색하지 못하면 예외는 처리되지 못합니다.

try 블럭 내에서 예외가 발생하지 않은 경우

  1. catch 블럭을 거치지 않고 전체 try-catch문을 빠져나가서 수행을 계속합니다.

 

다음 예제는 try 구문 수행 도중 0을 0으로 나누었을때 ArithmeticException 예외가 발생하고 예외가 발생시 정수 5를 출력하고 try-catch 구문을 벗어나는 예제입니다.

public class ExceptionEx5 {
	public static void main(String[] args) {
		System.out.println(1);
		System.out.println(2);
		try {
			System.out.println(3);
			System.out.println(0/0);	// 예외 발생
			System.out.println(4);		// 수행되지 않음
		}catch (ArithmeticException e) {
			System.out.println(5);
		}		
		System.out.println(6);
		
	}
}
1
2
3
5
6

try 구문 수행 도중 예외가 발생하면 예외가 발생한 그 다음 문장들은 수행되지 않습니다.

 

5. 예외의 발생과 catch 블럭

예외 클래스 타입 인스턴스의 메서드

  • printStackTrace : 예외 발생 당시의 호출 스택(Call Stack)에 있었던 메서드의 정보와 예외 메시지를 화면에 출력함
  • getMessage() : 발생한 예외 클래스의 인스턴스에 저장된 메시지를 얻을 수 있음

 

public class ExceptionEx8 {
	public static void main(String[] args) {
		System.out.println(1);
		System.out.println(2);
		try {
			System.out.println(3);
			System.out.println(0/0);	// 예외 발생
			System.out.println(4);		// 수행되지 않음
		}catch (ArithmeticException ae) {
			// 참조변수 ae를 통해, 생성된 ArithmeticException 인스턴스에 접근할 수 있음
			ae.printStackTrace(); 
			System.out.println("예외메시지 : " + ae.getMessage());
		}		
		System.out.println(6);
		
	}
}
1
2
3
java.lang.ArithmeticException: / by zero
	at ch08.ex_08_exception8.ExceptionEx8.main(ExceptionEx8.java:16)
예외메시지 : / by zero
6

 

6. 예외 발생시키기

throw 키워드를 이용한 예외 발생 단계

  1. 연산자 new를 이용해서 발생시키려는 예외 클래스의 인스턴스를 생성함
  2. 키워드 throw를 이용해서 예외를 발생시킴
Exception e = new Exception("고의로 발생시켰음");
throw e;

 

 

7. 메서드에 예외 선언하기

예외를 메서드에 선언하는 방법

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

메서드에 예외 선언 특징

  • 메서드 선언부를 보고 어떤 예외들이 처리되어져야 하는지 쉽게 파악 가능함
  • 예외를 메서드의 throws에 명시하는 것은 예외를 처리하는 것이 아닌 자신을 호출한 메서드에게 예외를 전달하여 예외 처리를 전가하는 것이다.

다음 예제는 메서드에서 예외가 발생시 main 메서드까지 예외 발생을 전달하는 예제입니다.

public class ExceptionEx12 {
	public static void main(String[] args) throws Exception {
		method1(); // 예외 처리를 하지 않아 프로그램이 종료됨
	}
	
	static void method1() throws Exception {
		method2();
	}
	
	static void method2() throws Exception {
		throw new Exception();
	}
}
Exception in thread "main" java.lang.Exception
	at ch08.ex_12_exception12.ExceptionEx12.method2(ExceptionEx12.java:21)
	at ch08.ex_12_exception12.ExceptionEx12.method1(ExceptionEx12.java:17)
	at ch08.ex_12_exception12.ExceptionEx12.main(ExceptionEx12.java:13)

위 수행결과를 통하여 예외가 발생한 곳은 method2이고 method2는 method1이 호출하고 method1은 main이 호출한 것임을 알 수 있습니다.

 

예외가 발생한 메서드 내에서 예외를 처리하기

public class ExceptionEx13 {
	public static void main(String[] args) throws Exception {
		method1();
	}
	
	static void method1(){
		try {
			throw new Exception();
		}catch (Exception e) {
			System.out.println("method1 메서드에서 예외가 처리되었습니다.");
			e.printStackTrace();
		}
	}
	
}
method1 메서드에서 예외가 처리되었습니다.
java.lang.Exception
	at ch08.ex_13_exception13.ExceptionEx13.method1(ExceptionEx13.java:17)
	at ch08.ex_13_exception13.ExceptionEx13.main(ExceptionEx13.java:12)

위 예제처럼 예외가 발생한 메서드(method1) 내에서 예외 처리되어지면 호출한 메서드(main)에서는 예외가 발생했다는 사실조차 모르게 됩니다.

 

예외가 발생할때 자신을 호출한 메서드로 전달하여 처리하기

public class ExceptionEx14 {
	public static void main(String[] args){
		
		try {
			method1();
		} catch (Exception e) {
			System.out.println("main메서드에서 에외가 처리되었습니다.");
			e.printStackTrace();
		}
	}
	
	static void method1() throws Exception{
		throw new Exception();
	}
	
	
}
main메서드에서 에외가 처리되었습니다.
java.lang.Exception
	at ch08.ex_14_exception14.ExceptionEx14.method1(ExceptionEx14.java:21)
	at ch08.ex_14_exception14.ExceptionEx14.main(ExceptionEx14.java:13)

 

위 예제처럼 예외가 발생한 메서드에서 예외를 처리하지 않고 호출한 메서드로 전달하여 호출한 메서드에서는 method1()을 호출한 라인에서 예외가 발생한 것으로 간주되어 예외 처리를 수행합니다.

 

8. finally 블럭

finally 블럭의 목적

예외의 발생여부에 관계없이 실행되어야할 코드를 포함시킬 목적으로 사용됩니다. 보통 입출력 클래스의 인스턴스들의 자원을 해제하는데 사용됩니다.

try-catch-finally 구문 형식

try{
    // 예외가 발생할 가능성 있는 문장들을 넣음
}catch(Exception e){
    // 예외 처리를 위한 문장을 넣음
}finally{
    // 예외의 발생여부에 관계없이 항상 수행되어야 하는 문장들을 넣음
    // finally 블럭은 try-catch 문의 맨 마지막에 위치함
}
  • 예외가 발생한 경우 : try->catch->finally 순으로 수행
  • 예외가 발생하지 않는 경우 : try->finally 순으로 수행

 

9. 자동 자원 반환 : try-with-resources문

왜 try-catch-finally 구문보다 try-with-resources 구문을 사용해야 하는가?

  • finally 블럭안에서 예외가 발생할 수 있는데 이때 이중 try-catch 구문을 넣게되어 가독성이 떨어짐
  • try 블럭과 finally 블럭 모두에서 예외가 발생하면 try 블럭의 예외가 무시됨

 

try-with-resources 구문 형식

		// CloseableResource 인스턴스는 try구문을 벗어나는 순간 자원해제됨 (close)
		try(CloseableResource cr = new CloseableResource()) {
			cr.exceptionWork(false);
		} catch (WorkException e) {
			e.printStackTrace();
		}catch (CloseException e) {
			e.printStackTrace();
		} catch (Exception e) {
			e.printStackTrace();
		}

 

try-with-resources 구문 특징

  • try 블럭을 벗어나면 자동으로 자원을 해제함( close 메서드)
  • try-with-resources문에 의해 자동으로 객체의 close()가 호출될 수 있으려면 클래스가 AutoCloseable이라는 인터페이스를 구현한 것이어야함
  • close 하는 과정에서 예외가 발생해도 try 블럭의 예외를 무시하지 않음
public interface AutoCloseable{
	void close() throws Exception;
}

입출력 클래스들은 내부에 AutoCloseable 인터페이스를 구현하였기 때문에 try-with-resources 구문에 넣을 수 있습니다.

 

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

예외 되던지기란 무엇인가?

예외 되던지기는 예외를 처리한후에 인위적으로 다시 발생시키는 방법을 의미합니다. 다시 발생한 예외는 이 메서드를 호출한 메서드에게 전달되고 호출한 메서드의 try-catch문에서 예외를 또다시 처리합니다.

 

예외 되던지기를 사용하는 이유는 무엇인가?

예외 되던지기를 하는 이유는 하나의 예외에 대해서 예외가 발생한 메서드와 이를 호출한 메서드 양쪽 모두에서 처리해줘야할 작업이 존재할때 사용합니다.

 

public class RethrowingTest {

	public static void main(String[] args) {
		try {
			method1();
		} catch (Exception e) {
			System.out.println("main 메서드에서 예외가 처리되었습니다.");
		}
	}
	
	static void method1() throws Exception {
		try {
			throw new Exception();
		}catch (Exception e) {
			System.out.println("method1 메서드에서 예외가 처리되었습니다.");
			throw e;	// re-throwing 수행
		}
		
	}

}
method1 메서드에서 예외가 처리되었습니다.
main 메서드에서 예외가 처리되었습니다.

 

11. 연결된 예외(chained exception)

원인 예외(cause exception)이란 무엇인가?

한 예외가 다른 예외를 발생시킬수도 있습니다. 예를 들어 예외 A가 예외 B를 발생시켰다면 A를 B의 원인 예외라고 합니다.

 

원인 예외 형식

try{
    startInstall();
    copyFiles();
}catch(SpaceException e){
    InstallException ie = new InstallException("설치중 예외발생");
    ie.initCause(e); // InstallException의 원인 예외를 SpaceException으로 지정
    throw ie; // InstallException을 발생시킴
}

 

원인 예외 관련 메서드

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

 

원인 예외를 사용하는 이유는 무엇인가?

  • 여러가지 예외를 하나의 큰 분류의 예외로 묶어서 다루기 위해서입니다.
  • checked Exception을 unchecked Exception으로 변경하기 위해서입니다.
    • unchecked Exception으로 변경되면 예외처리가 선택적이 되므로 억지로 예외 처리를 하지 않아도 됩니다.

 

 

 

 

 

 

References

source code : https://github.com/yonghwankim-dev/java_study/tree/main/ch08
https://www.nextree.co.kr/p3239/
[도서] Java의 정석, 남궁 성 지음