[Java] Exception 예외처리(try-catch, throw, throws)

devdo·2022년 1월 7일
8

Java

목록 보기
28/59
post-thumbnail

예외(Exception)란?

일단 예외처리를 알기전에 오류와 예외에 대한 개념을 알아야 한다.

오류(Error) vs 예외(Exception)

오류(Error)는 시스템 비정상적인 상황이 생겼을 때 발생한다. 이는 시스템 레벨에서 발생하기 때문에 심각한 수준의 오류이다. 따라서 개발자가 미리 예측하여 처리할 수도 없기에 오류에 대한 처리는 신경쓰지 않는다.

ex. 수학에서는 0으로 나눌 수 없기 때문에 발생하는 오류

예외(Exception)는 오류와 반대로, 비정상적인 상황이 예측하여 처리하는 것이다. 개발자는 자신이 구현한 로직에서 예외를 예측하고 그에 따른 예외처리를 신경써야 한다.

예외클래스

위 그림은 예외클래스의 계층 구조이다. 모든 예외 클래스는 Throwable 클래스를 상속받고 있다.

Throwable을 상속받은 클래스는 Error와 Exception이 있는데, 개발로직은 Exception에 대한 것만 처리하면 된다.

Exception은 수많은 자식 클래스들이 있다. ComplieException(Checked Exception)RuntimeException(Unchecked Exception)을 구분할 필요가 있다.


Checked Exception vs Unchecked Exception

결론적으로, ❗둘다 Runtime Exception 이다. 하지만 쉽게 이해 하기 위한 개념적 설명에서 자주 런타임, 컴파일 시점으로 둘을 나눠서 설명한다.

그 이유로는 Checked Exception 은 강제적으로 try -catch 문구를 강제한다. 그래서 컴파일 시점에서 발생하는 예외라고 느껴지는 것 같다.

일반적으로 생각하는 둘의 차이 (둘 다 RuntimeException이다!)

1) Checked Exception

체크 예외는 예외를 잡아서 처리하거나(try ~ catch), 이게 안되면, 예외를 밖으로 던지는 throw 예외를 필수로 선언해야 한다.

그렇지 않으면 컴파일 오류가 발생한다. 이것 때문에 장점과 단점이 동시에 존재한다.

  • 장점 : 개발자가 실수로 예외를 누락하지 않도록 컴파일러를 통해 문제를 잡아주는 훌륭한 안전 장치이다.

  • 단점 : 하지만 실제로는 개발자가 모든 체크 예외를 반드시 잡거나 던지도록 처리해야 하기 때문에, 너무 번거로운 일이 된다. 크게 신경쓰고 싶지 않은 예외까지 모두 챙겨야 한다. 특히 의존관계가 생긴다!

  • 종류

* IOException : 파일, 소켓 등에 대한 읽기 또는 쓰기와 같이 잘못될 수 있는 입출력 작업에 대한 일반적인 예외
* SQLException : 데이터베이스 관련 예외를 처리하는 데 사용
* ClassNotFoundException : 일반적으로 Java의 리플렉션 메커니즘을 사용할 때 런타임에 클래스를 찾을 수 없을 때 발생
* FileNotFoundException

2) RuntimeException(Unchecked Exception)

런타임 예외란 프로그래밍 소스코드를 작성하여 컴파일과정에서는 문제를 발견하지 못하고 정상적으로 컴파일이 진행되었으나,

프로그램을 실행중에 발생하는 오류사항들을 대체로 예외라고 정의한다. Unchecked Exception은 이 RuntimeException를 상속한다.

Unchecked Exception은 복구 불가능한 예외이다.
=> 예외 발생시 런타임 중지! 즉, 프로그램이 종료가 된다는 뜻이다!
(Unchecked Exception)

ex) 범위를 넘어선 배열접근, 정수를 0으로 나누는 경우(ArithmeticException), Null포인터 오류등이 있다.

그럼 이러한 오류가 발생하면 개발자는 어떻게 처리를 해주어야 하느냐?

결론적으로 말하자면, 이 Exception에 대한 에러 메시지를 출력하는 것이다.


💡사실, Checked Exception을 만나더라도 더 구체적인 Unchecked Exception을 발생시켜 정확한 정보를 전달하고 로직의 흐름을 끊는 것이 좋다. ex. SQLException

Spring이나 JPA 등에서 SQLException을 처리하지 않는 이유도 적절한 RuntimeException으로 던져주고 있기 때문이다.

  • 종류
* NullPointerException : null 참조를 사용하여 객체의 메서드나 필드에 액세스하려고 하면 발생
* ArithmeticException : 산술 연산(: 0으로 나누기)으로 인해 오류가 발생
* ArrayIndexOutOfBoundsException : 잘못된 인덱스가 있는 배열 요소에 액세스하려고 하면 발생
* IllegalArgumentException : 잘못된 인수가 메소드에 전달되면 발생
* InvalidFormatException : 데이터 형식이 유효하지 않은 상황에 직면하고 이 문제를 나타내기 위해 예외를 발생
* NumberFormatException : 문자열을 숫자로 변환하려고 시도했지만 형식이 유효하지 않은 경우 발생
...

