예외처리

gustjtmd·2022년 2월 1일
0

Java

목록 보기
13/40

자바 예외처리의 기본

자바에서 말하는 예외

프로그램 실행 중에 발생하는 '예외적인 상황'을 줄여서 '예외'라 한다
즉 예외는 단순한 문법 오류가 아닌 실행 중간에 발생하는 '정상적이지 않은 상황'을 뜻한다
코드로 확인해보자

public class ExceptionCase {
    public static void main(String[] args) {
        Scanner kb = new Scanner(System.in);
        System.out.print("a/b...a? ");
        int n1 = kb.nextInt();
        System.out.print("a/b...b? ");
        int n2 = kb.nextInt();
        System.out.printf("%d / %d = %d\n",n1, n2, n1/n2);
        System.out.println("Good bye~");
    }
}


a/b...a? 8
a/b...b? 0
Exception in thread "main" java.lang.ArithmeticException: / by zero
	at review.ch18.ExceptionCase.main(ExceptionCase.java:12)
    
------------------------------------------------------------------------

a/b...a? R
Exception in thread "main" java.util.InputMismatchException
	at java.base/java.util.Scanner.throwFor(Scanner.java:939)
	at java.base/java.util.Scanner.next(Scanner.java:1594)
	at java.base/java.util.Scanner.nextInt(Scanner.java:2258)
	at java.base/java.util.Scanner.nextInt(Scanner.java:2212)
	at review.ch18.ExceptionCase.main(ExceptionCase.java:9)
    
---------------------------------------------------------------------
    
위 코드는 문법적으로 논리적으로 문제가 없으나 나누는 수가 0이 될 수 없음에도 불구하고
0을 입력한 프로그램 사용자에게 있다 
또 숫자를 입력해야 하나 문자를 입력한 프로그램 사용자에게 있다

그리고 이러한 상황을 가리켜 '예외'라 한다.

예외 발생 순간에 프로그램이 종료되었는데 우리가 원하는 방식의 예외처리가 아니다.
최소한 다음과 같이 예외의 원인을 설명하고
"숫자를 입력해야 합니다 다시 실행해주세요"

코드의 마지막 문장인 다음 문장을 실행하여 인사 정도는 해야 우리가 생각하는 예외처리 일것이다

System.out.println("Good bye~~!");

예외의 처리는 가상머신이 아닌 우리가 하면 된다. 가상 머신은 예외의 원인은 알지만
프로그래머가 원하는 예외의 처리 방식까지는 알지 못한다 따라서 프로그램을 종료하는 것이다.

예외의 처리를 위한 try~catch

예외가 발생하는 두가지 상황을 보았는데 이떄 출력한 메시지에서 다음 클래스의 이름을 확인할수
있다.

java.lang.ArithmeticException
	-> 수학 연산에서의 오류 상황을 의미하는 예외 클래스
    
java.lang.InputMismatchException
	-> 클래스 Scanner를 통한 값의 입력에서의 오류 상황을 의미하는 예외 클래스
    
이렇듯 자바는 예외 상황별로 그 상황을 알리기 위한 클래스를 정의하고 있다.
이러한 클래스를 가리켜 '예외 클래스'라 한다.

수학 연산 관련 오류가 발생하면 가상머신은 예외 클래스 ArithmeticException의 인스턴스를
생성한다. 그리고 이 인스턴스를 프로그래머가 처리하면 예외는 처리된 것으로 간주하여 프로그램을
종료시키지 않는다.

try{
	'관찰 영역'
}catch(Exception name){
	'처리 영역'
}

이렇게 try ~ catch문은 try 영역과 catch 영역으로 구분되는데
이 둘은 하나의 문장이므로 항상 연결되어 있어야 하고 동작하는 방식은 다음과 같다

"try 영역에서 발생한 예외의 상황을 catch 영역에서 처리한다"

코드로 확인해보자

public class ExceptionCase2 {
    public static void main(String[] args) {

        Scanner kb = new Scanner(System.in);

        try {
            System.out.print("a/b...a? ");
            int n1 = kb.nextInt();
            System.out.print("a/b...b? ");
            int n2 = kb.nextInt();
            System.out.printf("%d / %d = %d\n", n1, n2, n1 / n2);  //예외 발생 지점
        } catch (ArithmeticException e) {
            System.out.println(e.getMessage());
        }
        System.out.println("Good bye~");
    }
}

