[Java] Error & Exception

Letmegooutside·2022년 1월 24일
0

Error (오류)

컴퓨터 하드웨어의 오동작 또는 고장으로 인해 응용프로그램에 이상이 생겼거나 JVM 실행에 문제가 생겼을 경우 발생하는 것

시스템 레벨에서 발생하므로 개발자가 미리 예측하여 처리할 수 없기 때문에, 애플리케이션에서 오류에 대한 처리를 신경 쓰지 않아도 된다.

종류

VirtualMachineError, OutOfMemoryError, StackOverflowError 등

Exception(예외)

사용자의 잘못된 조작 또는 개발자의 잘못된 코딩으로 인해 발생하는 프로그램 오류

예외가 발생하면 프로그램이 종료가 된다는 것은 에러와 동일하지만 예외는 예외처리를 통해 프로그램을 종료되지 않고 정상적으로 작동되게 만들어줄 수 있다.
자바에서는 try catch문을 통해 예외처리를 해줄 수 있다.

즉 예외는 개발자가 구현한 로직에서 발생하므로 발생할 상황을 미리 예측하여 처리할 수 있다.

예외 처리

Exception 예외가 발생할 것을 대비하여 미리 예측해 이를 소스상에서 제어하고 처리하도록 만드는 것

이렇게 예외 처리를 하게되면 갑작스러운 Exception이 발생하여도 시스템 및 프로그램이 불능상태가 되지 않고 정상 실행 상태를 유지할 수 있다.
예외 처리의 유무에 따라 시스템 안정성이 확보되고 다양하고 급작스럽게 발생하는 에러에 대해 예외 정보를 로그에 남기면서 해당 예외 발생 케이스에 대한 대응을 할 수 있기 때문에 예외 처리는 중요하다.

예외(Exception)의 종류

Exception에는 크게 2가지 종류가 있다.

  • 컴파일 시점에 발생하는 예외를 Exception(일반예외) 라고 하고
  • 프로그램 실행 시에 발생하는 예외를 RuntimeException(실행예외) 라고 한다.

이 2가지 종류의 Exception을 처리하기 위해 자바에서는 java.lang.Exception이라는 최상위 부모 클래스를 제공한다.
따라서 모든 Exception들의 조상은 결국 java.lang.Exception이다.

보라색 java.lang.Exception은 자바에서 예외처리를 할 수 있게 제공해주는 최상위 부모 클래스이며
하늘색 Exception들은 컴파일 시 발생하는 일반 예외이고, 초록색 RuntimeException은 프로그램 실행시 발생하는 실행예외이다.

예외처리 코드 및 실행 순서(try-catch-finally)

try {

    // 예외를 처리하길 원하는 실행 코드;

} catch (e1) {

    // e1 예외가 발생할 경우에 실행될 코드;

} catch (e2) {

    // e2 예외가 발생할 경우에 실행될 코드;

}

...

finally {

    // 예외 발생 여부와 상관없이 무조건 실행될 코드;

}

예외 처리 코드에는 크게 3가지 블록이 존재한다.

  • try 블록 : 실제 코드가 들어가는 곳으로써 예외 Exeption이 발생할 가능성이 있는 코드

  • catch 블록 : try 블록에서 Exeption이 발생하면 코드 실행 순서가 catch 쪽으로 오게됨. 즉 예외에 대한 후 처리 코드

  • finally 블록 : try 블록에서의 Exeption과 발생 유무와 상관 없이 무조건 수행되는 코드

catch 블록과 finally 블록은 선택적인 옵션으로 반드시 사용할 필요는 없다

실행 순서

  • Exeption 발생
    try 블록 수행 → catch 블록 수행 → finally 블록 수행 (생략가능)

  • Exeption 미발생
    try 블록 수행 → finally 블록 수행 (생략가능)

예외 처리 코드는 예외 종류(일반예외, 실행예외)에 따라 예외 처리 코드 (Try-Catch-Finally)의 강제 여부가 갈린다.
다시 말해, 컴파일시 예외 검사 대상이 되는 일반예외는 예외 처리 코드에 반드시 감싸서 코드를 짜야한다.
반면 프로그램 실행이후 발생하는 실행예외는 따로 컴파일러가 예외 처리 코드를 강제하지 않기 때문에 온전히 개발자의 경험에 의해서 예외 처리 코드를 사용해야한다.

