본문 바로가기

자바

📖 Exception에 대하여

반응형

2021-02-24 글

오늘 수업 시간에 자바의 Exception에 대해 배웠다.
수업을 듣고 들은 내용을 자바의 정석의 예외 처리를 함께 보면서 정리 ✍️

프로그램 오류 종류

  • 컴파일 에러 - 컴파일 시에 발생하는 에러
  • 런타임 에러 - 실행 시에 발생하는 에러
  • 논리적 에러 - 실행은 되지만, 의도와 다르게 동작하는 것

프로그램에서 실행 도중 발생할 수 있는 모든 경우의 수를 고려해 이에 대한 적절한 대비가 필요하다!

Error와 Exception의 차이?

Error

애플리케이션이 정상적으로 동작하는데 심각한 문제가 있는 경우 사용한다.
ex) 메모리 부족이나 스택오버플로우 등

개발자가 Error를 사용하는 일은 거의 없다고 한다.

Exception

비즈니스 로직 상에서 에러가 발생하는 경우 사용한다.
발생하더라도 수습이 가능하여 프로그래머가 이에 대한 적절한 처리를 할 수 있다.

예외 클래스 계층 구조

모두 Throwable을 상속하고 있으며, 여기서부터 Error와 Exception이 발생한다.
여기서 모든 예외의 조상은 Exception 클래스이다.

또 예외 클래스들은 두 개의 그룹으로 나뉘어 질 수 있다.

  • RuntimeException 클래스와 그 자손 클래스들
  • Exception 클래스와 그 자손 클래스들

이 글에서는 RuntimeException 클래스와 그 자손 클래스들을 Unchecked Exception이라 하고,
Exception 클래스와 그 자손 클래스들을 Checked Exception이라고 하겠다.

Checked Exception

import lotto.view.ErrorView;

public class CustomException extends Exception {
    public CustomException() {
        ErrorView.printErrorMessage();
    }
}

...

public class Application {
    public static void main(String[] args) throws CustomException {
        LottoNumber lottoNumber = LottoNumber.from(1);
    }
}

Exception을 상속받아 사용하는 경우 컴파일 시점에 Exception을 확인할 수 있다.
만약 컴파일 시점에 Exception에 대한 처리(try/catch)를 하지 않을 경우 컴파일 에러가 발생한다.
Exception이 발생하는 메소드에서 throws 예약어를 활용해
Exception을 호출 메소드에 전달해야 한다.

상위 메서드로 throw를 던지는 행위는 상위 메서드들의 책임이 그만큼 증가하기 때문에,
그리 좋은 방법 같지는 않다.

Unchecked Exception

public class IllegalLottoNumberException extends IllegalArgumentException {
    public IllegalLottoNumberException() {
        ErrorView.printIllegalLottoNumberMessage();
    }
}

...

public class LottoNumber implements Comparable<LottoNumber> {
    public static final int MIN_LOTTO_NUMBER = 1;
    public static final int MAX_LOTTO_NUMBER = 45;
    private static final Pattern NUMBER_PATTERN = Pattern.compile("^[0-9]*$");

    private final int value;

    public LottoNumber(String number) {
        validateLottoNumber(number);
        this.value = Integer.parseInt(number);
    }

    private void validateLottoNumber(String number) {
        if (isBlank(number) || isInvalidNumberFormat(number) || isInvalidLottoNumberRange(number)) {
            throw new IllegalLottoNumberException();
        }
    }

Runtime Time Exception 이라고 한다.
컴파일 시점에 Exception이 발생할 것인지의 여부를 판단할 수 없다.
Exception이 발생하는 메소드에서 throws 예약어를 활용해 Exception을 처리할 필요가 없지만, 처리해도 무방하다.

Checked Exception VS Unchecked Exception

구분 Checked Exception Unchecked Exception
확인 시점 컴파일타임 런타임
처리 반드시 예외 처리해야 한다. throws를 통해 그냥 던져도, 처리해도 상관 없다.
대표 예외 IOException, SQLException IllegalArgumentException, NullPointEception

너무 치명적이면 Checked 쓰고,
굳이 경고가 필요없고, 서비스 코드를 만드는 입장이라면 최상단에서
에러를 핸들링 할 수 있으니 Unchecked를 쓰는게 좋다.

참고로 코틀린에서는 Checked Exception은 코드만 늘어날 뿐이라 생각(?)해서
여러가지 이유로... Checked Exception이 없다고 한다.
또 프로그래머 입장에서 메인과 같은 중앙에서 핸들링하는 녀석을 만들어 관리하는데,
굳이 우리가 Checked Exception을 제공할 필요가 있냐는 말이다.

수업시간에 다양한 말들로 둘을 구분지어 보았는데,
*Checked Exception는 *

