자바-9(예외 처리)

dragonappear·2021년 3월 21일
0

Java

목록 보기
9/22

# 학습할 것 (필수)
1. Exception과 Error의 차이는?
2. 자바에서 예외 처리 방법 (try, catch, throw, throws, finally)
3. 자바가 제공하는 예외 계층 구조
4. RuntimeException과 RE가 아닌 것의 차이는?
5. 커스텀한 예외 만드는 방법
6. 예외처리 비용
7. 예외처리 전략


1.Exception과 Error의 차이는?

예외(Exception)와 에러(error)

프로그램을 사용하다가 프로그램이 비정상적으로 종료되는 경험을 해본 적이 있을 것이다. 이러한 결과를 초래하는 원인을 프로그램 에러(error) 또는 오류라고한다.

에러는 크게 컴파일 에러런타임 에러로 구분할 수 있다. 컴파일 에러는 말 그대로 컴파일 과정에서 일어나는 에러이고, 런타임 에러는 실행 과정에서 일어난 에러이다.

  1. 컴파일 에러는 기본적으로 자바 컴파일러가 문법 검사를 통해서 오류를 잡아준다. 우리는 컴파일러가 잡아준 오류를 고치면 성공적으로 컴파일을 해서 프로그램을 실행할 수 있다.

그러나 컴파일이 문제없이 되더라도 실행 과정(runtime)에서 오류가 발생할 수 있는데, 이런 런타임 에러를 방지하기 위해서는 프로그램 실행 도중 일어날 수 있는 모든 경우의 수를 고려하여 대비할 필요가 있다.

자바에서는 런타임 에러를 예외(Exception)와 에러(error) 두 가지로 구분하여 대응하고 있다.

  1. 에러는 메모리 부족(OutOfMemoryError), 스택오버플로우(StackOverFlow) 처럼 JVM 이나 하드웨어 등의 기반 시스템의 문제로 발생하는 것이다. 발생했을 떄를 대비해서 프로그래머가 뭔가 할 수 있는게 없다.
    발생하는 순간 무조건 프로그램은 비정상 종료되기 때문에 애초에 발생하지 않도록 해야한다.

  2. 예외는 발생하더라도 프로그래머가 미리 적절한 코드를 작성해서 프로그램이 비정상적으로 종료되지 않도록 핸들링 해줄 수 있다.


2.자바에서 예외 처리 방법 (try, catch, throw, throws, finally)

1. try-catch

예외 처리를 위해서는 try-catch 이용하며 구조는 아래와 같다.

try{
	// 예외가 발생할 가능성이 있는 코드
} catch(Exception1 e1){
	// Exception1이 발생했을 떄, 이를 처리하기 위한 코드
} catch(Exception2 e2){
	// Exception2이 발생했을 떄, 이를 처리하기 위한 코드
} catch(ExceptionN eN){
	// Exception3이 발생했을 떄, 이를 처리하기 위한 코드
}

try블럭에는 여러개의 catch 블록이 올 수 있으며, 이 중 발생한 예외의 종류와 일치하는 단 한개의 catch 블록만 실행된다. catch 블럭 안의 Exception N은 예외 클래스이며 eN은 해당 클래스의 인스턴스를 가리키는 참조 변수이다.

package me.whiteship.livestudy.week9;

public class ExceptionDemo {

    public static void main(String[] args) {
        try{
            // 1을 0으로 나눴으므로 예외가 발생한다.
            System.out.println(1/0);
        } catch (IllegalArgumentException e){
            System.out.println(e.getClass().getName());
            System.out.println(e.getMessage());
        } catch(ArithmeticException e){
            System.out.println(e.getClass().getName());
            System.out.println(e.getMessage());
        } catch (NullPointerException e){
            System.out.println(e.getClass().getName());
            System.out.println(e.getMessage());
        }
    }
}

출력:

java.lang.ArithmeticException
/ by zero

Process finished with exit code 0

위처럼 참조 변수를 통해서 발생한 예외 클래스의 인스턴스를 참조할수있다. 해당 인스턴스에는 발생한 예외에 대한 정보가 담겨 있어, 이를 통해 Message, StackTrace 등 여러 정보를 얻어올 수 있다.

0으로 숫자를 나눌 경우 ArithmeticException 이 발생하는 것을 확인할 수 있다. 이때 앞에 IllegalArgumentException이 포함된 catch 블록은 try 블록에서 발생한 예외가 속한 클래스가 아니므로 실행되지 않을 것을 확인할 수 있다. 또 NullPointerException이 포함된 catch 블록은 앞에서 ArithmeticException 블록이 예외를 잡아냈으므로 실행되지 않는다.

