java [6] 예외처리

lsy·2022년 10월 22일
0

자바

목록 보기
7/14

프로그램 오류

프로그램이 실행 중에 어떤 원인에 의해서 오작동 하거나 비정상적으로 종료되는 경우가 있다. 이것을 프로그램의 오류 또는 에러라고 하는데, 그것들의 종류에는 세 가지가 있다.

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

또 여기서, 자바에서 런타임 에러는 두 가지 종류가 있다.

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

에러가 발생하면 프로그램의 종료를 막을 수 없지만 예외가 발생하면 프로그래머가 적절한 코드를 작성해놓았다면 프로그램의 비정상적인 종료를 막을 수 있다.

예외 클래스의 계층구조

자바에서는 실행 시 발생할 수 있는 오류(예외, 에러)를 클래스로 정의하였다.

모든 예외의 최고 조상은 Exception클래스이며, 에러의 최고 조상은 Error클래스이다. Exception클래스부터 상속계층도를 도식화하면 다음과 같다.

위 그림에서 볼 수 있듯 예외 클래스는 다음과 같은 두 그룹으로 나눠질 수 있다.

1. Exception클래스와 그 자손들(RuntimeException 제외)
2. RumtimeException클래스와 그 자손들

Exception클래스는 주로 외부의 영향으로 발생할 수 있는 것들로서, 프로그램의 사용자들의 동작에 의해서 발생하는 경우가 많다. 예를 들면, 존재하지 않는 파일의 이름을 입력했다던가, 클래스의 이름을 잘못 적었다던가 하는 경우에 발생한다.

RuntimeException클래스는 프로그래머의 실수에 의해서 발생될 수 있는 예외들이다. 예를 들면, 배열의 범위를 벗어난다던가, 정수를 0으로 나누려고한다던가, 클래스간의 형변환을 잘못했다던가 하는 경우에 발생한다.

예외처리

에러는 어쩔 수 없지만, 예외는 try-catch문을 사용하여 처리할 수 있다.

예외처리는, 프로그램 실행시 발생할 수 있는 예외에 대비해 코드를 작성하는 것이고, 목적은 프로그램의 비정상 종료를 막고 정상적인 실행상태를 유지하는 것이다.

예외를 처리하지 못하면, 프로그램이 비정상적으로 종료되고 처리되지 못한 예외는 JVM의 예외처리기가 받아서 원인을 화면에 출력한다.

class ExceptionEx3 {
	public static void main(String args[]) {
		int number = 100;
		int result = 0;

		for(int i=0; i < 10; i++) {
			try {
				result = number / (int)(Math.random() * 10);
				System.out.println(result);
			} catch (ArithmeticException e)	{
				System.out.println("0");	
			}
		}
	} 
}

위 코드는 try-catch문을 사용해 정수를 0으로 나눌 때 발생하는 예외인 ArithmeticException을 처리한 것이다. 만일 예외처리를 하지 않았다면 정수를 0으로 나누자마자 프로그램이 종료됐을 것이다.

try-catch 흐름

try-catch는 두 가지 경우로 나눌 수 있다.

try블럭 내에서 예외가 발생한 경우
1. 발생한 예외와 일치하는 catch블럭이 있는지 확인한다.
2. 일치하는 catch블럭을 찾는다면 그 블럭 내의 문장들을 수행하고 try-catch문을 빠져나간다. 찾지 못한다면 예외는 처리되지 못하고 프로그램이 종료된다.

try블럭 내에서 예외가 발생하지 않은 경우
1. 그대로 try-catch문을 빠져나간다.

자세히 설명하면, try블럭 내에서 예외가 발생한 경우, 첫 번째 catch블럭부터 차례대로 내려가면서 catch블럭의 괄호내에 선언된 참조변수의 종류와 생성된 예외클래스의 인스턴스에 instanceof 연산자를 이용해서 검사하게 되는데, 검사결과가 true인 catch블럭을 만날 때까지 검사를 계속한다. 검사결과가 true인 catch블럭을 만나면 예외가 처리되고 그렇지 않다면 예외는 처리되지 않는다.

또, 모든 예외 클래스는 Exception클래스의 자손이므로, catch블럭의 괄호에 Excption클래스의 참조 변수를 선언해놓으면 어떤 종류의 예외가 발생하더라도 해당 catch블럭에 의해 처리된다.

printStackTrace(), getMessage()

예외가 발생했을 때 생성되는 예외 클래스의 인스턴스에는 발생한 예외에 대한 정보가 담겨 있으며, printStackTrace()와 getMessage()를 이용해 이 정보들을 얻을 수 있다.

