자바 공부 기록 2회독(7) - 2024.1.23

동준·2024년 1월 23일
0

개인공부(자바)

목록 보기
9/16

6. 예외 처리

개인적으로 자바가 정말 깐깐하고 정적인 언어라는 점을 느끼게 한 부분이 형변환과 더불어 수많은 예외 처리의 경우였다. 자바는 유연함이 (거의) 없다....

반대로 말하면 예외의 가능성들을 최대한 통제하면서 안정적인 개발 환경을 제공한다는 뜻도 될 것 같은데... 그렇게 말하면 에러의 원인은 전부 나잖아

사실 자바 공부 시작할 때부터 예외는 친숙하다. 이미 실행을 시키면서 예외를 수많이 접해봤기 때문에.. 예를 들면 NPE라던가 NPE라던가 NPE라던가 NPE라던가...

1) 에러? 예외?

모든 공부의 시작은 명확한 정의의므로, '에러'와 '예외'를 구분하자

  • 에러 : 컴퓨터 하드웨어의 고장으로 인한 프로그램 실행 오류
  • 예외 : 잘못된 코드 작성으로 인한 오류

(1) '일반 예외'와 '실행 예외'

자바에서는 '일반 예외(Exception)'와 '실행 예외(Runtime Exception)'로 구별

  • 일반 예외 : 컴파일러가 예외 처리 코드 여부를 검사하는 예외
  • 실행 예외 : 컴파일러가 예외 처리코드 여부를 검사하지 않는 예외

모든 에러와 예외 클래스는 Throwable을 상속받아 만든다.
덤으로, 예외 클래스는 java.lang.Exception 클래스를 상속받고, 실행 예외는 RuntimeException과 그 자식 클래스를 일컫는다. 그 밖의 예외는 전부 일반 예외다.

(2) 정리

  • 에러는 내 잘못이 아닐 수도 있다.컴터 제조사의 잘못일 수 있다 하지만 예외는 무조건 내 잘못이다.
  • 모든 에러와 예외의 최상위 클래스는 Throwable 클래스다. 에러는 곧바로 Throwable을 상속받고, 거기서 이어서 일반 예외를 비롯한 모든 예외가 Exception 클래스를 상속받는다.
  • 그리고, Exception 클래스를 상속받은 RuntimeException 클래스가 실행 예외의 최상위 클래스가 된다.

2) Exception 최상위 클래스

Exception이 일반 예외를 비롯한 모든 예외의 최상위 클래스라는 점 때문에 지켜야 될 문법이 있다. 예외 처리는 하위 클래스부터 처리한다.

public class Example {
    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 (Exception e) { // 모든 예외를 잡는 캐치 블록
                System.out.println(e.getMessage()); // 그래서 범위가 더 넓은 예외일 수록 더 뒤의 캐치 블록에 위치해야 한다
            } */ catch (ArrayIndexOutOfBoundsException e) {
                System.out.println("배열 인덱스 초과 : " + e.getMessage());
            } catch (NumberFormatException e) {
                System.out.println("숫자 변환 예외 : " + e.getMessage());
            } catch (Exception e) {
                System.out.println(e.getMessage()); // 이런 식으로
            }
        }
    }
}

만약 마지막 Exception 클래스가 들어간 Catch 블록을 삭제하고 중간의 주석을 해제하면 문법 오류가 발생한다. catch 블록은 여러 개 작성돼도 하나씩만 실행되는데, 이미 앞의 catch 블록에서 모든 예외의 상위 클래스인 Exception 클래스 타입에는 하위 예외 클래스들이 전부 포함되기 때문에 상위 클래스 catch 블록이 먼저 검사 대상이 되면 안 되기 때문이다.

3) 리소스 처리

리소스는 데이터를 제공하는 객체를 의미한다.
데이터를 제공하기 위해서 리소스는 열어야 하며, 리소스의 역할이 끝나면 닫아야 한다. 만약 닫지 않으면 리소스가 불안정한 상태로 남기 때문이다.

리소스를 닫는 방법은 finally 블록을 쓰거나, try-with-resources 블록을 사용하면 된다.

(1) finally 블록 활용

import java.io.FileInputStream;
import java.io.IOException;

public class CloseResourceByFinallyExample {
    public static void main(String[] args) throws IOException {
        FileInputStream fis = null;

        try {
            fis = new FileInputStream("file.txt");
            System.out.println(fis);
        } catch (IOException e) {
            System.err.println(e.getMessage());
        } finally {
            fis.close(); // finally 문법을 사용한 리소스 닫기
        }
    }
}

finally 블록은 예외의 발생 여부와 관계없이 무조건 실행된다.

(2) try-with-resources 블록 활용

