본문 바로가기

WhiteShip Java Study : 자바 처음부터 멀리까지

예외 처리

선장님과 함께하는 자바 스터디입니다.

자바 스터디 Github

github.com/whiteship/live-study

 

whiteship/live-study

온라인 스터디. Contribute to whiteship/live-study development by creating an account on GitHub.

github.com

나의 Github

github.com/cmg1411/whiteShip_live_study

 

cmg1411/whiteShip_live_study

✍ 자바 스터디할래. Contribute to cmg1411/whiteShip_live_study development by creating an account on GitHub.

github.com

 

  • 자바에서 예외 처리 방법 (try, catch, throw, throws, finally)
  • 자바가 제공하는 예외 계층 구조
  • Exception과 Error의 차이는?
  • RuntimeException과 RE가 아닌 것의 차이는?
  • 커스텀한 예외 만드는 방법

 

예외

  • 예외란 프로그램이 실행될 시점에, 의도치 않은 상황으로 인해 프로그램이 오동작할 위험이 있을 때, 자바가 프로그램을 멈추어 오동작을 막는 것이다.
  • 프로그램의 동작이 멈추어버리기 때문에 이 예외를 처리하는 방법을 익혀서 오동작할 위험에 대처할 수 있어야 한다.

 

Throwable

  • JAVA 에서는 시스템의 비정상적인 동작의 처리를 위해 Throwable 이라는 인터페이스를 제공한다.
  • 또한, 이를 구현하는 여러가지 계층적인 클래스들이 존재한다.

checked Exception / Compiletime Exception

초록색으로 칠해진 부분. 컴파일타임에 체크한다. 반드시 예외에 대한 처리를 try / catch 든 throws 든 해야한다.

public class CheckedException {
    public static void main(String[] args) {
        final ObjectMapper objectMapper = new ObjectMapper();
        final String str;
        Object object = new Object();

        try {
            str = objectMapper.writeValueAsString(object);
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }
    }
}

ObjectMapper 의 writeValueAsString 메서드는 구현부를 보면 throws 를 통해 JsonProcessingException 을 던지고있다.

    public String writeValueAsString(Object value) throws JsonProcessingException {
        SegmentedStringWriter sw = new SegmentedStringWriter(this._jsonFactory._getBufferRecycler());

        try {
            this._writeValueAndClose(this.createGenerator((Writer)sw), value);
        } catch (JsonProcessingException var4) {
            throw var4;
        } catch (IOException var5) {
            throw JsonMappingException.fromUnexpectedIOE(var5);
        }

        return sw.getAndClear();
    }

JsonProcessingException 은 IOException의 하위 클래스이다. 따라서 checked Exception 이고, 컴파일타임에 예외를 검사하며, 반드시 예외처리를 해줘야하기에 try/ catch 로 묶은 모습이다.

 

코드로 처리할 수 있는 오류들은 checked 예외를 쓴다. 코드로 처리할 수 있으므로 예외처리를 강제하여 나은 코드를 만들 수 있다.

 

 

 

 

Unchecked Exception / Runtime Exception

빨간색으로 칠해진 부분. Unchecked 라는 의미는 checked 예외처럼 명시적인 예외처리를 하지 않아도 컴파일은 된다는 뜻이다.

프로그램이 실행되는 runtime 에 동작이상이 발생하면 예외를 발생시키며 프로그램을 멈춘다.

try / catch 나 throw 같은 코드로 어떻게 해보려 할 수 없고, 코드를 변경해서 예외를 발생시키지 않게 해야 한다.

물론 try / catch, throw 로 처리할 수 도 있다.

 

예외복구가 어렵고, 코드로 복구를 하기 어려운 경우 사용한다.

 