a/b...a? 2
a/b...b? 0
/ by zero
Good bye~

-----------------------------------------------------------------------------

System.out.printf("%d / %d = %d\n", n1, n2, n1 / n2);  //예외 발생 지점

이 순간 가상머신은 ArithmeticException 인스턴스를 생성한다 그리고 예외 발생 지점을
감싸는 try 영역에 이어서 등장하는 catch 영역에서 이 인스턴스를 인자로 받을수 있는지 확인하고
받을수 있으면 catch 영역으로 인스턴스를 전달한다.

이렇듯 catch 영역으로 예외 인스턴스가 전달되면 가상머신은 예외가 처리된 것으로 판단한다
따라서 문자열 Good bye~까지 출력이 된다.

try로 감싸야 할 영역의 결정

try{
	1. ...
    	2. 예외 발생 지점
    	3. ... 건너 뜀
        4. ... 건너 뜀
}catch(Exception e{
	...
}
5. 예외 처리 이후 실행 지점

둘 이상의 예외를 처리하기 위한 구성

위에서 보인 코드 나눗셈에서는 문자열, 0으로 나누는 예외의 발생 가능성이 있다
이를 다음 코드를 통해서 한번에 예외 처리 해보자

public class ExceptionCase7 {
    public static void main(String[] args) {
        Scanner kb = new Scanner(System.in);

        try{
            System.out.print("a/b...a? ");
            int n1 = kb.nextInt();
            System.out.print("a/b...b? ");
            int n2 = kb.nextInt();
            System.out.printf("%d / %d = %d\n", n1, n2, n1 / n2);  //예외 발생 지점
        }catch (InputMismatchException | ArithmeticException e){
            System.out.println(e.getMessage());
        }
        System.out.println("Good bye~");
    }
}


a/b...a? R
Good bye~

Throwable 클래스와 예외처리의 책임 전가

자바의 최상위 클래스 Object를 제외하고 예외 클래스의 최상위 클래스는

java.lang.Throwable 이다.

이 클래스에서 발생한 예외의 정보를 알 수 있는 메소드가 정의되어 있는데 대표적인 메소드는

public String getMessage()
	-> 예외의 원인을 담고 있는 문자열을 반환
    
public void printStackTrace()
	-> 예외가 발생한 위치와 호출된 메소드의 정보를 출력
    
printStackTrace 메소드의 기능 확인을 위한 다음 코드를 실행해보자
이는 예외가 발생하는 코드이다.

public class ExceptionMessage {
    public static void md1(int n){
        md2(n,0);	//아래 메소드 호출
    }
    public static void me2(int n1, int n2){
        int r = n1/ n2;	//예외 발생 지점
    }

    public static void main(String[] args) {
        md1(3);
        System.out.println("Good bye");
    }
}


Exception in thread "main" java.lang.ArithmeticException: / by zero
	at Java.ch18.ExceptionMessage.md2(ExceptionMessage.java:14)
	at review.ch18.ExceptionMessage.md1(ExceptionMessage.java:7)
	at review.ch18.ExceptionMessage.main(ExceptionMessage.java:14)
    
----------------------------------------------------------------------------

위 코드의 메소드 호출 흐름은 다음과 같다

main -> md1 -> md2

예외는 md2에서 발생하였는데 해당 예외를 처리하지 않았다.
이러한 경우 md2를 호출한 md1에게 예외 처리를 넘기고
md1도 예외처리를 하지 않았으면 md1을 호출한 main에게 예외처리를 넘긴다

이렇듯 예외는 처리되지 않으면 그 책임이 넘어간다 그리고 그 끝은 main인데 main조차
예외처리를 하지 않으면 가상머신이 대신 예외를 처리한다 
그 방법은 예외 관련 메시지의 출력과 프로그램 종료이다.
예외처리 한 코드

public class ExceptionMessage {
    public static void md1(int n){
        md2(n,0);   //이 지점으로 mnd2로부터 예외가 넘어온다
    }
    public static void me2(int n1, int n2){
        int r = n1/ n2; //이 지점에서 예외가 발생한다
    }

    public static void main(String[] args) {
        try{
            md1(3); //이 지점에서 md1으로부터 예외가 넘어온다
        }catch (Throwable e){
            e.printStackTrace();
        }
        System.out.println("Good bye");
    }
}


java.lang.ArithmeticException: / by zero
	at Java.ch18.ExceptionMessage.md2(ExceptionMessage.java:14)
	at review.ch18.ExceptionMessage.md1(ExceptionMessage.java:7)
	at review.ch18.ExceptionMessage.main(ExceptionMessage.java:15)
Good bye

------------------------------------------------------------------------

try{
            md1(3); //이 지점에서 md1으로부터 예외가 넘어온다
        }catch (Throwable e){
            e.printStackTrace();
        }
        
위 코드는 md1으로부터 넘어오는 예외를 처리하기 위한 try ~ catch 문이다
이렇듯 md1에서 넘어오는 예외를 처리하기 위해서는 md1의 호출문을 try ~ catch 문으로 감싸면
된다.

그런데 실제 넘어오는 예외는 Throwable이 아니다 그러나 모든 예외 클래스는 Throwable을 
상속하므로 상속 관계에 의해 md2에서 발생한 예외를 위와 같이 처리할 수 있지만
이는 좋은 예외처리의 예는 아니다.

예외 상황을 알리기 위한 정의된 클래스의 종류

자바에서 발생시키는 예외의 종류는 다양한다 그리고 그 수만큼 예외 클래스도 다양하게 정의되어
있다. 대표적인 클래스들을 확인해보자
배열 접근에 잘못된 인덱스 값을 사용해서 발생하는 ArrayIndexOutOfBoundsException

public class ArrayIndexOutOfBounds {
    public static void main(String[] args) {
        int [] arr = {1,2,3};
        for(int i = 0; i < 4; i++)
            System.out.println(arr[i]);
    }
}


1
2
3
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 3 out of bounds for length 3
	at review.ch18.ArrayIndexOutOfBounds.main(ArrayIndexOutOfBounds.java:7)


허용할수 없는 형 변환을 강제로 진행하는 경우 발생하는 ClassCastException

public class ClassCast {
    public static void main(String[] args) {
        Board pbd1 = new PBoard();
        PBoard pbd2 = (PBoard) pbd1;

        System.out.println(".. intermediate location ..");
        Board ebd1 = new Board();
        PBoard ebd2 = (PBoard)ebd1; //Exception
    }
}


.. intermediate location ..
Exception in thread "main" java.lang.ClassCastException: class review.ch18.Board cannot be cast to class review.ch18.PBoard (review.ch18.Board and review.ch18.PBoard are in unnamed module of loader 'app')
	at review.ch18.ClassCast.main(ClassCast.java:12)


null이 저장된 참조변수를 대상으로 메소드를 호출할때 발생하는 NullPointerException

public class NullPointer {
    public static void main(String[] args) {
        String str = null;
        System.out.println(str);    //null 출력
        int len = str.length();     //Exception
    }
}


null
Exception in thread "main" java.lang.NullPointerException
	at review.ch18.NullPointer.main(NullPointer.java:7)

예외처리에 대한 나머지 설명들

예외 클래스의 구분

예외 클래스는 최상위 클래스 Throwable를 상속하는 예외클래스는 다음과 같다

Error 클래스를 상속하는 예외 클래스
Exception 클래스를 상속하는 예외 클래스
RuntimeException 클래스를 상속하는 예외 클래스
	-> RuntimeException 클래스는 Exception 클래스를 상속한다
    

Error 클래스를 상속하는 예외 클래스

'Error 클래스를 상속하는 예외 클래스'의 예와 그 발생 상황을 정리하면

VirtualMachineError '가상머신에 심각한 오류 발생'
IOError		     '입출력 관련해서 코드 수준 복구가 불가능한 오류 발생'Error 클래스를 상속하는 예외는 처리의 대상이 아니다(처리할수 있는 예외가 아님)

따라서 이런 유형의 예외가 발생하면 그냥 프로그램이 종료되도록 놔두고 이후에 원인을 파악하는
과정이 이어져야 한다.

RuntimeExcpeion 클래스를 상속하는 예외 클래스


'RuntimeException 클래스를 상속하는 예외 클래스'는 다음과 같다

ArtimeticException
ClassCastException
IndexOutOfBoundsException
NegativeArraySizeException	배열 생성시 길이를 음수로 지정하는 예외의 발생
NullPointerException
ArrayStoreException	배열에 적절치 않은 인스턴스를 저장하는 예외의 발생

RuntimeExeption을 상속하는 예외 역시 대부분의 경우 프로그래머가 예외처리 하지 않는다.

예를들어서

Object[] ar1 = new Object[-5];
		-> NegativeArraySizeException 예외의 발생
        
위 상황은 코드를 수정해야 할 상황이지 예외처리를 해야할 상황이 아니다.

물론 상황에 따라서 프로그래머가 예외처리를 해야 하는 경우도 드물지만 있을수 있다
프로그램 사용자의 실수로 인해 이들 예외가 발생하는 경우가 있기 때문이다.
그러나 말 그래도 드문 경우이다

Exception 클래스만 상속하는 예외 클래스의 예외처리

Exception 클래스만 상속하는 예외는 반드시 try ~ catch 문으로 처리하거나 다른 영역으로 넘긴다고 명시해야 한다.
Exception을 상속하는 예외 클래스 중에서 빨리 접하게될 클래스는 

java.io.IOException 이다.

코드를 세세히 이해할 필요는 없고 어느 지점에서 예외가 발생하는지 확인해보자

public class IOExceptionCase {
    public static void main(String[] args) {
        Path file = Paths.get("C:\\javastudy\\Simple");
        BufferedWriter writer = null;
        
        try{
            writer = Files.newBufferedWriter(file); //IOException 발생 가능
            writer.write('A');  //IOException 발생 가능
            writer.write('Z');  //IOException 발생 가능
            
            if(writer != null)
                writer.close(); //IOException 발생 가능
        }
        catch (IOException e){
            e.printStackTrace();
        }
    }
}

----------------------------------------------------------------------------------

만약 try ~ catch문을 지우면 컴파일 오류가 발생한다
앞서 확인한 Error 클래스를 상속하는 예외나 RuntimeException 클래스를 상속하는 예외의
경우 예외의 처리는 선택이지만

Exception 클래스를 상속하는(RuntimeException을 상속하지 않는)
예외는 try ~ catch 문으로 처리하거나 다른 영역으로 넘긴다고 반드시 명시해야 한다.

위 코드와 내용은 같지만 다른 영역으로 넘긴다고 명시한 코드

public class IOExceptionCase3 {
    public static void main(String[] args) {
        try{
            md1();
        }catch (IOException e){
            e.printStackTrace();
        }
    }
    public static void md1() throws IOException{
        md2();
    }
    
    public static void md2() throws IOException{ //IOException 예외 넘긴다고 명시!
        Path file = Paths.get("C:\\javastudy\\Simple");
        BufferedWriter writer = null;

        writer = Files.newBufferedWriter(file); //IOException 발생 가능
        writer.write('A');  //IOException 발생 가능
        writer.write('Z');  //IOException 발생 가능

        if(writer != null)
            writer.close(); //IOException 발생 가능
    }
}


위 코드의 메소드 호출 과정은 다음과 같다

main -> md1 -> md2

이 중에서 md2 내에서 IOException 예외가 발생할 수 있다.
그런데 IOExceptionException을 상속한다 따라서 이에 대해 try ~ catch 문을 작성하거나

다음과 같이 예외의 처리를 이 메소드를 호출한 메소드에게 넘긴다는 표시를 해야한다.

public static void md2() throws IOException{ //IOException 예외 발생하면 넘긴다

위의 메소드 정의에서 throws IOExceptionIOException예외가 메소드 내에서 발생할 경우
md2를 호출한 영역으로 예외의 처리를 넘긴다는 뜻이다.

md1역시 throws IOException이 선언되어 있으니
md1을 호출한 영역으로 예외의 처리를 넘긴다는 뜻이다.

끝으로 main 메소드에서 IOException 예외가 전달될수 있는 md1을 호출하므로
두가지 선택중 하나를 해야한다

1) try ~ catch 문을 통해서 IOException을 직접 처리하거나
2) throws IOException 선언을 추가해서 예외의 처리를 넘겨야함