참고)
https://literatejava.com/exceptions/checked-exceptions-javas-biggest-mistake/


개발자의 Exception 처리

처리하는 방법에는 여러가지가 있겠는데 어떤 에러가 발생할지 알고 있다면 Excepiton클래스명을 직접 명시해주거나 아니면 Excepiton을 통해 한꺼번에 처리해주는 방법도 있다.

그리고
자바 예외처리방법 3가지를 잘 쓰면 된다.

1) try-catch (finally) - 다른 작업 흐름으로 유도, Checked Exception으로!
2) throws - 호출한 쪽(부모)에게 예외 처리 위임하도록
3) throw - 명확한 의미의 예외로 바로 처리 ==> 개발자들이 비즈니스 로직에서 처리하는 방식! 바로, UncheckedException으로!


1) try-catch

기본적으로 try - catch 구문은 이런 형식이다.

try {
     예외가 생길 가능성이 있는 코드 작성
} catch(예외발생 클래스명 e){
     예외처리 코드
}

마지막에 finally를 추가하거나 하는 방법이 있다.

실제 try - catch 에서 에러를 어떻게 잡을까? 코드를 보면서 확인하자면,

public class ExceptionTest {
    public static void main(String[] args) {
        int n1, n2;

        n1 = 12;
        n2 = 0;

        try {
            System.out.println(n1/n2);
        } catch (Exception e) {
            System.out.println("e.getMessage(): "+e.getMessage());
        }
    }
}

// 결과
// e.getMessage(): / by zero

그렇다면 try 부분에서 n1(12)을 n2(0)로 나눠주고 있는데 이부분에서 try~catch문을 사용하지 않는다면 어떻게 될까?

결과

이런식으로 ArithmeticException (런타임 예외) 이라고 뜨면서 에러가 발생하게 된다.

자바에서는 Exception 클래스를 상속받은 다양한 Exception 클래스을 가지고 있다. 여러 에러가 발생 할 가능성을 가지고 있다면, 각각의 에러에 대하여 예외처리 구문을 작성해줄수도 있다.

public class ExceptionTest {
    public static void main(String[] args) {
//        int n1, n2;

        int[] intArray = {0, 1, 2};

//        n1 = 12;
//        n2 = 0;

        try {
            // index 번호를 넘어선 에러
            System.out.println(intArray[3]);
        } catch (ArrayIndexOutOfBoundsException e) {
            System.out.println("ArrayIndexOutOfBoundsException: " + e.getMessage());
        } catch (ArithmeticException e) {
            // n1/n2 라면 발생했을 것
            System.out.println("ArithmeticException: " + e.getMessage());
        }
    }
}

// 결과
// ArrayIndexOutOfBoundsException: 3

intArray라는 변수는 길이가3인 배열이다. 그치만 try구문에서는 intArray[3]에 접근하고 있다.

이렇게 존재하지 않는 배열에 접근하게 되는 경우 ArrayIndexOutOfBoundsException (런타임 예외) 이 발생하여 catch문으로 이동하여 catch구문이 동작될 것이다.

그렇다면 예외가 발생했던 안했던 어떤 소스코드를 실행해주고싶다!

이럴 땐 어떻게 할까? 바로 그럴때 finally를 사용한다.

finally 사용

public class ExceptionTest {
    public static void main(String[] args) {
//        int n1, n2;

        int[] intArray = {0, 1, 2};

//        n1 = 12;
//        n2 = 0;

        try {
            // index 번호를 넘어선 에러
            System.out.println(intArray[3]);
        } catch (ArrayIndexOutOfBoundsException e) {
            System.out.println("ArrayIndexOutOfBoundsException: " + e.getMessage());
        } catch (ArithmeticException e) {
            // n1/n2 라면 발생했을 것
            System.out.println("ArithmeticException: " + e.getMessage());
        } finally {
            System.out.println("아 몰랑 일단 실행한다.");
        }
    }
}

결과

ArrayIndexOutOfBoundsException: 3
아 몰랑 일단 실행한다.

이런식으로 작성해주면 finally의 구문이 에러가 있든 없든 무조건 실행된다.



2) throws

throws는 자신을 호출하는 메소드에 예외처리의 책임을 떠넘기는 것이다.

  • 기본적으로 체크 예외 전략이다.
  • 언체크(런타임)예외는 체크예외와 다르게 throws 예외 선언을 하지 않아도 된다.
    예외를 잡지 않아도 자연스럽게 상위로 넘어가기 때문이다.

코드를 보며 정리해보자면,

public class ThrowTest {

    public static void main(String[] args) {

        int n1, n2;

        n1=12;
        n2=0;

        try {
            throwTest(n1, n2);
        } catch (ArithmeticException e) {
            // n1/n2 라면 발생했을 것
            System.out.println("ArithmeticException: " + e.getMessage());
        }
    }

    public static void throwTest(int a, int b) throws ArithmeticException{
        System.out.println("throw a/b: "+ a/b);
    }
}

