본문 바로가기
Language/Java

[Java] 예외처리(exception handling), 에러와 예외의 차이

by 계범 2022. 3. 6.

프로그램 오류(에러의 종류, 에러와 예외)

프로그램 실행 중 오작동을 일으키거나 비정상적인 종료가 일어나게 하는 원인을 프로그램 에러 또는 오류라고 한다.

 

발생시점에 따라 에러를 구분할 수 있다.

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

컴파일 에러는 소스코드(*.java)에 대해 오타나 잘못된 구문, 자료형 체크 등의 기본적인 검사에서 오류가 있을 때에 발생한다.

 

컴파일이 잘 수행되고 클래스파일을 생성하여 실행하였다고 해도 프로그램 실행 시에 동작을 멈춘 상태로 오랜 시간 지속되거나, 갑자기 프로그램이 실행을 멈추고 종료되는 경우 등이 런타임 에러이다.

 

자바에서는 실행 시 발생할 수 있는 프로그램 오류를 예외와 오류로 나눈다.

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

 

예외 클래스의 계층구조

모든 클래스의 조상은 Object클래스이므로 Exception과 Error클래스도 Object클래스의 자손들이다.

RuntimeException클래스들은 주로 프로그래머의 실수에 의해서 발생될 수 있는 예외들로 자바의 프로그래밍 요소들과 관계가 깊다.

 

  • ArrayIndexOutOfBoundsException : 배열의 범위 벗어남
  • NullPointerException : 값이 null인 참조변수의 멤버를 호출
  • ClassCastException : 클래스간의 형변환 잘못됨
  • ArithmeticException : 정수를 0으로 나눔

 

그 외 Exception클래스들은 주로 사용자의 실수와 같은 외적 요인에 의해 발생되는 예외들이다.

 

  • FileNotFoundException : 파일을 찾을 수 없음
  • ClassNotFoundException : 클래스를 찾을 수 없음
  • DataFormatException : 데이터 형식이 잘못됨

 

예외처리하기 try-catch문

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

 

예외처리(exception handling)
정의 - 프로그램 실행 시 발생할 수 있는 예외에 대비한 코드를 작성하는 것
목적 - 프로그램의 비정상 종료를 막고, 정상적인 실행상태 유지

 

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

 

try-catch를 통한 예외처리 방법

public class Test{
	public static void main(String[] args){
    	try{
        	// 예외가 발생할 가능성이 있는 문장
        } catch (Exception1 e){
        	// Exception1이 발생했을 경우, 이를 처리하기 위한 문장
        } catch (Exception2 e){
        	// Exception2이 발생했을 경우, 이를 처리하기 위한 문장
        } catch (Exception2 e){
        	// Exception3이 발생했을 경우, 이를 처리하기 위한 문장
        }
    }
}

try-catch문은 if문과 다르게 안의 문장이 하나여도 괄호{}를 생략할 수 없다.

 

하나의 메서드에 여러 개의 try-catch문을 써도 되고, 중첩으로 써도 된다. 그리고 하나의 try문에서 발생한 catch문엔 참조변수(여기선 'e')를 동일한 이름으로 써도 된다. 왜냐하면 해당 catch문 괄호{}안에서만 유효하기 때문이다.

만약 중첩 try-catch문을 쓸 경우, 외부 변수와 내부 변수 이름은 동일하면 안된다.

 

try-catch문에서의 흐름

> try블럭 내에서 예외가 발생한 경우,
1. 발생한 예외와 일치하는 catch 블럭이 있는지 확인
2. 일치하는 catch블럭을 찾으면, 그 catch블럭 내의 문장들을 수행하고 전체 try-catch문을 빠져나가서 그 다음 문장을 계속 수행.
3. 일치하는게 없으면 예외가 발생하는 문장부터 수행하지 않고, JVM의 '예외처리기' 화면에 예외를 출력해줌.

> try블럭 내에서 예외가 발생하지 않은 경우
1. try문만 실행되고 빠져나와서 다음 문장을 계속 수행.
class ExceptionEx5 {
	public static void main(String args[]) {
			System.out.println(1);			
			System.out.println(2);
			try {
				System.out.println(3);
				System.out.println(0/0);	
				System.out.println(4); 	// 실행되지 않는다.
			} catch (ArithmeticException ae)	{
				System.out.println(5);
			}	// try-catch의 끝
			System.out.println(6);
	}	// main메서드의 끝
}

 

예외의 발생과 catch블럭

catch블럭은 괄호()와 블럭{} 두 부분으로 나눠져 있는데, 괄호()내에는 처리하고자 하는 예외와 같은 타입의 참조변수 하나를 선언해야한다.

 

