예외처리

goose_bumps·2024년 6월 14일

Java

목록 보기
12/21

프로그램 실행 중 어떠한 원인에 의해 작동이 중지되거나 비정상적으로 종료되는 경우가 있는데, 그 원인을 프로그램 에러 또는 오류라고 한다.
에러에는 컴파일 시 발생하는 컴파일 에러, 컴파일 시에는 문제가 없었지만 실행 중 발생하는 런타임 에러, 그리고 실행은 되지만 의도와는 다르게 작동되는 논리적 에러가 있다.

  • 컴파일 에러 : 컴파일 시 발생
  • 런타임 에러 : 컴파일 시 문제가 없지만 실행 간 발생
  • 논리적 에러 : 실행은 되지만 의도와 다르게 작동

컴파일 간에는 문제가 없었는데 왜 실행 간 런타임 에러가 발생할까?
컴파일러가 소스코드의 기본적인 사항을 검사할 수는 있지만, 실행도중에 발생할 수 있는 잠재적인 오류까지 검사할 수는 없다.
자바에서는 런타임, 즉 실행 시 발생할 수 있는 프로그램의 오류는 에러와 예외, 두 가지로 구분한다.
에러는 StackOverFlow나 OutOfMemory와 같이 발생하면 복구가 어려운 오류이고 예외는 발생하더라도 수습이 가능하여 프로그래머가 적절한 코드를 작성하여 방지할 수 있다.
이것을 예외 처리라고 한다.

런타임 에러

  • 에러 : 프로그램 코드에 의해서 수습될 수 없는 심각한 수준의 오류
  • 예외 : 프로그램 코드에 의해서 수습할 수 있는 다소 미약한 오류

1. 예외 클래스

자바에서는 런타임 시 발생할 수 있는 오류를 Exception 클래스와 Error 클래스로 정의해놓았다. 이 클래스들은 다른 클래스들과 마찬가지로 Object 클래스를 상속한다.

여기서 예외 처리를 하려면 Exception 클래스를 사용해야 한다.
Exception 클래스는 모든 예외 클래스들의 공통 조상이고, 자손 클래스들을 크게 RuntimeException과 그 외 클래스들로 분류할 수 있다.

<Exception 클래스>

  • RuntimeException 클래스와 그 자손 클래스들(주로 프로그래머의 실수로 인해 발생)
  • 그 외 클래스들과 그 자손들(주로 외부의 영향으로 발생)

RuntimeException 클래스들은 주로 프로그래머의 실수로 인해 발생하는 예외들이다. 배열의 범위를 벗어난 ArrayIndexOutOfBoundsException, NullPointerException, ClassCastException 등이 있다.

그 외의 클래스들은 주로 외부의 영향으로 인해 발생하는 예외들로, FileNotFoundException, ClassNotFoundException처럼 외부에서 파일이나 클래스의 이름을 잘못 입력한 경우의 예외들이다.

2. try-catch문으로 예외처리

1) try-catch

프로그래머는 발생할 수 있는 예외에 대해서 처리를 해주어야 한다.
예외를 처리하지 않으면 프로그램이 비정상적으로 종료되기 때문에 이를 방지하기 위한 목적으로 예외 처리를 해주는 것이다.

예외처리에는 try-catch문을 사용한다.

		try {
			//예외 발생할 가능성이 있는 문장
		}
		catch(Exception1 e) {
			//Exception1이 발생하였을 경우 처리하기 위한 문장
		}
        catch(Exception2 e) {
			//Exception2이 발생하였을 경우 처리하기 위한 문장
		}
        catch(Exception3 e) {
			//Exception3이 발생하였을 경우 처리하기 위한 문장
		}

try문 내에는 예외 발생할 수 있는 문장을 여러 개 적을 수 있다. 순서대로 실행되다가 예외가 발생할 경우 해당 예외 타입의 인스턴스를 생성한다.
그 후 try문을 벗어나 catch문을 순서대로 찾으면서 예외에 해당하는 catch문의 참조변수에 인스턴스가 대입된다.
그리고 해당 catch문이 실행된 후 try-catch 블록을 탈출한다.

