도도한 개발자

[Java] #14. 예외 (Exception) 본문

Backend/Java

[Java] #14. 예외 (Exception)

Kiara Kim 2022. 3. 16. 22:45

자바에서는 예외적인 일 발생하게 되면 "예외" 라는 것을 던져버린다. null인 객체에 메소들르 호출한다든디, 5개의 공간을 가지는 배열을 만들고 6번째 값을 불러온다든지, 어떤 파일을 읽으라고 했는데 읽을 파일이 존재하지 않는다든지, 네트워크 연결이 되어 있는 어떤 서버가 갑자기 작동을 멈춰서 연결이 끊겨버린다는지 하는 경우가 여기에 속한다.

 

* try-catch

 

배열 범위 밖의 값을 읽으려고 할 때를 살펴보자.

 

public class ExceptionSample {

	public static void main(String[] args) {
		ExceptionSample sample = new ExceptionSample();
		sample.arrayOutOfBounds();
	}
	
	public void arrayOutOfBounds() {
		int[] intArray = new int[5];
		System.out.println(intArray[5]);
	}
}

 

main() 메소드에서 arrayOutOfBounds() 라는 메소드를 호출했다. arrayOutOfBounds() 메소드에선 5개의 공간을 가지는 int 타입의 intArray 배열을 만들고 이 배열의 6번째 값을 출력한다. 얼핏 보면 컴파일이 안될 것 같지만 자바에선 이런 잘못을 컴파일 시 점검해주지 않아 정상적으로 컴파일된다. 그러나 실행해보면

 

Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 5 out of bounds for length 5
	at chapter14.ExceptionSample.arrayOutOfBounds(ExceptionSample.java:38)
	at chapter14.ExceptionSample.main(ExceptionSample.java:33)

 

ArrayIndexOutOfBoundsException이라는 것이 발생한다. 이는 "배열 범위 밖에 있는 위치를 요청한 예외"라는 의미이다. 예외의 첫 줄에는 어떤 예외가 발생했다고 출력된다. 이 호출 관계의 가장 윗줄에는 예외가 발생한 클래스와 메소드 이름과 줄의 번호를 출력하고, 그 아래에는 메소드를 호출한 클래스(chapter14.ExceptionSample)와 메소드 이름(arrayOutOfBounds) 및 줄의 번호(38)가 출력된다.

 

예외를 처리하기 위해서 다음과 같이 arrayOutOfBoundsTryCatch() 메소드를 만들었다.

 

public void arrayOutOfBoundsTryCatch() {
		int[] intArray = new int[5];
		try {
			System.out.println(intArray[5]);
		} catch(Exception e) {
        
		}
	}

 

방금 추가한 것이 예외를 처리하는 "try-catch 블록"이다. try 뒤에 중괄호로 예외가 발생하는 문장들을 묶어주고, catch 괄호 안에 예외가 발생했을 때 처리를 해준다. 이 예제처럼 예외가 발생하는 부분만 중괄호로 묶어주면 된다.

 

이렇게 try-catch로 묶어 주면 정상적으로 컴파일도 되고 실행도 되는 것으로 보이지만 실제로는 예외가 발생한 것이다. 이를 확인하기 위해 출력문을 추가해보자.

 

public void arrayOutOfBoundsTryCatch() {
	int[] intArray = new int[5];
	try {
		System.out.println(intArray[5]);
		System.out.println("This code should run");
	} catch(Exception e) {
		System.err.println("Exception occured");
	}
	System.out.println("This code must run");
}

 

catch 문에 있는 System.err에 대하여, 오류가 발생하는 부분에는 이렇게 사용하는 것을 생활화 하는 것이 좋다. eclipse와 같은 IDE에선 출력 결과가 다른 색으로 표기되기 때문이다. 

 

먼저 결과를 보자.

 

Exception occured
This code must run

 

