Programming/JAVA

예외란?

잇나우 2021. 2. 13. 22:20
반응형

예외란?

자바에서는 오류(Error)예왹(Exception)두가지의 개념이 있다.


출처 : https://5balloons.info/introduction-to-exception-handling/

에러(Error)

시스템의 비정상적인 상황이 생겼을 때 발생한다. 시스템 레벨에서 발생하기 때문에 개발자가 미리 예측하여 처리할 수가 없다. 에러는 크게 컴파일 에러런타임 에러로 구분할 수 있다.

컴파일 에러
컴파일 과정에서 일어나는 에러로 기본적으로 자바 컴파일러가 문법 검사를 통해서 오류를 잡아준다.

런타임 에러
실행과정에서 일어나는 에러로 컴파일이 문제없이 되더라도 실행과정(Runtime)에서 오류가 발생할 수 있다. 이러한 런타임 에러를 방지하기 위해 프로그램 실행 도중 일어날 수 있는 모든 경우의 수를 고려하여 대비해야 한다. 자바에서는 런타임 에러를 예외(Exception)와 에러(Error) 두 가지로 구분하여 대응하고 있다.

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

예외
프로그램내에서 복구할 수 있는 장애로, 예외가 발생하더라도 프로그램이 비정상적으로 종료되지 않도록 코드를 작성하여 핸들링 해줄 수 있다. Exception이 발생하면 런타임 시스템은 Call stack내에서 이를 핸들링할 수 있는 메서드를 찾기 시작한다. 만약 Call stack내에서 해당 Exception을 핸들링할 수 있는 메서드를 찾지 못한다면 런타임 시스템은 종료된다.

콜 스택

런타임 시스템은 예외를 제어할 수 있는 코드 블럭을 가지고 있는 메서드를 찾기 위해 Call stack에서 검색한다. 이 코드 블럭을 Exception handler라고 부른다. 검색은 오류가 발생한 메서드로부터 시작하여 메서드가 호출된 메서드로 역순으로 콜 스택을 통해서 진행된다. 적절한 핸들러가 발견되면 예외를 핸들러로 전달한다.
만약에 선타임 시스템이 적절한 Exception handler를 찾지 못하고 콜 스택의 모든 메서드를 검색하면, 런타임 시스템은 종료됩니다. 즉 프로그램은 종료됩니다. 이는 비정상 종료라고 볼 수 있습니다.

예외를 사용하여 에러를 처리하면 기존 처리 기술에 비해 몇 가지 장점이 있다. (return을 할 때, 에러라고 응답을 하거나, C언어에서는 goto문을 사용하는 등의 방법)

  1. 에러를 처리하는 코드와 일반 코드가 분리될 수 있다.
  2. 콜 스택을 따라 에러 전파가 가능해 ㅣㄹ질적으로 처리가 될 수 있는 지점에서 처리를 해줄 수 있다.
  3. 오류를 그룹화할 수 있고 분류할 수 있다.

Checked Exception, Unchecked(Runtime) Exception

Runtime Exception은 CheckedException과 UnCheckedException을 구분하는 기준이다.
Exception의 자식 클래스 중 RuntimeException을 제외한 모든 클래스는 CheckedException이며, RuntimeException과 그의 자식 클래스들을 UnCheckedException이라고 부른다.

구분 Checked Exception Unchecked Exception
처리여부 반드시 예외를 처리해야 함 명시적인 처리를 강제하지 않음
확인시점 컴파일 시점 실행 시점
대표적인 예외 Exception을 상속한 하위 클래스 중 Runtime Exception을 제외한 모든 예외
- IOException
- SQLException
- ...
Exception을 상속한 하위 클래스 중 Runtime Exception 하위 예외
- NullPointerException
- IllegalArgumentException
- IndexOutOfBoundException
- ...

Checked Exception은 발생할 가능성이 있는 메서드라면 반드시 try/catch로 감싸거나 throw로 던져서 처리해야 한다. 즉 꼭 처리해야 하는 Exception이다.
UnChecked Exception은 명시적인 예외처리를 하지 않아도 된다. 이 예외는 피할 수 있지만 개발자가 부주의해서 발생하는 경우가 대부분이고, 미리 예측하지 못했던 상황에서 발생하는 예외가 아니기 때문에 굳이 로직으로 처리를 할 필요가 없도록 만들어져 있다.

