본문 바로가기
JAVA/기본

예외 처리(exception handling)

by 히포파타마스 2021. 8. 2.

예외 처리(exception handling)

 

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

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

 

에러는 발생 시점에 따라 다음과 같이 구분된다.

 

[에러의 종류]

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

컴파일은 되지만, 프로그램 실행 후(runtime) 에러가 발생하는 경우를 런타임 에러라 한다.

ex) Null값을 참조하는 NullpointException, 0을 나눌 때 발생하는 ArithmeticException 등

 

런타임 에러는 에러(error)와 예외(exception)로 구분될 수 있다.

 

[에러와 예외]

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

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

 

 

 

■ 예외 클래스의 계층 구조

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

 

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

 

[Exception 상속 계층도]

Exception
	- IOException
	- ClassNotFoundException
	- ...
	- RuntimeException
		- ArithmeticException
		- ClassCastException
		- NullPointerException
		- ...
		- IndexOutOfBoundsException

 

● Exception 클래스와 그 자손 클래스(RuntimeException 제외)

◎ Exception 클래스라고 불린다.

◎ 주로 외부의 영향으로 발생할 수 있는 것들로서, 프로그램 사용자의 동작에 의해 발생하는 경우가 많다.

ex) 존재하지 않는 파일의 이름을 입력하는 FileNotFoundException 등

 

● RuntimeException

◎ 프로그래머의 실수로 발생하는 예외

 

 

 

 

1. 예외 처리 - try-catch

예외 처리(exception handling)란, 프로그램 실행 시 발생할 수 있는 예기치 못한 예외의 발생에 대비한 코드를 작성하는 것이다.

예외 처리의 목적은 예외의 발생으로 인한 실행 중인 프로그램의 갑작스러운 비정상 종료를 막고, 정상적인 실행상태를 유지할 수 있도록 하는 것이다.

 

처리되지 못한 예외는 JVM의 예외 처리기가 받아서 예외의 원인을 화면에 출력한다.

 

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

 

[예외 처리 예 - try-catch]

try {
	// 예외가 발생할 가능성이 있는 문장을 넣는다.
} catch (Exception1 e1) {
	// Exception1이 발생했을 경우, 이를 처리하기 위한 문장을 적는다.
} catch (Exception2 e2) {
	// Exception2이 발생했을 경우, 이를 처리하기 위한 문장을 적는다.
}

하나의 try블록 다음에는 여러 종류의 예외를 처리할 수 있도록 하나 이상의 catch문이 올 수 있다.

catch문은 하나 이상의 예외를 처리할 수 있으며 예외와 부합하는 단 하나의 catch문만 실행된다.

 

 

 

■ try-catch문 흐름

try-catch문에서 예외가 발생할 때와 발생하지 않았을 때의 흐름은 다음과 같다.

 

● try블럭 내에서 예외가 발생한 경우

◎ 발생한 예외와 일치하는 catch블록이 있는지 확인한다.

◎ 일치하는 catch블록이 있으면, 그 catch블록을 실행하고 전체 try-catch문을 빠져나간다. 일치하는 catch블록이 없다면 예외 처리는 되지 않는다.

※try블록에서 예외가 발생하면 그 이후의 try블록내 코드는 실행되지 않는다. 

 

● try블럭 내에서 예외가 발생하지 않는 경우

◎ catch블록을 거치지 않고 전체 try-catch문을 빠져나간다.

 

try-catch문은 try블록 내에서 예외가 발생했을 때, catch문의 ()(괄호) 안에 표기된 예외와 실제 발생한 예외를 instanceof 연산자를 통해 검사한다.

예외가 일치하면 catch문의 괄호에 선언된 참조 변수 타입으로 객체가 생성된다.

생성된 예외 클래스의 인스턴스에는 발생한 예외에 대한 정보가 담겨 있다.

※ 모든 예외 클래스는 Exception의 자손이므로, catch블록의 ()에 Exception 타입의 참조 변수를 선언하면 모든 종류의 예외를 처리할 수 있다.

 

 

 

■ 멀티 catch블록

여러 catch 블록을 '|' 기호를 이용해서, 하나의 catch블록으로 합칠 수 있다.

 

[멀티 catch 블럭 예시]

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

● '|' 기호로 연결할 수 있는 예외에는 제한이 없다.

