자바의 정석

SUADI·2022년 6월 14일

8장 - 예외처리

1.1 프로그램 오류

프로그램이 비정상적으로 종료되거나 오작동을 하는 경우 프로그램 에러 또는 오류라고 한다. 발생 시점에 따라 컴파일 에러 또는 런타임 에러로 나눌 수 있다. 컴파일 에러는 컴파일 시 발생하는 에러이고 런타임 에러는 프로그램 실행 도중 발생하는 에러이다.

  • 소스코드를 컴파일 하면 컴파일러가 소스코드(.java)에 대해 기본적인 검사를 수행하여 오류가 있는지를 알려준다. 이 때 오타나 잘못된 구문, 자료형이 잘못되어 있는 경우 컴파일 에러가 난다.

  • 컴파일을 성공적으로 마치고 난다고 해서 프로그램 실행 시 에러가 나지 않는 것은 아니다. 실행 중에 발생하는 에러를 런타임에러라고 하고, 런타임에러는 또 에러(error)와 예외(Exception), 두 가지로 나뉜다.

  • 에러는 메모리 부족(OutOfMemoryError)나 스택오버플로우(StackOverFlowException)와 같이 발생하면 복구할 수 없는 심각한 오류이고, 예외는 ArithmeticException, IndexOutOfBoundException과 같이 수습이 가능한 덜 심각한 오류이다.

  • 예외도 두가지로 나뉘게 되는데 사용자들의 동작과 같은 외부의 영향으로 발생하는 Exception과 프로그래머의 실수에 의해서 발생되는 예외인 RuntimeException으로 나뉜다. Exception이 발생할 가능성이 있는 문장들에 대해 예외처리를 해주지 않으면 컴파일 조차 되지 않는 반면(checked 예외), RuntimeException의 경우 프로그래머의 실수에 의해 발생하기 때문에 예외처리를 강제하지 않는다.(unchecked 예외)

예외처리(try-catch문)

  • 실행 중에 발생하는 에러(error)는 어쩔 수 없지만, 예외는 프로그래머가 미리 파악하여 예외에 대비한 코드를 작성할 수 있어야 한다. 이 때 사용되는게 try-catch문이다.
try {
	// 예외가 발생할 가능성이 있는 문장넣기
} catch(예외가 발생할 것 같은 Exception e) {
	// 예외 처리
} catch ...
  • try문, catch문 안에 또 다른 try-catch문이 작성될 수 있다.

  • ArithmeticException의 예를 들면,

package sample;

public class Sample {
    public static void main(String[] args) {
        for (int i=0; i<10; i++) {
            try {
                int result = 100 / (int) (Math.random() * 10);
                System.out.println(result);
                System.out.println("성공");
            } catch (ArithmeticException ae) {
                System.out.println(0);
                System.out.println("예외");
            } catch (Exception e) {
            	
            }
        }
    }
}
  • 100을 0~9 중 무작위로 수를 뽑아 나누는 프로그램이다. 0이 걸릴 경우 catch문으로 넘어가 0을 출력하도록 했다.

  • try 문에서 예외가 발생하면 그 이후의 문장은 수행되지 않고 바로 catch문으로 이동하게 된다. 따라서 0으로 나누어지지 않은 수 다음에는 성공이 출력되고, 0으로 나누어진 수 다음은 예외가 출력된다.

  • 여러 catch문 중 발생한 예외의 종류와 일치하는 단 한개의 catch문만 수행된다. 이 코드에서는 ArithmeticException이 일어났고 그에 해당하는 첫번째 catch문만을 수행하게 된다. 만약 어떤 종류의 예외가 발생하더라도 예외처리를 해주려면 마지막 catch문에 Exception 타입의 참조변수를 선언하면 된다. 하지만 전에 학습했던 것처럼 메소드를 오버라이딩하는 경우 선언된 예외의 개수 혹은 예외의 상속관계에 따라 오버라이딩이 안되는 경우가 있으니 예외처리를 할 때 이런점까지 고려를 해야 한다.

  • 예외가 발생했을 때 생성되는 예외 클래스의 인스턴스에는 예외에 대한 정보가 담겨 있으며, getMessage 메서드와 printStackTrace 메서드를 호출해서 정보를 얻을 수 있다.