try-with-resources 블록을 사용하려면 해당 리소스(클래스)는 AutoCloseable의 인터페이스를 구현해서 AutoCloseable 인터페이스의 close() 메소드를 재정의해야 한다.

멀리 갈 것 없이, 방금 위의 FileInputStream 클래스가 AutoCloseable 인터페이스를 구현하고 있어서 close() 메소드를 호출할 수 있는 것이다.

public class Resource implements AutoCloseable {
    private String name;

    public Resource(String name) {
        this.name = name;
        System.out.println("[MyResource(" + name + ") 열기]");
    }

    @Override
    public void close() throws Exception {
        System.out.println("[MyResource(" + this.name + ") 닫기]");
    }

    public String read1() {
        System.out.println("[MyResource(" + this.name + ") 읽기]");
        return "100";
    }

    public String read2() {
        System.out.println("[MyResource(" + this.name + ") 읽기]");
        return "abc";
    }
}

AutoCloseable 인터페이스를 구현토록 커스터마이징한 임의의 클래스를 try-with-resources 블록에 활용해보자.

public class AutoCloseableExample {
    public static void main(String[] args) {
        // try-with-resource 블록을 사용하면 예외 발생 여부와 상관없이 리소스 자동 폐쇄

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

        System.out.println();

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

        System.out.println();

        Resource res1 = new Resource("A");
        Resource res2 = new Resource("B");

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

//        [MyResource(A) 열기]
//        [MyResource(B) 열기]
//        [MyResource(A) 읽기]
//        [MyResource(B) 읽기]
//        [MyResource(B) 닫기]
//        [MyResource(A) 닫기]

        // 이런 순서로 나오는 이유는, 리소스는 열린 순서의 역순으로 닫히기 때문
    }
}

보다시피 finally 블록이 없음에도 리소스 객체를 닫을 수 있다.

4) 예외 처리 떠넘기기

보통 예외를 떠넘긴다고 하는데, 정확한 의미는 예외 처리를 떠넘긴다는 것이다. 즉, 예외가 발생했을 때의 그 예외의 처리 여부나 조건 등은 해당 예외가 발생한 곳에서 알아서 정하라는 의미가 된다.

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

    public static void findClass() throws ClassNotFoundException {
        Class.forName("java.lang.String2");
    } // 실제로 예외가 발생하는 위치는 findClass 메소드 내부
}

//    main 메소드에서 예외 처리하는 방식으로 findClass 가 처리하는 방식이 대체, 따라가게 됨.
//    나열할 예외 클래스가 많다면, throws Exception 이나 throws Throwable 로 처리할 수 있음

상기의 클래스는 findClass()가 예외 처리를 떠넘기고 있고, 그 예외의 발생 처리 방법은 main 메소드에서 정의하는 대로 따르겠다는 의미로써 throws ClassNotFoundException을 추가로 작성하였다.

public class ThrowExample2 {
    public static void main(String[] args) throws Exception {
        findClass();
    }
    // main 메소드가 떠넘긴 예외는 최종적으로 JVM에서 처리된다.
    // JVM은 예외의 내용을 콘솔에 출력하는 것으로 예외 처리를 한다.

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

심지어 main 메소드에서도 발생하는 예외 처리를 떠넘기고 있다. 이렇게 되면 최종적으로 예외는 JVM이 처리하게 되며, JVM은 기본적으로 예외 내용을 콘솔에 출력하는 것으로 처리한다.

5) 예외 커스터마이징

Exception 최상위 예외 클래스를 상속받아서 직접 예외를 작성할 수 있다. 보통은 보편적인 생성자메시지 출력 생성자(예외 객체의 공통 메소드인 getMessage()를 사용하기 위함)를 오버로딩으로 만드는 것이 일반적이다.

public class UnderAmountException extends Exception {
    public UnderAmountException() {
    } //  메시지를 지정하지 않고 예외를 던질 때 기본 생성자가 호출

    public UnderAmountException(String message) {
        super(message);
    } // 예외 객체를 생성할 때 발생한 예외에 대한 설명이나 부가 정보를 포함하고자 할 때 사용
    // 현재 여기서는 예외 객체의 공통 메소드인 getMessage()의 리턴값으로 사용하려고 함
}
import java.util.Scanner;

public class Example {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        int amount = scanner.nextInt();

        try {
            count(amount);
        } catch (UnderAmountException e) {
            System.err.println(e.getMessage()); // 떠넘겨진 예외 처리 : 메시지 출력
        }
    }

    public static void count(int amount) throws UnderAmountException { // 예외 처리 떠넘기기
        if (amount < 10) {
            throw new UnderAmountException("[입력값 : " + amount + "] 10보다 작은 값을 입력");
        } // 조건부 예외 발생(두 번째 생성자 호출)
    }
}

대충 이런식?

profile
scientia est potentia / 벨로그 이사 예정...

0개의 댓글