try-catch를 사용할 때 try 블록 내에서 예외가 발생하면, 예외가 발생한 줄 이후에 있는 try 블록 내의 코드들은 수행되지 않는다. 그 다음에 catch 블록에 있는 문장이 실행된다. 다시 말해, try 블록 내에서 예외가 발생하지 않으면 catch 블록 코드는 실행되지 않는다. 지금까지의 내용을 정리해보자.

 

· try-catch에서 예외가 발생하지 않을 경우

   try 내에 있는 모든 문장이 실행되고 try-catch 문장 이후의 내용이 실행된다.

· try-catch에서 예외가 발생하는 경우

   try 내에서 예외가 발생한 이후의 문장들은 실행되지 않는다.

   catch 내에 있는 문장을 반드시 실행되고, try-catch 문장 이후의 내용이 실행된다.

 

** 일반적으로 catch 문장에서 사용할 변수에 대해서는 try 앞에 미리 선언해 놓는다.

 

 

* finally

 

단어의 의미는 "드디어, 마침내"의 의미다. 자바에서 예외를 처리할 때 finally는 "어떠한 경우에도 반드시 실행하라"라는 의미이다. 다음과 같이 새로운 클래스를 만들어 확인해보자.

 

public class FinallySample {

	public static void main(String[] args) {
		FinallySample sample = new FinallySample();
		sample.finallySample();
	}
	
	public void finallySample() {
		int[] intArray = new int[5];
		try {
			System.out.println(intArray[5]); // @
		}catch (Exception e) {
			System.out.println(intArray.length);
		}finally {
			System.out.println("Here is finally");
		}
		System.out.println("This code must run");
	}
}

 

결과는 다음과 같다.

 

5
Here is finally
This code must run

 

@에서 예외가 발생하여 catch 블록이 실행되고, finallt 블록이 실행된 후 try-catch 이후의 문장이 실행된 것을 알 수 있다. 예외가 발생하지 않도록 @의 문장에서 배열의 위치를 5가 아닌 4로 바꿔보면 결과는 다음과 같다.

 

0
Here is finally
This code must run

 

배열의 초기화만 하면 각 타입의 기본값이 할당된다. int의 기본값은 0이므로 위와 같이 0이 출력된 것을 볼 수 있다.

 

** finally 블록은 코드의 중복을 피하기 위해서 반드시 필요하다.

 

 

* 두 개 이상의 catch

 

try-catch 문을 쓸 때 catch에 Exception e라고 썼는데 이는 예외의 종류를 명시한 것이다. 즉, 다른 예외도 있다는 뜻이다. 여러 catch 문을 사용하는 클래스를 만들어보자. 우선 Exception e를 포함하자.

 

public class MultiCatchSample {

	public static void main(String[] args) {
		MultiCatchSample sample = new MultiCatchSample();
		sample.multiCatch();
	}
	
	public void multiCatch() {
		int[] intArray = new int[5];
		try {
			System.out.println(intArray[5]);
		} catch (Exception e) {
			System.out.println(intArray.length);
		}
	}
}

 

여기에 다른 catch 블록을 추가하자.

 

public void multiCatch() {
	int[] intArray = new int[5];
	try {
		System.out.println(intArray[5]);
	} catch (ArrayIndexOutOfBoundsException e) {
		System.out.println("ArrayIndexOutOfBoundsException occured");
	} catch (Exception e) {
		System.out.println(intArray.length);
	}
}

 

예외는 어차피 발생을 하지만 어떤 결과가 나올까?

 

ArrayIndexOutOfBoundsException occured

 

단지 앞에 있는 catch로 처리하는 것이 아니다. Exception에 대해, 모든 예외의 부모 클래스는 java.lang.Exception 클래스다. 만약 (Exception e) catch 문이 (ArrayIndexOutOfBoundsException e) catch 문보다 앞에 있다면 코드는 컴파일조차 되지 않을 것이다. 예외는, 부모 예외 클래스가 이미 catch를 하고, 자식 클래스가 그 아래에서 catch를 하도록 되어 있을 경우 자식 클래스가 예외를 처리할 기회가 없다. ArrayIndexOutOfBoundsException은 Exception 클래스의 자식 클래스이기 때문에 절대로 Exception 클래스로 처리한 catch 블록 이후에 선언한 블록은 처리될 일이 없다. 

 

