[java] 19. 예외처리(2)


메서드에 예외 선언하기



예외를 처리하는 방법에는 지금까지 배워 온 try-catch 문을 사용하는 것 외에,

예외를 메서드에 선언하는 방법이 있습니다.

메서드에 예외를 선언하려면, 메서드의 선언부에 키워드 throws를 사용해서

메서드 내에서 발생할 수 있는 예외를 적어주기만 하면 됩니다.


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



만일 아래와 같이 모든 예외의 최고 부모인 Exception 클래스를 메서드에 선언하면,

이 메서드는 모든 종류의 예외가 발생할 가능성이 있다는 뜻입니다.


void method() throws Exception{
	//메서드의 내용
}



이렇게 예외를 선언하면, 이 예외뿐만 아니라 그 자식타입의 예외까지도 발생할 수 있습니다.

메서드의 선언부에 예외를 선언함으로써 메서드를 사용하려는 사람이 메서드의 선언부를 보았을때,

이 메서드를 사용하기 위해서는 어떠한 예외들이 처리되어져야 하는지 쉽게 알 수 있습니다.

사실 예외를 메서드의 throws에 명시하는 것은 예외를 처리하는 것이 아니라,

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

예외를 전달받은 메서드가 또다시 자신을 호출한 메서드에게 전달할 수 있으며,

이런 식으로 계속 호출스택에 있는 메서드들을 따라 전달되다가 제일 마지막에 있는 main 메서드에서도

예외가 처리되지 않으면, main 메서드마저 종료되어 프로그램이 전체가 종료됩니다.

다음 예제를 보시죠!


image



위의 결과로부터 다음과 같은 사실을 알 수 있습니다.

method2()에서 throw new Exception(); 문장에 의해 예외가 강제적으로 발생했으나

try-catch문으로 예외처리를 해주지 않았으므로, method2()는 종료되면서

예외를 자신으 호출한 method1()에게 넘겨줍니다.

method1()에서도 역시 예외처리를 해주지 않았으므로 종료되면서 main메서드에게 예외를 넘겨줍니다.

그러나 main 메서드에서 조차 예외처리를 하지 않았으므로 main 메서드가 종료되어

프로그램이 예외로 인해 비정상적으로 종료되는 것입니다.

이처럼 예외가 발생한 메서드에서 예외처리를 하지 않고 자신을 호출한 메서드에게 예외를 넘겨줄 수는 있지만,

이것으로 예외처리된 것은 아니고 예외를 단순히 전달만 하는 것입니다.

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

<method1()에서 예외처리한 경우>


image



<main()에서 예외처리 한 경우>


image



만약 예외가 발생한 메서드(method1) 내에서 처리되어지면,

호출한 메서드(main)에서는 예외가 발생했다는 사실조차 모르게 됩니다.



finally 블럭



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

try-catch문의 끝에 선택적으로 덧붙여 사용할 수 있으며,

try-catch-finally의 순서로 구성됩니다.


try{

	//예외가 발생할 가능성이 있는 문장들을 넣는다.
	
}catch(Exception1 e1){

	//예외처리를 위한 문장을 적는다.
	
}finally{

	//예외의 발생여부에 관계 없이 항상 수행되어야하는 문장들을 적는다.
	//finally 블럭은 try-catch 문의 맨 마지막에 위치해야 한다.

}



예외가 발생한 경우에는 try->catch->finally 순으로 실행되고,

예외가 발생하지 않은 경우에는 try->finally의 순으로 실행됩니다.

다음 예제를 보시죠!


image



위의 예제가 하는 일은 프로그램설치를 위한 준비를 하고 파일들을 복사하고 설치가 완료되면,

프로그램을 설치하는데 사용된 임시파일들을 삭제하는 순서로 진행됩니다.

프로그램의 설치과정 중에 예외가 발생하더라도, 설치에 사용된 임시파일들이 삭제되도록 catch블럭에

deleteTempFiles() 메서드를 넣었습니다.

결국 try블럭의 문장을 수행하는 동안에, 예외의 발생여부에 관계없이 deleteTempFiles() 메서드는 실행되야합니다.