printStatckTrace()예외 발생 다시의 호출스택에 있었던 메서드 정보와 예외 메시지를 화면출력한다.
getMessage()바랭한 예외클래스의 인스턴스에 저장된 메시지를 얻을 수 있다.

참조 변수 중복

package me.whiteship.livestudy.week9;

public class ExceptionDemo2 {
    public static void main(String[] args) {
        try{
            methodA();
        }   catch (RuntimeException e){
            try{
                methodB();
            } catch(IllegalArgumentException e){ // 에러 발생: 해당 변수의 이름을 e로할수없음.
                // ...
            }
        }
    }
}

Try-catch문의 흐름:

try-catch문은 예외가 발생하는 경우와 아닌 경우로 나눠서 실행 흐름을 살펴볼 수 있다.

  1. 예외가 발생하지 않는 경우:
package me.whiteship.livestudy.week9;

public class ExceptionDemo {

    public static void main(String[] args) {
        System.out.println(1);
        System.out.println(2);
        try{
            System.out.println(3);
            System.out.println(4);
        } catch (Exception e){ // 이 부분만 스킵됨
            System.out.println(5);
        }
        System.out.println(6);
    }
}

출력:

1
2
3
4
6

Process finished with exit code 0

try 블록 내 코드를 실행 중에 예외가 발생하지 않으면 catch 블록을 스킵한 후 try-catch문 뒤에 있는 문장을 실행한다.

  1. 예외가 발생하는 경우
package me.whiteship.livestudy.week9;

public class ExceptionDemo {

    public static void main(String[] args) {
        System.out.println(1);
        System.out.println(2);
        try{
            System.out.println(3);
            System.out.println(1/0); // 예외발생: ArithmeticException 인스턴스 생성
            System.out.println(4); // 생략된다
        } catch (IllegalArgumentException e){ // false->생략
            System.out.println(5);
        } catch (ArithmeticException e){ // true->try-catch문 실행 후 탈출
            System.out.println(6);
        } catch(NullPointerException e){
            System.out.println(7);
        }

        System.out.println(8);
    }
}

결과:

1
2
3
6
8

Process finished with exit code 0

try블록내의 문장을 실행하다가 예외가 발생하는 경우 해당 예외의 인스턴스를 생성하고 첫 번째 catch블록으로 이동한다. 따라서 try 블록 안에 예외가 발생한 문장 다음의 코드는 실행되지 않는다.

첫번쨰 catch 블록부터 차례대로 살펴보면서 괄호() 내에 선언된 참조 변수의 종류와 생성된 예외 클래스의 인스턴스에 instancof연산자를 이용해서 검사를 한다. 검사 결과가 false이면 다음 블록으로 이동하고, true이면 해당 catch 블록의 코드를 실행한 후 try-catch문을 탈출한다. 만약 catch 문안에서 예외가 처리되지 않으면 프로그램은 종료된다.

2. Multi-catch block

JDK 1.7부터 여러 CATCH BLOCK을 하나로 합칠 수 있게 되었다.

package me.whiteship.livestudy.week9;

public class ExceptionDemo {

    public static void main(String[] args) {
        try{
            System.out.println(1/ 0);
        } catch(IllegalArgumentException | ArithmeticException e){
            System.out.println(e.getMessage());
        }
    }
}

단 이때, 나열된 예외 클래스들이 부모-자식 관계에 있따면 오류가 발생한다.

package me.whiteship.livestudy.week9;

public class ExceptionDemo {

    public static void main(String[] args) {
        try{
            System.out.println(1/ 0);
        } catch(RuntimeException | ArithmeticException e){
           // 에러발생: ArithmeticException은 RuntimeException을 상속받는 클래스이다.
           	System.out.println(e.getMessage());
        }
    }
}

왜냐하면, 자식 클래스로 잡아낼 수 있는 예외는 부모 클래스로도 잡아낼 수 있기 때문에 사실상 코드가 중복된 것이나 마찬가지이기 때문이다.
이때 컴파일러는 중복된 코드를 제거하라는 의미에서 에러를 발생시킨다.

또한 멀티캐치는 하나의 블록으로 여러 예외를 처리하는 것이기 때문에 멀티 캐치 블록 내에서는 발생한 예외가 정확이 어디에 속한 것인지 알수없다. 그래서 참조 변수 e에는 '|'로 연결된 예외들의 공통조상 클래스에 대한 정보가 담긴다.

3. throw

throw 키워드를 이용해서 고의로 예외를 발생시킬 수도 있다. 예를 들어서 사용자가 "하이"라는 닉네임을 사용하지 못하게 하고 싶다면 다음과 같이 예외를 발생시켜 프로그램을 중단시킬 수 있다.

package me.whiteship.livestudy.week9;

import java.util.Scanner;

public class ExceptionDemo {

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

        System.out.println("아이디를 입력하세요");
        String userName = scanner.nextLine();

