Java 재활 훈련 8일차 - Exception

0

java

목록 보기
8/18

Exception

java에서는 예와(exception)라고 부르는 오류가 있다. exception은 잘못된 문법, 코딩으로 인해 발생한 오류를 말한다. exception이 발생하면 프로그램은 곧바로 종료된다는 점에서 에러와 동일하지만, exception의 처리를 통해 실행 상태를 유지할 수 있다. exception에는 다음 두 가지가 있다.

  • 일반 예외(Exception): 컴파일 시점에 처리해야 하는 예외로, 컴파일러가 예외 처리를 강제한다. IOException, SQLException가 있다.
  • 실행 예외(Runtime Exception): 컴파일 시점에는 확인되지 않으며, 실행 중에 발생할 수 있는 예외이다. 이는 컴파일러가 exception 처리를 강제하지 않는다. NullPointerException, ArrayIndexOutOfBoundsException가 있다.

java는 exception이 발생하면 exception 클래스로부터 객체를 생성한다. 이 객체는 exception을 처리하는 데 사용된다. java의 모든 error와 exception class들은 Throwable을 상속받아 만들어지고, 추가적으로 exception class는 java.lang.Exception 클래스를 상속받는다.

                Throwable
                    ^
                    |--------------------------
                    |                         |
                Exception                   Error
                    ^                       
                    |
     ------------------------------------------------
     |                      |                       |
classNotFoundException  InterruptedException  RuntimeException
                                                    ^
                                                    |
    -------------------------------------------------
    |                   |
NullPointerException  ArrayIndexOutOfBoundsException ...

RuntineException을 상속받은 exception들은 'runtime exception'이다. 반면에 RuntimeException을 상속하지 않고 같은 위치에 있어 Exception을 상속하고 있는 exception들은 '일반 예외(exception)'이다.

exception을 처리하는 방법은 try-catch-finally block으로 구성된다.

try{
    // 에외 발생 가능성 코드
} catch(ExceptionClass e) {
    //예외 처리
} finally {
    //항상 실행
}

try에 있는 '예외 발생 가능성 코드'의 Exception발생 여부에 따라 코드 분기를 다음과 같이 나눌 수 있다.

  1. try에서 exception이 발생하지 않음 -> 모든 try code 실행 -> finally code 실행
  2. try에서 exception이 발생 -> 발생한 즉시 try code에서 catch의 code 실행 -> finally code 실핼

finally는 모든 분기에서 실행되며, 생략이 가능하다.

다음의 예제를 보도록 하자.

public class Main {
    public static void main(String[] args) {
        printLength(null);
    }

    public static void printLength(String data) {
        try {
            int res = data.length();
            System.out.println("문자 수 " + res);
        } catch (NullPointerException e) {
            System.out.println("getMessage: " + e.getMessage());
            System.out.println("toString: " + e.toString());
            e.printStackTrace();
        } finally {
            System.out.println("Finally!");
        }
    }
}

printLength는 String을 받으면 그 길이를 계산해서 출력하는 함수이다. 문제는 null이 올 수 있다는 것인데, null이 오게되면 NullPointerException이 발생한다. 이는 runtime exception이므로 컴파일 단계에서는 확인할 수 없다.

실행해보면 다음과 같은 결과가 나온다.

getMessage: Cannot invoke "String.length()" because "data" is null
toString: java.lang.NullPointerException: Cannot invoke "String.length()" because "data" is null
Finally!
java.lang.NullPointerException: Cannot invoke "String.length()" because "data" is null
	at Main.printLength(Main.java:8)
	at Main.main(Main.java:3)

catch block을 보면 getMessage로 예외가 발생한 이유를 확인할 수 있다. toStringgetMessage와 같이 exception이 발생한 이유도 설명해주지만, exception의 종류도 반환해준다는 차이가 있다.

printStackTrace는 exception이 발생한 callstack을 반환해준다는 특징이 있다.

다음의 예제는 'runtime exception'이 아니라 일반 exception으로 compile단계에서 확인이 가능하다.