printStackTrace()는 예외발생 당시에 호출스택(Call Stack)에 있던 메서드의 정보와 예외 메시지를 화면에 출력한다.

getMessage()는 발생한 예외클래스의 인스턴스에 저장된 메시지를 얻을 수 있다.

멀티 catch블럭

JDK1.7부터 여러 catch블럭을 ' | ' 을 통해서 묶을 수 있게 되었다.

try {
    ...
} catch(ExceptionA e) {

} catch(ExceptionB e) {

}
// 위 코드와 아래 코드는 같다.
try {
    ...
} catch(ExceptionA | ExceptionB e) {

}

근데 만약, 위 코드에서 ExceptionA와 ExceptionB가 서로 조상, 자손 관계라면 컴파일 에러가 발생한다. 두 예외가 조상,자손 관계라면, 그냥 조상 클래스만 써주는 것과 똑같기 때문이다.

그리고 멀티 catch는 예외가 발생했을 때, 실제로 어떤 예외가 발생한 것인지 알 수 없다. 그래서 참조 변수 e로 멀티 catch블럭에 선언한 예외 클래스들의 공통 분모인 조상 예외 클래스에 선언된 멤버만 사용할 수 있다.

예외 발생시키기

키워드 throw를 통해서 프로그래머가 고의로 예외를 발생시킬 수 있다.

class ExceptionEx9 {
	public static void main(String args[]) {
		try {
			Exception e = new Exception("고의로 발생시켰음.");
			throw e;	 // 예외를 발생시킴
		//  throw new Exception("고의로 발생시켰음.");  

		} catch (Exception e)	{
			System.out.println("에러 메시지 : " + e.getMessage());
		     e.printStackTrace();
		}
		System.out.println("프로그램이 정상 종료되었음.");
	}
}

사용방법은, 위 코드처럼 발생시키고자 하는 예외 클래스의 객체를 만든 뒤, throw를 통해서 예외를 발생시키면 된다.

추가적으로, Exception클래스에 속해 있는 예외가 발생할 것 같은 문장에는 예외처리를 해주지 않으면 컴파일이 되지 않지만(checked예외), RuntimeException에 속해있는 예외가 발생할 것 같은 문장들은 프로그래머의 실수에 의해 발생하는 것이기 때문에 예외처리를 강제하지 않아 컴파일이 된다(unchekced예외).

메서드에 예외 선언하기

메서드에 예외를 선언하여 예외를 처리할 수 있다. 사용방법은 다음과 같다.

void method() throws Exception1, Exception2, ... ExceptionN {
}

이렇게 선언하면 메서드내에서 해당 예외가 발생할 수 있다는 것을 알려줄 수 있으며, 이 메서드를 사용하는 쪽에서 해당 예외에 대한 처리를 하도록 강요한다.

또, 메서드에 예외를 명시하면 예외를 처리하지 않고 자신을 호출한 메서드에게 예외를 전달하여 예외처리를 떠맡길 수도 있다.

class ExceptionEx12 {
	public static void main(String[] args) throws Exception {
		method1();	 // 같은 클래스내의 static멤버이므로 객체생성없이 직접 호출가능.
  	}	// main메서드의 끝

	static void method1() throws Exception {
		method2();
	}	// method1의 끝

	static void method2() throws Exception {
		throw new Exception();
	}	// method2의 끝
}

method2 메서드에서 예외가 발생하고, 메서드내에 예외처리문이 없으므로 그것이 method1 메서드로 떠넘겨진다. method1 메서드 역시 예외처리문이 없으므로 main 메서드로 떠넘겨진다. 역시 예외처리문이 없으므로 프로그램이 강제종료된다. 따라서 어느 한 곳에서 try-catch문을 통해 예외처리를 해주어야 한다.

finally블럭

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

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

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

위 코드를 보면, try블럭 안에 return이 있다. return을 만나면 바로 끝날 것 같지만 return을 만나더라도 프로그램이 종료되기 전 finally블럭에 있는 문장을 무조건 실행한다.

자동 자원 반환

JDK1.7부터 try-with-resources문이라는 try-catch문의 변형이 새로 추가되었다. 클래스 중에 사용한 후 꼭 닫아주거나 반납해줘야하는 자원들이 있는데, 그 때 사용한다.