try-catch문의 흐름을 이해하기 위해 다음 예시를 보자.

		System.out.println("try-catch문 진입 전");
		try {
			System.out.println("예외 발생 안하는 문장1");
			System.out.println("예외 발생 안하는 문장2");
			System.out.println("예외 발생 안하는 문장3");
			System.out.println(3/0); //ArithmeticException 발생
			System.out.println("예외 발생 안하는 문장4");
		}
		catch(ArrayIndexOutOfBoundsException e) {
			System.out.println("ArrayIndexOutOfBoundsException");
		}
		catch(ArithmeticException e) {
			System.out.println("ArithmeticException");
		}
		catch(ClassCastException e) {
			System.out.println("ClassCastException");
		}
		System.out.println("try-catch문 탈출");

이 코드를 실행시키면 다음과 같은 결과가 출력된다.

try-catch문 진입 전
예외 발생 안하는 문장1
예외 발생 안하는 문장2
예외 발생 안하는 문장3
ArithmeticException
try-catch문 탈출

try문에서 3을 0으로 나누었기 때문에 정수를 0으로 나누면 안된다는 원칙에 위배되어 ArithmeticException이 발생하였다.
ArithmeticException 타입의 인스턴스가 생성되고 try문을 벗어나 이 타입에 해당하는 예외를 찾는다.
2번째 catch블럭 소괄호의 예외가 타입이 일치하여 해당 블럭이 실행되고, try-catch문을 벗어난다.

* 실수는 0으로 나누는 것은 금지되어 있지 않아 예외가 발생하지 않음.


몇 가지 주의할 점이 있다.
try 블럭이나 catch 블럭 내에 try-catch문을 또 적는 것은 가능하다. 하지만, catch 블럭 내에 try-catch문이 포함된 경우 참조변수명은 다르게 해야 한다.

		try {
			//예외 발생할 가능성이 있는 문장
		}
		catch(Exception1 e1) {
			try {
				//예외 발생할 가능성이 있는 문장
			}
			catch(Exception2 e2) {
				//Exception2가 발생하였을 경우 처리하기 위한 문장
			}
		}

catch 블럭끼리는 참조변수 명이 같아도 상관없지만 참조변수는 catch 블럭 내에서만 유효하기 때문에 같은 블럭 내에 있는 또 다른 catch 블럭에서는 다른 참조변수명을 사용해야 한다.

2) 예외 발생과 발생 원인 파악

서론에서 모든 예외 클래스의 공통 조상은 Exception 클래스라고 언급했었다.
그렇다면 만약, catch 블럭에 Exception 클래스를 입력하면 어느 catch 블럭이 실행될까?

ArithmeticException이 발생한 상황을 가정해보자.
해당 타입의 인스턴스는 같은 타입의 예외를 찾을 것이다. 그런데 모든 예외 클래스의 공통 조상인 Exception을 찾았다면 어떻게 될까?

		try {
			System.out.println(3/0);
		}
		catch(Exception e) {
			System.out.println("Exception");
		}

이 경우에는 catch 블록이 실행된다.
Exception은 모든 예외 클래스의 공통 조상이기 때문에 모든 예외 처리가 가능하다.

그래서 이를 응용하면 이렇게도 사용가능하다.

		try {
			System.out.println(3/0);
		}
		catch(ArithmeticException e) {
			if(e instanceof ArithmeticException) {
				System.out.println("true");
			}
			System.out.println("ArithmeticException");
		}
		catch(Exception e) {
			System.out.println("Exception"); //ArithmeticException이 처리하지 못한 모든 예외가 처리됨
		}

ArithmeticException이 발생했기 때문에 순서대로 찾으면서 첫 번째 catch블록이 실행된다. ArithmeticException 타입의 인스턴스가 생성되어 첫 번째 catch 블록의 참조변수에 저장되므로 instanceof 연산자는 true가 되기 때문에 true가 출력이 될 것이다.

try 블록에 ArithmeticException 외에 다른 예외들은 Exception이 포함되어 있는 catch 블록에서 처리된다.