그렇다면 왜 이렇게 여러 catch를 쓸 수 있게 한 걸까? 다음과 같이 추가 예외를 처리하는 메소드를 만들자.

 

public void multiCatch() {
	int[] intArray = new int[5];
	try {
    	intArray = null;	// @1
		System.out.println(intArray[5]);
	} catch(NullPointerException e) {	//@2
    	System.out.println("NullPointerException occured");
    } catch (ArrayIndexOutOfBoundsException e) {
		System.out.println("ArrayIndexOutOfBoundsException occured");
	} catch (Exception e) {
		System.out.println(intArray.length);
	}
}

 

@1과 @2를 추가했다. intArray를 null로 만들어 버리고 null인 intArray의 5번째 값을 출력하라고 했다. 결과는

 

NullPointerException occured

 

이렇게 나온다. 왜 그럴까? intArray의 5번째 값을 찾는 작업을 하기 전에 해당 객체가 null인지 확인하는 작업을 먼저 할지, 아니면 그 반대일지를 생각해보자. null인 객체를 갖고 작업하면 안 되기 때문에 해당 객체가 null인지 확인하는 작업은 반드시 선행되어야만 한다. 그리고 catch문을 사용할 때에는 지금과 같이 Exception 클래스로 catch 하는 것을 가장 하래에 추가하는 것이 권장된다. 예상치도 못한 예외가 발생하면 제대로 처리되지 않기 때문에 예외들이 빠져 나가지 않게 꽁꽁 묶어 주는 것이 좋다. 정리해보자.

 

· try 다음에 오는 catch 블록은 1개 이상 올 수 있다.

· 먼저 선언한 catch 블록의 예외 클래스가 다음에 선언한 catch 블록의 부모에 속하면, 자식이 속하는 catch 블록은 절대 실행될 일이 없으므로 컴파일이 되지 않는다.

· 하나의 try 블록에서 예외가 발생하면 그 예외와 관련이 있는 catch 블록을 찾아서 실행한다.

· catch 블록 중 발생한 예외와 관련있는 블록이 없으면, 예외가 발생되면서 해당 쓰레드는 끝난다. 따라서 catch 블록에는 Exception 클래스로 묶어주는 것을 생활화 하자.

 

 

* 예외의 종류는 세 가지

 

· checked exception

· error

· runtime exception 혹은 unchecked exception

 

error와 unchecked exception을 제외한  모든 예외는 checked exception이다. 먼저 error에 대해 알아보자.

 

** error

 

에러는 자바 프로그램 밖에서 발생한 예외를 말한다. Exception 클래스는 에러가 아니다. 뭔가 자바 프로그램에 오류가 발생했을 때, 오류의 이름이 Error로 끝나면 에러이고, Exception으로 끝나면 예외이다. Error와 Exception으로 끝나는 오류의 가장 큰 차이는 프로그램 안에서 발생했는지, 밖에서 발생했는지 여부이다. 하지만 더 큰 차이는 프로그램이 멈추어 버리느냐, 계속 실행할 수 있느냐의 차이다. 즉, Error는 프로세스에 영향을 주고, Exception은 쓰레드에만 영향을 준다.

 

** runtime exception

 

런타입 예외는 예외가 발생할 것을 미리 감지하지 못했을 때 발생한다. 이에 해당하는 모든 예외들은 RunTimeException을 확장한 예외들이다. 이 예외를 묶어주지 않는다고 해서 컴파일할 때 예외가 발생하지 않지만 실행시 발생할 가능성이 있다. 그리고 컴파일시 체크를 하지 않기 때문에 unchecked exception이라고도 부르는 것이다.

 

 

* 모든 예외의 조상. java.lang.Throwable 클래스

 