        try {
            if (userName.equals("하이")) {
                throw new IllegalArgumentException("부적절한 이름입니다.");
            }
        } catch (IllegalArgumentException e) {
            System.out.println(e.getMessage());
        }
    }
}

출력:

아이디를 입력하세요
하이
부적절한 이름입니다.

Process finished with exit code 0

new 키워드로 예외 인스턴스를 생성하고, throw 키워드로 해당 예외를 발생시킨 것이다.

throw new ... 부분은 다음문장을 축약한것이다

IllegalArgumentException e = new IllegalArgumentException("부적절한 이름입니다.");

throw e;

4. throws

throws 키워드를 통해 메서드에 예외를 선언할 수 있다.

void method() throws Exception1,Exception2, ... ExceptionN{
	// 메서드 내용
}

throws는 메서드 선언부에 예외를 선언해둠으로써 해당 메서드를 사용하는 사람들이 어떤 예외를 처리해야 하는 지를 알려주는 역할을 한다.

throws 자체는 예외의 처리와는 관계가 없다. throws로 예외가 선언된 메서드를 사용할때, 사용자가 알아서 예외를 처리해줘야 한다. 즉 throws는 해당 메서드에서 예외를 처리하지 않고, 해당 메서드를 사용하는 쪽이 예외를 처리하도록 책임을 전가하는 역할은 한다.

package me.whiteship.livestudy.week9;

import java.io.File;
import java.util.ArrayList;
import java.util.Scanner;

public class ExceptionDemo {

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

        try{
            System.out.println("파일 이름을 입력하세요");
            String fileName = scanner.nextLine();

            File f = createFile(fileName);
        }   catch (Exception e){
            System.out.println(e.getMessage());
        }
    }

    static File createFile(String fileName) throws Exception{
        if(fileName == null || fileName.equals("")){
            throw new Exception("파일 이름이 유효하지 않습니다.");
        }

        File f= new File(fileName);
        f.createNewFile();
        return f;

    }
}

출력:

파일 이름을 입력하세요

파일 이름이 유효하지 않습니다.

Process finished with exit code 0

createFile이라는 메서드는 파일이름을 입력받아서 파일을 생성하는 역할을 한다. 만약 파일 이름이 null 이거나 빈 문자열이라면 예외를 던진다.

createFile 메서드 내에서는 해당 예외에 대한 처리를 하지 않고, 단지 throws로 예외를 선언하기만 했다. 그래서 createFile을 사용하는 사람들이 각자 상황에 맞게 예외를 어떻게 처리할지 선택해야 한다.

4. finally

finally는 try-catch와 함께 예외의 발생 여부와 상관없이 항상 실행되어야 할 코드를 포함시킬 목적으로 사용된다. try-catch문의 끝에 선택적으로 덧붙여 사용할 수 있으며, try-cath-finally의 순서로 구성된다.

try{
	// 예외가 발생할 가능성이 있는 문장을 넣는다.
} catch(){
	// 예외 처리를 위한 문장을 넣는다.
} finally{
	// 예외 발생 여부와 상관없이 항상 실행되어야 할 문장을 넣는다.
}

예외가 발생한 경우에는 try->catch->finally 순으로 실행되고, 예외가 발생하지 않은 경우에는 try-finally 순으로 실행된다.

한가지 짚고 넘어갈 점은 finally 블록 내의 문장은 try,catch 블록에 return문이 있더라도 실행된다는 것이다.

package me.whiteship.livestudy.week9;

public class ExceptionDemo {

    public static void main(String[] args) {
        methodA();
        System.out.println("methodA가 복귀한 후 실행될 문장");
    }
    
    static void methodA(){
        try {
            System.out.println("트라이 블록 수행");
            return;
        } catch (Exception e){
            System.out.println("캐치 블록 수행");
        } finally {
            System.out.println("파이널리 블록 수행");
        }
    }
}

output:

트라이 블록 수행
파이널리 블록 수행
methodA가 복귀한 후 실행될 문장

Process finished with exit code 0

methodA를 보면 try문에서 "트라이 블록 수행"을 출력하고 return을 하는데도 , 그전에 finally 블록이 수행되는 것을 확인할 수 있다.

catch 블록을 수행하다가 return문을 만날 때에도 finaly 블록이 수행되고 리턴한다.

5. try-with-resources

  • exception 발생시 resource를 자동으로 close()해준다.
  • 사용 로직을 작성할 떄 객체는 AutoCloseable 인터페이스를 구현한 객체여야 한다.
  • 자바 7부터 추가

try-catch-finally 예제코드

package me.whiteship.livestudy.week9;

import java.io.FileNotFoundException;
import java.io.FileOutputStream;

public class ExceptionDemo {