throws을 명시 해야하는 것은 Checked Exception이다. throws의 목적은 메서드 수행 중 발생할 수 있는 예외이니 호출하는 측에서 이를 대비하라는 것이다.
Checked Exception은 컴파일 시 발생되는 예외이며,
UnChecked Exception은 컴파일 통과 후 런타임 시 발생할 수 있는 예외이다.

예외처리 방법

예외를 처리하는 일반적인 3가지 방법 (토비의 스프링 3.1 Vol.1 4장 예외)

  1. 예외가 발생하면 다른 작업 흐름으로 유도하는 예외 복구
  2. 처리하지 않고 호출한 쪽으로 예외를 던져버리는 예외처리 회피
  3. 호출한 쪽으로 던질 때 명확한 의미를 전달하기 위해 다른 예외로 전환하여 던지는 예외 전환

예외 복구

public void exceptionExam() {
    int num = 10;
    while(num-- > 0) {
        try {
            // 예외가 발생할 가능성이 있는 시도
            // 작업 성공 시 리턴
            return;
        } catch(Exception e) {
            // 로그 출력, 정해진 시간만큼 대기 등등
        } finally {
            // 리소스 반납 및 정리 작업
        }
    }
    throw new FailedException(); 직접 예외 발생
}

예외복구의 핵심은 예외가 발생하여도 애플리케이션은 정상 흐름으로 진행된다는 것이다. 위 예제는 예외가 발생하면 그 예외를 잡아서 일정 시간만큼 대기하고 다시 재시도를 반복한다. 그리고 최대 재시도 횟수를 넘기면 예외를 발생시킨다.
재시도를 통해 정상적인 흐름을 타게 한다거나, 예외가 발생하면 이를 미리 예측하여 다른 흐름으로 유도시키도록 구현하면 예외가 발생하여도 정상적으로 작업을 종료할 수 있다.

예외처리 회피

public void test() throws SQLException {
    // do someting..
}

예외가 발생하면 throws를 통해 호출한 쪽으로 예외를 던지고 그 처리를 회피하는 것이다. 하지만 무책임하게 예외를 던지는 것은 위험하다.
호출한 쪽에서 다시 예외를 받아 처리하도록 하거나, 해당 메소드에서 이 예외를 던지는 것이 최선의 방법이라는 확신이 있을때만 사용해야 한다.

예외 전환

try {
    // do something
} catch (SQLException e) {
    throw FailedException();
}

위의 예제 코드와 같이 예외를 잡아서(catch) 다른 예외를 던지는 것이다.
호출한 쪽에서 예외를 받아서 처리할 때 조금 더 명확히 인지할 수 있도록 돕기 위한 방법이다.
예를 들어 Checked Exception 중 복구가 불가능한 예외가 잡혓다면, 이를 Unchecked Exception으로 전환하여 다른 계층에서 일일이 예외를 선언할 필요가 없도록 할수도 있다.

try, catch, thorw, throws, finally

try, catch, finally블록으로 예외가 발생한 메서드 내에서 예외 처리를 수행할 수 있다.

try {
    // 예외 발생 가능성이 있는 코드
} catch(예외타입1 매개변수명) {
    // 예외타입1의 예외가 발생할 경우 처리 코드
} catch(예외타입2 매개변수명) {
    // 예외타입2의 예외가 발생할 경우 처리 코드
} finally {
    // 항상 처리할 필요가 있는 코드
}

try

try블록은 예외가 발생할 가능성이 있는 범위를 지적하는 블록이다. 만약 try안의 코드가 실행중 예외가 발생한다면, 예외는 연관된 예외 처리코드에 의해 처리된다. try 블럭과 연관된 예외 처리 코드를 선언하는 방법은 catch 블럭에 선언하는 것이다. try 블록은 최소 하나 이상의 catch블록이 있어야 하고 catch블록은 try 블록 다음에 위치해야 한다.

catch