여기서도 주의해야 할 점이 있다.

		try {
			System.out.println(3/0);
		}
		catch(Exception e) {
			System.out.println("Exception"); 
		}
		catch(ArithmeticException e) {
			System.out.println("ArithmeticException");
		}

Exception이 포함된 catch 블록이 먼저 작성되어 있는데 이 경우에는 컴파일 에러가 발생한다. 어차피 어떤 예외가 발생해도 첫 번째 catch 블록에서 처리되기 때문에 그 다음에는 작성할 필요가 없기 때문이다.

따라서, 조상 격의 예외 클래스를 자손보다 먼저 작성해놓으면 안된다.


예외 발생의 원인, 정보를 알 수 있는 방법이 있다.
getMessage(), printStackTrace() 메서드를 사용하면 된다.

하지만, 이런 생각이 들 수 있다.
어차피 예외가 발생해서 프로그램이 비정상적으로 종료되어도 발생 원인에 대한 정보가 출력되지 않나??

예외처리의 목적 자체가 프로그램의 비정상적인 종료를 방지하는 것이다. 따라서, 비정상적인 종료를 통해서 정보를 얻는 것은 맞지 않다.

위의 메서드를 사용하면 프로그램을 정상적으로 종료시키면서 예외 발생 원인에 대한 정보가 출력이 가능하다.

		try {
			System.out.println(3/0);
		}
		catch(Exception e) {
			e.printStackTrace();
			System.out.println(e.getMessage());
		}

try 블록에서 ArithmeticException이 발생하면서 해당 타입의 인스턴스가 생성된다. 이 인스턴스는 같은 타입의 catch 블록을 찾아 참조변수에 저장된다.
참조변수는 해당 인스턴스의 정보를 가지기 때문에 위의 메서드를 실행시키면 다음과 같은 결과가 출력된다.

java.lang.ArithmeticException: / by zero
at Review/exceptionReview.ExceptionReview.main(ExceptionReview.java:7)
/ by zero

이 메서드들을 사용하면 예외가 발생해도 프로그램을 비정상적으로 종료하지 않도록 해주는 동시에, 예외가 발생한 원인을 알 수 있다.

3) 멀티 catch 블록

JDK 1.7부터 여러 개의 catch 블록을 ' | ' 기호를 사용하여 하나의 catch 블록으로 합칠 수 있게 되었다.
이를 멀티 catch 블록이라 한다.

		try {
			//예외 발생할 가능성이 있는 문장
		}
		catch(Exception1 e) {
			//Exception1이 발생하였을 경우 처리하기 위한 문장
		}
        catch(Exception2 e) {
			//Exception2이 발생하였을 경우 처리하기 위한 문장
		}

기존의 catch 블록들을 멀티 catch 블록으로 바꾸어보겠다.

		try {
			//예외 발생할 가능성이 있는 문장
		}
		catch(Exception1 | Exception2 e) {
			//Exception1 또는 Exception2가 발생하였을 경우 처리하기 위한 문장
		}

코드의 중복성을 제거하고 간결화가 가능해졌다. 하지만, 주의해야 할 점이 있다.
합치려는 두 블록이 조상과 자손의 관계에 있다면 컴파일 에러가 발생한다.

		try {
			//예외 발생할 가능성이 있는 문장
		}
		catch(Exception | ArithmeticException e) {
			//처리 문장
		}

어차피 Exception 클래스만 정의해두면 되기 때문에 불필요한 코드는 제거하라는 의미에서 컴파일 에러가 발생하는 것이다.

또 한 가지 주의해야 할 점은 참조변수 e로 멤버를 사용할 경우 소괄호 내에 예외 클래스들의 공통 조상 클래스의 멤버만 사용가능하다.

		try {
			System.out.println(3/0);
		}
		catch(ArithmeticException | ArrayIndexOutOfBoundsException e) {
			e.printStackTrace();
			System.out.println(e.getMessage());
		}

printStatckTrace()와 getMessage()는 Exception 클래스의 메서드이므로 참조변수 e로 사용이 가능하다.

왜 이렇게 된거냐면 멀티 catch 블록 자체가 하나의 블록으로 여러 예외를 처리하기 때문에 멀티 catch 블록 내에서는 실제로 어떤 예외가 발생하게 되었는지 알 수가 없다.