public class Main {
    public static void main(String[] args) {
        try {
            Class.forName("java.lang.String");
            System.out.println("java.lang.String class가 존재");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }

        System.out.println();

        try {
            Class.forName("java.lang.String2");
            System.out.println("java.lang.String2 class가 존재");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

Class.forName의 경우 특정 클래스 경로를 문자열로 입력하여 해당 클래스가 없다면 ClassNotFoundException exception을 반환한다. 이때, ClassNotFoundException은 'runtime exception'이 아니라 일반 exception이기 때문에 컴파일 단계에서 반드시 try-catch로 감싸주어야 한다. 안그러면 compile error가 발생하기 때문이다.

결과는 다음과 같다.

java.lang.String class가 존재

java.lang.ClassNotFoundException: java.lang.String2
	at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:641)
	at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:188)
	at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:528)
	at java.base/java.lang.Class.forName0(Native Method)
	at java.base/java.lang.Class.forName(Class.java:462)
	at java.base/java.lang.Class.forName(Class.java:453)
	at Main.main(Main.java:13)

catch문을 잘보면 특정 exception class가 있는 것을 볼 수 있다. 이는 try문에 실행되는 특정한 exception을 잡아서 처리하기 위함인데, 만약 try에 여러 exception들이 발생할 수 있다면, 어떻게 이들을 구분하여 잡을 수 있을까?? 답은 간단한데, 여러 개의 catch를 사용하면 된다.

다음의 예제는 배열의 인덱스가 초과되었을 경우 발생하는 ArrayIndexOutOfBoundsException과 문자열을 정수로 변환하는데, 문자열이 숫자 타입이 아닐 때 발생하는 NumberFormatException을 각각 다르게 exception 처리한다.

public class Main {
    public static void main(String[] args) {
        String[] array = {"100", "1oo"};

        for(int i = 0; i <= array.length; i++) {
            try {
                int value = Integer.parseInt(array[i]);
                System.out.println("array" +"[" + i + "]" + value);
            } catch (ArrayIndexOutOfBoundsException e) {
                System.out.println("배열 인덱스가 초과됨 " + e.getMessage());
            } catch (NumberFormatException e) {
                System.out.println("숫자로 변환할 수 없음 " + e.getMessage());
            }
        }
    }
}

try 코드를 보면 배열의 인덱스를 넘어가면 발생하는 exception인 ArrayIndexOutOfBoundsException이 발생할 수 있고, 문자열이 숫자가 아닌데 정수형 타입으로 변환하려는 시도로 인해 NumberFormatException이 발생 할 수도 있다. 두 eception에 대해서 각각의 catch로 잡아내는 것이다.

결과는 다음과 같다.

array[0]100
숫자로 변환할 수 없음 For input string: "1oo"
배열 인덱스가 초과됨 Index 2 out of bounds for length 2

만약, 두 개 이상의 exception을 하나의 catch block으로 동일하게 exception 처리하고 싶다면, catch block의 exception class에 |로 연결하면 된다.

String[] array = {"100", "1oo"};
for(int i = 0; i <= array.length; i++) {
    try {
        int value = Integer.parseInt(array[i]);
        System.out.println("array" +"[" + i + "]" + value);
    } catch (ArrayIndexOutOfBoundsException e) {
        System.out.println("배열 인덱스가 초과됨 " + e.getMessage());
    } catch (NullPointerException | NumberFormatException e) {
        System.out.println("숫자로 변환할 수 없음 " + e.getMessage());
    }
}

다음과 같이 |으로 서로 다른 exception에 대해서 같은 처리를 해줄 수 있다.

try-with-resources

try문과 같이 특정 분기에 따라 code의 흐름이 달라지는 경우에 resource들을 관리하기 어려울 때가 있다. 가령 특정 'file'을 'open'했다면 'close'까지도 해주어야 한다. 그런데, try-catch의 코드 분기 흐름 때문에 실행해야했던 'close' 함수가 실행되지 않아 resource가 회수되지 않을 수 있다.

물론 finally 코드를 넣어서 안전하게 close할 수 있지만, 이는 직접 넣어주어야 하는 단점이 있다. 마치 python이 with문을 통해서 자원을 회수하듯이 java도 try-with-resources 블록을 통해서 리소스르 자동으로 회수하도록 할 수 있다. try-with-resources의 기본 문법은 다음과 같다.