아래에 throwTest 메소드 뒤에 throws ArithmeticException 부분이 보일 것이다. ArithmeticException 예외가 발생하면

<이 메소드를 호출한곳(main메서드)에서 예외처리를 넘겨주라는 뜻이다.

그래서 예외처리를 넘겨받은 main메서드는 어떻게 처리할까?

반드시 try-catch구문으로 메서드호출부분을 감싸줘야 한다.
그렇지 않으면 예외처리를 하는 구문이 없으면 오류처리를 아무도 안하게 되는 것이다! throws를 쓰면 그 호출한 메서드에서 try-catch 구문을 해줘야되는 것을 잊으면 안된다!


3) throw

throw는 throws랑 확연히 다르다. 제일 헷갈렸던 부분이었는데,
throw는 개발자가 직접 예외를 발생시키고싶을 때 쓰는 것이다.

주로 RuntimeException(UnCheckedException) 처리를 위해 쓰는 방식이다.


//💡 throw는 체크예외에서도 당연히 쓸수 있다!
throw new IOException("IO Exception occurred");

Spring 프레임워크를 쓸 때 ExceptionHandler 와 찰떡궁합인 방식이다.

이것도 코드를 보면서 이해해보자.

public class ThrowTest {

    public static void main(String[] args) {

        int n1, n2;

        n1=12;
        n2=0;

        try {
            throwTest(n1, n2);
        } catch (ArithmeticException e) {
            // n1/n2 라면 발생했을 것
            System.out.println("ArithmeticException: " + e.getMessage());
        }
    }

    public static void throwTest(int a, int b) throws ArithmeticException{
        throw new ArithmeticException();
    }
}

결과

ArithmeticException: null

위에 throws 구문 코드에서 예외 발생 메서드 throwTest 구문 안에 throw new ArithmeticException();을 넣어주는 것을 확인할 수 있다.

이런식으로 사용자가 직접 예외를 발생시켜주고싶은 부분에 throw new XXXExcepiton();을 통하여 예외를 발생시켜주고 throws를 통하여 예외처리를 던져주는 것이다.

그러면 main에서 throwExample 메소드를 호출할때 예외처리를 해줄 것이다.
하지만 결과 를 보면 뭔가 이상하다.

바로 main문에서 예외처리를 해준 메시지 처리가 null로 나오기 때문이다.

throw는 Exception을 던질 때 예외 내용도 던져주지 않는다.

그래서 개발자가 직접 Exception을 따로 커스터마이징해서 만들고 그 안에 메시지를 넣어서 던져주는 방식이다.

throw는 개발자가 직접 예외를 발생시키고싶을 때 쓰는 것이지만 추가적으로 Exception 재정의해서 사용하기도 한다. (CustomException)


예외 처리 전략

추가로 Exception 처리에 많은 전략들이 있다. 구글에 "자바 예외 전략", "Java Exception Strategy"를 검색해서 많은 자료들을 볼 수 있을 것이다.

1) CheckedException 처리전략

  • ✨ 기본원칙은 UnckecedException(런타임) 예외를 사용하자.

  • CheckedException는 비즈니스 로직상 의도적으로 던지는 예외로만(try ~ catch & throws) 사용하자.

  • throws Exception 사용 x => 구체적인 체크 예외로 밖으로 던지는 방식(try ~ catch & throws)으로 하자.
    SQLException,ConnectionException 같은 시스템 예외는 Controller, Service 클래스에서는 대부분 복구가 불가능하고 처리할 수 없는 체크 예외이다. 따라서 다음과 같이 처리해주어야 한다.

void method() thorws SQLException, ConnectionException {...}

2) UnCheckedException 처리전략

  • 런타임 예외를 사용하면 중간에 기술이 변경되어도 해당 예외를 사용하지 않는 controller, service에서는 코드를 변경하지 않아도 된다.
  • 그래서 스프링에서도 기본적으로 런타임 예외를 제공한다.
throw new ArithmeticException("Division by zero");

✳️ 결론

1) CheckedException => try ~ catch 문, throws(의존관계) 로 처리!
2) UnCheckedException(RuntimeException) => 기본적으로 복구 불가능한 예외(발생시 런타임 중지)로, CheckedExceptoin이어도 더 구체적인 UnCheckedException으로 발생시켜! throw로 exception을 던지고, ExceptionHandler로 처리!


exception 스택 트레이스 e.printStackTrace();

기존 예외 e는 꼭 같이 넣어주자!

=> 기존 예외까지 포함해서 e(exception)의 printStackTrace에서 예외들을 다 확인할 수 있다!

디버깅(Debugging)용으로 쓰자!

public void call() {
	try {
    	runSQL();
    } catch (SQLException e) {
    	e.printStackTrace();            // e.printStackTrace()을 확인이 정말 편하다!
    	throw new RuntimeException(e);	// => RuntimeException()(x) 기존 예외(e) 꼭 넣어주자!
    }
}


참고

profile
배운 것을 기록합니다.

0개의 댓글