예외 발생과 처리 순서

  1. 예외가 발생하면, 발생한 예외에 해당하는 클래스의 인스턴스가 만들어진다.
    • ex) 1/0 ArithmeticException 발생. ArithmeticException 인스턴스 생성
  2. 예외가 발생한 문장이 try블럭 내이면, 첫번째 catch블럭부터 예외 처리할 수 있는 catch 블럭 확인
  3. 확인방법은 catch 블럭의 괄호 내 선언된 참조변수의 종류와 생성된 예외클래스의 인스턴스에 instanceof연산자를 이용하여 true 결과값을 나올때까지 확인
  4. 만약 true가 안나오면 예외는 처리되지 않음

모든 예외 클래스는 Exception의 자손이므로, Exception클래스 타입으로 참조변수를 선언하면 어떤 예외든 처리가능.

 

class ExceptionEx7 {
	public static void main(String args[]) {
		System.out.println(1);			
		System.out.println(2);
		try {
			System.out.println(3);
			System.out.println(0/0);
			System.out.println(4); 		// 실행되지 않는다.
		} catch (ArithmeticException ae)	{
			if (ae instanceof ArithmeticException) 
				System.out.println("true");	
			System.out.println("ArithmeticException");
		} catch (Exception e)	{
			System.out.println("Exception");
		}	// try-catch의 끝
		System.out.println(6);
	}	// main메서드의 끝
}

앞의 예외처리에 안걸리더라도, 마지막에 Exception으로 선언해둬서 모든 예외가 걸리게 해뒀다.

 

printStackTrace() 와 getMessage()

예외가 발생했을 때 던져주는 인스턴스의 위의 두가지 메서드를 통해 예외에 대한 정보를 얻을 수 있다.

 

printStackTrace(): 예외발생 당시의 호출스택(Call Stack)에 있었던 메서드의 정보와 예외 메시지를 화면에 출력
getMessage(): 발생한 예외클래스의 인스턴스에 저장된 메시지를 얻을 수 있음

이로 인해 try-catch문으로 정상적으로 진행하더라도 예외에 대한 정보를 얻을 수 있다.

 

멀티 catch블럭

JDK1.7부터 여러 catch블럭 '|' 기호를 이용해서, 하나의 catch블럭으로 합칠 수 있게 되었고, '멀티 catch블럭'이라고 부른다.

 

try{

} catch (ExceptionA | ExceptionB e){

}

 

멀티 catch블럭의 괄호 내의 어떤 error가 발생한건지는 확인할 수 없기 때문에,

참조변수 e는 공통 조상 예외 클래스에 선언된 멤버만 사용할 수 있다.

(한 곳에만 있는 멤버는 사용 불가)

 

예외 발생시키기

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

 

1. 연산자 new를 이용해서 발생시키려는 예외 클래스의 객체 생성
Exception e = new Exception("고의 발생 예외");
2. 키워드 throw를 이용해서 예외를 발생시키기
throw e;
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 예외를 try-catch문으로 처리 안해준다면,

고의로 발생한 예외의 종류에 따라 해당부분에서 오류가 날 것이다.

RuntimeException은 실행 도중에 발생.

compile 관련 에러는 컴파일 도중 발생.

 

메서드에 예외 선언하기

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

 

void method() throws Exception1, Exception2, ...{
	// 메서드의 내용
}
예외 발생 키워드 throw, 메서드 선언 시 사용 키워드 throws 구분하여 사용할 것

 

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

 

메서드에 예외를 선언할때 일반적으로 RuntimeException클래스들은 적지 않는다. 선언한다고 문제가 되진 않지만, 보통 반드시 처리해주어야하는 예외들만 선언한다.

 

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

자신을 호출한 메서드에게 예외를 전달하여 예외처리를 떠맡기는 것이다.

 

이렇게 예외를 전달받은 메서드가 계속 호출스택에 있는 메서드들을 따라가며 전달되다가 마지막 main메서드에서도 예외가 처리되지 않으면, main메서드마저 종료되어 프로그램이 전체 종료된다.

 

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의 끝
}

결과

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

1) 예외가 발생했을 때, 모두 3개의 메서드(main,method1, method2)가 호출스택에 있었으며,

2) 예외가 발생한 곳은 제일 윗줄의 method2

3) main 메서드가 method1()을, method1()은 method2()를 호출했다는 것을 알 수 있다.

 

파일관련 예제

import java.io.*;

class ExceptionEx16 {
	public static void main(String[] args) 
	{
		try {
			File f = createFile(args[0]);
			System.out.println( f.getName()+"파일이 성공적으로 생성되었습니다.");
		} catch (Exception e) {
			System.out.println(e.getMessage()+" 다시 입력해 주시기 바랍니다.");
		}
	}	// main메서드의 끝

	static File createFile(String fileName) throws Exception {
		if (fileName==null || fileName.equals(""))
			throw new Exception("파일이름이 유효하지 않습니다.");
		File f = new File(fileName);		//  File클래스의 객체를 만든다.
        // File객체의 createNewFile메서드를 이용해서 실제 파일을 생성한다.
		f.createNewFile();
		return f;		// 생성된 객체의 참조를 반환한다.
	}	// createFile메서드의 끝
}	// 클래스의 끝
성공결과