앞에서 본 Exception과 Error의 공통 부모 클래스는 Object 클래스다. 그리고 공통 부모 클래스가 하나 더 있는데, 바로 java.lang 패키지에 선언된 Throwable 클래스다. 상속관계가 이렇데게 되어 있는 이유는 둘의 성격은 다르지만 모두 동일한 이름의 메소드를 사용하여 처리할 수 있도록 하기 위함이다. Throwable에 어떤 생성자가 있는지 보자.

 

· Throwable()

· Throwable(String message)

· Throwable(String message, Throwable cause)

· Throwable(Throwable cause)

 

Throwable 클래스에 선언되어 있고, Exception 클래스에서 Overriding한 메소드는 10개가 넘는다. 그 중 가장 많이 사용되고 있는 메소드를 보자.

 

· getMessage()

· toString()

· printStackTrace()

 

각 메소드에 대해 알아보자.

 

** getMessage()

 

예외 메시지를 String 형태로 제공 받는다. 메세지를 활용하여 별도의 예외 메시지를 사용자에게 보여주려고 할 때 좋다.

 

** toString()

 

예외 메세지를 String 형태로 제공 받는다. getMessage() 메소드보다는 더 자세하게, 예외 클래스 이름도 같이 제공한다.

 

** printStackTrace()

 

가장 첫 줄에는 예외 메시지를 출력하고, 두 번째 줄부터는 예외가 발생하게 된 메소드들의 호출 관계(스택 트레이스)를 출력한다.

 

 

* 예외를 던지는 throws

 

지금까지는 예외를 처리하는 방법에 대해 다뤘다. 이제는 예외를 발생시키는 방법을 알아보자. 자바에서는 예외를 던질 수 있다. 다음의 소스를 보자.

 

public class ThrowSample {

	public static void main(String[] args) {
		ThrowSample sample = new ThrowSample();
		sample.throwException(13);	
	}
	