catch블록은 매개변수의 예외 객체가 발생했을 때 참조하는 변수명으로 반드시 java.lang.Throwable 클래스의 하위 클래스 타입으로 선언되어야 한다. 지정된 타입의 예외가 발생하면 try 블록의 나머지 코드들은 실행되지 않고 JVM은 예외 객체를 발생시키며 catch블록에서 동일한 예외 타입의 블록을 수행한다.
catch문은 여러개의 블록으로 구성할 수 있다. catch 블럭 작성시 부모 클래스가 자식 클래스보다 먼저 catch블록을 사용하게 되면 컴파일 에러가 발생한다. 예외가 부모 클래스 catch블록에 걸리게 되고 자식 클래스 catch블록에는 걸리지 않게된다. 좁은범위의 Exception, 자식 클래스 Exception을 위에 두어야 한다. catch 블럭은 하나 이상의 예외 타입을 처리할 수 있으며, catch의 매개변수는 암묵적으로 final이기 때문에 catch블록 내에서 값을 할당할 수 없다.

  • Multicatch Blcok
    Java 7 이후의 버전에서는 여러개의 catch 블럭 중 예외 처리가 동일하다면 catch 블럭을 묶어 처리할 수 있다.
      try {
          // code
      } catch(IllegalStateException | IllegalArugmentException e) {
          // catch
      }
    multicatch로 묶는 Exception이 상속관계라면 불가능하다.

finally

finally rty 블럭을 벗어날 때 항상 실행된다. 예기치 못한 예외가 발생하더라도 finally 블럭의 코드는 동작한다. finally는 예외 처리 이상의 용도로 유용하다. 정리하는 코드를 finally 블럭에 담아두는 것은 좋은 습관이다. 데이터베이스나 파일을 사용 후 닫는 기능과 같이 항상 수행해야할 필요가 있는 경우 사용된다. finally 블록에서 return을 하는 경우는 안티 패턴에 해당한다. try 블록 안에서 발생한 예외는 무시되고 finally를 거쳐 정상 종료되기 떄문에 예외를 알 수가 없다.

try또는 catch 블록이 실행되는 동안 JVM이 종료되면 finally 블럭이 실행되지 않을 수 있고 try, catch 코드가 실행하는 동안 스레드가 중단되거나 종료되면 애플리케이션이 동작중이더라도 finally 블럭이 실행되지 않을 수 있다.

finally 블럭은 리소스 누수를 막기위한 중요 도구이다. 파일을 닫거나 리소스를 복구할 때, 코드를 finally 블럭에 넣어 리소스가 항상 복구되도록 한다. 혹은 try-with-resources문을 사용해서 자동으로 시스템 리소스가 필요없는 시점에 릴리즈하도록 하는 것을 고려해야 한다.

trows

예외가 발생한 메서드를 호출한 곳으로 예외 객체를 넘기는 방법이다.

throw

인위적으로 예외를 발생시킬 때 사용할 수 있는 예약어이다. 개발자가 임의로 예외를 발생시킬 때 사용되며 특정 예외를 만났을 때 더욱 구체적인 예외로 처리하고자 할 때에도 사용된다.

public static void main(String[] args) {
    try {
        // do something
    } catch(Exception e) {
        throw new IllegalStateException("...");
    } finally {
        // do something
    }
}

try-with-resources

try-with-resources는 하나 이상의 리소스를 정의하는 try와 관련된 문법이다. 리소스는 프로그램이 끝나기 전에 반드시 종료되어야 하는 객체를 의미한다. try-with-resources는 Exception 시 resources를 자동으로 close() 해준다. 사용 로직을 작성할 때 객체는 java.lang.AutoCloseable, java.io.Closeable을 구현한 객체여야 한다.

아래 예제는 BufferedReader를 이용하여 파일의 첫번째 라인을 읽는 코드이다. BufferedReader는 프로그램이 종료될 때, 반드시 닫혀야 하는 리소스이다.

static String readFristLineFromFile(String path) throws IOException {
    try (BufferedReader br = new BufferedReader(new FileReader(path))) {
        return br.readLine();
    }
}

위 예제에서 리소스는 try-with-reources 문에 정의된 BufferedReader이다. 선언문은 try 키워드 바로 뒤 괄호 안에 표시된다. BufferedReader 클래스는 Java 7 이후로 AutoCloseable 인터페이스를 구현한다.
BufferedReader 인스턴스는 try-with-resource문에서 선언되기 떄문에 try 문이 정상적으로 완료되거나 갑작스럽게 종료되는 것과 관계없이 닫힌다.
Java 7 이전에는 finally 블록을 사용해서 리소스가 닫히도록 할 수 있다.

try-catch-finally예제

FileOutputStream out = null;
try {
    out = new FileOutputStream("file.txt");
} catch(FileNotFoundException e) {
    e.printStackTrace();
} finally {
    if (out != null) {
        try {
            out.close();
        } catch(IOException e) {
            e.printStackTrace();
        }
    }
}

try-with-resources 예제