● '|' 기호로 연결된 예외 클래스는 상속관계에 있을 수 없다(중복 에러 발생).

● 복수의 예외 중 어떤 예외가 처리됐는지 바로 알 수 없다.

● 예외 발생으로 생성된 예외 인스턴스는 '|' 기호로 연결된 예외들의 부모 클래스의 메서드만 사용 가능하다.

ex) catch(NullPointException | ArithmeticException e) {} 에서 예외 처리가 됐다면 e는 개별 예외 클래스의 메서드는 사용하지 못하지만 공통 부모 클래스인 RuntimeException과 같은 클래스의 메서드는 사용 가능하다.

 

 

 

■ finally블록

finally블록은 try-catch문과 함께 사용되며, 예외의 발생 여부에 상관없이 실행된다.

 

[finally 예제]

public class main {
	public static void main(String[] args) {

		try {
			throw new Exception();
		} catch (Exception e) {
			System.out.println("catch문 실행");
		} finally {
			System.out.println("finally문 실행");
		}
	}
}

 

[finally 사용 결과]

catch문 실행
finally문 실행

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

 

심지어 finally문은 try블록에서 return문이 실행되는 경우에도 실행된다.

 

[finally - return]

public class main {
	public static void main(String[] args) {

		try {
			return;
		} catch (Exception e) {
			System.out.println("catch문 실행");
		} finally {
			System.out.println("finally문 실행");
		}
	}
}

 

[finally 결과 - return]

finally문 실행

 

 

 

■ 자동 자원 반환 기능

try문에 자원 객체를 전달하면, try블록이 끝날 때 자동으로 자원 객체를 종료해준다.

이때, 자원 객체 종료 시에 발생할 수 있는 예외가 자동으로 처리된다.

 

[자동 자원 반환 기능 예 - try-with-resource]

public class main {
	public static void main(String[] args) {

		try (FileInputStream fis = new FileInputStream("test.txt");
			DataInputStream dis = new DataInputStream(fis);) {
			System.out.println("try문 실행");
		} catch (IOException e) {
			System.out.println("catch문 실행");
			e.printStackTrace();
		}
	}
}

catch문마다, 또는 finally문을 추가해서 close()를 해줄 필요 없이 자동으로 close()된다.

 

※ try문에 전달할 수 있는 객체는 AutoCloseable이라는 인터페이스를 구현한 것이어야만 한다.

 

 

 

 

2. 예외 처리 - throw, throws

throw를 사용해서 프로그래머가 고의로 예외를 발생 시킬 수 있다.

 

[예외 발생 - throw]

class main {
	public static void main(String args[]) {
		throw new Exception("에러 발생");
	}
}

throw 뒤에 발생시키고 싶은 예외 클래스의 인스턴스를 생성하면 해당 예외가 발생한다.

예외 클래스는 매개변수로 String을 받으며 이는 에러 발생시 메시지로 사용된다.

 

throw로 예외를 발생시켰을 때, 발생시킨 예외가 RuntimeException이면 컴파일은 되지만 실행 시 에러가 발생한다.

반면 Exception를 발생시키면 컴파일 조차 되지 않는다.

이런 이유로 RuntimeException 클래스들은 컴파일러가 예외처리를 확인하지 않기에 'unchecked 예외'라고 부르고, 예외처리를 확인하는 Exception 클래스들은 'checked 예외'라고 부른다.

 

만약 반환 값이 있는 메서드라면 catch문 내에도 return문이 필요하다.

하지만 catch문 내에서 throw로 에러를 발생하면 해당 예외 인스턴스가 반환 값으로 적용되기 때문에 return이 코드가 정상적으로 실행된다.

 

 

■ 메서드에 예외 선언 - throws 

메서드에 throws를 사용해서 메서드를 호출한 메서드에 예외를 전달할 수 있다.

 

[예외 처리 - throws]

class main {
	public static void main(String args[]) throws Exception {
		method1();
	}
    
	static void method1() throws Exception {
		method2();
	}
    
	static void method2() throws Exception {
		throw new Exception();
	}
}

method2에서 Exception 예외를 발생시키고, main - method1 - method2 순으로 메서드를 호출한다.

모든 메서드에 throws Exception을 추가해 예외를 상위 메서드로 전달하였다.

 

