본문 바로가기
Back-end

예외처리(4)

by 신재권 2021. 6. 22.

finally 블럭

finally 블럭은 예외의 발생 여부에 상관없이 실행되어야할 코드를 포함시킬 목적으로 사용된다. try-catch문의 끝에 선택적으로 덧붙여 사용할 수 있으며, try-catch-finally의 순서로 구성된다.

try{
	//예외가 발생할 가능성이 있는 문장들을 넣는다.
}catch(Exception1 e1){
		//예외처리를 위한 문장을 적는다.
}finally{
	//예외의 발생여부에 관계없이 항상 수행되어야하는 문장들을 넣는다.
	//finally블럭은 try-catch문의 맨 마지막에 위치해야 한다.
}

예외가 발생한 경우 try→catch→finally의 순으로 실행되고, 예외가 발생하지 않은 경우에는 try→finally의 순으로 실행된다.

public class FinallyTest {

	public static void main(String[] args) {
		try{
			startInstall();			//프로그램 설치에 필요한 준비를 한다.
			copyFiles();			//파일들을 복사한다.
			deleteTempFiles();  	//프로그램 설치에 사용된 임시파일들을 삭제한다.
		}catch(Exception e){
			e.printStackTrace();
			deleteTempFiles(); 		//프로그램 설치에 사용된 임시파일들을 삭제한다.
		}
	}
	
	static void startInstall(){
		//프로그램 설치에 필요한 준비를 하는 코드를 적는다.
	}
	static void copyFiles(){ 
		//파일을 복사하는 코드를 적는다
	}
	static void deleteTempFiles(){
		//임시파일들을 삭제하는 코드를 적는다.
	}

}

이 예제가 하는 일은 프롤그램설치를 위한 준비를 하고 파일들을 복사하고 설치가 완료되면 ,프로그램을 설치하는데 사용된 임시파일들을 삭제하는 순서로 진행된다.

프로그램의 설치과정 중에 예외가 발생하더라도, 설치에 사용된 임시파일들이 삭제되도록 catch블럭에 deleteTempFiles()메서드를 넣었다.

결국 try블럭의 문장을 수행하는 동안에 (프로그램을 설치하는 과정에), 예외의 발생여부에 관계없이 deleteTempFiles()메서드는 실행되어야 하는 것이다.

이럴때 finally 블럭을 사용하면 좋다. 아래의 코드는 위의 예제를 finally 블럭을 사용해서 변경한 것이며, 두 예제의 기능은 동일하다.

public class FinallyTest2 {

	public static void main(String[] args) {
		try{
			startInstall();			//프로그램 설치에 필요한 준비를 한다.
			copyFiles();			//파일들을 복사한다.
			
		}catch(Exception e){
			e.printStackTrace();
		}finally{
			deleteTempFiles(); 		//프로그램 설치에 사용된 임시파일들을 삭제한다.
		}
	}
	
	static void startInstall(){
		//프로그램 설치에 필요한 준비를 하는 코드를 적는다.
	}
	static void copyFiles(){ 
		//파일을 복사하는 코드를 적는다
	}
	static void deleteTempFiles(){
		//임시파일들을 삭제하는 코드를 적는다.
	}
	

}
public class FinallyTest3 {

	public static void main(String[] args) {
		//method1()은 static메서드이므로 인스턴스 생성없이 직접 호출이 가능하다.
		FinallyTest3.method1();
		System.out.println("method1()의 수행을 마치고 main메서드로 돌아왔습니다.");
	}

	static void method1(){
		try{
			System.out.println("method1 () 이 호출되었습니다.");
			return; //현재 실행중인 메서드를 종료한다.
		}catch(Exception e){
			e.printStackTrace();
		}finally{
			System.out.println("method1()의 finally 블럭이 실행되었습니다.");
		}
	}
}

위의 결과에서 알 수 있듯이, try블럭에서 return 문이 실행되는 경우에도 finally블럭의 문장들이 먼저 실행된 후에, 현재 실행중인 메서드를 종료한다.

이와 마찬가지로 catch블럭의 문장 수행 중에 return 문을 만나도 finally 블럭의 문장들은 수행된다.

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

JDK 1.7부터 try-with-resources문이라는 try-catch문의 변형이 새로 추가되었다. 이 구문은 주로 입출력과 관련된 클래스를 사용할 때 유용한데, 아직 입출력에 대해 배우지 않았으면 유용함을 느끼기에는 아직 이르다.

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

그래야 사용했던 자원(resources)이 반환되기 떄문이다.

try{
	fis = new FileInputStream("score.dat");
	dis = new DataInputStream(fis);
	...
}catch(IOException ie){
	ie.printStackTrace();
}finally{
	dis.close();  //작업 중에 예외가 발생하더라도, dis가 닫히도록 finally블럭에 넣음
}

의 코드는 DataInputStream을 사용해서 파일로부터 데이터를 읽는 코드인데, 데이터를 읽는 도중에 예외가 발생하더라도 DataINputStream이 닫히도록 finally 블럭 안에 close()를 넣었다. 여기까지는 별 문제가 없어 보이지만, 진짜 문제는 close()가 예외를 발생시킬 수 있다는데 있다. 그래서 위의 코드는 아래와 같이 해야 올바른 것이 된다.