main 메ㅗㅅ드도 예외를 넘기면 이 예외는 main을 호출한 가상머신에게 넘어간다
그러면 프로그램은 종료가 된다.

끝으로 throws 선언을 통해서 둘 이상의 예외에 대해 그 처리를 넘긴다는 표시도 가능하다

throws IOException, IndexOutOfBoundsException{...}

프로그래머가 정의하는 예외

프로그래머가 직접 예외 클래스를 정의하고 이를 기반으로 특정 상황에서 예외가 발생하도록
할 수가 있다.
이 클래스의 핵심은 Exception을 상속하는데 있다.

class ReadAgeException extends Exception{ //Exception을 상속하는 것이 핵심
	public ReadAgeException(){
    		super("유효하지 않은 나이가 입력되었습니다.");
    }
}

Exception을 상속하는 점을 제외하면 일반클래스와 차이가 없다
생성자에서는 상위 클래스의 생성자를 호출하면서 예외 상황에 담고 있는 문자열을 전달하는데
이 문자열은 앞서 보였던 Throwable 클래스에 저으이된 다음 메소드 호출시 반환이된다

public String getMessage()

코드로 확인해보자

class ReadAgeException extends Exception{
    public ReadAgeException(){
        super("유효하지 않은 나이가 입력되었습니다.");
    }
}