저 소괄호 안의 의미는 "ArithmeticException 또는 ArrayIndexOutOfBoundsException 둘 중 하나가 발생했다" 정도이다. 그래서 각 예외 클래스의 멤버를 사용하려면 코드를 다음과 같이 작성해야 한다.

		try {
			...
		}
		catch(Exception1 | Exception2 e) {
        
        	if(e instanceof Exception1){
            	Exception1 e1 = (Exception1)e;
                e1.method1();
                }
                
			e.printStackTrace();
			System.out.println(e.getMessage());
		}

하지만, 멀티 catch 블록으로 합치는 경우는 대부분 코드를 간단히 하는 정도의 수준이므로 이러한 제약을 너무 고민할 필요는 없다.

3. 예외 발생시키기

1) throw

throw 키워드를 사용하면 고의로 예외를 발생시킬 수 있다.

Exception e = new Exception("고의로 발생");
throw e;

우선 예외 클래스의 인스턴스를 만든 후 참조변수 e에 저장하고 throw를 사용하여 참조변수에 저장된 예외 클래스의 인스턴스를 발생시키는 것이다.

저렇게 두 줄 말고도 한 줄로 처리가 가능하다.

thorw new Exception("고의로 발생");

public class ExceptionReview {
	public static void main(String[] args) {
		try {
			throw new ArithmeticException("고의로 발생");
		}
		catch(ArithmeticException e) {
			System.out.println(e.getMessage());
			e.printStackTrace();
		}
		catch(ClassCastException e) {
			System.out.println(e.getMessage());
			e.printStackTrace();
		}
		catch(IndexOutOfBoundsException e) {
			System.out.println(e.getMessage());
			e.printStackTrace();
		}
	}
}

고의로 발생
java.lang.ArithmeticException: 고의로 발생
at Review/exceptionReview.ExceptionReview.main(ExceptionReview.java:7)

try문에서 고의로 ArithmeticException을 발생시켰고 해당하는 예외 클래스를 가지는 catch문을 순서대로 찾아 실행시킨 결과다.

고의로 발생시킨 예외는 반드시 처리해주어야 한다.
처리하지 않으면 컴파일 에러가 발생할 수 있기 때문이다.

public class ExceptionReview {
	public static void main(String[] args) {
		throw new Exception();
	}
}

Exception in thread "main" java.lang.Error: Unresolved compilation problem:
Unhandled exception type Exception
at Review/exceptionReview.ExceptionReview.main(ExceptionReview.java:6)

예외처리를 해주지 않아 컴파일 에러가 발생한 것을 알 수 있다.

하지만, 예외처리를 해주지 않아도 컴파일 에러가 발생하지 않는 경우도 있다.

public class ExceptionReview {
	public static void main(String[] args) {
		throw new RuntimeException();
	}
}

Exception in thread "main" java.lang.RuntimeException
at Review/exceptionReview.ExceptionReview.main(ExceptionReview.java:6)

RuntimeException과 그 자손 클래스들은 프로그래머에 의해 실수로 발생하는 것들이기 때문에 예외처리를 강제하지 않는다. 즉, 컴파일러가 예외처리를 확인하지 않는 Unchecked Exception이다.
따라서, 컴파일 에러는 발생하지 않지만 비정상적인 종료는 발생한다.

RuntimeException과 그 자손 클래스들 : Unchecked Exception -> 예외 미처리 시 컴파일 에러 발생X
그 외 Exception 클래스들 : Checked Exception -> 예외 미처리 시 컴파일 에러 발생

2) 메서드에 예외 선언 throws

Java는 타 언어와 다르게 메서드 선언부에 발생가능한 예외를 선언할 수 있다.

void 메서드명() throws 예외클래스명;

예외를 선언한다고 해서 예외를 발생시키는 것은 아니다.
어떠한 메서드 A가 ArithmeticException을 발생시킬 가능성이 있는 경우 미리 선언부에 예외를 선언해놓는다. 이 메서드 A를 호출하는 곳에서는 ArithmeticException에 대한 예외처리를 해주어야 하는 의무가 생긴다.
예외처리를 해주지 않으면 컴파일 에러가 발생하기 때문이다.