Error vs Exception

  • 두 클래스 모두 java.lang 에 위치하며, Throwable 인터페이스를 구현한다.
  • Error 클래스는 시스템 레벨에서 발생하는 심각한 문제다. 프로그램 내부의 문제보다는 하드웨어라든지 환경의 문제로 인해 발생한다. Unchecked 인 이유는 우리가 try / catch 같은 걸로 에러를 잡아도 해결할 수 없는 문제들이기 때문이다.
  • 예를 들어 StackOverFlow, OutOfMemory 같은 에러들은 코드로 어떻게 할 수 없고 메모리를 더 늘리든지, 최적화를 하든지 해야한다.

 

 

예외처리 3가지 방법 - 토비의 스프링 3.1

  • 예외 복구 : 예외 상황을 파악하고 문제를 해결하여 에플리케이션을 정상상태로 돌려놓음
  • 예외처리를 강제하는 check 예외는 복구할 가능성이 있는 것만 사용한다. API 사용자들이 복구하는 코드를 짤 수 있도록 강제하는 것이다.

 

  • 예외처리 회피 : 자신이 예외를 처리하지 않고 호출한 곳으로 throws 하는 것. 또는 try / catch 로 잡아서 로그만 남기고 throw 로 다른데로 책임을 던짐.
  • 다른 곳에서 예외를 처리하는 것이 낫다는 분명한 확신이 있어야 한다.

 

  • 예외 전환 : 회피와는 달리 발생한 예외를 적절한 예외로 변환해서 던진다.
  • 내부에서 발생한 예외를 그대로 던지는 것이 그 예외상황에 대한 적절한 의미를 부여해주지 못하는 경우에 의미를 분명하게 하기 위해 전환한다.
  • 또는 예외를 처리하기 쉽고 단순하게 만들기 위해 포장한다.

 

 

 

 

예외처리 - try / catch

  • try 블럭에는 예외가 일어날 수 있는 코드를 작성하고
  • catch 블럭에는 예외가 일어났을때 처리할 코드를 작성한다.
  • try 구문에서 여러가지 예외가 발생할 수 있다면 멀티 catch 블럭으로 작성할 수 있다.
  • try 블럭에서 예외가 발생하면 바로 catch 블럭으로 가서 프로그램이 종료되기 때문에 멀티 catch 블록중 하나만 실행한다.
  • 멀티 catch 블록은 위에서 부터 아래로 검사한다.
  • Java 7 이후애 멀티 catch 블록에서 | 를 사용할 수 있다.

일반적인 try / catch 사용

public class UnCheckedException {
    public static void main(String[] args) {
        final int x = 10;
        final int y = 0;
        final int result;
        
        try {
            result = x / y;
        } catch (ArithmeticException e) {
            System.out.println("0으로는 나눌 수 없습니다.");
        }
    }
}

 

멀티 catch 블록. Try 안의 두 코드 모두 예외를 발생시키는 코드이지만, 예외는 하나만 발생한다.

public class UnCheckedException {
    public static void main(String[] args) {
        final int x = 10;
        final int y = 0;
        final int result;
        
        try {
            result = x / y;
            Class c = Class.forName("hi");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (ArithmeticException e2) {
            System.out.println(" 0으로는 나눌 수 없다 ");
        }
    }
}

 

 

 

멀티 catch 블록은 위에서 아래로 검사하기 때문에 아래 catch 문에는 위 catch 문의 하위 타입 예외를 사용할 수 없다.

아래에서는 ArithmeticException 은 Exception 의 하위 예외이다.

ArithmeticException이 발생해도 모두 Exception 에서 걸려서 예외처리되기 때문에 그 아래 catch 문은 의미가 없다.

Java 에서는 이런 경우를 컴파일 타임의 에러로 발생시켜 컴파일조차 할 수 없게 도와준다.

        try {
            result = x / y;
        } catch (Exception e) {
            System.out.println("0으로는 나눌 수 없습니다.");
        } catch (ArithmeticException e2) {
            
        }

 

 

만약 예외 처리 구문이 비슷하다면 | 기호를 이용하여 멀티 catch 를 구현할 수 있다.