package sample;

public class Sample {
    public static void main(String[] args) {
        try {
            System.out.println(1);
            System.out.println(2);
            System.out.println(0 / 0);
            System.out.println(4);
        } catch (ArithmeticException ae) {
            ae.printStackTrace();
            System.out.println("예외 메세지 : " + ae.getMessage());
        }
        System.out.println(5);
    }
}
1
2
예외 메세지 : / by zero
5
java.lang.ArithmeticException: / by zero
	at sample.Sample.main(Sample.java:8)
  • 여타 다른 메서드와 마찬가지로 인스턴스.메서드 의 방법으로 호출해서 사용하면 된다. getMessage()를 통해 발생한 예외 클래스의 인스턴스에 저장된 메세지를 리턴할 수 있고, printStackTrace()를 통해 호출스택에 있던 메서드의 정보와 예외 메세지를 화면에 출력해 준다.

  • 예외 종류에 따라 여러개의 catch문을 작성할 수도 있지만, 하나의 catch문에 | 기호를 사용하여 한꺼번에 작성할 수도 있다.

try {
	....
} catch(ExceptionA | ExceptionB | ExceptionC... e) {
	...
}

1.6 고의로 예외 발생시키기

Exception e = new Exception("예외 문구 작성");
throw e;
throw new Exception();
  • 예외를 발생시키고 싶은 곳에 위의 두가지 방법 중 하나를 사용하면 예외를 고의로 발생시킬 수 있다. 생성자에 문자열을 적으면 예외 발생시 예외 클래스의 인스턴스에 메세지로 저장되며, getMessage()로 리턴할 수 있다.

  • 고의로 예외를 발생시키는 이유는 뭘까? 이유까지는 교재에 나와있지는 않지만 추측은 해볼 수 있다. 다음 파트에 throw를 배우게 되는데 예외를 호출할 메서드로 미루는 것이다. 즉, 예외가 발생한 메서드가 아닌 호출할 메서드에서 예외처리를 하도록 하는 것이다. 근데 예외를 한 메서드에서만 처리할 수도 있지만 두 메서드에서 나눠서 예외를 처리하는 경우도 있다고 한다. 아직까진 이해가 잘안되지만 예외를 나눠서 처리하는 경우, 예외가 발생한 메서드에서 예외처리를 한 후, catch문에서 다시 예외를 고의로 발생시켜서 호출할 메서드로 다시 그 예외를 던지고, 호출할 메서드에서 또 예외처리를 하도록 할 수 있다.

1.7 메서드에 예외 선언

  • throw를 이용해서 메서드에 예외를 선언해야만 한다. try-catch문으로 예외를 처리한다면, throw를 통해 이러한 예외를 해결해야한다는 것을 명시하는 것이다. 정확히는 선언된 메서드가 예외를 처리하는 것이 아니라, 이 메서드를 호출하고자 하는 메서드에게 예외를 전달하여 예외처리를 떠맡기는 것이다.
package sample;

public class Sample {
    public static void main(String[] args) {
        try {
            methodA();
        } catch (Exception e) {
            e.printStackTrace();
            System.out.println(e.getMessage());
        }
    }

    static void methodA() throws Exception{
        methodB();
    }

    static void methodB() throws Exception{
        throw new Exception("예외 발생!");
    }
}
  • main, a, b 메서드 세 개가 있고, b에서 예외를 발생시켰다. 따라서 예외가 발생했으니 호출할 메서드에서 예외를 처리해야한다는걸 명시하기 위해 메서드 선언부에 throws Exception을 한다. a에서는 b를 호출하였다. 호출할 메서드에서 예외를 처리해야 하는데 또 예외를 미루고 싶다면 throw Exception을 메서드 선언부에 선언하여 예외를 던진다. 마지막으로 메인메서드가 남았는데 throw로 예외를 계속 던지면 마지막 메서드에서는 해결을 해야만 한다. main 메서드에서 try-catch문으로 a메서드를 호출하고 catch문에 예외메세지를 출력한다.