    public static void main(String[] args) {
        FileOutputStream out = null;
        try {
            out = new FileOutputStream("thewing.txt");
            // 생략
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } finally {
            if(out!=null){
                try {
                    out.close(); // close() 예외가 발생할수있다.
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

try-with-resources 예제코드1

package me.whiteship.livestudy.week9;

import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;

public class ExceptionDemo {

    public static void main(String[] args) {
        try(FileOutputStream out = new FileOutputStream("thewing.txt")){
            
        } catch (IOException e){
            e.printStackTrace();
        }
    }
}

try-with-resources 예제코드2

package me.whiteship.livestudy.week9;

public class ExceptionDemo implements AutoCloseable {
    public ExceptionDemo() {
        System.out.println("객체 생성");
    }

    public static void main(String[] args) {
        try(ExceptionDemo ed = new ExceptionDemo()) {
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    public void close() throws Exception {
        System.out.println("나 닫는다?");
    }
}

3.자바가 제공하는 예외 계층 구조

자바에서는 실행 시 발생할 수 있는 오류(Exception & Error)를 클래스로 정의하고 있다. 예외와 에러의 상속계층도는 다음과 같다.

(https://madplay.github.io/post/java-checked-unchecked-exceptions)

exception 과 error는 Throwable 이라는 클래스를 상속받고 있으며 Throwalbe은 Object를 직접 상속받고 있다.

위 사진에 보면 알수있듯이 모든 예외의 부모의 클래스는 java.lang.Throwable 클래스이다

  • Exception 이나 Error를 처리할 때 Throwable로 처리해도 무관하다.

  • Throwable 클래스에 선언되어있고, Exception 클래스에서 Overriding한 메소드는 10개가 넘으며 가장 많이 사용되는 메소드는 getMessage,toString,printStackTrace가 있다.

  1. getMessage
    • 예외 메시지를 String 형태로 제공받는다.
    • 예외가 출력되었을 때 어떤 예외가 발생되었는지를 확인할 때 매우 유용하다.
    • 메시지를 활용하여 별도의 예외 메시지를 사용자에게 보여주려고 할때 좋다.

예제코드:

package me.whiteship.livestudy.week9;

public class ExceptionDemo {
    public static void main(String[] args) {
        ExceptionDemo exceptionDemo = new ExceptionDemo();
        exceptionDemo.throwable();
    }

    void throwable(){
        try {
            int [] arr = new int[5];
            arr = null;
            System.out.println(arr[5]);
        } catch (Throwable t) {
            System.out.println(t.getMessage());
        }
    }
}

출력:

null

Process finished with exit code 0
  1. toString()
    • 예외메시지를 String 형태로 제공받는다.
    • getMessage() 메소드보다는 약간 더 자세하게, 예외클래스 이름도 같이 제공한다.

예제코드:

package me.whiteship.livestudy.week9;

public class ExceptionDemo {
    public static void main(String[] args) {
        ExceptionDemo exceptionDemo = new ExceptionDemo();
        exceptionDemo.throwable();
    }

    void throwable(){
        try {
            int [] arr = new int[5];
            arr = null;
            System.out.println(arr[5]);
        } catch (Throwable t) {
            System.out.println(t.toString());
            System.out.println(t.getMessage());
        }
    }
}

출력:

java.lang.NullPointerException
null

Process finished with exit code 0
  1. printStackTrace()
    • 가장 첫 줄에는 예외 메시지를 출력하고, 두 번째 줄부터는 예외가 발생하게 된 메소드들의 호출관계(스택 트레이스)를 출력해준다.
    • printStackTrace()는 서비스 운용시 사용하면 안된다.

예제코드:

 package me.whiteship.livestudy.week9;

public class ExceptionDemo {
    public static void main(String[] args) {
        ExceptionDemo exceptionDemo = new ExceptionDemo();
        exceptionDemo.throwable();
    }

    void throwable(){
        try {
            int [] arr = new int[5];
            arr = null;
            System.out.println(arr[5]);
        } catch (Throwable t) {
            System.out.println(t.toString());
            System.out.println(t.getMessage());
            t.printStackTrace();
        }
    }
}

출력코드:

java.lang.NullPointerException
null
java.lang.NullPointerException
	at me.whiteship.livestudy.week9.ExceptionDemo.throwable(ExceptionDemo.java:13)
	at me.whiteship.livestudy.week9.ExceptionDemo.main(ExceptionDemo.java:6)
  • 매우 자세한 메시지를 볼수있다.
  • 이 메서드는 개발할 때에만 사용해야한다.
  • 운영할 시스템에 적용시 엄청난 양의 로그가 쌓이게 된다. 꼭 필요한 곳에만 사용할 것.

Built-in Exceptions in Java with examples

Java 라이브러리에서 제공하는 기본 Exception들의 다양한 예를 보자.

  1. Arithmetic Exception
    산술 연산에서 예외 조건이 발생했을 때 발생한다.
    코드:
    package me.whiteship.livestudy.week9;

public class ExceptionDemo {
    public static void main(String[] args) {
        try {
            int a= 30 , b = 0 ;
            int c = a / b; // 예외 발생
            System.out.println(c);
        } catch (ArithmeticException e){
            System.out.println(e.getMessage());
        }
    }
}

output:

    / by zero
  1. ArrayIndeoxOutOfBounds Exception
    잘못된 인덱스로 Array에 엑세스했을 때 발생한다. 인덱스가 음수이거나 배열 크기보다 크거나 같을 때 발생한다.

예제코드:

package me.whiteship.livestudy.week9;

public class ExceptionDemo {
    public static void main(String[] args) {
        try {
            int[] arr = new int[5];
            System.out.println(arr[8]);
        } catch (ArrayIndexOutOfBoundsException e){
            System.out.println(e.getMessage());
        }
    }
}

output:

8
  1. ClassNotFoundException
    정의한 클래스를 찾을 수 없을때 발생하는 예외이다.

예제코드:

public static void main(String[] args) 
    { 
        Object o = class.forName(args[0]).newInstance(); 
        System.out.println("Class created for" + o.getClass().getName()); 
    } 
} 
  1. FileNotFoundException
    이 예외는 파일에 엑세스할 수 없거나 열리지 않을 때 발생한다.

예제코드:

package me.whiteship.livestudy.week9;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;

public class ExceptionDemo {
    public static void main(String[] args) {
        try {
            File file = new File("E://file.txt");

            FileReader fr = new FileReader(file);
        } catch (FileNotFoundException e){
            System.out.println(e.getMessage());
        }
    }
}

output:

E:\file.txt (지정된 경로를 찾을 수 없습니다)
  1. IOException
    입출력 작업이 실패하거나 중단될 때 발생한다.

예제코드:

public static void main(String args[]) 
    { 
        FileInputStream f = null; 
        f = new FileInputStream("abc.txt"); 
        int i; 
        while ((i = f.read()) != -1) { 
            System.out.print((char)i); 
        } 
        f.close(); 
    } 
} 
  1. InterruptedException
    Thread가 waiting, sleeping 또는 어떤 처리를 하고 있을때 interrupt가 되면 발생하는 예외이다.

예제코드:

package me.whiteship.livestudy.week9;

import java.io.*;

public class ExceptionDemo extends Thread {
    public static void main(String[] args) {
        ExceptionDemo exceptionDemo = new ExceptionDemo();
        exceptionDemo.start();
        exceptionDemo.interrupt();

    }

    public void run(){
        try {
            Thread.sleep(1000);
            System.out.println("task");
        } catch (InterruptedException e) {
            System.out.println(e.getMessage());
        }

        System.out.println("thread is running");

    }
}

output:

sleep interrupted
thread is running
  1. NoSuchMethodException
    찾을 수 없는 메서드에 액세스할 때 이 예외가 발생합니다.

예제코드:

package me.whiteship.livestudy.week9;

import java.io.*;

public class ExceptionDemo  {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException {
        Class c = Class.forName("NoSuchMethodException");

        c.getDeclaredMethod("test");
  }
}

output:

Exception in thread "main" java.lang.ClassNotFoundException: NoSuchMethodException
	at java.net.URLClassLoader.findClass(URLClassLoader.java:382)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:418)
	at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:355)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:351)
	at java.lang.Class.forName0(Native Method)
	at java.lang.Class.forName(Class.java:264)
	at me.whiteship.livestudy.week9.ExceptionDemo.main(ExceptionDemo.java:7)
  1. NullPointerException
    이 예외는 null 객체의 멤버를 참조할때 발생한다.

예제코드:

package me.whiteship.livestudy.week9;

import java.io.*;

public class ExceptionDemo extends Thread {
    public static void main(String[] args) {
        try {
            String a = null;
            System.out.println(a.charAt(0));
        } catch (NullPointerException e) {
            System.out.println(e.getMessage());
        }

    }
}

output:

null
  1. NumberFormatException
    메서드가 문자열을 숫자 형식으로 변환할 수 없는 경우 이 예외가 발생한다.

예제코드:

package me.whiteship.livestudy.week9;

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

        try {
            int num = Integer.parseInt("string");
        } catch (NumberFormatException e) {
            System.out.println(e.getMessage());
        }
    }
}

output:

For input string: "string"
  1. StringIndexOutOfBoundsException
    문자열에 엑세스하는 인덱스가 문자열보다 큰 경우거나 음수일 대 발생하는 예외이다.

예제코드:

package me.whiteship.livestudy.week9;

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