    public static void main(String[] args) {
        final int x = 10;
        final int y = 0;
        final int result;

        try {
            result = x / y;
            Class c = Class.forName("hi");
        } catch (ClassNotFoundException | ArithmeticException e) {
            System.out.println("에러 발생");
        }
    }

물론 여기서도 두 예외는 상속관계에 있으면 안된다. 그럼 컴파일 오류난다.

 

 

finally

  • try / catch 구문 이후에는 finally 키워드를 붙인 블록을 추가로 붙일 수 있다.
  • 문제가 없어 try 구문이 모두 실행되었든, 중간에 예외가 발생하여 catch 구문이 실행됬든 어쨋든 마지막에 실행할 구문을 작성할 수 있다.
  • try 블럭에서 예외가 발생하지 않는다면 catch 블럭을 실행하지 않고 바로 finally 블록으로 간다.
public class Final {
    public static void main(String[] args) {
        int x = 10;
        int y1 = 0;
        int y2 = 5;

        try {
            System.out.println(x / y1);
        } catch (Exception e) {
            System.out.println("예외 발생");
        } finally {
            System.out.println("계산 끝");
        }

        try {
            System.out.println(x / y2);
        } catch (Exception e) {
            System.out.println("예외 발생");
        } finally {
            System.out.println("계산 끝");
        }
    }
}
예외 발생
계산 끝
2
계산 끝

 

예외처리 - throws

  • 예외를 Try /catch 로 처리하지 않고, 넘기는 방법이 있다.
  • 예외처리는 예외가 일어날 수 있는 어떤 코드에서 하고, 그 어떤 코드는 어떤 메서드안에 있을 것이다.
  • throws로 그 메서드를 실행하는 곳으로 예외처리를 넘길 수 있다.
  • throws 키워드 이후에 발생할 수 있는 예외를 한개 이상 작성하면 되는데, 이는 해당 메서드에서 그러한 예외가 발생할 수 있고, 발생하면 그 예외를 호출한 부분에 넘긴다는 뜻이다.
  • 그럼 예외가 발생할 수 있는 메서드를 사용하는 쪽에서 예외를 처리하면 된다.
public class Throws {
    private int x;
    private int y;

    public Throws(int x, int y) {
        this.x = x;
        this.y = y;
    }
    
    public int divide() throws ArithmeticException {
        return x / y;
    }
}

 

public class UseThrows {
    public static void main(String[] args) {
        Throws t = new Throws(3, 0);
        int result;

        try {
          result = t.divide();
        } catch (ArithmeticException e) {
            System.out.println("예외 발생");
       }
    }
}

 

 

주의점

finally는 try 나 catch 에 return 문이 있어도 실행되기 때문에, finally 에 return 문이 있다면

이전의 return 문을 덮어쓰게 된다.

따라서 finally 안의 return 은 anti 패턴이다.

 

다음 예를 보면 쉽게 알 수 있다.

public class Final {
    public static void main(String[] args) {
        Final f = new Final();
        System.out.println(f.finallyUse());
    }

    public int finallyUse() {
        int x1 = 10;
        int y3 = 0;
        int y4 = 5;

        try {
            System.out.println(x1 / y3);
            return x1 / y3;
        } catch (Exception e) {
            System.out.println("예외 발생");
            return 0;
        } finally {
            System.out.println("계산 끝");
            return 100;
        }
    }
}

예외 발생 - throw

  • throw 키워드를 사용하면 강제로 예외를 발생시킬 수 있다.
  • 객체를 생성하는 것 처럼 throw new 예외() 로 예외를 발생시킨다.
  • 이미 존재하는 예외는 throw 예외 로 던질 수 있다.
public class Throw {
    public void createException() {
        throw new IllegalArgumentException("예외!!!!!!");
    }
}
public class UseThrow {
    public static void main(String[] args) {
        Throw t = new Throw();
        t.createException();
    }
}

 

 

 

Try-with-resource