public class MyExceptionClass {
    public static void main(String[] args) {
        System.out.println("나이 입력 : ");

        try{
            int age = readAge();
            System.out.printf("입력된 나이 : %d \n", age);
        }catch (ReadAgeException e){
            System.out.println(e.getMessage());
        }
    }

    public static int readAge() throws ReadAgeException{
        Scanner kb = new Scanner(System.in);
        int age = kb.nextInt();

        if(age < 0)
            throw new ReadAgeException();   //예외의 발생

        return age;
    }
}


나이 입력 : 12
입력된 나이 : 12

나이 입력 : -1
유효하지 않은 나이가 입력되었습니다.

------------------------------------------------------------------------

정수를 입력받았는데 그 수가 음수인것은 문법적으로 오류가 아니지만 프로그램 내용상으로
사람의 나이이므로 오류가 맞다.
이러한 상황을 예외로 처리하기 위해서 예외 클래스를 직접 정의하였다.

if(age < 0)
            throw new ReadAgeException();   //예외의 발생
            
이렇듯 예외 클래스의 인스턴스를 생성하고 이를 대상으로 throw 선언을 하면 이로써 예외가
발생이 된다.

물론 이렇게 발생한 예외도 Exception을 상속하는 예외이므로
try ~ catch 문으로 처리하거나 throws 선언을 통해 넘겨야 한다