        try {
            String a = "This is life";
            char c = a.charAt(50);
            System.out.println(c);
        } catch (StringIndexOutOfBoundsException e) {
            System.out.println(e.getMessage());
        }
    }
}

output:

String index out of range: 50

4.RuntimeException과 RE가 아닌 것의 차이는?

Checked exceptions VS Unchecked Exceptions

위 그림을 살펴보면 Exception을 상속받는 클래스들 중에서 RuntimeException을 제외하고는 모두 CheckedException이라고 표시되어있다.

Checked Excepton은 컴파일 시점에서 확인될 수 있는 예외이다. 만약 코드 내에서 Checked exception을 발생시킨다면, 해당 예외는 반드시 처리되거나, 해당 코드가 속한 메서드의 선언부에 예외를 선언해줘야 한다.

예를 들어 Checked Exception 중에 하나인 IOException을 발생시키는 메서드를 선언했다고 가정하자

package me.whiteship.livestudy.week9;

import java.io.IOException;

public class ExceptionDemo {

    public static void main(String[] args) {
        methodA();
    }

    static void methodA(){
       throw new IOException();
    }
}

위 코드는 컴파일 자체가 안된다. IOException은 check exception 이기 때문에 컴파일 단계에서 예외가 확인이 된다.따라서 위 코드를 컴파일하려면 try-catch로 에외를 처리하거나 throws로 예외를 던져야한다