  • try-with-resource 는 Java 7 부터 생긴 기능이다.
  • Closeable 인터페이스를 구현한 객체를 사용할 때에는 객체 사용을 닫아주는 close() 사용을 위해 try / finally 문을 작성해야한다.
  • 이를 더 쉽고 정확하게 작성할 수 있게 되었다.
  • 또한 이를 사용하면 여러 예외가 발생했을때 하나가 아니라 다른 예외들도 suppressed 로 알려주기 때문에 디버그하기도 쉽다.

try / finally 를 이용한 자원 회수

static void copy(String src, String dst) throws IOException {
    InputStream in = new FileInputStream(src);
    try {
        OutputStream out = new FileOutputStream(dst);
        try {
            byte[] buf = new byte[BUFFER_SIZE];
            int n;
            while ((n = in.read(buf)) >= 0)
                out.write(buf, 0, n);
        } finally {
            out.close();
        }
    } finally {
        in.close();
    }
}

 

try-with-resource 를 이용한 자원회수

static void copy(String src, String dst) throws IOException {
    try (InputStream in = new FileInputStream(src);
         OutputStream out = new FileOutputStream(dst)) { // 이런식으로 사용하면 된다.
        byte[] buf = new byte[BUFFER_SIZE];
        int n;
        while ((n = in.read(buf)) >= 0)
            out.write(buf, 0, n);
    }
}

 

  • 내부적으로 try 블럭 안에 catch 구문을 만들고, 최상위 Throwable 을 구현하는 예외를 잡아서 모두 close() 를 실행하도록 구현한 후 throw를 통해 다시 예외를 던져준다.
  • try / catch 이후에 예외를 닫는 코드를 하나 더 자동으로 생성함으로써 예외가 발생안하더라도 자원을 닫게 해준다.
  • 따라서 뒤에 finally 구문도 작성 가능하다.

 

 

 

suppressed 되는 다른 오류들 (위 코드의 오류 아님)

Exception in thread "main" Day9.firstException
	at Day9.TryCatchProblem.run(TryCatchProblem.java:7)
	at Day9.TryWithResource.tryFinallyBad(TryWithResource.java:52)
	at Day9.TryWithResource.main(TryWithResource.java:9)
	Suppressed: Day9.secondException
		at Day9.TryCatchProblem.close(TryCatchProblem.java:13)
		at Day9.TryWithResource.tryFinallyBad(TryWithResource.java:51)
		... 1 more

 

 

 

 

catch 블럭의 printStackTrace()

  • catch 블럭에서 받은 예외를 이용하여 e.printStackTrace() 를 사용할 수 있다.
  • 해당 메서드는 예외가 발생하기까지의 이력을 로그로 출력한다.
  • 메서드가 실행되면 Thread 가 실행되고, Stack 영역에 stack frame이 쌓인다.
  • stack frame은 함수의 매개변수, 호출이 끝나고 돌아갈 반환 주소값, 지역변수 등 메서드를 실행하는데 필요한 정보들을 저장한다.
  • printStacktrace() 는 예외가 발생하기까지의 스택에 쌓인 정보들을 pop 하여 보여주는 것이다.

 

 

예외가 발생하면 프로그램이 종료?

자바 에플리케이션은 main 메서드에서 프로그램을 돌릴 것이고, 프로그램의 깊숙히에서 예외를 회피하여 던져서 main 메서드까지 왔다고 치자. main 메서드에서도 예외가 적절히 처리되지 않으면 JVM 에서 해당 스레드를 강제 종료시키게 된다.

 

우리는 보통 싱글스레드 프로그래밍을 하니 프로그램이 종료되는것처럼 보인다.

 

 

 

예외 처리의 비용

예외처리에는 많은 비용이 든다. 위에서 본 것 처럼 예외를 던지고 회피하는 과정에서 스택에 많은 메모리를 소비한다.

예외의 Stack Trace 는 Throwable.fillInStackTrace 를 통해서 만들어지는데, 이를 오버라이딩하는 방식으로 trace 가 가지는 비용문제를 해결할 수 있다.

meetup.toast.com/posts/47

 

Java Exception 생성 비용은 비싸다. : TOAST Meetup

Java Exception 생성 비용은 비싸다.

meetup.toast.com

 

 

 

 

 

 

 

사용자 정의 예외