C:\jdk1.8\work\ch8>java ExceptionEx22 test2.txt
test2.txt파일이 성공적으로 생성되었습니다.

실패결과

C:\jdk1.8\work\ch8>java ExceptionEx22 ""
파일이름이 유효하지 않습니다. 다시 입력해 주시기 바랍니다.

Exception 인스턴스를 만들 때, 아래와 같이 message를 넣을 수 있다.

new Exception("파일이름이 유효하지 않습니다.");

그럼 예외처리를 하는 메서드에서 getMessage()로 가져올 수 있다.

 

finally 블럭

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

 

try {
	// 예외가 발생할 가능성이 있는 문장
} catch{
	//예외처리 위한 문장
} finally{
	//예외와 상관없이 실행
}

try문이나 catch문에 return이 존재하더라도, finally문이 먼저 실행된다.

 

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

JDK1.7부터 추가된 문법이다.

 

사용했던 자원을 반환해주는 문법으로 주로 입출력에 사용되는 클래스에 쓰인다.

 

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();
		}	
	} // main의 끝
}

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); }
}

main 메서드에 두 개의 try-catch문이 있는데,

첫번째 것은 close()에서만 예외 발생시키고,

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

 

첫번째는 일반적인 예외가 발생했을때와 같은 형태로 출력된다.

exceptionWork(false)가 호출됨
close()가 호출됨
CloseException: CloseException발생!!!
        at CloseableResource.close(TryWithResourceEx.java:33)
        at TryWithResourceEx.main(TryWithResourceEx.java:6)

두번째는 exceptionWork()에서 발생한 예외에 대한 내용이 출력되고,

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

exceptionWork(true)가 호출됨
close()가 호출됨
WorkException: WorkException발생!!!
        at CloseableResource.exceptionWork(TryWithResourceEx.java:28)
        at TryWithResourceEx.main(TryWithResourceEx.java:14)
        Suppressed: CloseException: CloseException발생!!!
                at CloseableResource.close(TryWithResourceEx.java:33)
                at TryWithResourceEx.main(TryWithResourceEx.java:15)

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

 

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

 

사용자정의 예외 만들기

Exception 클래스를 상속받아서 만들면 된다.

 

class MyException extends Exception{
	MyException(String msg){ // 문자열을 매개변수로 받는 생성자
    	super(msg); // 조상인 Exception클래스의 생성자를 호출한다.
    }
}

 

예외 던지기(exception re-thorwing)

'예외 되던지기(exception re-throwing)'이란 이미 처리한 예외를 인위적으로 다시 발생시키는 방법을 뜻한다.

 

예외가 발생할 가능성이 있는 메서드에서 try-catch문을 사용해서 예외를 처리해주고 catch문에서 필요한 작업을 행한 후에 thorw문을 사용해서 예외를 다시 발생시킨다. 다시 발생한 예외는 이 메서드를 호출한 메서드에게 전달되고 호출한 메서드의 try-catch문에서 예외를 또다시 처리한다.

 

예외가 발생한 메서드와 호출한 메서드 양쪽 모두에서 예외를 가지고 처리할 작업이 있을 때 쓰인다.

주의할 점은 예외가 발생할 메서드에서 try-catch문을 사용해서 예외처리를 해줌과 동시에 메서드 선언부에 발생할 예외를 throws에 지정해줘야 한다는 것이다.

 

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메서드의 끝
}

 

연결된 예외(chained exception)

한 예외가 다른 예외를 발생시킬 수도 잇는데,

예외 A가 예외 B를 발생시키면 연결된 예외라고 하고, A를 B의 '원인 예외(cause exception)'이라고 한다.

 

Exception클래스의 조상인 Throwable 클래스를 통해서 원인예외를 등록하고 반환할 수 있다.

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

 

원인 예외를 사용하는 이유

  1. 여러가지 예외를 하나의 큰 분류의 예외로 묶으면서 실제 발생한 예외는 알기 위해
  2. 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())
    	throw new  RuntimeException(new MemoryException("메모리가 부족합니다."));
}

 

MemoryException은 Exception의 자손이므로 반드시 예외를 처리했어야했는데,

RuntimeException안에 감싸서 unchecked예외로 변경했다.

그래서 startInstall()의 선언부에 선언하지 않아도 된다.

 

checkedException VS uncheckedException

구분 checkedException uncheckedException
범위 RuntimeException클래스를 제외한 Exception 하위 클래스들 RuntimeException 하위 클래스들
오류처리 반드시 처리해줘야함 명시적으로는 처리하지 않아도 됨.
Exception 발생시점 Compile 과정 Runtime 도중
프로그램
구동
처리하지않으면 Compile과정에서 Exception이 발생해서
구동하지 않음
처리하지 않아도 프로그램 구동에는 문제 없음.(추후 구동 중 Exception 발생할 수 있음)

 

참조

'Java의 정석' 책

댓글