package me.whiteship.livestudy.week9;

import java.io.IOException;

public class ExceptionDemo {

    public static void main(String[] args) throws IOException {
        methodA();
    }

    static void methodA() throws IOException{
       throw new IOException();
    }
}

이렇게 예외를 던져주면 컴파일은 가능하다.

Unchecked Exception은 컴파일단계에서 확인되지 않는 예외이다. 이 예외는 컴파일러가 예외를 처리하거나 선언하도록 강제하지 않는다. 프로그래머가 알아서 처리 해야한다.예를 들어 위의 예시를 RuntimeException으로 바꾸면 컴파일 에러가 발생하지 않는다.

package me.whiteship.livestudy.week9;

import java.io.IOException;

public class ExceptionDemo {

    public static void main(String[] args) {
        methodA();
    }

    static void methodA(){
        throw new RuntimeException();
    }
}

예외를 처리하거나 던지지 않아도 컴파일이 잘된다.

왜 이렇게 예외를 두 타입으로 나눠놨을까?

예외는 메서드의 파라미터나 반환값만큼이나 중요한 공용 인터페이스 중 하나이다.

메서드를 호출하는 쪽은 그 메서드가 어떤 예외를 발생시킬 수 있는가에 대해 반드시 알아야 한다.
따라서 Java는 checked exception을 통해 해당 메서드가 발생시킬 수 있는 예외를 명세하도록 강제하고 있다.

Runtime Exception은 왜 예외를 명세하지 않아도 되도록 했을까?
Runtime Exception은 프로그램 코드의 문제로 발생하는 예외이다. 따라서 클라이언트 쪽(메서드를 호출하는 쪽)에서 이를 복구(or 회복)하거나 대처할 수 있을 거라고 예상하긴 어렵다.
또 Runtime Exception은 프로그램 어디서나 매우 빈번하게 발생할 수 있기 때문에 모든 Runtime Exception을 메서드에 명시하도록 강제하는 것은 프로그램의 명확성을 떨어뜨릴 수 있다.

따라서 클라이언트가 exception을 적절히 회복할 수 있을 것이라고 예상되는 경우 checked exception으로 만들고, 그렇지 않은 경우 unchecked exception으로 만드는 것이 좋다.


5.커스텀한 예외 만드는 방법

기존에 정의된 예외 클래스 외에 필요에 따라 새로운 예외를 정의할 수 있다. Exception 클래스를 상속받거나, 필요에 따라 예외 클래스를 상속받아 만든다.

커스텀 예외를 만들때 참고해야 할 4가지

링크텍스트
링크텍스트

  1. Always Provide a Benefit

자바 표준 예외들에는 포함되어 있는 다양한 장점을 가지는 기능들이 있다.

이미 JDK가 제공하고 있는 방대한 수의 예외들과 비교했을 때 만들고자 하는 커스텀 예외는 어떠한 장점도 제공하지 못한다면? 커스텀 예외를 만드는 이유를 다시 생각해볼 필요가 있다

어떠한 장점을 제공할 수 없는 예외를 만드는 것 보다 오히려 UnsupportedOperationException 이나, IllegalArugmentException 과 같은 표준 예외 중 하나를 사용하는 것이 낫다.

  1. Follow the Naming Convention
    JDK가 제공하는 예외 클래스들을 보면 클래스의 이름이 모두 "Exception" 으로 끝나는 것을 알 수 있다.

