자바의 정석 8장 - 예외처리(Exception handling)

청포도봉봉이·2023년 12월 27일
1

자바의 정석

목록 보기
8/16
post-thumbnail

예외처리

프로그램 오류

프로그램이 실행 중 어떤 원인에 의해서 오작동을 하거나 비정상적으로 종료되는 경우가 있다. 이러한 결과를 초래하는 원인을 프로그램 에러 또는 오류라고 한다.

이를 발생시점에 따라 컴파일 에러(compile-time error)런타임 에러(runtime-error)로 나눌 수 있는데, 글자 그대로 컴파일 에러는 컴파일 할 때 발생하는 에러이고 프로그램의 실행 도중에 발생하는 에러를 런타임 에러라 한다. 이 외에도 논리적 에러(logical error)가 있는데, 컴파일도 잘 되고 실행도 잘되지만 의도한 것과 다르게 동작하는 것을 말한다. 예를 들어, 창고의 재고가 음수가 된다던가, 게임 프로그램에서 비행기가 총알을 맞아도 죽지 않는 경우가 이에 해당된다.

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

소스코드를 컴파일 하면 컴파일러가 소스코드(.java)에 대해 오타나 잘못된 구문, 자료형 체크 등의 기본적인 검사를 수행하여 오류가 있는지를 알려 준다. 컴파일러가 알려 준 에러들을 모두 수정해서 컴파일을 성공적으로 마치고 나면, 클래스 파일(.class)이 생성되고, 생성된 클래스 파일을 실행할 수 있게 되는 것이다.

하지만 컴파일을 에러없이 성공적으로 마쳤다고 해서 프로그램의 실행 시에도 에러가 발생하지 않는 것은 아니다. 컴파일러가 소스코드의 기본적인 사항은 컴파일 시에 모두 걸러 줄 수는 있지만, 실행도중에 발생할 수 있는 잠재적인 오류까지 검사할 수 없기 때문에 컴파일은 잘되었어도 실행 중에 에러에 의해서 잘못된 결과를 얻거나 프로그램이 비정상적으로 종료될 수 있다. 여러분은 이미 실행도중에 발생하는 런타임 에러를 여러 번 경험했을 것이다. 예를 들면 프로그램이 실행 중 동작을 멈춘 상태로 오랜 시간 지속되거나, 갑자기 프로그램이 실행을 멈추고 종료되는 경우 등이 이에 해당한다.

런타임 에러를 방지하기 위해서는 프로그램의 실행도중 발생할 수 있는 모든 경우의 수를 고려하여 이에 대한 대비를 하는 것이 필요하다. 자바에서는 실행 시(runtime) 발생할 수 있는 프로그램 오류를 에러(error)예외(exception), 두 가지로 구분하였다.

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

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

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

예외 클래스의 계층구조

자바에서는 실행 시 발생할 수 있는 오류(Exception과 Error)를 클래스로 정의하였다. 앞서 배운 것처럼 모든 클래스의 조상은 Object클래스이므로 Exception과 Error클래스 역시 Object클래스의 자손들이다.

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

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

  1. Exception클래스와 그 자손들
  2. RuntimeException클래스와 그 자손들

RuntimeException클래스들은 주로 프로그래머의 실수에 의해서 발생될 수 있는 예외들로 자바의 프로그래밍 요소들과 관계가 깊다. 예를 들면, 배열의 범위를 벗어난다던가, 값이 null인 참조변수의 멤버를 호출하려 했다던가, 클래스간의 형변환을 잘못했다던가, 정수를 0으로 나누려고 하는 경우 등의 경우에 발생한다.

Exception 클래스들: 사용자의 실수와 같은 외적인 요인에 의해 발생하는 예외
RuntimeException 클래스들: 프로그래머의 실수로 발생하는 예외

예외처리하기 (try-catch문)

예외처리(Exception Handling)란, 프로그램 실행 시 발생할 수 있는 예기치 못한 예외의 발생에 대비한 코드를 작성하는 것이며, 예외처리의 목적은 예외의 발생으로 인한 실행 중인 프로그램의 갑작스런 비정상 종료를 막고, 정상적인 실행상태를 유지할 수 있도록 하는 것이다.

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

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

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

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

하나의 try블럭 다음에는 여러 종류의 예외를 처리할 수 있도록 하나 이상의 catch블럭이 올 수 있으며, 이 중 발생한 예외의 종류와 일치하는 단 한 개의 catch블럭만 수행된다. 발생한 예외의 종류와 일치하는 catch블럭이 없으면 예외는 처리되지 않는다.

try-catch문에서의 흐름

try-catch문에서, 예외가 발생한 경우와 발생하지 않았을 때의 흐름(문장의 실행순서)이 달라지는데, 아래에 이 두 가지 경우에 따른 문장 실행순서를 정리하였다.

► try블럭 내에서 예외가 발생한 경우,
1. 발생한 예외와 일치하는 catch블럭이 있는지 확인한다.
2. 일치하는 catch블럭을 찾게 되면, 그 catch블럭 내의 문장들을 수행하고 전체 try-catch문을 빠져나가서 그 다음 문장을 계속해서 수행한다. 만일 일치하는 catch블럭을 찾지 못하면, 예외는 처리되지 못한다.

► try블럭 내에서 예외가 발생하지 않은 경우,
1. catch블럭을 거치지 않고 전체 try-catch문을 빠져나가서 수행을 계속한다.

예외의 발생과 catch블럭

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

예외가 발생하면, 발생한 예외에 해당하는 클래스의 인스턴스가 만들어진다. 예외가 발생한 문장이 try블럭에 포함되어 있다면, 이 예외를 처리할 수 있는 catch블럭이 있는지 찾게 된다.