try(FileOutputStream out = new FileOutputStream("file.txt")) {
    // do something
} catch(IOException e) {
    e.printStackTrace();
}

try 블록과 try-with-resource 문 모두에서 예외가 던져지면, try 블록에서 던져진 예외를 던집니다. try-with-resource의 예외는 억제된다. 억제된 예외를 가져오는 방법은 Throwable.getSuppressed 메서드를 호출해서 억제된 예외를 찾아올 수 있다.

AutoCloseable과 Closeable 인터페이스 구현

Closeable인터페이스는 AutoCloseable 인터페이스를 상속한다. Closeable인터페이스의 close 메서드는 IOException 타입의 예외를 던지는 반면, AutoCloseable 인터페이스의 close메서드는 Exception 타입의 예외를 던진다. 결과적으로 AutoCloseable 인터페이스의 하위 클래스는 close 메서드를 오버라이딩 할 때 IOException과 같은 좀 더 상세한 예외로 던지거나 예외를 발생시키지 않을 수 있다.

e.printStackTrace();

예외타입의 매개변수에 printStackTrace(); 를 통해 예외가 발생하기 까지의 이력을 프린트 해준다.
메서드가 실행되면 메모리의 STACK영역에 Stack Frame이 쌓이게 된다. STACK영역은 Stack Frame을 저장하는 Stack이며 JVM은 오직 Stack Frame을 push하고 pop하는 작업만 하게 된다. 예외가 발생되면 예외의 내용을 보여주는 Stack Trace의 각 라인은 하나의 Stack Frame을 표현하는 것이며, 메모리 영역의 STACK에 쌓여있는 Stack Frame 들을 pop하여 출력한다.

예외처리 비용

  • 예외를 사용한다는 것 자체가 비용이 비싸다
    try-catch를 동작 하면서 발생하는 검사들도 하나의 원인이겠지만, Throwable 생성자의 fillInStackTrace() 메서드가 주 원인이다. 이 메서드는 예외가 발생한 메서드의 Stack Trace를 모두 출력해주기 때문이다.
  • 충분히 로직으로 사용할 수 있는 것이라면 Exception 보다 Return type 이나 입력 값등을 통해 작업하는 것이 좋다.

커스텀 예외 만들기

throw를 할 때 다른 사람이나 언어가 제공한 예외를 사용할 수 있습니다. java는 많은 예외 클래스를 제공하지만 직접 작성할 수도 있습니다.
커스텀 예외를 만들때 참고해야 할 4가지 Best Practices

1. Always Provide a Benefit

자바 표준 예외들에는 포함되어 있는 다양한 장점을 가지는 기능들이 있다. 이미 JDK가 제공하고 있는 방대한 수의 예외들과 비교했을 때 만들고자 하는 커스텀 예외는 어떠한 장점도 제공하지 못한다면 커스텀 예외를 만드는 이유를 다시 생각해볼 필요가 있다. 어떠한 장점을 제공할 수 없는 예외를 만드는 것보다 오히려 UnsupportedOperationException이나 IllegalArgumentException과 같은 표준 예외 중 하나를 사용하는 것이 낫다.

2. Follow the Naming Convention

JDK가 제공하는 예외 클래스들을 보면 클래스의 이름이 모두 Exception으로 끝난다. 이러한 네이밍 규칙은 자바 생태계 전체에 사용되는 규칙이다. 커스텀 예외도 이 네이밍 규칙을 따르는 것이 좋다.

3. Provide javadoc Comments for Your Exception Class

기본적으로 API의 모든 클래스, 멤버변수, 생성자들에 대해서는 문서화 하는것이 일반적인 Best Practices이다. 문서화되어 있지 않은 API들은 사용하기가 어렵다. 클라이언트와 직접 관련된 메서드들 중 하나가 예외를 던지면 그 예외는 바로 예외의 일부가 된다. JavaDoc은 예외가 발생할 수도 있는 상황과 예외의 일반적인 의미를 기술한다.목적은 다른 개발자들이 API를 이해하고 일반적인 에러 상황들을 피하도록 돕는 것이다

4. Provide a Constructor That Sets the Cause

커스텀 예외를 던지기 전에 표준 예외를 Catch 하는 케이스가 많다. 보통 Catch된 예외에는 제품에 발생한 오류를 분석하는데 필요한 중요한 정보가 포함되어 있다.

Exception과 RuntimeException