잘못된 catch 구문의 구성

다음 세개의 예외 클래스가 정의되었다고 가정해보자

class FirstException extends Exception{...}
class SecondException extends FirstException {...}
class ThirdException extends SecondException {...}

그리고 세 종유 예외가 모두 발생 가능한 영역을 다음과 같이 try ~ catch 문으로
구성하였다고 가정해보자

try{
...
}
catch(FirstException e){...}
catch(SecondException e){...}
catch(ThirdException e){...}

예외 처리의 내용만 놓고 보면 문제 없을것 같지만 컴파일 오류가 발생한다

그 이유는 catch(FirstException e){...} 가 모든 예외의 인스턴스를 처리할수 있기 때문에 
두번째 세번째 catch 구문은  실행될 일이 없기 때문이다.

때문에 위와 같이 catch문을 구성하고자 한다면 순서를 수정해야 한다
try{
	...
}
catch(ThirdException e){...}
catch(SecondException e){...}
catch(FirstException e){...}

finally 구문

try ~ catch 문은 하나의 문장이므로 try 구문 홀로 존재할 수 없다
반드시 catch 구문이 하나 이상 등장해야 한다 
그런데 try에 이어서 다음과 같이 finally 구문을 둘 수도 있다

try{
	...
}finally{...		//코드의 실행이 try 안으로 진입하면 무조건 실행해라
}

또는 다음과 같이 try ~ catch ~ finally를 하나의 문장으로 묶을수도 있다.

try{
	...
}catch(...){
	...
}finally{
	...	//코드의 실행이 try 안으로 진입하면 무조건 실행된다
}