즉, 예외를 선언하는 것은 예외를 발생시키는 것은 아니지만 해당 메서드를 호출하는 곳에 예외처리를 강제한다.
하지만, 선언한 예외가 Unchecked Exception일 경우에는 컴파일 에러는 발생하지 않는다.

class ExceptionDeclare{
	public void methodA() throws RuntimeException{
		System.out.println("methodA");
	}
	
	public void methodB() throws Exception{
		System.out.println("methodB");
	}
}

public class ExceptionReview {
	public static void main(String[] args) {
		ExceptionDeclare ed = new ExceptionDeclare();
		ed.methodA(); //Unchecked Exception은 예외처리 하지 않아도 컴파일 에러 발생하지 않음
		// ed.methodB(); 예외처리를 하지 않아 컴파일 에러 발생
	}
}

Checked Exception에 해당하는 예외 클래스를 예외 처리 해주지 않아 컴파일 에러가 발생하였다.
다음과 같이 예외처리를 해주면 된다.

class ExceptionDeclare{
	public void methodA() throws RuntimeException{
		System.out.println("methodA");
	}
	
	public void methodB() throws Exception{
		System.out.println("methodB");
	}
}

public class ExceptionReview {
	public static void main(String[] args) {
		ExceptionDeclare ed = new ExceptionDeclare();
		ed.methodA();
		try{
			ed.methodB();
		}
		catch(Exception e) {
			System.out.println("Exception has occurred");
		}
	}
}

선언된 예외는 호출하는 쪽에서 처리해도 되고 호출되는 쪽에서도 예외처리 해도 무방하다.
다음 예시를 보자.

public class ExceptionReview {
	public static void main(String[] args) {
		methodA();
	}	
	static void methodA() {
		try {
			throw new Exception();
		}
		catch(Exception e) {
			e.printStackTrace();
		}
	}
}

호출받는 methodA에서 예외처리를 해주었기 때문에 main 메서드에서 예외처리를 해주지 않아도 된다.

public class ExceptionReview {
	public static void main(String[] args) {
		try {
			methodA();
		}
		catch(Exception e) {
			e.printStackTrace();
		}
	}	
	static void methodA() throws Exception{
		throw new Exception();
	}
}

반대로 호출하는 입장에서 예외 처리를 전가받아 예외 처리를 해주어도 무방하다.

호출하는 입장에서 예외 처리를 하지 않고 다시 예외를 전가해도 된다.

public class ExceptionReview {
	public static void main(String[] args) throws Exception{
		methodA();
	}
	static void methodA() throws Exception{
		System.out.println("methodA");
	}
}

하지만 이것도 예외를 전가한 것이기 때문에 어딘가에서는 반드시 예외처리는 해주어야 한다.

4. finally 블럭

finally 블럭은 예외 발생여부에 무관하게 반드시 실행되어야 하는 코드를 포함시킬 목적으로 사용된다.
try - catch - finally 순서로 구성되며,
예외가 발생할 경우 try - catch - finally 순으로, 예외가 발생하지 않을 경우 try - finally 순으로 실행된다.

		try {
			//예외 발생 가능한 문장
		}
		catch(Exception e) {
			//예외 처리를 위한 문장
		}
		finally {
			//예외 발생 여부에 관계없이 실행되어야 하는 문장
		}

finally의 필요성을 느끼기 위해 다음 예시를 보자.

class Test{
	int a;
	public Test(int a) {
		this.a = a;
	}
}
public class ExceptionReview {
	public static void main(String[] args) {
		Test t = new Test((int)(Math.random() * 10));
		int[] Array = new int[5];
		for(int i = 0; i < Array.length; i++) {
			Array[i] = i;
		}
		try {
			System.out.println(Array[t.a]);
			System.out.println(Array.length);
		}
		catch(IndexOutOfBoundsException e) {
			System.out.println(t.a + " is out of Index");
			System.out.println(Array.length);
		}
	}
}

Test 클래스의 생성자에 1~10 범위의 난수를 넣어 정수 a의 값을 지정한다.
크기가 5인 정수 배열을 생성하고 a 번째 인덱스의 값을 호출하는데, 5를 넘어가면 예외가 발생한다.

