프로그래밍 언어/Java

[JAVA] 06. 예외처리

ssung.k 2021. 1. 7. 01:18

1. 예외처리(exception handling)

프로그램 오류

프로그램이 실행 중 어떤 원인에 의해서 오작동을 하거나 비정상적으로 종료되는 경우가 있습니다.

이러한 결과를 초래하는 원인을 프로그램 에러 또는 오류라고 합니다.

컴파일에러 : 컴파일 시 발생하는 에러
런타임에러 : 실행 시에 발생하는 에러
논리적에러 : 실행은 되지만 의도와 다르게 동작하는 에러

 

런타임에러를 방지하기 위해서는 프로그램이 실행 도중 발생할 수 있는 모든 경우의 수를 고려하여 이에 대한 대비를 하는 것이 필요합니다.

자바에서는 실행 시 발생할 수 있는 프로그램 오류를 에러(error)와 예외(exception) 두 가지로 구분하였습니다.

 

에러는 메모리부족이나 스택오버플로우와 같이 일단 발생하면 복구할 수 없는 심각한 오류이고, 예외는 발생하더라도 수습될 수 있는 덜 심각한 것입니다.

에러가 발생하면, 프로그램의 비정상적인 종료를 막을 길이 없지만 예외는 발생하더라도 프로그래머가 이에 대한 적절한 코드를 미리 작성해놓음으로써 프로그램의 비정상적인 종료를 막을 수 있습니다.

 

 

예외 클래스의 계층구조

자바에서는 실행 시 발생할 수 있는 오류를 클래스로 정의하였습니다.

image-20201231174305869

 

모든 예외의 최고 조상은 Exception 클래스이며, 예외클래스들은 다음과 같이 두 그룹으로 나누어집니다.

  • Exception클래스와 그 자손들
  • RuntimeException과 그 자손들

 

RuntimeException 클래스들은 주로 프로그래머들의 실수에 의해서 발생될 수 있는 예외입니다.

예를 들면, 배열의 범위를 벗어나거나 null인 참조변수의 멤버를 호출하거나 클래스간의 형변환을 잘못했거나 하는 문제입니다.

 

Exception클래스들은 주로 외부의 영향으로 발생하는 것입니다.

존재하지 않는 파일의 이름을 입력하거나, 실수로 클래스의 이름을 잘못 적었거나 등의 문제입니다.

 

 

예외처리하기 try-catch문

프로그램의 실행 도중에 발생하는 에러는 어쩔 수 없지만 예외는 프로그래머가 이에 대한 처리를 미리 해줘야 합니다.

예외를 처리하기 위해서는 try-catch문을 사용하며 그 구조는 다음과 같습니다.

try {
  
} catch (Exception e1){
  
} catch (Exception e2){
  
}

catch의 () 에는 예외와 같은 타입의 참조변수를 하나 선언해야 합니다.

이 때 Exception은 다른 예외들의 조상이기 때문에 모든 예외를 처리할 수 있습니다.

 

또한 try 블럭이나 catch 블럭에 또 다른 try-catch이 포함될 수 있습니다.

 

 

printStackTrace()와 getMessage()

예외가 발생했을 때 생성되는 예외클래스의 인스턴스에는 발생한 예외에 대한 정보가 담겨있습니다.

  • printStackTrace() : 예외발생 당시의 호출 스택에 있었던 메서드의 정보와 예외 메세지를 화면에 출력
  • getMessage() : 발생한 예외클래스의 인스턴스에 저장된 메세지을 반환
public class HelloWorld {

	public static void main(String[] args) {
		try {
			System.out.println(0/0);
		} catch (Exception e) {
			e.printStackTrace();
    /*
      java.lang.ArithmeticException: / by zero
	at HelloWorld.main(HelloWorld.java:5)
    */
			System.out.println("예외메시지: " + e.getMessage());
      // 예외메시지: / by zero
		}
	}
}

 

멀티 catch 블럭

catch 블럭에 | 기호를 이용해서 하나의 catch블럭으로 합칠수 있으며 이를 멀티 catch 블럭이라고 합니다.

try {
  catch (ExceptionA | ExceptionB e){
    e.printStackTrace();
  }
}

 

| 로 연결할 수 있는 예외클래스의 개수에는 제한이 없지만, 예외클래스가 조상과 자손의 관계라면 컴파일 에러가 발생합니다.

왜냐하면 두 예외클래스가 조상과 자손의 관계에 있다면 조상 클래스만 써줘도 똑같기 때문입니다.

 

 

예외 발생시키기

키워드 throw를 사용해서 프로그래머가 고의로 예외를 발생시킬 수 있으며 방법은 아래와 같습니다.

Exception e = new Exception("사용자 정의 에러");
throw e;

 

Exception 인스턴스를 생성할 때 생성자에 String을 넣어주면 getMessage()를 이용하여 얻을 수 있습니다.

 

 

메서드에 예외 선언하기

예외를 처리하는 방법에는 try-catch문 외에 예외를 메서드에 선언하는 방법도 있습니다.

메서드에 예외를 선언하려면, 메서드의 선언부에 키워드 throws를 사용해서 메서드 내에서 발생할 수 있는 예외를 적어주면 됩니다.

void method throws Exception1, Exception2 {
  
}

 

메서드에 예외를 선언함으로써 메서드를 사용하려는 사람이 메서드의 선언부를 보았을 때 이 메서드를 사용하기 위해서 어떠한 예외들이 처리되어져야 하는지 쉽게 알 수 있습니다.

 