try{
	fis = new FileInputStream("score.dat");
	dis = new DataInputStream(fis);
	...
}catch(IOException ie){
	ie.printStackTrace();
}finally{
	try{
		if(dis != null)
			dis.close();
	}catch(IOException ie){
		ie.printStackTrace();
	}
}

finally블럭 안에 try-catch문을 추가해서 close()에서 발생할 수 있는 예외를 처리하도록 변경했는데, 코드가 복잡해져서 별로 보기에 좋지 않다. 더 나쁜 것은 try블럭과 finally블럭에서 모두 예외가 발생하면, try블럭의 예외는 무시된다는 것이다.

이러한 점을 개선하기 위해서 try-with-resources문이 추가된 것이다. 위의 코드를 try-with-resources문으로 바꾸면 다음과 같다.

//괄호() 안에 두 문장 이상 넣을 경우 ';'로 구분한다.
try(FileInputStream fis = new FileInputStream("score.dat");
		DataInputStream dis = new DataInputStream(fis)) {
	while(true){
		score = dis.readInt();
		System.out.println(score);
		sum += score;
	}
}catch(EOFException e){
		System.out.println("점수의 총 합은 "+sum+"입니다.")
} catch(IOException ie){
		ie.printStackTrace();
}

try-with-resources문의 괄호 ()안에 객체를 생성하는 문장을 넣으면, 이 객체는 따로 close()를 호출하지 않아도 try블럭을 벗어나는 순간 자동적으로 close()가 호출된다. 그 다음에 catch블럭 또는 finally블럭이 수행된다.

  • try블럭의 괄호()안에 변수를 선언하는 것도 가능하며, 선언된 변수는 try블럭 내에서만 사용할 수 있다.

이처럼 try-with-resources문에 의해 자동으로 객체의 close()가 호출될 수 있으려면, 클래스가 AutoCloseable이라는 인터페이스를 구현한 것이어야만 한다.

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

위의 인터페이스는 각 클래스에서 적절히 자원 반환 작업을 하도록 구현되어 있다. 그런데 , 위의 코드를 잘 보면 close()도 Exception을 발생시킬 수 있다. 만일 자동으로 호출된 close()에서 예외가 발생하면 어떻게 될까?

public class TryWithResourceEx {

	public static void main(String[] args) {
		try(CloseableResource cr = new CloseableResource()){
			cr.exceptionWork(false); //예외가 발생하지 않는다.
		} catch(WorkException e){
			e.printStackTrace();
		}catch (CloseException e){
			e.printStackTrace();
		}
		System.out.println();
		
		try(CloseableResource cr =new CloseableResource()){
			cr.exceptionWork(true); //예외가 발생한다.
		}catch(WorkException e){
			e.printStackTrace();
		}catch(CloseException e){
			e.printStackTrace();
		}
	}

}

class CloseableResource implements AutoCloseable{
	public void exceptionWork(boolean exception) throws WorkException{
		System.out.println("exceptionWork("+exception+")가 호출됨 ");
		
		if(exception){
			throw new WorkException("WorkException발생!!");
		}
	}
	
	public void close() throws CloseException{
		System.out.println("close()가 호출됨");
		throw new CloseException("CloseException발생!!");
	}
}

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

class CloseException extends Exception{
	CloseException(String msg){super(msg);}
}
========================
exceptionWork(false)가 호출됨 
close()가 호출됨
CloseException: CloseException발생!!

exceptionWork(true)가 호출됨 
close()가 호출됨
	at CloseableResource.close(TryWithResourceEx.java:36)
	at TryWithResourceEx.main(TryWithResourceEx.java:7)
WorkException: WorkException발생!!
	at CloseableResource.exceptionWork(TryWithResourceEx.java:30)
	at TryWithResourceEx.main(TryWithResourceEx.java:15)
	Suppressed: CloseException: CloseException발생!!
		at CloseableResource.close(TryWithResourceEx.java:36)
		at TryWithResourceEx.main(TryWithResourceEx.java:16)

main 메서드에 두 개의 try-catch문이 있는데, 첫번째 것은 close()에서만 예외를 발생시키고, 두번 째 것은 exceptionWork()와 close()에서 모두 예외를 발생시킨다.

첫 번째는 일반적인 예외가 발생했을 때와 같은 형태로 출력되지만, 두 번째는 출력형태가 다르다. 먼저 exceptionWork()에서 발생한 예외에 대한 내용이 출력되고, close()에서 발생한 예외는 억제된(suppressed)이라는 의미의 머릿말과 함께 출력됬다.

두 예외가 동시에 발생할 수는 없기 때문에, 실제 발생한 예외를 WorkException으로 하고, CloseException은 억제된 예외로 다룬다. 억제된 예외에 대한 정보는 실제로 발생한 예외인 WorkException에 저장된다.

Throwable에는 억제된 예외와 관련된 다음과 같은 메서드가 정의되어 있다.

void addSuppressed(Throwable exception) //억제된 예외를 추가
Throwable[] getSuppressed() //억제된 예외(배열)를 반환

만일 기존의 try-catch문을 사용했다면, 먼저 발생한 WorkException은 무시되고, 마지막으로 발생한 CloseException에 대한 내용만 출력될 것이다.

'Back-end' 카테고리의 다른 글

java.lang패키지와 유용한 클래스(1)  (0) 2021.06.23
예외처리(5)  (0) 2021.06.23
예외처리(3)  (0) 2021.06.21
예외처리(2)  (0) 2021.06.21
예외처리(1)  (0) 2021.06.21