📝 이것이 자바다

Chapter 12. 예외 처리

j_estory 2023. 1. 18. 21:46

🏷 예외와 예외 클래스 

 

컴퓨터 하드 웨어 고장으로 인해 응용프로그램 실행 오류가 발생하는 것을 자바에서는 에러라고 한다.

자바에서는 에러 이외에 예외라고 부르는 오류가 있다.

예외란 ..?

잘못된 사용 또는 코딩으로 인한 오류를 말한다.

예외가 발생하면 프로그램은 곧바로 종료된다는 점에서 에러와 동일하지만,

예외 처리를 통해 계속 실행 상태를 유지할 수 있다.

 

✔️ 예외의 종류

 

  • 일반 예외 (Exception)
    • 컴파일러가 예외 처리 코드 여부를 검사하는 예외를 말한다.
  • 실행 예외 (Runtime Exception)
    • 컴파일러가 예외 처리 코드 여부를 검사하지 않는 예외를 말한다. 

자바는 예외가 발생하면 예외 클래스로부터 객체를 생성한다. 

이 객체는 예외 처리 시 사용된다. 

 

자바의 모든 에러와 예외 클래스는 Trowable을 상속받아 만들어지고,

추가적으로 예외 클래스는 java.lang.Exception 클래스를 상속받는다.

 

실행 예외는 RuntimeException과 그 자식 클래스에 해당한다. 

그 밖의 예외 클래스는 모두 일반 예외이다.

 

🏷 예외 처리 코드

 

예외가 발생했을 때, 프로그램의 갑작스러운 종료를 막고 정상 실행을 유지할 수 있도록 처리하는 코드를 예외 처리 코드라고 한다.

예외 처리 코드는 try-catch-finally 블록으로 구성된다. 

해당 블록은 생성자 내부와 메소드 내부에서 작성된다. 

 

try 블록에서 작성한 코드가 예외 없이 정상 실행되면 catch 블록은 실행되지 않고, finally 블록이 실행된다.

그러나 try 블록에서 예외가 발생하면 catch 블록이 실행되고 연이어 finally 블록이 실행된다.

 

예외 발생 여부와 상관없이 finally 블록은 항상 실행된다. 

심지어, try 블록과 catch 블록에서 return문을 사용하더라도 finally 블록은 항상 실행된다.

 

✅ 실행 예외

 

public class ExceptionHandlingExample {
	public static void printLength(String data) {
		// data가 null일 경우 NullpointException 발생
    	int result = data.length();
    }
}

위의 예제와 같은 경우에 NullPointerException이 발생되는데, 

해당 예외는 실행 예외이므로, 컴파일할 때 예외처리 코드가 없어도 되지만,

실행 중에 발생하면 프로그램은 즉시 종료된다.

 

✔️ 예외 정보를 출력하는 3가지 방법

 

  • e.getMessage() : 예외가 발생한 이유 리턴
  • e.toString() : 예외의 종류도 함께 리턴
  • e.printStackTrace() : 예외가 어디서 발생했는지 추적한 내용까지도 출력

테스트 해봤는데 ... e.getMessage() 랑 e.toString()이 잘 안됨..

 

✅ 컴파일 예외

 

다음 예제는 Class.forName("패키지 ... 클래스")는 ClassPath 위치에서 주어진 클래스를 찾는 코드이.

찾지 못했을 경우, ClassNotFoundException 이라는 일반 예외가 발생한다. 

따라서 소스가 컴파일 되려면 예외 처리 코드를 반드시 작성해야 한다.