예외가 발생하든 안하든 배열의 길이를 출력하는 코드는 실행된다.

이 경우에 finally 블럭에 따로 빼서 작성하는 것이 더 가독성이 좋다.

		try {
			System.out.println(Array[t.a]);
		}
		catch(IndexOutOfBoundsException e) {
			System.out.println(t.a + " is out of Index");
		}
		finally {
			System.out.println(Array.length);
		}

try블럭에서 return문이 실행되어도 finally블럭의 문장이 먼저 실행 후, 현재 실행 중인 메서드를 종료한다.

class Test{
	void method1() {
		try {
			System.out.println("try 블럭");
			return;
		}
		catch(Exception e) {
			e.printStackTrace();
		}
		finally {
			System.out.println("finally 블럭");
		}
	}
}

public class ExceptionReview {
	public static void main(String[] args) {
		Test t = new Test();
		t.method1();
		System.out.println("method1 종료");
	}
}

try 블럭
finally 블럭
method1 종료

5. 사용자 정의 예외

기존에 정의되어 있는 예외 외에도 필요에 따라 사용자가 새로운 예외 클래스를 만들 수 있다.
Exception 클래스나 RuntimeException 클래스를 상속하면 된다.

class MyException extends Exception{
	MyException(String msg){
    	super(msg);
        }
}

생성자에 매개변수를 넣고 super에 대입한 것은 Exception 클래스의 생성자가 String값을 받아서 메시지로 저장할 수 있기 때문이다. getMessage() 메서드를 통해 출력한 것이 바로 Exception 클래스 생성 시 입력한 String 값이다.

	try {
		throw new MyException("사용자 정의 예외 클래스");
		}
	catch(Exception e) {
		System.out.println(e.getMessage());
		}

사용자 정의 예외 클래스

기존에는 예외 클래스를 Exception을 상속받아 Checked 예외로 작성하는 경우가 많았지만, 최근에는 RuntimeException을 상속받아 Unchecked 예외로 작성하는 추세이다.
과거에는 프로그래밍 경험이 적은 개발자도 견고한 프로그램을 작성할 수 있도록 예외 처리를 강제하였지만, 프로그래밍 환경이 달라진 현재에는 예외 처리가 선택사항인 Unchecked 예외가 더 환영받고 있다.

사용자 정의 예외 클래스를 활용하여 예시를 만들어보겠다.

class InstallException extends Exception{
	InstallException(String msg){
		super(msg);
	}
}
class RunException extends RuntimeException{
	RunException(String msg){
		super(msg);
	}
}

public class ExceptionReview {
	public static void main(String[] args) {
		try {
			checkException();
		}
		catch(InstallException e) {
			System.out.println(e.getMessage());
		}
		catch(RuntimeException e) {
			System.out.println(e.getMessage());
		}
	}
	
	static void checkException() throws InstallException {
		if(InstallState()==true) {
			throw new InstallException("Install imcomplete");
		}
		if(RunState()==true) {
			throw new RunException("Cannot Run");
		}
	}
	
	static boolean InstallState() {
		return false;
	}
	static boolean RunState() {
		return true;
	}
}

InstallException과 RunException이라는 임의의 클래스를 상속을 통해 예외 클래스로 정의하였고 checkException 메서드는 Checked 예외인 InstallException을 throw 할 수 있기 때문에 선언부에 InstallException을 throws 해주어야 컴파일 에러가 발생하지 않는다.

사용자 정의 예외를 통해 다양한 예외 클래스를 만들 수 있지만 가급적이면 기존의 예외 클래스를 활용하는 것이 더 좋다.

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

예외를 전가받은 입장에서 다시 예외를 던지는 경우도 있다. 예외를 처리한 후 인위적으로 다시 발생시키는 방법을 통해서 가능한데 예외 되던지기라고 한다.
예외처리 자체를 전가하는 쪽과 전가받는 쪽에 나눠서 처리하는 것이다.