예외가 발생한 메서드 내에서 자체적으로 처리해도 되는 것은 메서드 내에서 try-catch문을 사용해서 처리하고 메서드 내에서 자체적으로 해결이 안 되는 경우에는 예외를 메서드에 선언해서 호출한 메서드에서 처리해주어야 합니다.

 

 

finally 블럭

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

try {
  
} catch (Exception e){
  
} finally {
  
}

 

try 블럭 안에서 return문이 실행되는 경우에도 finally 블럭의 문장들이 먼저 실행된 후에 현재 실행 중인 메서드를 종료합니다.

public class HelloWorld {

	public static void main(String[] args) {
		try {
			System.out.println("main 함수 실행");
			return;
		} catch (Exception e) {
			
		} finally {
			System.out.println("finally 실행");
		}
	}
}

/*
main 함수 실행
finally 실행
*/

 

 

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

try, with, resources문은 try-catch문의 변형입니다.

이는 입출력과 관련된 클래스를 사용할 때 유용합니다.

주로 입출력에 사용되는 클래스 중에서는 사용한 후에 꼭 닫아 자원을 반환시키는 작업이 필요한 경우가 많습니다.

 

일반적으로 자원을 반환하는 예제입니다.

try {
  fis = new FileInputStream("score.dat");
  dis = new DataInputStream(fis);
} catch (IOException ie){
  ie.printAStackTrace();
} finally {
  dis.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();
  }
}

 

하지만 위 코드도 문제가 있습니다.

try 블럭과 catch 블럭에서 모두 예외가 발생하면 try 블럭의 예외는 무시됩니다.

 

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

try (fis = new FileInputStream("score.dat");
     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의 () 안에 객체를 생성하는 문장을 넣으면 이 객체는 따로 close()를 호출하지 않아도 try 블럭을 벗어나는 순간 자동적으로 close()가 호출됩니다.

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

 

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

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

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

 

만일 close()에서 Exception을 발생시키면 어떤 일이 벌어지는지 확인해봅시다.

public class HelloWorld {

	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 발생
	at CloseableResource.close(HelloWorld.java:36)
	at HelloWorld.main(HelloWorld.java:6)

exceptionWork(true)가 호출됨
close()가 호출됨
WorkException: WorkException 발생
	at CloseableResource.exceptionWork(HelloWorld.java:30)
	at HelloWorld.main(HelloWorld.java:14)
	Suppressed: CloseException: CloseException 발생
		at CloseableResource.close(HelloWorld.java:36)
		at HelloWorld.main(HelloWorld.java:15)

 

두 번째 같은 경우에는 exceptionWork()와 close()에서 모두 예외를 발생시킵니다.

따라서 WorkException에 대한 내용이 먼저 출력이 되고, CloseException은 Suppressed로 억제된 예외로 다루게 됩니다.

 

사용자정의 예외 만들기

프로그래머가 새로운 예외 클래스를 정의하여 사용할 수 있습니다.

class MyException extends Exception {
  MyException(String msg){
    super(msg); // Exception의 생성자 호출
  }
}

 

 

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

한 메서드에서 발생하는 예외를 몇 개는 try-catch에서 자체적으로 처리하고 그 나머지는 선언부에 지정하여 호출한 메서드에서 처리하도록 함으로써 양쪽에서 나눠 처리되도록 할 수 있습니다.

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

그러기 위해서는 예외를 처리한 후에 인위적으로 다시 발생시켜야 하는데 이를 예외 되던지기라고 합니다.

public class HelloWorld {

	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;
		}
	}
}
method1메서드에서 예외가 처리되었습니다.
main 메서드에서 예외가 처리되었습니다.

 

 

연결된 예외(chained exception)

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

예외 A가 예외 B를 발생시켰다면 A를 B의 원인 예외라고 합니다.

 

아래 예제를 봅시다.

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

 

InstallException 인스턴스를 생성한 후 initCause()로 SpaceException을 InstallException의 원인 예외로 등록하였습니다. 그리고 throw를 통해 예외를 던집니다.

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

Throwble initCause(Throwble cause) // 지정된 예외를 원인 예외로 등록
Throwble getCause() // 원인 예외를 반환

 

발생한 예외를 그냥 처리하면 될텐데 왜 원인 예외로 등록해서 다시 예외를 발생시킬까요?

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

그렇다고 해서 조상 예외 클래스로만 예외를 핸들링한다면 실제로 발생한 예외가 어떤 것인지 알 수 없다는 문제가 생깁니다. 또한 상속관계에 있어야한다는 문제점도 있죠.

 

 

아래 예제를 실행한 결과를 확인하면 연결된 예제가 어떻게 동작하는지 확인할 수 있습니다.

public class HelloWorld {

	public static void main(String[] args) {
		try {
			install();
		} catch (InstallException e) {
			e.printStackTrace();
		}
	}
	
	
	static void install() throws InstallException {
		try {
			startInstall();
		} catch (SpaceException e){
			InstallException ie = new InstallException("설치중 예외발생");
		  	ie.initCause(e);
		  	throw ie;
		} 
	}
	
	static void startInstall() throws SpaceException {
		throw new SpaceException("설치할 공간이 부족합니다.");
	}
}

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);
	}
}
InstallException: 설치중 예외발생
	at HelloWorld.install(HelloWorld.java:16)
	at HelloWorld.main(HelloWorld.java:5)
Caused by: SpaceException: 설치할 공간이 부족합니다.
	at HelloWorld.startInstall(HelloWorld.java:23)
	at HelloWorld.install(HelloWorld.java:14)
	... 1 more