이럴 때 finally 블럭을 사용하면 좋습니다.


image



또한 try블럭에서 return문이 실행되는 경우에도 finally 블럭의 문장들이 먼저 실행된 후에,

현재 실행 중인 메서드를 종료합니다.


image



자동 자원 반환 -try-with-resources 문



JDK 1.7부터 try-with-resources문이라는 try-catch 문의 변형이 새로 추가되었습니다.

이 구문은 주로 입출력(I/O)과 관련된 클래스를 사용할 때 유용한데,

아직 입출력에 대해 배우지 않았으므로 유용함을 느끼기는 아직 이를 것입니다.

입출력을 배우기 전까지 그냥 참고만 하시길 바랍니다.

주로 입출력에 사용되는 클래스 중에서는 사용한 후에 꼭 닫아 줘야 하는 것들이 있습니다.

그래야 사용했던 자원이 반환되기 때문입니다.


fis=new FileInputStream("score.dat");
dis=new DataInputStream(fis);

try{
	...
}catch(IOException ie){
	ie.printStackTrace();
}finally{
	dis.close(); //작업 중에 예외가 발생하더라도, dis가 닫힘
}



위의 코드는 DataInputStream을 사용해서 파일로부터 데이터를 읽는 코드인데,

데이터를 읽는 도중에 예외가 발생하더라도 DataInputStream이 닫히도록 finally 블럭 안에 close()를 넣었습니다.

하지만, close()도 예외를 발생시킬 수 있기에, 다음과 같이 코드를 수정해야합니다.


fis=new FileInputStream("score.dat");
dis=new DataInputStream(fis);

try{
	...
}catch(IOException ie){
	ie.printStackTrace();
}finally{
	try{ //close()에 대한 예외처리
		if(dis!=null)
			dis.close();
	}catch(IOException ie){
		ie.printStackTrace();
	}
}



finally 블럭 안에 try-catch 문을 추가해서 close()에서 발생할 수 있는 예외를 처리하도록 변경했는데,

코드가 복잡해져서 별로 보기에 좋지 않습니다.

또, try 블럭과 finally 블럭에서 모두 예외가 발생하면, finally 블럭에서 예외가 처리될 때 try 블럭에서의 예외는 무시됩니다.
(try문에서의 예제가 먼저 처리되더라도 finally 예외가 발생했을때 try 예외는 잊혀지고 finally 예외만 처리함)

이러한 점을 개선하기 위해서 try-with-resources 문이 추가되었습니다.


try(FileInputStrem fis = new FileInputStream("score.dat");
	DataInputStream dis = new DataInputStream(fis)){
	...
}catch (IOException ie){
	ie.printStackTrace();
}



try-with-resources 문의 괄호 ()안에 객체를 생성하는 문장을 넣으면,

이 객체는 따로 close()를 호출하지 않아도 try 블럭을 벗어나는 순간 자동적으로

close()가 호출됩니다.

그 다음에 catch블럭 또는 finally 블럭이 수행됩니다.

이처럼 try-with-resources 문에 의해 자동으로 객체의 close()가 호출될 수 있으려면,

클래스가 AutoCloseable이라는 인터페이스를 구현한 것이어야만 합니다.


public interface AutoCloseable{
	void close() throws Exception;
}



위의 인터페이스는 각 클래스에서 적절히 자원 반환작업을 하도록 구현되어 있습니다.

그런데, 위의 코드를 잘 보면 close()도 Exception을 발생시킬 수 있습니다.

만일 자동 호출된 close()에서 예외가 발생하면 어떻게 될까요?

예제를 보시죠!

일단, 인터페이스 AutoCloseable을 구현한 클래스 CloseableResource를 정의하고,

예외 클래스인 WorkException 과 CloseException 을 정의하겠습니다.


image



그리고 try-with-resources 문을 사용하여 CloseableResource의 인스턴스를 생성해보겠습니다.


image



image



일단 예제를 봤을 때 try-with-Resources를 이용하면

try문이 끝날 때 자동적으로 CloseableResource의 close() 메서드가 호출되는 것을 알 수 있습니다.