  • 내부 구현이 어떻게 되는지 알려주고 싶어!
  • 내가 집 수리할건데 손 다칠수도 있으니까 구급차 미리 불러!
  • 너 진짜 조심해야해!

*Unchecked Exception는 *

  • 내부에서 예외에 대한 방어가 있는데 클라이언트한테 공개는 안할거야, 대신 값을 잘 넣어주면 좋겠어.
  • 내가 집 수리할건데 아냐 다치지 않고 잘 할 수 있어!
  • 클라이언트가 입력할 값을 잘 알고 있겠지 ~

이런 느낌으로 이야기가 나왔다.
Checked인지 Unchecked인지는 클라이언트에게 얼마나 책임을 떠넘기느냐에 대한 관점으로 봐도 좋을 것 같다.

예외 처리 - try-catch문

try {
    // 예외 발생 가능성 있는 문장들
} catch (Exception1 e1){
    // Exception1이 발생했을 경우, 처리하는 문장
} catch(Exception2 e2) {
    // Exception2가 발생했을 경우, 처리하는 문장
}

이렇게 하나의 try 블럭 다음에는 여러 종류의 예외를 처리할 수 있도록
하나 이상의 catch 블럭이 올 수 있다.

흐름

try 블럭에서 예외가 발생하면, 예외가 발생한 위치 이후에 있는
try 블럭의 문장들은 수행되지 않는다.
때문에 try 블럭에 포함시킬 코드의 범위를 잘 선택해야 한다.

예외가 발생하면 발생한 예외에 해당하는 클래스의 인스턴스가 만들어진다.
try블럭에서 예외가 발생되고, 첫 catch 문부터 차례대로 catch 블럭의
괄호 내 선언된 참조변수의 종류와 생성된 예외 클래스의 인스턴스에 instanceof연산자를 이용해 검사한다.
검사한 결과가 true인 블럭을 만날 때 까지 계속 검사한다.

try에서 발생한 예외의 종류와 일치하는 단 하나의 catch 블럭만 수행된다.

public class LottoNumber {
    private final int number;

    private LottoNumber(int number) {
        this.number = number;
    }

    public static LottoNumber from(final int number) throws CustomException {
        throw new CustomException();
    }
}

다음과 같이 LottoNumber 객체를 from()으로 생성했을 때
Exception을 상속받고 있는 CustomException을 발생시킨다고 해보자.

인텔리제이에서도 알 수 있듯이 CustomException의 상위 클래스인
Exception을 미리 catch해주었기 때문에 하위 catch에서
CustomException를 잡는 것은 소용이 없다.

모든 예외 클래스는 Exception 클래스의 자손이므로,
Exception 클래스 타입의 참조변수를 선언하면 해당 블럭에서 어떤 예외던지 처리된다.

또 위와 같은 코드를 실행시켜보면 아래와 같은 출력 결과가 나온다.

멀티 catch 블럭
JDK1.7부터 catch 블럭을 |을 이용해 하나의 catch 쁠럭으로 합칠 수 있게 되었다.

하지만 이렇게 부모 자식 관계가 있다면 컴파일 에러가 발생한다.

중첩된 try-catch

하나의 메서드 안에 여러개의 try-catch를 사용할 수 있으며,
중첩으로도 사용이 가능하다.
하지만 catch블럭 괄호 내 참조 변수는 catch 블럭 내에서 유효한데,
위의 예제에서는 e의 참조변수의 영역이 서로 겹치기 때문에 컴파일 에러가 난다.

Custom Exception

기존에 정의된 예외 클래스 외에 필요에 따라 사용자 정의 클래스를 정의하여 사용할 수 있다.

class MyException extends Exception {
    MyException(String msg) {
        super(msg); 
    }
}

생성시 string을 인자로 받아서 메시지로 저장할 수 있다.

반응형