Exception과 RuntimeException은 예외의 원인을 기술하고 있는 Throwable을 받을 수 있는 생성자 메서드를 제공한다. 만들고자 하는 커스텀 예외도 이렇게 하는 것이 좋다. 발생한 Throwable을 파라미터를 통해 가져올 수 있는 생성자를 최소한 하나를 구현하고 수퍼클래스에 Throwable을 전달해 주어야 한다.

public class TestException extends Exception {
    public TestException(String message, Throwable cause, ErrorCode code) {
        super(message, cause);
        this.code = code;
    }
}

Custom Checked Exception 구현

Checked Exception을 구현하기 위해서는 Exception 클래스를 상속받아야 하는데, 커스텀 예외를 구현하기 위해 필요한 유일한 필수사항이다. 하지만 위 4가지 Best Practices에서 설명했듯이 발생한 예외를 생성자에 주입하기 위한 생성자 메서드를 제공해야 하며, 표준 예외보다 더 나은 이점들을 제공해야 한다.

Custom Unchecked Exception 구현

Unchecked Exception 예외 구현은 Checked Exception 예외 구현과 동일하다. 하지만 한가지 차이가 있는데 RuntimeException을 상속 받는다.

예외 포장

애플리케이션은 종종 다른 예외를 던지는 것으로 예외에 대한 응답을 한다. 한 예외가 다른 예외를 발생시키는 것은 매우 유용할 수 있다. 예외 포장은 프로그래머가 이를 수행하는데 도움이 된다.
Throwable의 다음 메서드와 생성자는 예외 포장을 지원한다.

  • Thorwable getCause()
  • Throwable initCause(Throwable)
  • Throwable(String, Throwable)
  • Throwable(Throwable)

initCauseThrowable인자와 Throwable 생성자의 인자는 현재 예외가 어떤 예외에 의해 발생했는지 알려준다.
getCause()는 현재 예외의 원인인 예외를 반환한다.
initCause()는 현재 예외의 원인을 설정한다.

try {
    // ...
} catch (IOException e) {
    throw new TestException("Other IOException", e);
}

IOException이 발생한 경우 새로운 TestException예외가 생성되며, 원이이 되는 예외를 포장하게 된다. 그리고 해당 예외는 다음 상위 레벨의 예외 처리기로 던져진다. 이를 좀 더 활용하면 CheckedExceptionUncheckedException으로 포장하여 코드를 보다 깔끔하게 관리할 수 있게 된다.

자바에서 제공하는 기본 예외들

ArithmeticException

산술 연산에서 예외 조건이 발생했을 때 발생한다. (0으로 나누었을 경우 등)

ArrayIndexOutOfBoundsException

잘못된 인덱스로 Array에 엑세스 했을 경우 발생,
인덱스가 음수이거나 배열 크기보다 크거나 같을 때 발생한다.

ClassNotFoundException

정의한 클래스를 찾을 수 없을 때 발생한다.

FileNotFoundException

파일에 엑세스 할 수 없거나 파일이 열리지 않을 경우 발생한다.

IOException

입출력 작업이 실패하거나 중단될 때 발생한다.

InterruptedException

Thread가 waiting, sleeping 또는 어떤 처리를 하고 있을 때 interrupt가 되면 발생하는 예외이다.

NoSuchMethodException

찾을 수 없는 메서드에 엑세스 할 때 발생하는 예외이다.

NullPointerException

null 객체의 멤버를 참조할 때 발생하는 예외이다.

NumberFormatException

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

StringIndexOutOfBoundsException

문자열에 엑세스 하는 인덱스가 문자열보다 큰 경우거나 음수일 때 발생하는 예외이다.

참고
https://www.notion.so/3565a9689f714638af34125cbb8abbe8
https://wisdom-and-record.tistory.com/46
https://github.com/ByungJun25/study/tree/main/java/whiteship-study/9week
https://velog.io/@dion/%EB%B0%B1%EA%B8%B0%EC%84%A0%EB%8B%98-%EC%98%A8%EB%9D%BC%EC%9D%B8-%EC%8A%A4%ED%84%B0%EB%94%94-9%EC%A3%BC%EC%B0%A8-%EC%98%88%EC%99%B8-%EC%B2%98%EB%A6%AC

반응형

'Programming > JAVA' 카테고리의 다른 글

Enum이란?  (0) 2021.03.02
멀티 쓰레드 프로그래밍이란?  (0) 2021.03.01
인터페이스란?  (0) 2021.02.06
패키지란?  (0) 2021.01.21
상속이란?  (0) 2021.01.07