try(FileInputStream fls = new FileInputStream("file.txt")){
        ...
}catch(IOException e) {
        ...
}

그동안 비워두었던 try에 하나의 입력을 주는 것이다. 이 입력에 쓰이는 대상이 바로 reosource이고, 해당 try-catch block이 종료되면 회수해야하는 자원인 것이다.

try-with-resources block을 사용하기 위해서는 조건이 하나 있는데, target이 되는 resource class에서 AutoCloseable 인터페이스를 구현해서 AutoCloseable 인터페이스의 close 메서드를 정의해야한다.

가령 FileInputStream은 다음과 같이 AutoCloseable 인터페이스를 구현하고 있다.

public class FileInputStream implements AutoCloseable {
    @Override
    public void close() throws Exception {...}
}

만약, 여러 resource들을 try-with-resources로 실행시키고 싶다면 다음과 같이 쓸 수 있다.

try(FileInputStream fls1 = new FileInputStream("file1.txt");
FileInputStream fls2 = new FileInputStream("file2.txt"))
{

}catch(IOException e) {

}

;을 경계로 나란히 resource들을 써주면 된다.

java8 이전 버전은 try 괄호 안에서 resource변수를 반드시 선언해야 했지만, java9 이후부터는 외부 리소스 변수를 사용할 수 있다. 따라서, 위 코드는 다음과 같이 변경할 수 있다.

FileInputStream fls1 = new FileInputStream("file1.txt");
FileInputStream fls2 = new FileInputStream("file2.txt");
try(fls1; fls2)
{

}catch(IOException e) {

}

다음의 예제는 AutoCloasable이라는 인터페이스를 구현한 MyResource 리소스를 try-with-resources 블록에서 사용한다. try 블록에서 exception 발생 여부와 상관없이 안전하게 close 메서드가 실행되는 것을 볼 수 있다.

  • MyResource.java
public class MyResource implements AutoCloseable{
    private String name;

    public MyResource(String name) {
        this.name = name;
        System.out.println("MyResource 생성자");
    }

    public String read1() {
        System.out.println("MyResource read1");
        return "100";
    }

    public String read2() {
        System.out.println("MyResource read1");
        return "abc";
    }

    @Override
    public void close() throws Exception {
        System.out.println("MyResource" + name + " 닫기");
    }
}
  • Main.java
public class Main {
    public static void main(String[] args) {
        try (MyResource res = new MyResource("A")) {
            String data = res.read1();
            int value = Integer.parseInt(data);
        } catch (Exception e) {
            System.out.println("예외 처리 " + e.getMessage());
        }

        System.out.println();

        try (MyResource res = new MyResource("A")) {
            String data = res.read2();
            int value = Integer.parseInt(data);
        } catch (Exception e) {
            System.out.println("예외 처리 " + e.getMessage());
        }

        System.out.println();

        MyResource res1 = new MyResource("A");
        MyResource res2 = new MyResource("B");
        try (res1; res2) {
            String data1 = res1.read1();
            String data2 = res1.read1();
        } catch (Exception e) {
            System.out.println("예외 처리 " + e.getMessage());
        }
    }
}

결과는 다음과 같다.

MyResource 생성자
MyResource read1
MyResourceA 닫기

MyResource 생성자
MyResource read1
MyResourceA 닫기
예외 처리 For input string: "abc"

MyResource 생성자
MyResource 생성자
MyResource read1
MyResource read1
MyResourceB 닫기
MyResourceA 닫기

AutoCloseable interface를 구현한 MyReourcetry-with-resources에 실행되어 자동으로 try문이 끝날 때마다 close를 호출하는 것을 볼 수 있다.

exception throw

메서드 내부에서 exception이 발생할 때 try-catch 블록으로 exception을 처리하는 것이 기본이지만, 메서드를 호출한 곳으로 exception을 떠넘길 수도 잇다. 이때 사용하는 키워드가 throws이다. throws는 메서드 선언부 끝에 작성하는데, 떠넘길 exception class를 쉼표로 구분해서 나열해주면 된다.