즉 컴파일러가 실행예외를 컴파일 시점에 판단하여 검사할 수 없기 때문에 개발자가 실행예외가 발생할 가능성이 있는 코드에 예외 처리 코드를 적용해줘야 한다.
따라서 개발자의 역량에 따라 실행 예외를 잘 막기도 하고 못 막기도 하며 이는 시스템의 안정성에도 영향을 주게 된다.

중요한 것은 일반예외와 달리 실행예외는 컴파일러가 따로 체크해주지 않으니 알아서 예외 처리 코드를 사용해야 한다는 것이다.

예외 종류에 따른 예외처리 코드

실행예외 : NullPointerException

NullPointerException : 실제 참조할 대상이 null인데 참조하려고 할때 발생하는 예외

public class RuntimeExceptionExample {
    public static void main(String[] args) {
        String[] array = null;
        System.out.println(array[0]);
        System.out.println("프로그램이 계속 실행될 것인가");
    }
}

위 코드는 null인 객체(array) 참조를 시도하면서 발생하는 NullPointerException이다.
위 코드가 수행이 가능한 이유는 컴파일 할 때 컴파일러가 따로 예외 처리 코드를 사용하라고 강제하지 않았고 개발자가 null 참조를 수행한다는 것을 놓쳤기 때문이다.

결국 실행 예외가 발생하였고 이를 방어할 예외 처리 코드를 적용하지 않았기 때문에 프로그램이 그전에 죽어버렸으므로 아래의 println은 찍히지 않았다.

그럼 이제 위 코드를 예외 처리 코드를 사용해서 실행 예외를 처리해보자.

public class RuntimeExceptionExample {
    public static void main(String[] args) {
        try {
            String[] array = null;
            System.out.println(array[0]);
        } catch (NullPointerException e) {
            String message = e.getMessage();
            System.out.println("예외 발생 "+message);
        } finally {
            System.out.println("예외 상관없이 수행되는 코드");
        }
        System.out.println("프로그램이 계속 실행될 것인가");
    }
}


예외 처리 코드를 사용하니, 프로그램이 죽지 않고 수행되었다.
여기서 주목할 것은 catch (예외유형 e) 형태이다.
발생한 실행 예외 유형이 NulllPointerException 이기 때문에 catch블록에서 잡을 수 있었고 예외 관련된 객체 e를 반환 받았다는 것으로 이해하면 된다. (매개변수로 전달한 예외 유형만 처리한다. 만약 위 코드에서 NulllPointerException가 아닌 다른 유형을 명시했다면 프로그램이 예외를 처리하지 못해 finally 블록의 코드만 수행되고 프로그램이 죽는다.)

e객체의 getMessage 메서드를 통해 개발자가 이해하기 쉬운 예외 메시지를 얻어 처리함을 확인할 수 있다.

일반예외 : ClassNotFoundException

ClassNotFoundException : 해당 클래스가 존재하지 않으면 발생하는 일반 예외

public class ExceptionExample {
    public static void main(String[] args) {
       Class object = Class.forName("java.lang.example");
       System.out.println("프로그램이 죽지 않고 수행될 것인가");
    }
}


(forName 메서드 : 파라미터로 클래스 정보를 넘겨주고, 해당 클래스가 존재하면 객체를 리턴해주는 메서드)

위 코드에서 볼 수 있듯이 컴파일러는 자동으로 일반 예외를 감지하고 예외 처리 코드를 사용하도록 강제한다.
만약 예외 처리 코드를 구현하지 않으면 프로그램 실행 조차 할 수 없게 된다.

위 코드에 예외 처리 코드를 적용해보자.

