Java 예외 처리
Java 프로그램이 실행 중 에러 또는 오류가 발생하면 프로그램이 예기치 않게 종료될 수 있습니다. 이러한 문제를 방지하기 위해 Java 프로그래밍에선 예외 처리를 해주는 것이 매우 중요합니다. 예외 처리를 통해 코드가 실행 중에 발생할 수 있는 오류를 처리함으로써, 애플리케이션이 갑작스럽게 종료되는 것을 방지하고 프로그램의 안정성을 높일 수 있습니다.
예외 계층 구조
Java에서는 실행 시 발생할 수 있는 에러(error)와 예외(exception)를 클래스로 정의하였습니다. Java에서 에러(error)와 예외(exception)는 Throwable 클래스를 상속받는 객체입니다. 즉 Throwable 클래스를 상속받는 자식 클래스로 Error와 Exception 클래스가 있습니다.
- Error: 시스템 레벨에서 발생하는 오류로, 프로그래머가 처리할 수 없는 오류들입니다. 예를 들어, 메모리 부족(OutOfMemory)이나 StackOverflow, JVM의 비정상적인 동작 같은 치명적인 문제인 경우입니다.
- Exception: 애플리케이션 수준에서 발생하는 예외로, 프로그래머가 처리할 수 있는 오류들입니다.
Exception 클래스는 다시 체크드 예외(Checked Exception)와 언체크드 예외(Unchecked Exception)로 나뉩니다.
- Checked Exception: 컴파일 시점에서 발생할 수 있는 에러입니다. Java 컴파일러가 소스코드(*. java)에 대해 잘못된 구문, 자료형 체크 등에 대한 검사를 수행하여 발견한 경우 에러를 발생시킵니다. 예시: IOException, SQLException
- Unchecked Exception: 런타임 에러로, 컴파일 시에는 잡지 못하고 실행 시에 프로그램이 종료되는 예외입니다. 예시: NullPointerException, ArrayIndexOutOfBoundsException
Java 예외 처리하는 방식
Java에서는 주로 try-catch 블록을 사용해 예외를 처리합니다. try 블록에서는 예외가 발생할 수 있는 코드를 작성하고, catch 블록에서는 try 블록에서 발생한 예외를 처리합니다. 또한 finally 블록을 통해 예외의 발생여부와 상관없이 무조건 실행됩니다. (finally 블록은 생략 가능합니다.)
try {
// 예외가 발생할 수 있는 코드
} catch (Exception e) {
// 예외 처리
} finally {
// 항상 실행되는 코드 (선택적)
}
finally 블록은 예외 발생 여부와 상관없이 항상 실행되기 때문에 자원 할당 해제와 같이 반드시 해주어야 하는 코드를 집어넣어 주시는 것이 좋습니다.
💁 try-with-resources 구문
Java 7 이후부터는 try-with-resources 구문을 사용해 자동으로 자원을 해제할 수 있습니다. 자원을 사용한 후 자동으로 닫히게 해 주기 때문에, 명시적으로 close() 메서드를 호출할 필요가 없습니다. 이를 통해 자원을 사용하는 코드를 쉽게 작성하게 해 줍니다. 자원이 AutoCloseable 인터페이스를 구현한 경우에만 사용 가능합니다.
try (BufferedReader br = new BufferedReader(new FileReader("file.txt"))) { // 파일 읽기 코드 } catch (IOException e) { e.printStackTrace(); }
메서드 단에서 예외 처리
메서드 내에서 예외가 발생했을 때, 이를 메서드 내부에서 예외를 처리하거나, 상위 메서드로 예외를 전달하는 방식으로 예외를 처리할 수 있습니다.
- 메서드 내부에서 예외 처리: try-catch 블록을 사용하여 예외를 메서드 내부에서 처리합니다. 위에서 다룬 기본적인 예외 처리 방법입니다.
- 메서드가 예외를 호출자(상위 메서드)에게 전달: throws 키워드를 사용해 메서드에서 발생한 예외를 호출한 메서드(상위 메서드)로 전달합니다.
1. 메서드 내부에서 예외 처리
기본적인 try-catch 블록을 사용한 예외 처리 방식입니다. 메서드 자체적으로 예외 처리를 완전히 끝내는 경우입니다.
public void readFile(String fileName) {
try {
BufferedReader reader = new BufferedReader(new FileReader(fileName));
String line = reader.readLine();
// 코드 ...
} catch (IOException e) {
System.out.println("파일을 읽는 도중 오류가 발생했습니다: " + e.getMessage());
} catch (Exception e) {
System.out.println("나머지 오류가 발생했습니다: " + e.getMessage());
} finally {
reader.close();
}
}
위 코드에서는 파일을 읽는 도중 IOException이 발생할 수 있고, 이를 메소드 내부에서 처리하고 있습니다.
2. 메서드가 예외를 호출자(상위메서드)에게 전달 throws
예외를 매소드 내부에서 처리하지 않고 호출한 상위 메서드로 예외를 전달할 수도 있습니다. 이때 메서드 선언부에 throws 키워드를 사용하여 어떤 예외가 발생할 수 있는지 명시해야 합니다.
public void readFile(String fileName) throws IOException {
BufferedReader reader = new BufferedReader(new FileReader(fileName));
String line = reader.readLine();
// 코드 ...
reader.close();
}
이 코드를 호출하는 쪽에서는 반드시 예외 처리를 해주어야 합니다. 예를 들어:
public static void main(String[] args) {
try {
readFile("test.txt");
} catch (IOException e) {
System.out.println("예외 발생: " + e.getMessage());
}
}
예외를 되던지는 법 (Rethrowing)
예외를 처리하면서 다른 예외로 감싸서 던지거나, 같은 예외를 다시 던질 수 있습니다. 예외를 다시 던짐으로써 더 많은 정보를 함께 전달할 수도 있고, 예외의 흐름을 더 이해하기 쉽게 할 수도 있습니다.
public class RethrowingExceptionExample {
public static void main(String[] args) {
try {
processFile("file.txt");
} catch (CustomException e) {
System.out.println("main 메소드에서 최종적으로 예외가 처리되었습니다.");
e.printStackTrace(); // 예외의 원인을 추적
}
}
public static void processFile(String fileName) throws CustomException {
try {
readFile(fileName);
} catch (IOException e) {
System.out.println("processFile 메소드에서 예외가 처리되었습니다.");
// IOException을 CustomException으로 감싸서 다시 던짐
throw new CustomException("파일 처리 중 문제가 발생했습니다.", e);
}
}
public static void readFile(String fileName) throws IOException {
if (fileName == null) {
System.out.println("readFile 메소드에서 예외가 발생했습니다.");
throw new IOException("파일 이름이 null입니다.");
}
// 코드 ...
}
}
// 사용자 정의 예외
class CustomException extends Exception {
public CustomException(String message, Throwable cause) {
super(message, cause); // cause는 원인 예외를 저장
}
}
위 코드에서 readFile 메서드에서 발생한 IOException을 processFile 메소드에서 CustomException으로 감싸서 다시 던집니다. 이렇게 하면 예외를 더 많은 정보와 함께 전달할 수 있습니다. 상위 메서드에서는 최종적으로 CustomException을 처리하지만, 그 내부적으로 어떤 예외가 발생했는지도 추적할 수 있습니다.
또한 예외가 발생한 메서드인 readFile 메서드와 이를 호출한 processFile, main 메서드들 모두 각각 예외 발생 시 처리해줘야 할 작업이 있는 경우 예외 되던지기 기법이 유용함을 확인할 수 있습니다.
예외 정보들을 얻는 방법
위에서 printStackTrace 메서드는 예외 객체에서 정보를 얻을 수 있는 메서드입니다. 자주 쓰이는 메서드는 다음과 같습니다.
- getMessage(): 예외 메시지를 반환합니다.
- printStackTrace(): 예외 발생 지점의 스택 트레이스를 출력합니다. 예외의 원인을 추적해갈 때 유용합니다.
- getCause(): 예외의 근본 원인을 반환합니다.
사용자 정의 예외
특정 상황에서 발생하는 예외를 구체적으로 처리하고 싶을 때, 사용자 정의 예외를 만들어 사용할 수 있습니다. 예를 들어, 특정 도메인 문제나 비즈니스 로직에서 발생하는 에러에 대해 사용자 정의 예외를 사용합니다.
보통 Exception 클래스로부터 상속받는 클래스를 만들지만, 필요에 따라 알맞은 예외 클래스를 선택해도 됩니다.
class MyCustomException extends Exception {
private final int code; // 불변
public MyCustomException (String message, int code) {
super(message);
this.code = code; // 생성자에서 단 한 번만 초기화
}
public MyCustomException (String message) {
super(message);
this(message, 500);
}
public int getCode() {
return code;
}
}
public class Main {
public static void main(String[] args) {
try {
throw new MyCustomException ("사용자 정의 예외 발생", 404);
} catch (MyCustomException e) {
System.out.println("에러: " + e.getMessage() + " (상태 코드: " + e.getCode() + ")");
}
}
}
// 에러 발생: 사용자 정의 예외 발생 (상태 코드: 404)
Java에서 예외 처리는 프로그램의 안정성을 높이고, 오류 발생 시 적절한 대응을 가능하게 하는 필수적인 부분입니다. 그렇다고 해서 예외를 남발하게 되면 성능에 영향을 미칠 수 있기 때문에, 적절하고 구체적인 예외 처리를 하는 것이 좋습니다. 예외를 잘게 나누거나, 되던지거나, 커스텀 예외를 만들거나 예외 처리를 어떤 방식으로 하는지는 개발자의 몫입니다. Java 예외 처리를 통해 안정성과 신뢰성이 높은 프로그램을 작성할 수 있습니다.
'Java' 카테고리의 다른 글
Java 날짜 및 시간 포맷 다루기. SimpleDateFormat, DateTimeFormatter, FastDateFormat (1) | 2024.09.24 |
---|---|
Java 날짜 시간 다루기: LocalDateTime, Instant, OffsetDateTime, ZonedDateTime의 차이. Duration, Period (0) | 2024.09.23 |
Java JVM 메모리 구조와 메모리 저장 방식 (1) | 2024.09.18 |
Java 상속(Inheritance), 인터페이스(Interface), 추상 클래스(Abstract Class) 차이점 (0) | 2024.09.18 |
Java 생성자, this()와 super() (0) | 2024.09.16 |