예외처리

예외 클래스

오류란 무엇인가?

  • 프로그램에서 오류가 발생하는 상황은 Compile error와 runtime error 두가지다
    • Compile error : 프로그램 코드 작성 실수로 발생하는 오류. 개발 환경에서 대부분의 원인을 알 수 있다.
    • Runtime error : 실행 중인 프로그램이 의도하지 않은 동작을 하거나 프로그램이 중지되는 실행 오류. 프로그램을 잘못 구현해 의도한 바와 다르게 실행됙어 생기는 오류를 버그라고 한다.
  • 실제 서비스 제공중인 프로그램이 오류로 인해 종료되면 서비스가 중지되므로 문제가 심각해진다. 실행 중 오류가 발생하면 상황을 재현해야 하는데 실제 시스템이나 서비스가 운영중인 상황에서는 쉽지 않다. 그래서 로그를 남기는것이 중요하다.
  • 자바에서는 비정상 종료를 최대한 줄이기 위해 다양한 예외 처리 방법을 가지고 있다.
  • 예외 처리를 하응 목적은 일단 프로그램이 비정상 종료되는 것을 방지하기 위한것이다. 그리고 예외가 발생했을 때 로그를 남겨 두면 예외 상황을 파악하고 버그를 수정하는데 도움을 받을 수 있다.

오류와 예외

  • Error : JVM에서 발생하는 시스템 오류. 예로는 할당가능한 동적 메모리가 없거나 스택 메모리의 오버플로우가 발생했을 때 등. 제어가 불가능하다
  • Exception : 프로그램에서 제어가 가능하다. 예를 들어 프로그램에서 파일을 읽어들이는데 파일이 없는 경우, 배열 값을 출력하는데 배열 요소가 없는 경우 등.
  • 오류 클래스는 모두 Throwable 클래스에서 상속받는다. 프로그램에서 제어하는 부분은 Exception 클래스와 그 하위에 있는 클래스들이다.

checked exception / unchecked exception

  • 검사 예외 / 비검사 예외 라고도 한다.
  • 검사예외 : 명시적으로 try-catch로 감싸줘야 하는 코드. Exception을 상속받은 throws가 선언된 경우에는 반드시 try-catch를 사용해서 감시해야된다. (Exception, IOException..)
  • 비검사예외 : try-catch는 선택사항이다. (RuntimeException, NPE..)
  • 최근에는 검사예외를 지양하고 있는데 그 이유는 다음과 같다
    1) 반드시 해당 오류를 처리해야함을 강제함
    2) 메서드 시그니처다보니 한번 정해지면 수정이 어려움
  • 추가적으로 읽어볼만한 글

예외 클래스의 종류

  • RuntimeException은 프로그램 실행 도중 발생할 수 있는 오류에 대한 예외를 처리한다. 또한 try-catch를 사용하지 않아도 컴파일 오류가 나지 않는다.
    • 예를들어 ArithmeticException은 산술 연산 중에 0으로 나누기 같은 경우 발생하는 예외다. 이 경우는 컴파일러에서 체그가 되지 않는다.

예외처리하기

try-catch

try {
    // 예외가 발생할 수 있는 코드 부분
} catch(처리할 예외 타입 e){
    //try 블록 안에서 예외가 발생했을때 예외를 처리하는 부분
}
  • 간단한 예외처리 예제
public class ArrayExceptionHandling {

    public static void main(String[] args) {
        int[] arr = new int[5];

        try {
            for (int i = 0; i <= 5; i++) {
                arr[i] = i;
                System.out.println(arr[i]);
            }
        } catch (ArrayIndexOutOfBoundsException e) {
            System.out.println(e);
            System.out.println("예외 처리 부분");
        }
        System.out.println("프로그램 종료");
    }

}
  • 에러가 나는 클래스를 예외로 잡아서 가지고 온다.
  • 예외처리를 하지 않는다면 마지막 "프로그램 종료" 부분까지 실행되지 않는다.

컴파일러에 의해 예외가 체크되는 경우

파일 입출력에서 발생하는 예외 처리하기

  • FileInputStream을 이용해 예외처리를 하는 코드를 간단하게 작성해보자
  • 클래스만 이용해 코드를 작성하게 되면 컴파일러가 위와 같이 알려준다.
public class StreamExceptionHandling {

    public static void main(String[] args) {
        try {
            FileInputStream fileInputStream = new FileInputStream("context.txt");
        } catch (FileNotFoundException e) {
//            e.printStackTrace();
            System.out.println(e);
        }
        System.out.println("Execution.");
    }

}
  • 아래는 실행결과로 예외가 처리된것을 알 수 있다.
java.io.FileNotFoundException: context.txt (No such file or directory)
Execution.