1.8 finally

  • finally문은 예외의 여부와 상관없이 반드시 실행되어야 할 코드를 작성할 목적으로 사용되고, try-catch문의 마지막에 작성한다.

1.9 try-with-resource

  • 입출력(I\O)과 관련된 클래스를 사용할 때 유용한 구문이다. 입출력에 사용되는 클래스 중 사용 후 꼭 닫아 줘야 하는 것들이 있다. 예를 들어, 점프투자바에서 배웠던 파일을 생성해주는 클래스 중 PrintWriter의 경우 객체를 생성한 후, 사용하고 close를 통해 닫아줘야 에러가 날 가능성이 적다. 이럴 경우 try문을 실행하다가 예외가 발생하게 되더라도 close문을 실행하도록 finally문을 쓰면
package sample;

import java.io.IOException;
import java.io.PrintWriter;

public class Sample {
    public static void main(String[] args) throws IOException {
        PrintWriter pw = null;
        try {
            pw = new PrintWriter("C:\\Users\\kcs91\\Desktop\\workspace\\java\\src\\sample.txt");

            for (int i = 1; i < 11; i++) {
                String data = i + " 번째 줄입니다.";
                pw.println(data);
            }
        } catch (IOException ie) {
            ie.printStackTrace();
        } finally {
            pw.close();
        }
    }
}
  • 하지만 close 메서드 조차도 에러가 날 확률이 있으므로 정확하게는 아래와 같이 finally문을 작성해야 한다.
		finally {
       	    try {
                if (pw != null)
                    pw.close();
            } catch (IOException ie) {
                ie.printStackTrace();
            }
        }
  • 코드가 복잡해져서 보기에 좋지 않으므로 try-with-resource문이 추가된 것이다.
package sample;

import java.io.IOException;
import java.io.PrintWriter;

public class Sample {
    public static void main(String[] args) throws IOException {
        try (PrintWriter pw = new PrintWriter("C:\\Users\\kcs91\\Desktop\\workspace\\java\\src\\sample.txt")){
            for (int i = 1; i < 11; i++) {
                String data = i + " 번째 줄입니다.";
                pw.println(data);
            }
        } catch (IOException ie) {
            ie.printStackTrace();
        }
    }
}
  • try문 옆에 소괄호 안에 객체를 생성하는 문장을 넣으면, 이 객체는 따로 close 메소드를 호출하지 않아도 try문을 벗어나는 순간 close문이 자동으로 호출된다.

1.10 사용자정의 예외 만들기

  • 기존의 예외 클래스 이외에 사용자가 직접 예외 클래스를 정의해서 사용할 수도 있다.
class A Extends Exception {
	A(String msg) {
    	super(msg)
    }
}
  • 필요에 따라 알맞은 예외 클래스를 상속 받아 예외 클래스를 만들 수 있다. 생성자를 만들어서 예외 메세지를 받도록 매개변수로 문자열을 추가했다. 예외 클래스의 최고 부모클래스인 Exception의 생성자를 super()로 호출했다.

1.11 예외 되던지기

  • 어느 한 메서드에서 예외를 모두 해결할 수도 있고 호출할 메서드와 예외가 발생한 메서드 양쪽에서 예외를 분담하여 처리하는 방법도 있다. 두 개의 메서드에서 예외를 분담하려면 예외를 고의로 발생시켜야만 가능하다.
package sample;

public class Sample {
    public static void main(String[] args) {
        try {
            method1();
        } catch (Exception e) {
            System.out.println("main 메서드에서 예외 처리");
        }
    }

    static void method1() throws Exception{
        try {
            throw new Exception();
        } catch (Exception e) {
            System.out.println("method1에서 예외 처리");
            throw e;
        }
    }
}
  • 코드 상으로는 method1의 try문에 고의로 예외를 발생시켰지만 method1에서 예외가 발생했다고 가정하자. catch문에서 다시 예외 클래스의 인스턴스 e를 던진다. 그럼 예외가 해결된 것이 아니므로 호출될 메서드에서 예외를 해결해야한다는 것을 명시하기 위해 method1 선언부에 throw Exception을 작성한다.

  • main 메서드에서 예외를 처리해야하므로 try-catch구문을 사용해서 try문에 method1을 호출한다.

0개의 댓글