이러한 네이밍 규칙은 자바 생태계 전체에 사용되는 규칙이다.

즉, 만들고자 하는 커스텀 예외 클래스도 이 네이밍 규칙을 따르는 것이 좋다.

  1. Provide javadoc Comments for Your Exception Class
    많은 커스텀 예외들이 어떠한 javadoc 코멘트도 없이 만들어진 경우들이 있다.

기본적으로 API의 모든 클래스, 멤버변수, 생성자들에 대해서는 문서화 하는 것이 일반적인 Best Practices 이다.

목적은 다른 개발자들이 API를 이해하고 일반적인 에러상황들을 피하도록 돕는 것이다.

/**
 * The MyBusinessException wraps all checked standard Java exception and enriches them with a custom error code.
 * You can use this code to retrieve localized error messages and to link to our online documentation.
 * 
 * @author TJanssen
 */
public class MyBusinessException extends Exception { ... }
  1. Provide a Constructor That Sets the Cause

보통 캐치된 예외에는 제품에 발생한 오류를 분석하는데 필요한 중요한 정보가 포함되어 있다.

아래 예제를 보면 NumberFormatException은 에러에 대한 상세 정보를 제공한다.

MyBusinessException의 cause 처럼 cause 정보를 설정하지 않으면 중요한 정보를 잃을 것이다.

public void wrapException(String input) throws MyBusinessException {
    try {
        // do something
    } catch (NumberFormatException e) {
        throw new MyBusinessException("A message that describes the error.", e, ErrorCode.INVALID_PORT_CONFIGURATION);
    }
}

1. 사용자 정의 예외 만들기

package me.whiteship.livestudy.week9;

import java.io.IOException;

public class ExceptionDemo {

    public static void main(String[] args) throws SpaceException {
        methodA(0);
    }

    static void methodA(int space) throws SpaceException{
        if(space < 1)
        throw new SpaceException("공간 부족");
    }

}


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

output:

Exception in thread "main" me.whiteship.livestudy.week9.SpaceException: 공간 부족
	at me.whiteship.livestudy.week9.ExceptionDemo.methodA(ExceptionDemo.java:13)
	at me.whiteship.livestudy.week9.ExceptionDemo.main(ExceptionDemo.java:8)

Process finished with exit code 1

2. 예외 되던지기(exception re-throwing)

한 메서드에서 발생할 수 있는 예외가 여러 개인 경우, 일부는 메서드 내부에서 처리하고 일부는 선언부에 지정해서 메서드를 호출한 쪽에서 ㅓ리하도록 할 수 있다.

또 하나의 예외에 대해서도 양쪽에서 처리하도록 할수 있는데 이를 '예외 되던지기'라고 한다. catch 문에서 throw를 사용해 예외를 다시 던지는 방식으로 구현 가능하다.

package me.whiteship.livestudy.week9;

import java.io.IOException;

public class ExceptionDemo {

    public static void main(String[] args) {
        try {
            methodA();
        } catch (Exception e){
            System.out.println("main에서 예외 처리");
        }
    }

    static void methodA() throws Exception{
        try{
            throw new Exception();
        }   catch (Exception e){
            System.out.println("methodA에서 예외처리");
            throw  e;
        }
    }

}

output:

methodA에서 예외처리
main에서 예외 처리

Process finished with exit code 0

3. Chained exception

한 예외가 다른 예외를 발생시킬 수도 있다. 예를 들어 예외 A가 예외 B를 발생시켰다면, A를 B의 '원인 예외'라고 한다. 원인 예외는 initCause()로 지정할 수 있다. initCause()는 Throwable 클래스에 정의되어 있기 때문에 모든 예외 클래스에서 사용할수있다.

package me.whiteship.livestudy.week9;

import java.io.IOException;

public class ExceptionDemo {

    public static void main(String[] args) {
        try {
            methodA(0);
        } catch (Exception e){
            e.printStackTrace();
        }
    }

    static void methodA(int num) throws IOException{
      try{
          if(num==0){
              throw new IllegalArgumentException();
          }
        } catch (IllegalArgumentException e){
          IOException ioException = new IOException();
          ioException.initCause(e); // IOException의 예외를 IllegalArgumentException으로 지정
          throw ioException;
      }
    }

}

output:

java.io.IOException
	at me.whiteship.livestudy.week9.ExceptionDemo.methodA(ExceptionDemo.java:21)
	at me.whiteship.livestudy.week9.ExceptionDemo.main(ExceptionDemo.java:9)
Caused by: java.lang.IllegalArgumentException
	at me.whiteship.livestudy.week9.ExceptionDemo.methodA(ExceptionDemo.java:18)
	... 1 more

Process finished with exit code 0