첫번 째는 close()에서만 예외를 발생시킨 것이고,

두번 째는 exceptionWork()와 close()에서 모두 예외를 발생시킨것입니다.

첫 번째는 일반적인 예외가 발생했을 때와 같은 형태로 출력되지만,

두 번째는 출력형태가 다릅니다.

먼저, exceptionWork()에서 발생한 예외에 대한 내용이 출력되고,

close()에서 발생한 예외는 억제된(suppressed)이라는 머리말과 함께 출력되었습니다.

두 예외가 동시에 발생할 수 없기 때문에, 실제 발생한 예외를 WorkException으로 하고,

CloseException은 억제된 예외로 다룹니다.

억제된 예외에 대한 정보는 실제로 발생한 예외인 WorkException에 저장됩니다.

만일 기존의 try-catch 문을 사용했다면, 먼저 발생한 WorkException은 무시되고,

마지막으로 발생한 CloseException에 대한 내용만 출력되었을 것입니다.



사용자 정의 예외 만들기



기존의 정의된 예외 클래스 외에 필요에 따라 프로그래머가 새로운 예외 클래스를 정의하여 사용할 수 있습니다.

보통 Exception 클래스로부터 상속받는 클래스를 만들지만, 필요에 따라서 알맞은 예외 클래스를 선택할 수 있습니다.


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



Exception 클래스로부터 상속받아서 MyException 클래스를 만들었습니다.

필요하다면 다음과 같이 멤버변수나 메서드를 추가할 수 있습니다.


class MyException extends Exception{
	//에러 코드 값을 저장하기 위한 필드를 추가
	private final int ERR_CODE; //생성자를 통해 초기화
	
	MyException(String msg, int errCode){
		super(msg);
		ERR_CODE=errCode;
	}
	
	MyException(String msg){
		this(msg,100);
	}
	
	public int getErrCode(){
		return ERR_CODE;
	}
}



이전의 코드를 좀더 개선하여 메시지뿐만 아니라 에러코드 값도 저장할 수 있도록

ERR_CODE 와 getErrCode()를 MyException 클래스의 멤버로 추가했습니다.

기존의 예외 클래스는 주로 Exception을 상속받아서 ‘cheked 예외’로 작성하는 경우가 많았지만,

요즘은 예외처리를 선택적으로 할 수 있도록 RuntimeException을 상속받아서 작성하는 쪽으로 바뀌어가고 있습니다.

checked 예외는 반드시 예외처리를 해주어야 하기 때문에 예외처리가 불필요한 경우에도

try-catch문을 넣어서 코드가 복잡해지기 때문입니다.

따라서 필요에 따라 예외처리의 여부를 선택할 수 있는 unchecked 예외가 강제적인 checked 예외보다 더 자주 쓰이는 추세입니다.

다음은 Exception을 상속받는 예외 클래스 SpaceException, MemoryException를 정의하여

설치프로그램을 구현한 것입니다.


image


image



예외 되던지기



한 메서드에서 발생할 수 있는 예외가 여럿인 경우, 몇 개는 try-catch문을 통해서

메서드 내에서 자체적으로 처리하고, 그 나머지는 선언부에 지정하여 호출한 메서드에서 처리하도록 함으로써,

양쪽에서 나눠서 처리되도록 할 수 있습니다.

심지어 단 하나의 예외에 대해서도 예외가 발생한 메서드와 호출한 메서드, 양쪽에서 처리하도록 할 수 있습니다.

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

이것을 예외 되던지기라고 합니다.

먼저 예외가 발생할 가능성이 있는 메서드에서 try-catch 문을 사용해서 예외를 처리해주고 catch문에서 필요한 작업을 행한 후에

throw문을 사용해서 예외를 다시 발생시킵니다.

다시 발생한 예외는 이 메서드를 호출한 메서드에게 전달되고 호출한 메서드의 try-catch문에서 예외를 또다시 처리합니다.

이 방법은 하나의 예외에 대해서 예외가 발생한 메서드와