이러헥 finally 구문은 코드의 실행이 try 안으로 진입하면 무조건 실행이 된다.
try에서 예외가 발생하건 안하건 catch가 실행하건 안되건 무조건 실행된다.

아까 위의 코드에서

writer = Files.newBufferedWriter(file); //IOException 발생 가능
위 코드가 실행되면(파일을 여는 코드)

writer.close(); 
반드시 실행해야 한다.(파일을 닫는 코드)

이를 finally를 활용한 코드로 확인해보자

public class FinallyCase {
    public static void main(String[] args) {
        Path file = Paths.get("C:\\javastudy\\Simple");
        BufferedWriter writer = null;

        try{
            writer = Files.newBufferedWriter(file); //IOException 발생 가능
            writer.write('A');  //IOException 발생 가능
            writer.write('Z');  //IOException 발생 가능
            
        }
        catch (IOException e){
            e.printStackTrace();
        }finally {
            if(writer != null)
                writer.close(); //IOException 발생 가능
        }
    }
}


이렇게 실행되면 좋지만 이러면 컴파일 오류가 발생한다 그 이유는
close의 호출문에서도 IOException 예외가 발생할수 있기 떄문이다

따라서 finally 구문을 다음과 같이 수정해야 한다.

finally {
            try{
                if(writer != null)
                    writer.close(); //IOException 발생 가능
            }catch (IOException e){
                e.printStackTrace();
            }
        }
        

이렇듯 finally 내에서도 try ~ catch 문을 작성할 수 있으며
이 상황에서는 선택이 아니라 필수이다. 너무 복잡한것 같은데 이를 위한 
try-with-resources문이라는 것이 등장하면서 구성이 단순해졌다.

try-with-resources 구문

앞서 
writer = Files.newBufferedWriter(file); //IOException 발생 가능
위 코드가 실행되면(파일을 여는 코드)

writer.close(); 
반드시 실행해야 한다.(파일을 닫는 코드)

한다고 하였는데 finally 구문으로 처리하면 코드가 복잡해진다
그러나 try-with-resources 문이 추가되어 이러한 코드의 구성이 단순해졌다.

try(resource){
	...
}catch(Exception name){...}

try에 이어 등장하는 소괄호 안에서는 종료의 과정을 피요로 하는 리소스를 생성할 수 있다
그러면 이 리소스는 try-with-resources문을 빠져 나오면서 자동으로 종료가 된다.

코드로 바로 확인해보자

public class TryWithResource {
    public static void main(String[] args) {
        Path file = Paths.get("C:\\javastudy\\Simple");

        try(BufferedWriter writer = Files.newBufferedWriter(file)){
            writer.write('A');  //IOException 발생 가능
            writer.write('Z');  //IOException 발생 가능
        }catch (IOException e){
            e.printStackTrace();
        }
    }
}

-----------------------------------------------------------------------------

try(BufferedWriter writer = Files.newBufferedWriter(file)){
            writer.write('A');  //IOException 발생 가능
            writer.write('Z');  //IOException 발생 가능
        }catch (IOException e){
            e.printStackTrace();
        }
        
이로써 참조변수 writer가 참조하는 인스턴스의 종료는 신경쓰지 않아도 된다
try 안에서 예외가 발생 하건 안 하건 
writer를 대상으로 한 다음 메소드의 호출은 보장되기 떄문이다

writer.close();	//직접 이 문장을 넣지 않아도 된다.

그러면 리소스의 종료 관련 메소드가 close인 경우에만 자동으로 호출될까? 

java.lang.AutoCloseable

이 인터페이스는 try-with-resources문에 의해 자동으로 종료 되어야 할 리소스 관련 클래스가
반드시 구현해야 하는 인터페이스다(BufferedWriter 클래스도 이 인터페이스 구현함)

그리고 이 인터페이스에는 다음 추상 메소드가 존재하는데

void close() throws Exceptiontry-with-resources문에서 호출하는 메소드는 AutoCloseable 
인터페이스의 close 메소드이다.

따라서 close 이외의 메소드 호출을 기대하는것은 아쉽지만 힘들다.

try-with-resources 역시 둘 이상의 리소스가 가능하다 세미클론으로 리소스를 구분하면 된다.

try(resource1; resourcd2){...
}catch(Exception name){...}
profile
반갑습니다

0개의 댓글