예외(Exception)
예외라는 것은 에러(Error)와 뜻이 다른데, 먼저 에러는 프로그램 코드에 의해서 수습될 수 없는 심각한 오류가 발생한 것이고 예외는 수습이 가능한 다소 미약한 오류를 뜻한다.
프로그램에서 발생하는 에러의 종류를 분류해 보면 3가지로 나눌 수 있다.
1. 컴파일 에러: 컴파일 할 때 발생하는 에러를 뜻하며 프로그램이 실행되기 전에 에러를 발생시켜 준다.
2. 런타임 에러: 실행할 때 발생하는 에러를 뜻하며 컴파일까지 완료했지만 프로그램을 실행하면 에러가 발생하여 프로그램이 종료된다.
3. 논리적 에러: 개발자의 작성 의도와 다르게 동작해서 나타나는 에러를 뜻하며 컴파일 과정을 거쳐 프로그램이 실행되고 난 후 종료되지 않지만 의도하지 않은 값이 나올 때를 말한다.
- 예외 처리
예외 처리란 앞서 설명한 것처럼 예외는 수습이 가능하기 때문에 프로그램을 작성하면서 발생할 수 있는 예외를 대비한 코드를 작성하는 것으로 프로그램의 비정상적인 종료를 사전에 막고 정상적인 실행상태를 유지하는 것이 목적이다.
예외가 발생하면 예외 객체가 생성되는데 해당 객체를 가지고 코드를 작성하여 사전에 핸들링을 할 수 있다.
모든 예외 및 에러 유형은 기본 클래스인 Throwable 클래스의 하위 클래스로 이루어져 있다. 여기서 우리가 주목해야 될 부분은 Exception 클래스로 개발자가 처리해야 되는 예외는 해당 클래스에 들어있다.
Throwable의 또 다른 분기로 Error 클래스가 있는데 JVM에서 런타임 환경 자체(JRE)와 관련된 오류를 나타내는 데 사용되므로 우리가 직접 처리할 수 없는 오류가 들어있다.
따라서 Exception 클래스에 관련된 예외를 어떻게 처리할 것인가를 고려하면서 개발을 진행해야한다.
- Checked와 Unchecked의 차이
위의 예외 계층 사진을 보면 Exception 클래스가 Checked Exception과 Unchecked Exception으로 나뉘는 것을 알 수 있다.
둘의 차이와 분류를 한 이유를 알아보자.
먼저 Throwable 클래스를 직접 상속받은 클래스 중 RuntimeException과 Error를 제외한 모든 클래스는 Checked Exception이다. 이와 반대로 RuntimException을 직접 상속받은 클래스는 UncheckedException이다.
간단한 예시를 들어보면 아래와 같이 ArithmeticException 코드를 살펴보면 RuntimeException 클래스를 상속받은 UncheckedException 인 것을 확인할 수 있다.
public class ArithmeticException extends RuntimeException {
public ArithmeticException() {
super();
}
public ArithmeticException(String s) {
super(s);
}
}
이 외에도 어떤 예외 클래스가 Checked 혹은 Unchecked인지는 아래의 그림과 같이 구분할 수 있다.
예외의 유형에는 사용자 정의 예외와 내장 예외가 존재하는데 먼저 내장된 예외를 살펴보면 Checked 예외와 Unchecked 예외가 있다.
Checked 예외와 Unchecked 예외의 차이는 컴파일러가 예외 처리 여부를 체크하는지, 안 하는지의 차이다.
Checked 예외는 컴파일러가 예외 처리 여부를 확인하게 되는데, 따라서 개발자가 직접 예외를 try-catch 문으로 작성하여 처리를 해줘야 한다.
하지만 반대로 Unchecked 예외는 컴파일러가 예외 처리 여부를 확인하지 않기 때문에 try-catch 문을 통한 예외 처리가 선택사항이 된다.
Exception 클래스 안에는 여러 가지 자손들이 있는데 그중 RuntimeException 클래스와 그 하위 클래스는 Unchecked 예외, 나머지 Exception 클래스와 그 하위 클래스들은 Checked 예외로 구분된다.
굳이 이렇게 예외를 또 나누는 이유는 개발자가 프로그램을 개발하면서 발생하는 예외를 선택적으로 처리할 수 있게 하기 위해서다.
메서드에서 Checked Exception이 발생하면 메서드는 예외를 catch하거나 예외를 호출자 메서드에 전달해야 한다. 하지만 Unchecked Exception은 자바 컴파일러가 예외를 catch하거나 throws 절에서 선언하도록 강제하지 않는다.
이를 통해 Checked Exception은 컴파일 과정에서 검사되고 처리되는 반면, Unchecked Exception은 컴파일 과정이 통과된 후 런타임에서 검사된다.
Checked Exception은 Exception 클래스를 확장하지만, Unchecked Exception은 RuntimeException 클래스를 확장한다.
예외 처리 동작 과정
예외가 발생했을 때 어떻게 처리되는지 동작 과정을 알아보자.
public class Ex_6 {
public static void main(String[] args) {
try {
methodA();
} catch (IOException e) {
e.printStackTrace();
System.out.println("main 메서드에서 예외를 catch 함");
} finally {
System.out.println("최종 종료");
}
}
public static void methodA() throws IOException {
try {
methodB();
} catch (SQLException e) {
e.printStackTrace();
}
}
public static void methodB() throws IOException, SQLException {
throw new IOException("예외 발생!!");
}
}
위의 예시 코드를 실행했을 때를 가정하여 동작 과정을 하나씩 살펴보면 아래와 같이 동작하게 된다.
public static void methodB() throws IOException, SQLException {
throw new IOException("예외 발생!!");
}
1. 위의 코드와 같이 프로그램 실행 중 특정 코드나 throw와 같은 명시적인 예외 등 예외가 발생한다.
2. 예외가 발생하면 JVM은 예외의 종류, 메시지, stack trace 등의 정보를 담은 예외 객체를 생성한다.
3. JVM은 예외가 발생한 시점의 호출 스택 정보를 수집하고, 이를 예외 객체에 포함한다. 해당 과정은 성능 비용이 많이 드는 작업으로, 예외 처리 성능에 영향을 줄 수 있다.
public static void methodA() throws IOException {
try {
methodB();
} catch (SQLException e) {
e.printStackTrace();
}
}
public static void methodB() throws IOException, SQLException {
throw new IOException("예외 발생!!");
}
4. JVM은 예외가 발생한 메서드의 try 블록에서 시작하여 메서드 호출 스택을 역순으로 탐색한다. 만약 예외가 발생한 메서드 내에 적절한 catch 블럭이 없으면 예외는 호출한 메서드로 전달된다. 이러한 과정은 호출 스택이 비워질 때까지 계속된다.
위의 코드와 같이 예외가 발생한 methodB의 try 블록부터 시작해야 되지만 try 블록이 없기 때문에 methodB를 호출한 methodA로 예외가 전달된다.
마찬가지로 methodA의 try 블록에서 catch 부분을 확인하여 전달받은 예외를 처리할 수 있는지 확인한다. 하지만 methodA도 IOException을 처리할 수 없기 때문에 methodA를 호출한 main 메서드로 예외를 전달한다.
public static void main(String[] args) {
try {
methodA();
} catch (IOException e) {
e.printStackTrace();
System.out.println("main 메서드에서 예외를 catch 함");
} finally {
System.out.println("최종 종료");
}
}
5. 호출 스택의 마지막인 main 메서드로 왔을 때 전달받은 IOException을 catch하는 블록이 발견되면 JVM은 해당 catch 블럭을 실행한다.
6. finally 블럭이 있다면 catch 블럭이 실행 여부와 상관없이 무조건 실행한다.
7. 예외가 처리된 후 프로그램은 catch나 finally 블럭 다음 코드로 이동하여 정상적으로 실행한다. 만약 호출된 메서드 중 어느 곳에서도 예외를 처리하지 못하면 예외는 main 메서드로 전파되며, 최종적으로 프로그램이 종료된다.
8. 예외가 어디서도 처리되지 않고 main 메서드까지 도달하면 JVM은 프로그램을 비정상 종료한다. 그 후 JVM이 스택 트레이스를 콘솔에 출력하여 예외가 발생한 원인을 개발자에게 제공한다.
예외 처리하는 방법
- try-catch 문
예외를 처리할 때는 try-catch 문을 사용하여 처리하게 되는데, 먼저 예외가 발생할 것이라 예상되는 코드를 try 문 블록에 위치시켜 동작하고 발생되는 예외를 catch 블럭에 있는지 확인하게 된다.
try {
System.out.println(3);
System.out.println(0/0);
System.out.println(4);
} catch (ArithmeticException e) { // 예외 객체를 catch로 잡을 수 있다.
e.printStackTrace(); // 호출스택에 있었던 메서드의 정보와 예외 메시지를 화면에 출력한다.
System.out.println(e.getMessage()); // 발생한 예외 클래스의 인스턴스에 저장된 메시지를 얻을 수 있다.
System.out.println("ArithmeticException");
} catch (Exception e) {
System.out.println(e.getMessage());
System.out.println("Exception");
}
System.out.println(6);
위의 코드와 같이 0/0 연산을 수행하는 코드에서 예외가 발생하게 되면 4를 출력하지 않고 catch 문에서 예외를 찾게 된다.
발생한 예외와 맞는 게 있다면 해당 catch문 블록 내의 문장들을 실행하게 되고 일치하는 catch문이 없다면 예외는 처리되지 않고 넘어간다.
만약 try 문에서 예외가 발생하지 않으면 catch문은 그냥 넘어가고 다음 문장을 실행한다.
- throw와 throws
throw 키워드를 통해 프로그램을 실행 중 예외가 발생했을 때 직접 처리할 수 있지만, 다른 메서드로 예외를 발생시킬 수 있다.
주로 사용자 정의 예외를 발생시키는 데 사용한다.
static void testMethod() {
try {
throw new testException("test 예외 발생");
} catch (Exception e) {
System.out.println(e.getMessage());
}
}
위의 코드와 같이 개발자가 만든 testException이라는 예외를 발생시킬 수 있다.
throw 키워드를 사용하며 주의할 점은 발생시키는 예외가 Throwable 유형이거나 Throwable의 하위 클래스여야 한다는 점이다.
throws 키워드는 메서드가 나열된 예외 중 하나를 발생시킬 수 있음을 나타내기 위한 키워드이다.
static void method1() throws Exception {
try {
throw new Exception();
} catch (Exception e) {
System.out.println(e.getMessage());
}
}
throws 키워드를 통해 method1()이 Exception 예외를 발생시킬 수 있다는 것을 명시해 주고 이를 처리하는 코드이다.
- 사용자 정의 예외처리
개발자가 직접 예외 클래스를 정의해서 만들 수 있는데 여기서 고려해야 될 부분은 Exception 클래스와 RuntimeException 중에서 어떤 클래스를 상속받을 것인지를 선택해야 한다.
class testException extends Exception {
private int ERROR_CODE;
testException(String msg, int errCode) {
super(msg);
ERROR_CODE = errCode;
}
testException(String msg) {
super(msg);
}
}
static void testMethod() {
try {
throw new testException("test 예외 발생");
} catch (Exception e) {
System.out.println(e.getMessage());
}
}
직접 testException이라는 예외 클래스를 생성하고 Exception 클래스를 상속받았다. 따라서 Checked 예외의 특성을 고려하여 try-catch문으로 직접 예외를 처리하는 코드를 작성해 봤다.
참고 자료
https://medium.com/geekculture/how-does-exception-handling-work-in-java-c71c45103e7d
'자바' 카테고리의 다른 글
Java - 애너테이션 (0) | 2023.11.13 |
---|---|
Java - 내부 클래스 (0) | 2023.11.11 |
Java - 열거형 (0) | 2023.11.09 |
Java - 배열 (1) | 2023.11.09 |
Java - 문자와 문자열 (0) | 2023.11.06 |