public class ExceptionReview {
	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에서 예외 처리

양쪽에서 예외를 처리했기 때문에 두 catch 블럭 모두 실행되었다.
method1에서 예외를 인위적으로 발생시키지 않는다면 main의 catch 블럭은 실행되지 않는다.

public class ExceptionReview {
	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에서 예외 처리");
		}
	}
}

method1에서 예외 처리

반환값이 있는 메서드의 경우 catch 블럭에도 return문이 있어야 한다.

static int method1() {
		try {
			System.out.println("method1 호출");
			return 1;
		}
		catch(Exception e){
			System.out.println(e.getMessage());
		}
	}

다음과 같이 catch 블럭에 return문이 없으면 컴파일 에러가 발생한다. 왜 그럴까?
try문에서 반환을 한다해도 만약 예외가 발생한다면 반환 전에 catch블럭으로 넘어갈 수 있다.
따라서, 예외가 발생하였을 때도 반환이 되어야 하기 때문에 catch 블럭에서 return 문이 있어야 하는 것이다.

하지만, 항상 실행되는 finally블럭에 return문이 있다면 catch 블럭에 return문이 없어도 무관하다. 어차피 예외 유무에 관계없이 항상 return 값이 반환되기 때문이다.

catch 블럭에서 return 문 대신 예외를 호출할 수 있다. 정확히는, catch 블럭에서 예외 되던지기를 하여 호출한 메서드로 예외를 전달하면 return문이 없어도 되는 것이다.

static int method1() throws Exception{
		try {
			System.out.println("method1 호출");
			return 1;
		}
		catch(Exception e){
			System.out.println(e.getMessage());
			throw new Exception();
		}
	}

단, 예외를 되던지기 할 경우(Checked 예외를 던질 경우) 메서드 선언부에는 예외를 throws 해야 컴파일 에러가 발생하지 않는다.

7. 연결된 예외(chained Exception)

A라는 예외가 B라는 다른 예외를 발생시킬 수 있는데, 이때 A는 B의 원인 예외가 된다.

		try {
			startInstall();
		}
		catch(SpaceException e) {
			InstallException ie = new InstallException("설치 중 예외 발생");
			ie.initCause(e);
			throw ie;
		}
		catch(MemoryException e) {
			InstallException ae = new InstallException("설치 중 예외 발생");
			ae.initCause(e);
			throw ae;
		}

InstallException이라는 예외 클래스의 객체를 생성하고 initCause를 사용하여 SpaceException과 MemoryException을 각각 원인 예외로 지정할 수 있다.

initCause는 Exception 클래스의 조상 클래스인 Throwable에 정의되어 있기 때문에 모든 예외에서 사용 가능하다.

원인 예외는 쉽게 말하자면 예외들은 하나의 큰 분류의 예외로 묶는 것이다.
저 예시에서는 SpaceException, MemoryException을 InstallException이라는 큰 분류의 예외로 묶은 것이다.

그럼 왜? 굳이 하나로 묶을 필요가 있을까?

다음 예시를 보자.


//예외 클래스 정의
class InstallException extends Exception{
	InstallException(String msg){
		super(msg);
	}
}
class SpaceException extends Exception{
	SpaceException(String msg){
		super(msg);
	}
}
class MemoryException extends Exception{
	MemoryException(String msg){
		super(msg);
	}
}

public class ExceptionReview {
	//main 메서드
	public static void main(String[] args) {
		try {
			install();
		}
		catch(InstallException e) {
			e.printStackTrace();
		}
		catch(Exception e) {
			e.printStackTrace();
		}
	}
	//install 메서드 // InstallException을 throws
	static void install() throws InstallException{
		try {
			startInstall();
		}
		catch(SpaceException e) {
			InstallException ie = new InstallException("설치 중 예외 발생");
			ie.initCause(e);
			throw ie;
		}
		catch(MemoryException e) {
			InstallException ae = new InstallException("설치 중 예외 발생");
			ae.initCause(e);
			throw ae;
		}
	}
	//startInstall 메서드 //SpaceException,MemoryException을 throws
	static void startInstall() throws SpaceException,MemoryException{
		if(!enoughSpace()) {
			throw new SpaceException("설치 공간 부족");
		}
		if(!enoughMemory()) {
			throw new MemoryException("메모리 부족");
		}
	}
	