리턴타입 메서드명(매개변수, ...) throws 예외클래스1, 예외클래스2, ... {
    
}

exception을 throw하는 method를 사용하는 곳에서 해당 exception이 발생했을 때 어떻게 처리할 지에 대해서 try-catch로 처리해야만 한다.

public class Main {
    public static void main(String[] args) {
        try {
            findClass();
        } catch (ClassNotFoundException e) {
            System.out.println("예외 처리" + e.toString());
        }
    }

    public static void findClass() throws ClassNotFoundException {
        Class.forName("java.lang.String2");
    }
}

findClass 메서드의 경우 ClassNotFoundException exception을 던지는 것을 볼 수 있다. findClass를 호출하는 함수에서, 해당 exception이 발생할 수 있기 때문에 try-catch로 처리해야만 한다. 만약 try-catch와 같은 처리를 해주지 않으면 컴파일 단계에서 실행도 안된다.

throw할 exception이 다수라면 다음과 같이 나열하면 된다.

public static void findClass() throws ClassNotFoundException, IOException {
    Class.forName("java.lang.String2");
}

그런데, 너무 많은 경우에는 Exception 클래스 하나만 throw하게 해주어도 된다. 왜냐면 모든 Exception들은 Exception의 자식이기 때문이다.

public static void findClass() throws Exception {
    Class.forName("java.lang.String2");
}

사용자 정의 exception

사용자의 비지니스 로직에 따른 exception들이 있기 때문에 사용자가 직접 exception을 만들어 사용할 수도 있다.

사용자 정의 exception은 컴파일러가 체크하는 '일반 예외(exception)'으로 선언할 수도 있고, 컴파일러가 체크하지않고 런타임에 발생하는 '실행 예외(runtime exception)'으로 선언할 수도 있다. 통상적으로 '일반 예외'는 Exception의 자식 클래스로 선언하고, '실행 예외'는 RuntimeException의 자식 클래스로 선언한다.

먼저 '일반 예외'로 CustomExeption을 만들어보도록 하자.

  • CustomException.java
public class CustomException extends Exception{
    public CustomException() {
        // 기본 생성자
    }

    public CustomException(String msg) {
        super(msg); // unput exception msg
    }
}

CustomException(String msg)는 사용자의 입력을 받아서, getMessage의 반환값으로 사용하기 위함이다.

다음은 '실행 예외'인 CustomRuntimeException을 만들어보도록 하자.

public class CustomRuntimeException  extends RuntimeException{
    public CustomRuntimeException() {

    }

    public CustomRuntimeException(String msg) {
        super(msg);
    }
}

두 exception 대부분 같으나 상속하는 대상이 Exception이냐 RuntimeException이냐에 따라 다른 것이다.

사용자 정의 exception을 반환하기 위해서는 code분기에서 throw로 던져주면 된다.

throw new CustomException();
throw new CustomException("custom exception");

throw new CustomRuntimeException();
throw new CustomRuntimeException("custom runtime exception");

exceptopn을 throw하는 메서드에, throws를 통해서 해당 exception을 나열하면 된다.

  • Main.java
import java.io.IOException;

public class Main {
    public static void main(String[] args) {
        try {
            callCustomException();
        } catch (CustomException e) {
            System.out.println("Main exception: " + e.getMessage());
        }

        System.out.println();

        try {
            callCustomRuntimeException();
        } catch (CustomRuntimeException e) {
            System.out.println("Main exception: " + e.getMessage());
        }
    }

    public static void callCustomException() throws CustomException {
        throw new CustomException("CustomException call!!!");
    }

    public static void callCustomRuntimeException() throws CustomRuntimeException {
        throw new CustomRuntimeException("CustomRuntmeException call!!!");
    }
}

결과는 다음과 같다.

Main exception: CustomException call!!!

Main exception: CustomRuntmeException call!!!

추가적으로 직접 위의 코드를 써보면서 CustomException은 compile 단계에서 반드시 처리하라고 나올 것이다. 반면에 CustomRuntimeException은 compile 단계에서는 처리하지 않아도 된다고 나올 것이다. 이것이 '일반 예외'와 '실행 예외'의 차이이다.

0개의 댓글