프로그램을 만들다 보면 다양한 예외상황 또는 에러가 발생하곤 한다. 에러를 처리하는 방법에 대해 알아보자
코드를 완벽하게 짰다고 해서 항상 프로그램이 성공적으로 도는 것은 아니다. 다양한 예외 상황이 발생할 수 있고, 이것에 대응하기 위해서 예외 처리 코드가 필요하다.
자바에서는 상속을 이용해서 모든 예외를 표현한다. 모든 클래스는 Object를 상속받는다.
모든 예외 클래스는 그 중 Throwable의 자손 클래스다. (extend로 상속받는)
이미지 출처 : https://www.protechtraining.com/bookshelf/java_fundamentals_tutorial/exceptions
Java에서는 예외 처리를 위해
Throwable
클래스를 기준으로 두 가지 주요 하위 클래스가 존재한다.
Error
시스템 수준의 치명적인 문제로 인해 프로그램이 강제 종료된다.
대부분 JVM 또는 시스템 리소스와 관련된 문제로 인해 발생한다.
try-catch
로 처리하지 않는다.대표적인 하위 클래스
StackOverflowError
OutOfMemoryError
(OOM)
LinkageError
Exception
일반적인 예외 상황을 나타내며, 예외 처리를 통해 복구할 수 있는 경우가 많다. 프로그램을 종료하지 않고 문제를 우회하거나 적절히 처리할 수 있다.
Checked Exception (컴파일 시 예외 처리 강제)
IOException
외부 자원 (파일, 네트워크 등)과의 입출력 중 발생한다.
SocketException
: 네트워크 통신 시 포트가 이미 사용 중이거나 소켓을 열 수 없는 경우 발생한다.Unchecked Exception (RuntimeException
계열)
RuntimeException
논리적 오류나 잘못된 프로그래밍으로 인해 발생한다.
IllegalArgumentException
: 메서드에 전달된 파라미터가 유효하지 않을 때 발생한다.대부분의 커스텀 예외 클래스는 RuntimeException
을 상속받는 방식으로 설계한다. 이유는 다음과 같다:
try-catch
처리 선택 가능).예외 클래스는 프로그램의 안정성과 유지보수성을 높이기 위해 체계적으로 설계되어 있다. 상황에 맞는 예외 클래스를 상속하고, 의미 있는 예외 메시지를 남기는 것이 중요하다.
10.2 그림처럼 자바에 미리 정의 되어있는 예외 클래스 들이 있다. 기본적으로 이미 있는 것을 사용하시되, 필요한 것으로 표현할 수 없거나 구체적인 목적을 가진 예외를 정의하고 싶다면, Throwable 또는 그 하위에 있는 예외 클래스를 상속받아서 자신만의 예외 클래스를 정의할 수 있다.
Exception
종류일 것이다.RuntimeException
을 상속받아서 정의하자IOException
을 상속받아서 정의하자IllegalArgumentException
을 사용try {
// 예외가 발생할 가능성이 있는 코드를 구현한다.
} catch (FileNotFoundException e) {
// FileNotFoundException이 발생했을 경우,이를 처리하기 위한 코드를 구현.
} catch (IOException e) {
// FileNotFoundException이 아닌 IOException이 발생했을 경우,이를 처리하기 위한 코드를 구현.
}
catch
는 예외 처리를 하고 싶은 최상위 클래스의 타입으로 변수를 catch
의 괄호 안에 선언한다.
Throwable
로 catch
문을 작성하면, Throwable
은 모든 예외의 부모이기 때문에 모든 예외으 경우가 잡힌다.try
: 예외가 발생할 수 있는 코드 작성catch
: 특정 예외 타입을 잡아 처리IOException
→ Exception
catch
블럭을 순차적으로 사용할 수 있으며,앞의 catch 블럭에서 잡혔다면, 뒤의 catch 블럭으로는 전파되지 않는다. 좁은 범위의 예외부터 앞에 선언하는 것이 좋다. 여기서 좁은 범위란 상속관계에서 자식 클래스에 위치 할수록 좁은 범위다. 예를 들어서 IOException 이 발생할 것 같아 예외처리를 하고, 그 외의 예외도 예외처리를 하고 싶다면 IOException 을 catch 하는 구문을 먼저, Exception 을 catch 하는 구문을 그 뒤에 작성한다. catch 를 앞에서 했지만 에러를 또 전파하고 싶다면, catch 문 안에서 throw 로 예외를 발생시키면 된다.
catch (Throwable e)
사용 시, 모든 예외와 에러를 포괄 가능catch
블럭에서 throw
로 예외를 다시 발생시켜catch (IOException e) {
// 로그 처리 등
throw e; // 예외 재전파
}
FileNotFoundException은 특별히 처리하고, FileNotFoundException 포함해서 모든 IOException은 뭔가 처리를 하고 싶은 경우 throw로 다음 catch문에 전파할 수 있다. 하지만 계속 전파하는 경우는 권장하지 않고 다른 디자인을 하는 것을 추천. 하지만 필요하면 써야지.
try{
//my code
}catch (FileNotFoundException e){
// my code
throw e;
}catch (IOException e){
}
try {
// 예외가 발생할 가능성이 있는 코드를 구현합니다.
} catch (FileNotFoundException e) {
// FileNotFoundException이 발생했을 경우,이를 처리하기 위한 코드를 구현합니다.
} catch (IOException e) {
// FileNotFoundException이 아닌 IOException이 발생했을 경우,이를 처리하기 위한 코드를 구현합니다.
} finally {
// 예외의 발생여부에 관계없이 항상 수행되어야하는 코드를 구현합니다.
}
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 구문");
}
}
}
}
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();
}
}
}
try 구문 안에서만 사용되는 자원을 try-catch 구문이 끝나면 자동으로 닫을 수 있도록 할 수 있도록 하는 문법이다.
Closable.close()
를 사용해야 한다.close()
가 호출된다.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();
}
}
void methodThrowsException() throws Exception {
//메소드의 내용
}
void methodThrowsRuntimeException() throws RuntimeException {
}
void caller() {
// methodThrowsException(); compile error
try {
methodThrowsException()
} catch (Exception exception) {
}
methodThrowsRuntimeException(); // no compile error
}
catch문을 이용해서 예외처리를 하지 않은 경우, 메소드에 throws로 예외가 발생할 수 있다는 것을 알려줘야 함.
throws 키워드가 있는 함수를 호출한다면, caller 쪽에서 catch와 관련된 코드를 작성해줘야 함.
이처럼, 키워드 throws를 사용해서 메소드 내에서 발생할 수 있는 예외, caller 쪽에서 꼭 처리해주어야하는 예외를 적어주면 된다.
단, java.lang.RntimeException
과 그것을 상속받은 예외 클래스는 throws
가 있고 caller에서 그 처리를 안해주더라도 compile error가 발생하지 않는다. 사용자 예외 클래스를 throws
에 대해서 예외처리를 강제하고 싶은 경우에는 java.lang.Exception
클래스를 상속받아서 구현하자.
다음 스니펫에 있는 divide()
함수는 매개변수(parameter)에 들어오는 값에 따라서 ArithmeticException
과
ArrayIndexOutOfBoundsException
이 발생할 수 있다.
ArithmeticException
이 발생할 때는 잘못된 계산임을 알리는 문구를 출력하세요.ArrayIndexOutOfBoundsException
이 발생할 때는 현재 배열의 index범위를 알려주는 문구를 출력하세요.[코드스니펫] 예외처리 퀴즈 - Main 함수
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
}
}
입력한 경로의 파일을 여는 프로그램을 만드시오.
접근 불가능한 경로를 접근했다는 사용자 정의 Error 클래스를 만드시오.(Error 타입을 상속)
경로가 /Users 경로의 하위 경로가 아니라면, 접근 불가능한 경로를 접근했다는 Error를 발생시키고, 프로그램이 강제종료 된다는 문구를
출력하고, 프로그램을 강제 종료 System.exit(1) 하시오.
접근 가능한 경로라면, 실제 파일이 존재하는지를 출력하고 정상 종료 하시오.
참고: 문자열 입력 받는 법
Scanner scanner = new Scanner(System.in);
String path = scanner.nextLine();
참고: File 확인하는 법
File file = new File("경로");
if(file.exists()) {
// file 존재
} else {
// 해당 파일 없음
}
답안
FileValidator.java
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);
}
}
}
IllegalPathAccessError
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.";
}
}
Main.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);
}
}
}