프로그램을 만들다 보면 다양한 예외상황 또는 에러가 발생하곤 한다. 이러한 에러를 처리하는 방법을 배워보자.
10.1 예외 처리의 목적
예외처리란(Exception, Error Handling)
•
코드를 완벽하게 짰다고 해서 항상 프로그램이 성공적으로 도는 것은 아니다. 다양한 예외 상황이 발생할 수 있는데, 이것에 대응하기 위해 예외 처리 코드가 필요하다.
•
예외처리의 목적
1.
예외의 발생으로 인한 실행 중인 프로그램의 비정상 종료를 막기 위해
2.
개발자에게 알려서 코드를 보완할 수 있도록 하기 위해
10.2 자바의 예외 클래스 위계구조 (hieararchy)
자바에서는 상속을 이용해서 모든 예외를 표현한다. 모든 예외 클래스는 Throwable의 자손 클래스이다.
•
자바에 미리 정의되어있는 예외 클래스들의 hiearchy의 예시이다. 실제로 위 그림보다 더 많을 수 있다.
•
Throwable에는 크게 두 종류의 자식 클래스가 있다.
◦
Error는 프로그램이 종료되어야 하는 심각한 문제를 표현한다. 대부분 컴퓨터나 JVM이 시스템적으로 동작할 수 없는 상황을 표현한다.
Java는 JVM내의 Heap이라는 메모리 공간을 운영체제로부터 할당받아 사용한다. 할당 받을 수 있는 최대 메모리 이상을 사용하면, JVM이 다운될 수 밖에 없다. 이 경우 OutOfMemoryError가 나면서 프로그램이 종료된다. 자바의 대표적인 에러 상황으로 줄여서 OOM이라고도 한다.
◦
Exception은 프로그램이 종료되지는 않지만 예외나 문제상황을 표현하기 위해 사용한다.
10.3 사용자 정의 예외 클래스
자바에 미리 정의 되어있는 예외 클래스 들이 있다. 기본적으로 이미 있는 것을 사용하되, 필요한 것으로 표현할 수 없거나 구체적인 목적을 가진 예외를 정의하고 싶다면, Throwable 또는 그 하위에 있는 예외 클래스를 상속받아서 자신만의 예외 클래스를 정의할 수 있다.
•
표현하려는 예외 상황은 대부분 Exception 종류일 것이다.
•
실행도중 발생하는 Exception은 RuntimeException 을 상속받아서 정의.
•
파일을 읽고 쓰거나, 원격에 있는 저장소로부터 데이터를 읽고 쓸 때 나는 에러를 표현하려면 IOException 을 상속받아서 정의.
•
메소드 등에 잘못된 인자(parameter, arguments)를 받은 경우에는 IllegalArgumentException 을 사용.
•
Error는 프로그램이 강제로 종료되어야 하는 경우에만 상속아서 에러 클래스를 구현하고, 해당 에러가 발생하면 프로그램이 종료되도록 구현해야 한다.
10.4 예외 처리
10.4.1 try-catch
try {
// 예외가 발생할 가능성이 있는 코드를 구현한다.
} catch (FileNotFoundException e) {
// FileNotFoundException이 발생했을 경우,이를 처리하기 위한 코드를 구현한다.
} catch (IOException e) {
// FileNotFoundException이 아닌 IOException이 발생했을 경우,이를 처리하기 위한 코드를 구현한다.
}
Java
복사
실행 대상이 되는 코드를 try 구문 안에 쓴다.
catch 는 예외 처리를 하고 싶은 최상위 클래스의 타입으로 변수를 catch 의 괄호 안에 선언한다.
•
Throwable 로 catch 문을 작성하면, Throwable 은 모든 예외의 부모이기 때문에 모든 예외의 경우가 잡힌다.
예외 처리 코드는 catch 의 중괄호 블럭 안에 작성한다.
catch는 이어서 작성할 수 있다. 예외 발생시, 순차적으로 확인한다.
앞의 catch 블럭에서 잡혔다면, 뒤의 catch 블럭으로는 전파되지 않는다. 좁은 범위의 예외부터 앞에 선언하는 것이 좋다. 여기서 좁은 범위란 상속관계에서 자식 클래스에 위치 할수록 좁은 범위이다.
예를 들어서 IOException 이 발생할 것 같아 예외처리를 하고, 그 외의 예외도 예외처리를 하고 싶다면 IOException 을 catch 하는 구문을 먼저, Exception 을 catch 하는 구문을 그 뒤에 작성한다. catch 를 앞에서 했지만 에러를 또 전파하고 싶다면, catch 문 안에서 throw 로 예외를 발생시키면 된다.
10.4.2 try-catch-finally
try {
// 예외가 발생할 가능성이 있는 코드를 구현한다.
} catch (FileNotFoundException e) {
// FileNotFoundException이 발생했을 경우,이를 처리하기 위한 코드를 구현한다.
} catch (IOException e) {
// FileNotFoundException이 아닌 IOException이 발생했을 경우,이를 처리하기 위한 코드를 구현한다.
} finally {
// 예외의 발생여부에 관계없이 항상 수행되어야하는 코드를 구현한다.
}
Java
복사
•
finally 구문은 필수는 아니다.
•
만약, 예외가 발생하지 않는다면 try → finally 순으로 실행된다.
•
try-catch-finally 예제
public class Main {
public static void main(String[] args) {
int number = 10;
int result;
for (int i = 10; i >= 0; i--) {
try {
result = number / i;
System.out.println(result);
} catch (Exception e) {
System.out.println("Exception발생: " + e.getMessage());
} finally {
System.out.println("항상 실행되는 finally 구문");
}
}
}
}
Java
복사
10.4.3 try-with-resource
import java.io.FileOutputStream;
import java.io.IOException;
public class Main {
public static void main(String[] args) {
try (FileOutputStream out = new FileOutputStream("test.txt")) {
// test.txt file 에 Hello Sparta 를 출력
out.write("Hello World".getBytes());
out.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
}
Java
복사
try 구문 안에서만 사용되는 자원을 try-catch 구문이 끝나면 자동으로 닫을 수 있도록 할 수 있도록 하는 문법.
•
입출력(I/O)과 함께 자주 쓰이는 구문이다.
•
기존의 try-catch(-finally)문은 자원을 닫고 싶다면 명시적으로 Closable.close() 를 사용해야 한다.
•
try-with-resource문은 try문을 벗어나는 순간 자동적으로 close() 가 호출된다.
•
단, try()안에서 입출력 스트림을 생성하는 객체가 AutoClosable 인터페이스를 구현한 객체여야 한다.
AutoClosable 인터페이스
왜 AutoClosable 인터페이스를 사용해야할까?? 바로 AutoClosable 인터페이스에는 예외가 발생할 경우 close() 메소드를 호출하기로 정의되어있기 때문이다.
만약에 try-with-resource가 아니라 일반 try catch문을 사용했다면 아래와 같은 코드가 된다.
코드가 길어질 뿐만 아니라 FileOutputStream 을 열고 닫을때 생기는 Exception 까지 그 상위에서 catch를 하거나 throws 로 감싸줘야한다.
import java.io.FileOutputStream;
import java.io.IOException;
public class Main {
public static void main(String[] args) throws IOException {
FileOutputStream out = new FileOutputStream("test.txt");
try {
// test.txt file 에 Hello Sparta 를 출력
out.write("Hello World".getBytes());
out.flush();
} catch (IOException e) {
e.printStackTrace();
}
out.close();
}
}
Java
복사
10.4.4 메소드에서의 예외 선언
void methodThrowsException() throws Exception {
//메소드의 내용
}
void methodThrowsRuntimeException() throws RuntimeException {
}
void caller() {
// methodThrowsException(); compile error
try {
methodThrowsException()
} catch (Exception exception) {
}
methodThrowsRuntimeException(); // no compile error
}
Java
복사
catch문을 이용해서 예외처리를 하지 않은 경우, 메소드에 throws로 예외가 발생할 수 있다는 것을 알려주어야 한다. throws 키워드가 있는 함수를 호출한다면, caller 쪽에서 catch와 관련된 코드를 작성해주어야 한다.
이렇게, 키워드 throws를 사용해서 메소드 내에서 발생할 수 있는 예외, caller 쪽에서 꼭 처리해주어야하는 예외를 적어주면 된다.
단, java.lang.RntimeException 과 그것을 상속받은 예외 클래스는 throws 가 있고 caller에서 그 처리를 안해주더라도 컴파일 에러가 발생하지 않는다. 사용자 예외 클래스를 throws에 대해서 예외처리를 강제하고 싶은 경우에는 java.lang.Exception 클래스를 상속받아서 구현해야한다.
Quiz-1
다음 스니펫에 있는 divide() 함수는 매개변수(parameter)에 들어오는 값에 따라서 ArithmeticException 과 ArrayIndexOutOfBoundsException 이 발생할 수 있다.
1.
throws 키워드를 통해서 divide() 함수에서 발생할 수 있는 exception의 종류가 무엇인지 알아보자.
2.
Main 함수에서 try-catch 문을 이용해서, 다음 동작을 구현한다.
a.
ArithmeticException 이 발생할 때는 잘못된 계산임을 알리는 문구를 출력.
b.
ArrayIndexOutOfBoundsException 이 발생할 때는 현재 배열의 index범위를 알려주는 문구를 출력.
class ArrayCalculation {
int[] arr = { 0, 1, 2, 3, 4 };
public int divide(int denominatorIndex, int numeratorIndex) {
return arr[denominatorIndex] / arr[numeratorIndex];
}
}
public class Main {
public static void main(String[] args) {
ArrayCalculation arrayCalculation = new ArrayCalculation();
System.out.println("2 / 1 = " + arrayCalculation.divide(2, 1));
System.out.println("1 / 0 = " + arrayCalculation.divide(1, 0)); // java.lang.ArithmeticException: "/ by zero"
System.out.println("Try to divide using out of index element = " + arrayCalculation.divide(5, 0)); // java.lang.ArrayIndexOutOfBoundsException: 5
}
}
Java
복사
Main
Answer-1
class ArrayCalculation {
int[] arr = { 0, 1, 2, 3, 4 };
public int divide(int denominatorIndex, int numeratorIndex)
throws ArithmeticException, ArrayIndexOutOfBoundsException {
return arr[denominatorIndex] / arr[numeratorIndex];
}
}
public class Main {
public static void main(String[] args) {
ArrayCalculation arrayCalculation = new ArrayCalculation();
System.out.println("2 / 1 = " + arrayCalculation.divide(2, 1));
try {
System.out.println("1 / 0 = " + arrayCalculation.divide(1, 0));
} catch (ArithmeticException arithmeticException) {
System.out.println("잘못된 계산입니다. " + arithmeticException.getMessage());
}
try {
System.out.println("Try to divide using out of index element = " + arrayCalculation.divide(5, 0));
} catch (ArrayIndexOutOfBoundsException arrayIndexOutOfBoundsException) {
System.out.println("잘못된 index 범위로 나누었습니다. 타당 index 범위는 0부터" + (arrayCalculation.arr.length - 1) + "까지 입니다.");
}
}
}
Java
복사
Quiz-2
입력한 경로의 파일을 여는 프로그램을 만든다.
접근 불가능한 경로를 접근했다는 사용자 정의 Error 클래스를 만든다.(Error 타입을 상속)
경로가 /Users 경로의 하위 경로가 아니라면, 접근 불가능한 경로를 접근했다는 Error를 발생시키고, 프로그램이 강제종료 된다는 문구를 출력하고, 프로그램을 강제 종료 System.exit(1) 한다.
접근 가능한 경로라면, 실제 파일이 존재하는지를 출력하고 정상 종료한다.
참고 : 문자열 입력 받는 법
Scanner scanner = new Scanner(System.in);
String path = scanner.nextLine();
Java
복사
참고 : File 확인하는 법
File file = new File("경로");
if(file.exists()) {
// file 존재
} else {
// 해당 파일 없음
}
Java
복사
Answer-2
public class FileValidator {
public static boolean validate(String path) throws IllegalPathAccessError {
if(path.startsWith("/Users/")){
File file = new File(path);
return file.exists();
} else {
throw new IllegalPathAccessError(path);
}
}
}
Java
복사
FileValidator.java
public class IllegalPathAccessError extends Error{
private String path;
public IllegalPathAccessError(String path) {
super();
this.path = path;
}
@Override
public String getMessage() {
return path + " is not allowed to access.";
}
}
Java
복사
IllegalPathAccessError.java
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
String path = scanner.nextLine();
try {
if(FileValidator.validate(path)) {
System.out.println("File " + path + " exists.");
} else {
System.out.println("File " + path + " doesn't exist.");
}
} catch (IllegalPathAccessError illegalPathAccessError) {
System.out.println(illegalPathAccessError.getMessage() + "\n");
illegalPathAccessError.printStackTrace();
System.out.println("Program is forced to quit.");
System.exit(1);
}
}
}
Java
복사
Main.java