/**
     * 컴파일 예외 발생 예제
     */
    public void classExceptionEx() {
        try {
            Class.forName("java.lang.String");
            System.out.println("java.lang.String 클래스가 존재합니다.");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }

 

🏷 예외 종류에 따른 처리

 

try 블록에는 다양한 종류의 예외가 발생할 수 있다.

이 경우, 다중 catch를 사용하면 발생하는 예외에 따라 예외 처리 코드를 다르게 작성할 수 있다.

catch 블록의 예외 클래스는 try 블록에서 발생된 예외의 종류를 말하는데, 해당 타입의 예외가 발생하면 catch 블록이 선택되어 실행된다.

 

catch 블록이 여러 개라 할지라도 catch 블록은 단 하나만 실행된다. 

그 이유는 try 블록에서 동시 다발적으로 예외가 발생하지 않으며,

하나의 예외가 발생하면 즉시 실행을 멈추고 해당 catch 블록으로 이동하기 때문이다. 

 

만약, 처리해야 할 예외 클래스들이 상속 관계에 있을 때는 하위 클래스 catch를 먼저 작성하고,

상위 클래스 catch 블록을 나중에 작성해야 한다. 

예외가 발생하면 catch 블록은 위에서부터 차례대로 검사 대상이 되는데,

하위 예외도 상위 클래스 타입이므로 상위 클래스 catch 블록이 먼저 검사 대상이 되면 안 된다.

try {
	ArrayIndexOutOfBoundsException 발생
    NumberFormatException 발생
} catch (Exception e) {
	// 예외 처리 1
    // 위의 오류 발생 두개가 모두 Exception 클래스와 상속이므로 모두 해당 catch 블록으로 잡히게 된다.
} catch (NumberFormatException e) {
	// 예외 처리 2
}

--> 올바른 방법

try {
	ArrayIndexOutOfBoundsException 발생
    NumberFormatException 발생
} catch (NumberFormatException e) {
	// 예외 처리 2
} catch (Exception e) {
	// 예외 처리 1
}

또한 두 개 이상의 예외를 하나의 catch 블록으로 동일하게 예외 처리를 하고 싶을 때에는

catch 블록에 예외 클래스를 기호 ( | ) 로 연결하면 된다.

catch(NullPointerException | NumberFormatException e) {
	
}

 

🏷 리소스 자동 닫기

 

리소스란 ..?

- 데이터를 제공하는 객체를 의미한다. 

- 리소스는 사용하기 위해 열어야 하며, 사용이 끝난 다음에는 닫아야 한다. 

- 예를들어, 파일 내용을 읽기 위해서는 파일을 열어야 하며, 다 읽고 난 후에는 파일을 닫아야 다른 프로그램에서 사용할 수 있다.

 

리소스를 사용하다가 예외가 발새될 경우에도 안전하게 닫는것이 중요하다.

그렇지 않으면 리소스가 불안정한 상태로 남아있게 된다. 

 

다음 코드는 file.txt 파일의 내용을 읽기 위해 FileInputStream 리소스를 사용하게 되는데,

예외 발생 여부와 상관없이 finally 블록에서 안전하게 close 한다.

FileInputStream fis = null;
try {
	fis = new FileInputStream("file.txt");
} catch(IOException e) {

} finally {
	fis.close();
}

좀 더 쉬운 방법으로 try 괄호에 리소스를 여는 코드를 작성하면 try 블록이 정상적으로 실행을 완료하였거나,

도중에 예외가 발생하면 자동으로 리소스의 close() 메소드가 호출된다.

try(FileInputStream fis = new FileInputStream("file.txt")) {

} catch(IOException e) {
	...
}

해당 방법을 사용하기 위해서는 조건이 하나 있는데, 

AutoCloseable 인터페이스를 구현해서 AutoCloseable 인터페이스의 close() 메소드를 재정의해야 한다. 

public class MyResource implements AutoCloseable {

    private String name;


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

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

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




@Test
public void 리소스_예외_테스트() {
    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();
        // NumberFormatException 발생
        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 = res2.read2();
    } catch (Exception e) {
        System.out.println("예외 처리: " + e.getMessage());
    }
}


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

[MyResource(]A) 열기]
[MyResource(]A) 읽기]
[MyResource(]A) 닫기]
예외 처리: For input string: "abc"

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

 

🏷 예외 떠넘기기

 

메소드 내부에서 예외가 발생할 때 try-catch 블록으로 예외를 처리하는 것이 기본이지만,

메소드를 호출한 곳으로 예외를 떠넘길 수 있다. 

이때 ! 사용하는 키워드가 throws이다.

throws 메소드는 선언부 끝에 작성하는데 예외 클래스를 쉼표로 구분해서 나열해주면 된다.

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

throws 키워드가 붙어 있는 메소드에서 해당 예외를 처리하지 않고 떠넘겼기 때문에 

이 메소드를 호출하는 곳에서 예외를 받아 처리해야 한다. 

 

나열해야 하는 클래스가 많은 경우에는 throws Exception 또는 thows Throwable 만으로 모든 예외를 떠넘길 수 있다. 

 

만약 main() 메소드에서 throws 키워드를 사용하여 예외를 넘기면 JVM이 최종적으로 처리를 하게 된다.

JVM은 예외와 내용을 콘솔에 출력하는 것으로, 예외를 처리하게 된다.

 

🏷 사용자 정의 예외

 

✔️ 사용자 정의 예외

 

사용자 정의 예외는 컴파일러가 체크하는 일반 예외로 선언할 수 있고, 컴파일러가 체크하지 않는 실행 예외로 선언할 수 있다.

통상적으로 일반 예외는 Exception의 자식 클래스로 선언하고, 

실행예외는 RuntimeException의 자식 클래스로 선언한다.

 

사용자 예외 클래스는 기본 생성자와 예외 메시지를 입력받는 생성자를 선언해준다. 

예외 메시지는 부모 생성자 매개값으로 넘겨주는데,

그 이유는 예외 객체의 공통 메소드인 getMessage()의 리턴 값으로 사용하기 위해서다.

public class InsufficientException extends Exception {
	public InsufficientException() {}
    
    public InsufficientException(String message) {
    	super(message);
    }
}

 

✔️ 예외 발생 시키기

 

사용자 정의 예외를 직접 코드에서 발생시키려면 throw 키워드와 함께 예외 객체를 제공하면 되고,

예외의 원인에 해당하는 메시지를 제공하고 싶다면 생성자 매개값으로 메시지를 전달한다.

void method() {
	try {
    	throw new Exception("예외 메시지");
    } catch(Exception e) {
    	String message = e.getMessage();
    }
}

직접 위와 같이 try-catch 블록으로 예외를 처리할 수도 있지만 !!

void method() throws Exception {
	throw new Exception("예외 메시지");
}

대부분은 메소드를 호출한 곳에서 예외를 처리하도록 떠넘긴다.

package com.example.thisisjava.exception_ex_11;

public class Account {

    private long balance;

    public Account() {}

    public long getBalance() {
        return balance;
    }

    public void deposit(int money) {
        balance += money;
    }

    public void withdraw(int money) throws InSufficientException {
        if (balance < money ) {
            throw new InSufficientException("잔고부족: " + (money-balance) + " 모자람");
        }
        balance -= money;
    }
}


package com.example.thisisjava.exception_ex_11;

public class InSufficientException extends Exception{
    public InSufficientException() {}
    public InSufficientException(String message) {
        super(message);
    }
}

@Test
public void 사용자_예외_처리() {
    Account account = new Account();
    account.deposit(10000);;
    System.out.println("예금액: " + account.getBalance());

    try {
        account.withdraw(30000);
    } catch (Exception e) {
        String msg = e.getMessage();
        System.out.println(msg);
    }
}