이를 호출한 메서드 양쪽 모두에서 처리해줘야 할 작업이 있을 때 사용됩니다.


image



결과에서 알 수 있듯이 method1()과 main 메서드 양쪽의 catch 블럭이 모두 수행되었음을 알 수 있습니다.

만약, 반환값이 있는 return문의 경우, catch 블럭에도 return 문이 있어야 합니다.

예외가 발생 했을 경우에도 값을 반환해야하기 때문입니다.

하지만 catch 블럭에서 예외 되던지기를 해서 호출한 메서드로 예외를 전달하면, return문이 없어도 됩니다.



연결된 예외



한 예외가 다른예외를 발생시킬 수도 있습니다.

예를 들어 예외 A가 예외 B를 발생시켰다면, A를 B의 원인 예외(cause exception)라고 합니다.


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



먼저, InstallException을 생성한 후에, initCause()로 SpaceException을 InstallException의 원인 예제로 등록합니다.

그리고 throw로 이 예외를 던집니다.

initCause()는 Exception클래스의 부모인 Throwalbe 클래스에 정의되어 있기 때문에 모든 예외에서 사용가능합니다.


Throwable initCase(Throwable cause) 지정된 예외를 원인 예외로 등록


Throwable getCause() 원인 예외를 반환



발생한 예외를 그냥 처리하지 않고, 원인 예외로 등록해서 다시 예외를 발생시키는 이유는

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

그렇다고 아래와 같이 InstallException을 SpaceException과 MemoryException의 부모로 해서 catch 블럭을 작성하면,

실제로 발생한 예외가 어떤 것인지 알 수 없는 문제가 생깁니다.

또, SpaceException과 MemoryException의 상속관계를 변경해야 합니다.


try{
	startInstall();
	copyFiles();
}catch(InstallException e){ //InstallException은 SpaceException과 MemoryException의 부모
	e.printStackTrace(); 
	}
}



그래서 생각한 것이 예외가 원인 예외를 포함시킬 수 있게 한 것입니다. 이렇게 하면,

두 예외는 상속관계가 아니어도 됩니다.

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

checked 예외로 예외처리를 강제한 이유는 프로그래밍 경험이 적은 사람도 보다 견고한 프로그램을 작성할 수 있도록 유도하기 위한 것인데,

지금은 자바가 처음 개발되던 1990년대와 컴퓨터 환경이 많이 달라졌습니다.

그래서 checked 예외가 발생해도 예외를 처리할 수 없는 상황이 하나둘 발생하기 시작했습니다.

이럴 때 할 수 있는 일이라곤 그저 의미없는 try-catch 문을 추가하는 것뿐인데,

checked 예외를 unchecked 예외로 바꾸면 예외처리가 선택적이 되므로 억지로 예외처리를 안해도 됩니다.


//checked 예외


static void startInstall() throws SpaceException, MemoryException{
	if(!enoughSpace()) //충분한 설치 공간이 없으면...
		throw new SpaceException("설치할 공간이 부족합니다.");
	if(!enoughMemory()) //충분한 메모리가 없으면...
		throw new MemoryException("메모리가 부족합니다.");
}




//원인 예외 등록해서 unchecked로 바꿈


static void startInstall() throws SpaceException{
	if(!enoughSpace()) //충분한 설치 공간이 없으면...
		throw new SpaceException("설치할 공간이 부족합니다");
	
	if(!enoughMemory()) //충분한 메모리가 없으면..
		//MemoryException을 원인 예제로 하는 RuntimeException 발생시킴(unchecked)
		throw new RuntimeException(new MemoryException("메모리가 부족합니다."));
}



MemoryException은 Exception의 자식이므로 반드시 예외를 처리해야하는데,

이 예외를 RuntimeException으로 감싸버렸기 떄문에 unchecked 예외가 되었습니다.

그래서 더이상 startInstall()의 선언부에는 MemoryException을 선언하지 않아도 됩니다.

참고로 위의 코드에서는 initCause() 대신 RuntimeException의 생성자를 사용했습니다.

다음은 예외 원인 등록에 대한 간단한 예제입니다.


image