이런 식으로 원인 예외를 등록해서 예외를 발생시키면 여러가지 예외를 하나의 큰 분류의 예외로 묶어서 다루는 것이 가능하다.

서로 연결되는 예외는 상속 관계가 아니어도 상관없다.

또 이 방식을 이용해서 checked exception을 unchecked exception으로 바꾸는 것도 가능하다.

public class ExceptionDemo {

    public static void main(String[] args) {
        try {
            methodA(0);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    static void methodA(int num){
        try {
            if (num == 0) {
                throw new IllegalArgumentException();
            }
        } catch (IllegalArgumentException e) {
            throw new RuntimeException(e);
        }
    }
}

output:

java.lang.RuntimeException: java.lang.IllegalArgumentException
	at exceptiondemo.ExceptionDemo.methodA(ExceptionDemo.java:21)
	at exceptiondemo.ExceptionDemo.main(ExceptionDemo.java:9)
Caused by: java.lang.IllegalArgumentException
	at exceptiondemo.ExceptionDemo.methodA(ExceptionDemo.java:18)
	... 1 more
Caused by: java.lang.IllegalArgumentException

6. 예외처리 비용

public Throwable(String message){
	fillInStackTrace();
    detailMessage = message;
}

위 코드는 Throwable 클래스의 상속자이다.

보통 예외처리는 처리 비용이 비싸다고 한다.
try-catch를 동작하면서 발생하는 검사들도 하나의 원인이겠지만,
Throwable 생성자의 fillInStackTrace() 메서드가 주 원인이다.

이 메서드는 예외가 발생한 메서드의 Stack Trace를 모두 출력해주기 때문이다.

stacktrace란? application이 실행된 시점부터 현재 실행 위치까지의 메서드 호출 목록이다.

커스텀 예외에서 이 메서드를 오버라이딩하여 statck trace를 최소화 해줄수있다.

@Override
pbulic synchronized Throwable fillInStackTrace(){
	return this;
}

7. 예외처리 전략

출처 : 토비의 스프링 3.1 Vol.1 4장 예외

1. 예외복구

  • 예외복구는 문제를 파악하고 해결하여 정상상태로 돌려놓는 방법이다.

  • 어떤 예외가 발생하였을때 다른 작업 흐름으로 자연스럽게 유도해주는 것이다.

  • 가장 대표적으로 try / catch / finally 절을 사용할수있다.

package me.whiteship.livestudy.week9;

public class ExceptionDemo  {
    public static void main(String[] args) {
        String s = null;
        printStr(s);
    }
    
    public static void printStr(String str){
        try {
            System.out.println("str length:" + str.length());
        } catch(NullPointerException e){
            System.out.println(e.getMessage());
        }
    }
}

2. 예외처리 회피

예외처리회피는 자신이 직접 예외처리하지않고 호출하는 메서드로 전파시키는 방법이다

throws 문으로 예외처리를 회피하는 방법과 catch 블럭으로 일단 예외를 잡은 후에 로그를 남기고 다시 throw 하는 방법이 있다.

// throws로 회피하기
public void process() throws SQLException {
    // jdbc 로직...
}

// catch 후 로그 남기고 다시 throw
public void process2() throws SQLException {
    try {
        // jdbc 로직...
    } catch (SQLException e) {
        System.out.println(e.getMessage())
        throw e;
    }
}

-> Unchecked Exception 이라면 throws 없이 회피가 가능하다.

3. 예외전환

예외 전환은 예외 회피와 비슷하다고 볼수있지만 발생한 예외를 그대로 넘기지 않고 더 적절한 예외로 전환하여 던지는 특징이 있다.

내부에서 발생한 예외가 모호하여 의미가 명확한 예외를 던지기 위해 전환할수있다.

// 예외의 에러 코드에 따라 분기하여 예외 전환
public void add(User user) throws DuplicateUserIdException, SQLException {
    try {
        // code ..
    } catch (SQLException e) {
        if (e.getErrorCode() == MysqlErrorNumbers.ER_DUP_ENTRY) {
            throw new DuplicateUserIdException();
        }

        throw e;
    }
}

이렇게 예외를 전환할 때는 기존 예외를 담아서 중첩예외로 만드는 것이 좋다

catch (SQLException e) {
    throw new DuplicateUserIdException(e);
}

예외를 처리하기 쉽게 포장하기 위해 전환할 수 있습니다.

예를 들어 Checked Exception을 잡아서 Unchecked Exception으로 던져서 해당 메소드를 사용 하는 곳에서 일일히 에러 처리를 할 필요가 없습니다.

private void process() {
    try {
        // code ...
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
}

참고

어썸오 블로그
자바의 정석(남궁성 저)
Geeksforgeeks
링크텍스트
링크텍스트
링크텍스트
링크텍스트
링크텍스트

0개의 댓글