public class ExceptionExample {
    public static void main(String[] args) {
        try {
            Class object = Class.forName("java.lang.example");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
       System.out.println("프로그램이 죽지 않고 수행될 것인가");
    }
}


catch 블록에 ClassNotFoundException 예외 유형을 명시하여 해당 일반 예외에 대한 정보를 e객체에 담았다.
추가로 e.printStackTrace()메서드를 통해 어떤 과정을 거치다가 에러가 발생했는지를 출력한다.
여기에 찍힌 정보를 보면 ExceptionExample.java의 8번째 줄에서 에러가 발생했음을 알 수 있다.
또한 java.lang.ClassNotFoundException: java.lang.example을 상단에 찍어줌으로써 example이라는 클래스를 찾을 수 없어서 발생한 일반 예외임을 알 수 있다.

예외의 throws

메소드를 선언할 때 매개 변수 소괄호 뒤에 throws라는 예약어를 적어 준 뒤 예외를 선언하면, 해당 메소드에서 선언한 예외가 발생했을 때 호출한 메소드로 예외가 전달된다.
만약 메소드에서 두 가지 이상의 예외를 던질 수 있다면, implements처럼 콤마로 구분하여 예외 클래스 이름을 적어주면 된다.

예외가 발생하는 경우 try-catch문을 통해 처리하지 않고 throws를 이용해 떠넘기면 현재 메서드를 호출한 곳으로 던져지게 된다.
만약 모든 메서드에서 throws를 이용해 예외를 떠넘기다 보면 최초 호출 지점인 main()메서드 내부로 예외가 던져지게 되며 main()메서드에서 마저 예외를 떠넘기게 된다면 JVM의 예외처리기까지 도달하여 프로그램은 그대로 종료된다.
이렇게 되면 사실상 예외를 처리하지 않은것이나 다름 없으므로 매우 무의미한 행동이라 할 수 있으므로 의도적인 경우가 아니라면 throws는 많은 생각과 필요에 의해 사용되어야 한다.

예외를 떠넘기는 이유

  • 메서드 선언부에 선언된 throws문을 통해 해당 API를 사용했을 때 어떤 예외가 발생할 수 있는지를 예측할 수 있다.
  • 현재 메서드 내에서 예외를 처리할 필요가 없다고 판단했을 경우에 사용한다.
    예외 처리에는 생각보다 많은 코드가 필요하게 되며 이는 코드를 읽기 어렵게 만들고 불필요한 코드가 많이 추가되게 만들어 버그를 만들기 쉽다.
    또한 API를 만드는데에 있어서 내가 처리하기 보다는 내가 만든 API를 사용하는 다른 개발자에게 원하는 처리를 하도록 기회를 줄 수 있다.
// 내가 메소드 만들었는데 여기서 NullPointerException 발생할 수 있으니까 던질게 호출하는 너네가 처리 해 
public void methodB() throws NullPointerException {
    // 내가 개발한 코드
}

// 다른 개발자
public void methodC() throws NullPointerException {
    try {
        methodB();
    } catch (NullPointerException e) {
        // 원하는대로 처리
    }
}

예를 들어 다음과 같이 내가 methodB()라는 메서드를 개발중에 있고 현재 이 메서드에서는 NullPointerException이 발생할 수 있다는것을 인지했다.
이 때 내가 직접 나중을 대비해 try-catch문을 사용하여 내가 원하는대로 처리해줄 수 있지만 내가 만든 코드를 사용하는 다른팀원들이 해당 예외가 발생했을때의 처리를 각각 원하는대로 구현하도록 기회를 줌과 동시에 메서드 선언부를 통해 NullPointerException가 발생할 수 있다는 것을 알려주는 것이다.

자바 예외 처리 전략

try {
    //예외발생 가능한코드
} catch (SomeException e) {
    // 여기 아무 코드없음
}

이렇게 catch문장을 처리해주는건 피해야한다.
(여기서 SomeException 이라는 것은 그냥 어떤 예외를 잡는다는 것을 의미할뿐 실제 존재한다는게 아니다)
catch 문 내에 아무런 작업 없이 공백을 놔두면 예외 분석이 어려워지므로 꼭 로그처리와 같은 예외 처리를 해줘야만한다




Reference
https://sujl95.tistory.com/62
https://limkydev.tistory.com/198
https://dololak.tistory.com/87

0개의 댓글