예를 들어 DataInputSteam클래스는 사용 후 꼭 close() 메서드를 이용해 자원을 반납해야하는데, close() 메서드는 예외를 발생시킬 수 있으므로 finally블럭에 넣기에 부적절하다. 이 때 사용할 수 있다.

try (FileInputStream fis = new FileInputStream("score.dat"); // 두 문장이상일 경우 ;로 구분
	DataInputSteam dis = new DataInputStream(fis)) {
 	
    ...   
}

위와 같이 사용하며, 자동으로 객체의 close()가 호출된다. 이 때 자동으로 객체의 close()가 호출되게 만들려면 해당 클래스가 AutoCloseable이라는 인터페이스를 구현한 것이어야만 한다.

사용자 정의 예외 만들기

다음과 같이 프로그래머가 필요에 따라 새로운 예외 클래스를 정의할 수 있다.

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

생성자로 문자열을 받아 예외의 메시지로 정할 수 있다.

기존 예외 클래스는 주로 Exception을 상속받아 checked예외로 작성한 경우가 많았지만, 요즘은 예외처리를 선택적으로 할 수 있도록 RuntimeException을 상속받아 작성하는 쪽으로 바뀌어가고 있다.

예외 되던지기

한 메서드에서 발생할 수 있는 예외가 여럿인 경우, 여러 메서드에서 나뉘어서 처리할 수 있게 만들 수 있다. 또는 하나의 예외에 대해서도 예외가 발생한 메서드와 호출한 메서드 양쪽에서 처리하도록 할 수 있다.

이것은 예외를 처리한 후 다시 인위적으로 발생시키는 방법을 통해서 가능하다. 이것을 예외 되던지기(exception re-thorwing)이라고 한다.

class ExceptionEx17 {
	public static void main(String[] args) 
	{
		try  {
			method1();		
		} catch (Exception e)	{
			System.out.println("main메서드에서 예외가 처리되었습니다.");
		}
	}	// main메서드의 끝

	static void method1() throws Exception {
		try {
			throw new Exception();
		} catch (Exception e) {
			System.out.println("method1메서드에서 예외가 처리되었습니다.");
			throw e;			// 다시 예외를 발생시킨다.
		}
	}	// method1메서드의 끝
}

method1()에서 예외가 발생하여 그것을 처리하고 있다. 그 후에 다시 throw를 통해 예외를 발생시킴으로써 main 메서드로 예외를 넘긴다. 그리고 그 예외를 main 메서드에서 처리한다.

즉, 하나의 예외를 method1()에서도 처리하고, main에서도 처리할 수 있게 됐다.

연결된 예외

예외 A가 예외 B를 발생시키면 예외 A를 B의 원인 예외라고 한다. 한 예외는 다른 예외를 발생시킬 수 있다.

try {
    ...
} catch (SpaceException e) {
    InstallException ie = new InstallException();
    ie.initCause(e);
    throw ie;
}

위 코드에서 SpaceExeption e가 발생하고, catch블럭 내에서 InstallException 예외 인스턴스를 새로 생성해 SpaceExeption의 인스턴스를 원인 예외로 등록한다. 그 후 ie를 throw한다.

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

이렇게 발생한 예외를 원인 예외로 등록해서 다시 예외를 발생시키는 이유는 여러가지 예외를 하나의 큰 분류의 예외로 묶어서 다루기 위해서이다.

또 다른 이유는 checked예외를 unchecked예외로 변경하기 위해서이다.

checked예외로 예외처리를 강제한 이유는 프로그래밍 경험이 적은 사람도 보다 견고한 프로그램을 작성할 수 있도록 유도하기 위한 것이었는데, 지금은 환경이 많이 달라져서 checked예외가 발생해도 예외를 처리할 수 없는 상황이 발생하기 시작했다. 이를 위해 할 수 있는 일은 의미없는 try-catch문을 작성하는 것 뿐인데, checked예외를 unchecked예외로 변경하면 예외처리가 선택적이 되므로 억지로 예외처리를 하지 않아도 된다.

static void startInstall() throws SpaceException {
    if(!enoughSpace())
   	    throw new SpaceException();
    if(!enoughMemory())
   	    throw new RumtimeException(new MemoryException());
}

MemoryException은 Exception의 자손이라 반드시 예외처리를 해야하지만 이렇게 RuntimeException의 생성자를 이용해 MemoryException을 원인 예외로 등록해주면 unchecked예외가 되어서 예외처리를 하지 않아도 된다. 추가로 unchecked예외가 되면 throws에 명시해주지 않아도 된다.

profile
server를 공부하고 있습니다.

0개의 댓글