  • 자바가 제공하는 Throwable 하위의 예외 이외에도, 개발자의 에플리케이션 로직에 맞는 예외를 정의해서 사용할 수 있다.
  • 사용자 정의 예외를 만들 때에는 다름과 같은 사항을 고려한다.
1. 자바가 제공하는 예외중에 사용할 수 있는 것이 있는가? 있다면 제공하는 것으로 쓰자. 굳이 새로 만들지 말자.
2. ~Exception 네이밍 룰을 지키자. 또한 다른 예외들과 구분할 수 있는 이름으로 짓자.
3. javadoc 을 통해 왜 만들었는지, 예외의 상황 등에 대해 문서화를 하자.

 

사용자 정의 예외는 클래스로 만들며, Exception / RuntimeException 을 상속하여 만들게 된다.

 

 

두 클래스중 상위 클래스인 Exception 클래스의 생성자는 여러 종류를 제공하는데, 보통은 다음과 같이 생성자를 정의하는것이 좋다.

public class CustomException extends RuntimeException {
    private ErrorCode code;
    
    public CustomException(String message, Throwable cause, ErrorCode code) {
        super(message, cause);
        this.code = code;
    }
}

Throwable cause 는 해당 예외 객체를 넣음으로써 예외 발생시 예외를 처리하는 곳을 위해 예외의 정보를 넣는다.

 

public class Main {
    public static void main(String[] args) {
        int x = 2;
        int y = 0;

        try {
            System.out.println(x / y);
        } catch (ArithmeticException e) {
            throw new CustomException("나누기오류", e, new ErrorCode("900"));
        }
    }
}

 

checked 예외 사용자정의

 

  • checked 예외를 사용자정의 예외로 만들고 싶다면, Exception 을 상속받도록 한다.
  • 조건에 맞게 throw new 로 사용자 정의 예외를 발생시킨다.
  • checked 예외는 예외에 대한 처리를 해야 하므로, 예외를 생성하는 곳에서 throws 를 통해 예외 처리를 넘기거나 그곳에서 처리한다.

 

 

Unchecked 예외 사용자정의

 

  • unchecked 예외를 사용자정의 예외로 만들고 싶다면, RuntimeException 을 상속받도록 한다.
  • 예외를 처리해야 하는 강제가 없기에 예외를 생성하는 곳에서 바로 처리하거나 throws 를 하지 않아도 된다.

 

 

 

 

마지막으로 catch 문을 비워두는것은 하지 말아햐 한다. 흔히들 예외를 흡수하는 예외 블랙홀이라고도 표현하는데, 예외를 흡수해버리면 이게 어디서 어떻게 일어났는지 디버그하기가 힘들어진다.

 

결론

예외의 복구처리를 하기 위한 전략이나 로직이 있다면 Checked 예외를 발생시켜 try / catch 로 예외를 처리하는 것이 좋지만,

 

하지만 이는 현실적으로 어려우므로 런타임에 해결이 불가능한 예외의 경우엔 쓸데없이 메소드 시그니처에 throws를 반복적으로 적거나, 의미 없는 try-catch를 하는 것을 방지하기 위해 Unchecked 예외로 래핑하고 예외에 대한 정보를 상세히 적도록 한다.

 

 

www.notion.so/3565a9689f714638af34125cbb8abbe8

sujl95.tistory.com/62

 

www.nextree.co.kr/p3239/

 

 

'WhiteShip Java Study : 자바 처음부터 멀리까지' 카테고리의 다른 글

enum  (0) 2021.02.08
멀티스레드 프로그래밍  (0) 2021.02.06
인터페이스  (0) 2021.02.04
패키지  (0) 2021.02.02
디스패치, 다이나믹 디스패치, 더블 디스패치  (2) 2021.02.01