	public void throwException(int number) {
		try {
			if(number > 12) {
				throw new Exception("Number is over than 12");
			}
			System.out.println("Number is " + number);
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
}

 

예외를 발생시키고자 할 때 try 블록 내에서 throw라고 명시한 후 개발자가 예외 클래스의 객체를 생성하면 된다. 그러면 다른 예외가 발생한 상황과 동일하게 throw한 문장 이후에 있는 모든 try 블록 내의 문장들은 무시되고, catch 블록으로 이동한다. catch 블록 중 throw한 예외와 동일하거나 상속 관계에 있는 예외가 있다면 그 블록에서 예외를 처리할 수 있다. 

 

만약 해당하는 예외가 없다면 발생된 예외는 메소드 밖으로 던져버린다. 즉, 예외가 발생된 메소드를 호출한 메소드로 던진다는 말이다. 이럴 때 사용하는 것이 throws 구문이다. throws 구문은 아래와 같이 메소드 선언할 때 사용하면 된다.

 

public void throwsException(int number) throws Exception {
	if(number > 12) {
		throw new Exception("Number is over than 12");
	}
	System.out.println("Number is " + number);
}

 

이렇게 메소드 선언에 해놓으면, 예외가 발생했을 때 try-catch 로 묶어주지 않아도 그 메소드를 호출한 메소드로 예외 처리를 위임하는 것이기 때문에 전혀 문제가 되지 않는다. 그러나 이렇게 throws로 메소드를 선언하면 개발이 어려워진다. 왜냐하면 이 메소드는 Exception을 던진다고 메소드 선언부에 throws 선언을 해놓았기 때문에, throwsException() 메소드를 호출한 메소드에서 반드시 다음과 같이 try-catch 블록으로 그 메소드를 감싸주어야만 한다.

 

public static void main(String[] args) {
	ThrowSample sample = new ThrowSample();
	try {
		sample.throwsException(13);
	} catch (Exception e) {
		
	}	
}

 

지금까지의 예외 throw와 throws에 대해 정리해보자.

 

· 메소드를 선언할 때 매개 변수 소괄호 뒤에 throws라는 예약어를 적어 준 뒤 예외를 선언하면, 해당 메소드에서 선언한 예외가 발생했을 때 호출한 메소드로 예외가 전달된다. 

   만약 메소드에서 두 가지 이상의 예외를 던질 수 있다면, implements처럼 콤마로 구분하여 예외 클래스 이름을 적어주면 된다.

· try 블록 내에서 예외를 발생시킬 경우에는 throw라는 예약어를 적어 준 뒤 예외 객체를 생성하거나, 생성되어있는 객체를 명시해준다.

   throw한 예외 클래스가 catch 블록에 선언되어 있지 않거나, throws 선언에 포함되어 있지 않으면 컴파일 에러가 발생한다.

·  catch 블록에서 예외를 throw할 경우에도 메소드 선언의 throws 구문에 해당 예외가 정의되어 있어야만 한다.

 

 

* 자바 예외 처리 전략

 

자바에 예외를 처리할 때에는 표준을 정해 두어야만 한다.

 

· 임의의 예외 클래스를 만들 때에는, 반드시 try-catch로 묶어줄 필요가 있을 경우에만 Exception 클래스를 확장한다.

  일반적으로 실행시 예외를 처리할 수 있는 경우에는 RuntimeException 클래스를 확장하는 것을 권장한다.

· catch 문 내에 아무런 작업 없이 공백을 놔두면 예외 분석이 어려워지므로 꼭 로그 처리와 같은 예외 처리를 해줘야만 한다.

 

 

* 실습

 

package chapter14;

public class Calculator {

	public static void main(String[] args) {
		Calculator calc = new Calculator();
		try {
			calc.printDivide(1, 2);
			calc.printDivide(1, 0);
		} catch (Exception e) {
			System.out.println(e.getMessage());
		}
		
	}
	
	public void printDivide(double d1, double d2) throws Exception {
		if(d2 == 0) 
			throw new Exception("Second value can't be Zero");
		double result = d1 / d2;
		System.out.println(result);
	}
}

 

 

* 정리

 

예외를 처리하기 위한 세가지 블록에는 어떤 것이 있나요?
예외는 try - catch - finally 블록으로 처리한다.


첫번째 문제의 답 중에서 "여기에서 예외가 발생할 것이니 조심하세요"라고 선언하는 블록은 어떤 블록인가요? 
예외가 발생 가능한 부분을 try 블록으로 묶어 준다.


첫번째 문제의 답 중에서 "예외가 발생하던 안하던 얘는 반드시 실행되어야 됩니다."라는 블록은 어떤 블록인가요?
finally 블록은 예외 발생 여부와 상관 없이 무조건 실행하도록 할 때 사용한다.


예외의 종류 세가지는 각각 무엇인가요?
예외는 다음의 3가지로 나뉜다.
- checked exception
- error
- runtime exception 혹은 unchecked exception


프로세스에 치명적인 영향을 주는 문제가 발생한 것을 무엇이라고 하나요?
error는 치명적인 오류를 의미한다. 기본적으로는 프로그램 내에서 발생한다기 보다는 JVM 이나 시스템에서 문제가 발생했을 때 error가 발생한다.


try나 catch 블록 내에서 예외를 발생시키는 키워드는 무엇인가요?
throw를 사용하여 새로운 예외를 발생시키면, 해당 예외를 호출한 메소드로 던진다.


메소드 선언시 어떤 예외를 던질 수도 있다고 선언할 때 사용하는 키워드는 무엇인가요?
throw가 메소드 내에 있다면 메소드 선언시 throws 를 사용하여 던질 예외의 종류를 명시하는 것이 좋다.
Exception클래스를 확장하여 예외 클래스를 만들 수 있다.


직접 예외를 만들 때 어떤 클래스의 상속을 받아서 만들어야만 하나요?

하지만, 이렇게 되면 무조건 해당 예외를 던지는 메소드에서 try-catch로 묶어야 한다는 단점이 있다.
따라서, RuntimeException 클래스를 확장하여 선언하는 것을 권장한다.

 

그럼 이만.