	static boolean enoughSpace() {
		return false;
	}
	static boolean enoughMemory() {
		return true;
	}
}

예시는 길지만 간단하다. 가장 아래에 있는 startInstall 메서드가 조건에 따라 SpaceException 또는 MemoryException을 던진다. 둘 다 Exception을 상속하기 때문에 throws로 선언해준다.

자, 이제 install 메서드가 startInstall 메서드를 호출한다. 예외를 전가받았기 때문에 catch 블럭을 2개 만들어준다.
여기서 catch 블럭마다 InstallException의 원인 예외로 지정해준다.
이제 SpaceException,MemoryException 둘 다 InstallException이라는 큰 분류로 묶였기 때문에 메서드의 선언부에는 InstallException만 throws 해주면 된다.

이제 main 메서드에서는 startInstall을 호출하고 InstallException에 대해서만 예외처리를 해주면 된다.
이미 하나의 분류로 묶었기 때문이다. 코드를 실행하면 다음과 같이 출력된다.

exceptionReview.InstallException: 설치 중 예외 발생
at Review/exceptionReview.ExceptionReview.install(ExceptionReview.java:37)
at Review/exceptionReview.ExceptionReview.main(ExceptionReview.java:22)
Caused by: exceptionReview.SpaceException: 설치 공간 부족
at Review/exceptionReview.ExceptionReview.startInstall(ExceptionReview.java:50)
at Review/exceptionReview.ExceptionReview.install(ExceptionReview.java:34)
... 1 more

main 메서드에서 SpaceException이 발생하여 InstallException이 있는 catch 블럭이 실행된 것이다.
그럼 큰 범주에서부터 stackTrace가 출력되어 "설치 중 예외 발생" 문구가 출력되고 그 다음 작은 분류인 SpaceException의 문구 "설치 공간 부족"이 출력된다.

이렇듯 하나의 범주로 묶으면 가장 큰 범주부터 작은 범주까지 예외가 출력이 된다.
또 만약에, 발생 가능한 예외가 수 백개가 있을 경우 main 메서드에서도 catch 블럭을 수 백개 만들어야 하는 불상사가 발생한다....

따라서, 하나의 범주로 묶어버리면 main 메서드의 코드가 더 간결해지기 때문에 유용하게 사용할 수 있다.


한 가지 더 묶는 이유가 있는데 바로 checked 예외를 unchecked 예외로 바꾸기 위해서이다.

과거에는 checked 예외로 필수 처리를 선호하였는데 최근에는 unchecked 예외를 통한 선택 처리를 선호하게 되었다. checked 예외로 할 경우 try-catch문을 작성해야 하므로 코드 난잡해질뿐더러 그렇다고 상속관계를 변경하기에는 부담이 크다.

그래서 예외를 감싸 하나로 묶어 unchecked 예외로 변경하는 방법을 사용한다.

static void startInstall() throws SpaceException,MemoryException{
		if(!enoughSpace()) {
			throw new SpaceException("설치 공간 부족");
		}
		if(!enoughMemory()) {
			throw new MemoryException("메모리 부족");
		}
	}

위에 있던 예시 중 일부를 가져온 것이다.

두 예외 클래스 모두 Exception을 상속하기 때문에 필수 처리해야 한다. 그렇다고 상속 관계를 바꿀 수는 없다. 지금이야 코드 몇 줄이지만 큰 프로젝트에서는 예외 클래스에 수많은 코드가 연관되어 있기 때문이다.

MemoryException을 unchecked 예외로 바꾸어보겠다.

static void startInstall() throws SpaceException{
		if(!enoughSpace()) {
			throw new SpaceException("설치 공간 부족");
		}
		if(!enoughMemory()) {
			throw new RuntimeException(new MemoryException("메모리 부족"));
		}
	}

선택 처리 예외인 RuntimeException 인스턴스로 감싸주기만 하면 된다.
이렇게 하면 상속관계를 변경하지 않아도 되고 RuntimeException은 선택 처리 예외이기 때문에 throws 해주지 않아도 된다.

0개의 댓글