[예외 처리 결과 - throws]

Exception in thread "main" java.lang.Exception
	at main.method2(main.java:11)
	at main.method1(main.java:7)
	at main.main(main.java:3)

main 메서드까지 예외가 처리되지 않고 전달될 경우 main 메서드는 종료되고 에러가 발생한다.

단, 예외가 발생한 메서드를 제외하고 예외와 관련된 코드는 실행되고 프로그램이 종료된다.

 

발생한 에러에는 예외가 발생한 곳과 메서드 호출 순서에 따라 예외가 전달된 경로를 나타낸다.

 

throws는 예외를 전달할 뿐, 예외를 처리하진 않는다.

 

위 예제처럼 throws를 통해서 예외를 넘길 수 있으며 호출 순서에 따라 어떤 메서드에서 예외를 처리할지 선택할 수 있다.

또한 해당 메서드가 어떤 에러를 발생시킬 수도 있는지에 대한 정보를 제공하는 역할도 한다.

 

 

 

 

 

3. 예외 처리 확장

■ 사용자 정의 예외 생성

기존에 정의된 예외 클래스 외에 필요에 따라 새로운 예외 클래스를 정의할 수 있다.

 

[예외 만들기]

class MyException extends Exception {
	MyException(String msg) {
		super(msg) // 부모인 Exception클래스의 생성자 호출.
	}
}

Exception을 상속받아서 MyException이라는 예외를 정의하였다.

Exception뿐만 아니라 RuntimeException을 상속받아 만들 수도 있다.

 

 

 

■ 연결 예외(chained exception)

예외 A, B가 있을 경우 예외 A를 예외 B의 원인 예외(cause exception)로 지정할 수 있다.

 

[연결 예외 - initCause]

public class main {
	public static void main(String[] args) {

		try  {
			makeError();
		} catch (NullPointerException e) {
			e.printStackTrace();
		}
	}

	static void makeError() throws NullPointerException{

		try {
			throw new IOException();
		} catch (IOException e) {
			NullPointerException ne = new NullPointerException();
			ne.initCause(e);
			throw ne;
		}
	}
}

initCause() 메서드로 예외 인스턴스에 원인 예외를 지정할 수 있다.

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

 

[연결 예외 결과 - initCause]

java.lang.NullPointerException
	at main.makeError(main.java:20)
	at main.main(main.java:9)
Caused by: java.io.IOException
	at main.makeError(main.java:18)
	... 1 more

연결 예외가 있는 예외 인스턴스는 자신의 예외 경로에 더해, Caused by : ~ 로 원인 예외의 경로도 나타내 준다.

 

연결 예외 지정은 RuntimeException의 생성자로도 할 수 있다.

 

[연결 예외 지정 - RuntimeException]

public class main {
	public static void main(String[] args) {

		try  {
			makeError();
		} catch (RuntimeException e) {
			e.printStackTrace();
		}
	}

	static void makeError() {
		throw new RuntimeException(new Exception());
	}
}

Exception 에러를 RuntimeException의 원인 에러로 지정하였다.

 

[연결 예외 결과 - RuntimeException]

java.lang.RuntimeException: java.lang.Exception
	at main.makeError(main.java:16)
	at main.main(main.java:9)
Caused by: java.lang.Exception
	... 2 more

initCause() 메서드와 동일한 메커니즘으로 결과가 나타난다.

 

위의 예시와 같이 원인 예외를 지정하는 이유는 다음과 같다.

 

● 여러 예외를 하나의 큰 분류의 예외로 묶어서 다룰 수 있다.

◎ 이 경우, 상속 관계에 관련 없이 하나의 예외로 묶어 처리할 수 있다는 장점이 있다.

 

● 반드시 처리해야 되는 checked 예외를 unchecked 예외로 바꿀 수 있다.

◎ unchecked 예외로 checked 예외를 덮을 경우 반드시 예외처리를 하지 않아도 된다.

'JAVA > 기본' 카테고리의 다른 글

Optional  (0) 2021.12.03
람다(Lambda)  (0) 2021.11.28
#5 생성자(Constructor)  (0) 2021.04.28
#4 오버로딩(overloading)  (0) 2021.04.28
#3 변수, 메서드  (0) 2021.04.23

댓글