첫 번째 catch블럭부터 차례로 내려가면서 catch블럭의 괄호()내에 선언된 참조변수의 종류와 생성된 예외클래스의 인스턴스에 instanceof연산자를 이용해서 검사하게 되는데,

검사결과가 true인 catch블럭을 찾게 되면 블럭에 있는 문장들을 모두 수행한 후에 try-catch문을 빠져나가고 예외는 처리되지만, 검사결과가 true인 catch블럭이 하나도 없으면 예외는 처리되지 않는다.

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

printStackTrace()와 getMessage()

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

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

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

멀티 catch 블럭

JDK1.7부터 여러 catch블럭을 | 기호를 이용해서, 하나의 catch블럭으로 합칠 수 있게 되었으며, 이를 멀티 catch블럭이라 한다. 아래의 코드에서 알 수 있듯이 멀티 catch블럭을 이용하면 중복된 코드를 줄일 수 있다.

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

만일 멀티 catch블럭의 |기호로 연결된 예외 클래스가 조상과 자손의 관계에 있다면 컴파일 에러가 발생한다.

try {
	...
}
catch (ParentException | ChildException e) { // 에러!!
	e.printStackTrace();
}

예외 발생시키기

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

  1. 먼저 연산자 new를 이용해서 발생시키려는 예외 클래스의 객체를 만든 다음
Exception e = new Exception("고의 예외 발생");
  1. 키워드 throw를 이용해서 예외를 발생시킨다.
throw e;

메서드에 예외 선언하기

예외를 처리하는 방법에는 지금까지 배워 온 try-catch문을 사용하는 것 외에, 예외를 메서드에 선언하는 방법이 있다.

메서드에 예외를 선언하려면, 메서드의 선언부에 키워드 throws를 사용해서 메서드 내에서 발생할 수 있는 예외를 적어주기만 하면 된다. 그리고 예외가 여러 개일 경우에는 쉼표(,)로 구분한다.

만일 아래와 같이 모든 예외의 최고 조상인 Exception클래스를 메서드에 선언하면, 이 메서드는 모든 종류의 예외가 발생할 가능성이 있다는 뜻이다.

void method() throws Exception {
	// 메서드 내용
}

finally블럭

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

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

자동 자원 반환 (try-with-resource)

JDK1.7부터 try-with-resource문이라는 변형이 새로 추가되었다. 주로 입출력에 사용되는 클래스 중에서 사용한 후에 꼭 닫아 줘야 하는 것들이 있다. 그래야 사용했던 자원(resource)이 반환되기 때문이다.

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

위의 코드는 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-resource문으로 바꾸면 다음과 같다.

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) {
	...
} catch (IOException ie) {
	...
}

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

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

사용자정의 예외 만들기

기존의 정의된 예외 클래스 외에 필요에 따라 프로그래머가 새로운 예외 클래스를 정의하여 사용할 수 있다. 보통 Exception 클래스 또는 RuntimeException클래스로부터 상속받아 클래스를 만들지만, 필요에 따라서 알맞은 예외 클래스를 선택할 수 있다.

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

Exception클래스로부터 상속받아서 MyException클래스를 만들었다. 필요하다면, 멤버 변수나 메서드를 추가할 수 있다. Exception클래스는 생성 시에 String값을 받아서 메시지로 저장할 수 있다.

class MyException extends Exception {
	// 에러 코드 값을 저장하기 위한 필도를 추가했다.
    private final int ERR_CODE; // 생성자를 통해 초기화 한다.
    
    MyException(String msg, int errCode) { // 생성자
		super(msg);
        ERR_CODE = errCode;
	}
    
    MyException(String msg) { // 생성자
		this(msg, 100);	// ERR_CODE를 100(기본값)으로 초기화한다.
	}
    
    public int getErrCode() {
		return ERR_CODE // 이 메서드는 주로 getMessage()와 함께 사용될 것이다.
	}
}

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

한 메서드에서 발생할 수 있는 예외가 여럿인 경우, 몇 개는 try-catch문을 통해서 메서드 내에서 자체적으로 처리하고, 그 나머지는 선언부에 지정하여 호출한 메서드에서 처리하도록 함으로써, 양쪽에서 나눠서 처리되도록 할 수 있다.

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

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

public void someMethod() throws CustomException {
    try {
        // 예외 발생 가능한 코드
    } catch (SpecificException e) {
        // 예외 처리 코드
        throw new CustomException("Custom exception occurred", e);
    }
}

열결된 예외(chained exception)

한 예외가 다른 예외를 발생시킬 수도 있다. 예를 들어 예외 A가 예외 B를 발생시켰다면, A를 B의 원인 예외(cause exception)라고 한다. 아래의 코드는 SpaceException을 원인 예외로 하는 InstallException을 발생시키는 방법을 보여준다.

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

먼저 InstallException을 생성한 후에, initCause()로 SpaceException을 InstallException의 원인 예외로 등록한다. 그리고 throw로 이 예외를 던진다.

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

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

발생한 예외를 그냥 처리하면 될 텐데, 원인 예외로 등록해서 다시 예외를 발생시키는지 궁금할 것이다. 그 이유는 여러가지 예외를 하나의 큰 분류의 예외로 묶어서 다루기 위해서이다.

public void someMethod() throws MainException {
    try {
        // 예외 발생 가능한 코드
    } catch (SpecificException e) {
        // 예외 처리 코드
        throw new MainException("Main exception occurred", e);
    }
}
profile
서버 백엔드 개발자

0개의 댓글