try-catch-finally

  • 프로그램에서 사용한 리소스는 프로그램이 종료되면 자동으로 해제된다. 그러나 끝나지 않고 계속 수행되는 서비스에 리소스를 계속 열기만 하면 문제가 발생한다. 사용한 리소스는 close()로 닫아줘야 한다.
        try {
            FileInputStream fileInputStream = new FileInputStream("context.txt");
            if (fileInputStream != null) {
                try {  // <<<<<<<<<<<<<<<<<<
                    fileInputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        } catch (FileNotFoundException e) {
//            e.printStackTrace();
            System.out.println(e);
        }
  • 만약 발생할 수 있는 상황이 여러개라면 예외 상황 수만큼 구현해야 한다. 모든 try-catch문을 작성하는것은 번거롭다.
  • 이때 사용할 수 있는 블록이 finally다.
public class StreamExceptionFinallyHandling {

    public static void main(String[] args) {
        FileInputStream fileInputStream = null;
        try {
            fileInputStream = new FileInputStream("context.txt");
        } catch (FileNotFoundException e) {
            System.out.println(e);
            return; // <<<<<<<<<<<<<<
        } finally {
            if (fileInputStream != null) {
                try {
                    fileInputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("Always execution."); // <<<<<<<<<<<<<<
        } 
        System.out.println("Execution.");
    }

}
  • 실행결과
java.io.FileNotFoundException: context.txt (No such file or directory)
Always execution.
  • return으로 탈출과 동시에 finally의; "Always execution."만 실행되는 것을 보면 예외와 상관없이 finally가 실행되는 것을 알 수 있다.

try-with-resources

  • close()를 명시적으로 호출하지 않아도 try블록 내에서 열린 리소스를 자동으로 닫을 수 있다 (since Java7)
  • try-with-resources를 사용하려면 해당 리소스가 AutoCloseable을 구현해야 한다. AutoCloseable에는 close()가 있고 자동으로 호출 된다.

AutoCloseable Interface

public class AutoCloseableObject implements AutoCloseable {
    @Override
    public void close() throws Exception {
        System.out.println("Resource closed.");
    }
}

public class AurtoCloseableTest {

    public static void main(String[] args) {
        try (AutoCloseableObject obj = new AutoCloseableObject()) {
            throw new Exception(); // 강제로 예외 발생
        } catch (Exception e) {
            System.out.println(e);
            System.out.println("Exception.");
        }
    }

}
  • 실행 결과
Resource closed.
java.lang.Exception
Exception.

향상된 try-with-resources (since Java9)

  • 자바9 부터는 try괄호 안에 외부에서 선언한 변수를 사용할 수 있다.
//...
AutoCloseableObject obj = new AutoCloseableObject();
try (obj) {
    throw new Exception();
} catch (Exception e) {
    System.out.println(e);
    System.out.println("Exception.");
}
//...

예외 처리 미루기

throws

  • try-catch로 감싸는것 외에도 throws 선언을 추가하는 방법으로 예외를 처리할 수 있다.

throws를 활용하여 예외 처리 미루기

public class ThrowsException {

    public Class loadClass(String fileName, String className)
        throws FileNotFoundException, ClassNotFoundException {
        FileInputStream fileInputStream = new FileInputStream(fileName);
        Class c = Class.forName(className);
        return c;
    }

    public static void main(String[] args) {
        ThrowsException test = new ThrowsException();
        try {
            test.loadClass("context.txt", "java.lang.String");
        } catch (FileNotFoundException | ClassNotFoundException e) {
            System.out.println(e);
        }
        System.out.println("Execution.");
    }
}
  • loadClass()에서 선언된 thorws는 두 예외를 메소드가 호출 될때 처리하도록 미룬다는 의미 입니다.
  • 미뤄진 예외처리는 실제로 메소드가 호출 되는 main()에서 발생합니다.
  • 예외를 처리할때 or 연산으로 처리가 가능합니다.
  • 예기치 못한 그외의 예외를 처리하려면 아래와 같이 추가하면 됩니다.
        try {
            test.loadClass("context.txt", "java.lang.String");
        } catch (FileNotFoundException | ClassNotFoundException e) {
            System.out.println(e);
        } catch (Exception e){ // <<<<<<<<<<<<<<<
            System.out.println(e);
        }

다중 예외 처리에서 주의 사항

  • 예외 처리를 할 때 Exception을 맨 위로 가지고 오면 아래에 있는 예외가 호출 되지 않습니다. (주의 요망)

사용자 지정 예외

  • 예외 클래스를 직접 만들어 예외를 발생시키고 예외 처리 코드를 구현할 수 있다.
  • RuntimeException만으로는 너무 두루뭉술 하므로 상황을 잘 설명할 수 있는 예외클래스를 작성하는것이 중요하다.

사용자 지정 예외 클래스 구현하기

public class CustomFormatException extends Exception {

    public CustomFormatException(String message) {
        super(message);
    }

}

public class CustomFormatTest {

    private String userId;

    public String getUserId() {
        return userId;
    }

    public void setUserId(String userId) throws CustomFormatException {
        if (userId == null) {
            throw new CustomFormatException("Must have ID String.");
        } else if (userId.length() < 8 || userId.length() > 20) {
            throw new CustomFormatException(
                "ID must be at least 8 characters and less than 20 characters.");
        }
        this.userId = userId;
    }

    public static void main(String[] args) {
        CustomFormatTest test = new CustomFormatTest();

        String userId = null;
        try {
            test.setUserId(userId);
        } catch (CustomFormatException e) {
            System.out.println(e.getMessage());
        }

        userId = "aaaaaa";
        try {
            test.setUserId(userId);
        } catch (CustomFormatException e) {
            System.out.println(e.getMessage());
        }
    }

}
  • 실행결과
Must have ID String.
ID must be at least 8 characters and less than 20 characters.

Comments