[9주차] 예외처리

janjanee·2022년 8월 1일
0
post-thumbnail

2021.01.25 작성글 이전

9. 예외처리

학습 목표 : 자바의 예외처리에 대해 학습하세요.

프로그램이 실행 중 어떤 원인에 의해서 오작동 하거나 비 정상적 종료되는 경우가 있다.
이러한 결과를 초래하는 원인을 프로그램 에러 또는 오류라고 한다.

발생시점에 따라 '컴파일 에러''런타임 에러' 로 나눌 수 있다.
또한, 컴파일도 잘되고 실행도 잘되지만 의도한 결과와 다르게 동작하는 것을 '논리적 에러' 라고 한다.

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

9-1. 자바에서 예외 처리 방법 (try, catch, throw, throws, finally)

예외처리란?
정의 - 프로그램 실행 시 발생할 수 있는 예기치 못한 예외의 발생에 대비한 코드를 작성
목적 - 프로그램의 갑작스런 비정상 종료를 막고, 정상적인 실행상태를 유지하도록 하는 것

발생한 예외를 처리하지 못하면, 프로그램은 비정상적으로 종료되며, 처리되지 못한 예외는 JVM의
'예외처리기(UncaughtExceptionHandler)'가 받아서 예외의 원인을 화면에 출력한다.

try-catch

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

try-catch의 구조는 다음과 같다.

try 블럭 안에 예외 발생 가능성이 있는 문장들을 작성하고, 예외가 발생 시 각 예외에 맞는 단 하나의 catch 블럭이 수행된다.
발생한 예외의 종류가 일치하는 catch 블럭이 없다면 예외는 처리되지 않는다.

public class ExceptionEx1 {
    public static void main(String[] args) {
        try {
            try {

            } catch (Exception e) {

            }
        } catch (Exception e) {
            try {

            } catch (Exception e1) {

            }
        }

        try {

        } catch (Exception e) {

        }
    }
}

위의 예제는 아무 일도 하지않는다. 단순히 try-catch문의 사용예이며, 메소드 내에 여러 try-catch 문이 사용될 수 있고,
try 블럭, catch 블럭 안에 또 다른 try-catch 블럭이 올 수 있다.

위의 예제는 100을 0~9 사이의 임의의 정수로 나눈 결과를 출력하는 일을 10번 반복한다.
그러나 10번을 반복하기 전에 예외가 발생하여 프로그램이 종료됐다.

에러메시지를 보면 ArithmeticException: /by zero ...라고 되어있는데 정수를 0으로 나누려
하기 때문에 발생한 에러이다. 친절하게 9번째 라인에서 오류가 발생했다고 알려준다.

예외가 발생하는 경우를 알았으니 비정상적인 프로그램 종료를 막기 위해 예외를 처리하는 코드를 추가해보자.
try 블록안에 예외가 발생할 가능성이 있는 코드를 넣고 catch 블록에서 ArithmeticException 예외 발생시
처리할 코드를 적는다.

출력 결과를 확인하면 0일 경우 '0으로 나누는건 안됨!'을 출력하고 프로그램이 정상적으로 종료되었다.

public class ExceptionEx6 {
    public static void main(String[] args) {
        System.out.println(1);
        System.out.println(2);
        try {
            System.out.println(3);
            System.out.println(0/0);
            System.out.println(4);
        } catch (Exception e) {
            System.out.println(5);
        }
        System.out.println(6);
    }
}

// 결과
1
2
3
5
6

모든 예외 클래스는 Exception 클래스의 자손이므로, catch블럭의 ()괄호에 Exception 클래스 타입의
참조변수를 선언해 놓으면 어떤 종류의 예외가 발생하더라도 이 catch 블럭에서 처리된다.

printStackTrace()와 getMessage()

예외가 발생했을 때 생성되는 예외 클래스의 인스턴스에는 발생한 예외에 대한 정보가 담겨있다.
getMesaage() , printStackTrace() 를 통해서 이 정보들을 얻을 수 있다.

image

멀티 catch 블럭

catch 블럭을 '|' 기호를 이용해서, 하나의 catch 블럭으로 처리 가능

try {
    ...
} catch (ExceptionA e) {
    ...
} catch (ExceptionB e2) {
    ...
}

위의 코드를 아래와 같이 멀티 catch 블럭을 사용하여 줄일 수 있다.

try {
    ...
} catch (ExceptionA | ExceptionB e) {
    ...
}

만약 멀티 catch 블럭의 예외 클래스가 조상과 자손의 관계에 있는 클래스라면 컴파일 에러가 발생한다.
이유는 조상 클래스 한 개만 써줘도 충분하기 때문이다.

try {
    ...
} catch (ParentException | ChildException e) {      // 에러
    ...
}

throw

throw 키워드를 사용해서 예외를 발생시킬 수 있다.

public class ExceptionEx9 {
    public static void main(String[] args) {
        try {
            throw new Exception("Error!!!");
        } catch (Exception e) {
            System.out.println("에러메시지: " + e.getMessage());
            e.printStackTrace();
        }
    }
}

throw new Exception();을 통해 예외를 발생 시키고, catch 블럭에서 예외를 처리한다.

throws

메소드에 예외를 선언하려면, 메소드 선언부에 키워드 throws를 사용해서 메소드 내에서 발생할 수 있는
예외를 적어주면 된다. 예외가 여러개라면 쉼표로 구분한다.

void method() throws Exception1, Exception2, ... ExceptionN {
    // 메소드
}
public class ExceptionEx12 {
    public static void main(String[] args) throws Exception {
        method1();
    }

    static void method1() throws Exception {
        method2();
    }

    static void method2() throws Exception {
        throw new Exception();
    }
}

위의 예제를 보면 method2()의 'throw new Exception();'으로 인해 예외가 발생했으나
예외를 처리하는 try-catch문이 없으므로 method2()를 호출한 method1()에게 예외를 넘겨준다.

method1()에서도 예외를 처리할 수 없으므로 method1()을 호출한 main 메소드에게 예외를 넘겨준다.
그러나, main에서도 예외처리를 할 수 없으므로 main 메소드가 종료되어 프로그램이 비정상 종료가 된다.

throws는 예외를 처리하는 것이 아니라 예외를 전달할 뿐이다.
따라서 어느 한 곳에서는 반드시 try-catch문으로 예외처리를 해야한다.

finally

예외 발생여부와 상관없이 반드시 실행되어야할 코드를 포함시킬 목적으로 사용

try-catch-finally의 순서로 구성된다.

try {
    // 예외 발생할 가능성 있는 문장들
} catch (Exception1 e1) {
    // 예외처리
} finally {
    // 예외 발생여부와 상관없이 항상 수행되어야 할 문장들
    // finally 블럭은 try-catch문의 맨 마지막에 위치
}
public class FinallyTest {
    public static void main(String[] args) {
        try {
            System.out.println("hello~");
        } catch (Exception e) {
            System.out.println("error: " + e.getMessage());
        } finally {
            System.out.println("finally: bye!");
        }
    }
}

위의 예제에서 예외가 발생하면 try -> catch -> finally 순으로 실행이 되며
예외가 발생하지 않았다면 try -> finally 순으로 실행이 된다.

💡 try, catch 블럭에서 'return;' 문이 실행되더라도 finally 블럭의 문장들은 전부 실행된다.

try-with-resources(자동 자원 반환)

Java 7 부터 try-with-resources문 제공

자원을 사용 후 꼭 닫아주어야 사용했던 자원이 반환되는 클래스가 있는데, 주로 입출력(I/O)관련 클래스를 사용할 때 유용하다.

  • 백기선 유투브 9주차 라이브 강좌 예제

    static void copy(String src, String dest) throws IOException{
    
          InputStream in = null;
          OutputStream out = null;
    
          try {
              out = new FileOutputStream(dest);
              try {
                  in = new FileInputStream(src);
                  byte [] buf = new byte[1024];
                  int n;
                  while ((n = in.read(buf)) >= 0)
                      out.write(buf, 0, n);
              } finally {
                  if (in != null) {
                      try {
                          in.close();
                      } catch (IOException e) {
                          e.printStackTrace();
                      }
                  }
              }
          } finally {
              if (out != null) {
                  try {
                      out.close();
                  } catch (IOException e) {
                      e.printStackTrace();
                  }
              }
          }
    
      }

    7 버전 이하라면 in, out을 올바르게 close를 하기 위해 위처럼 복잡하게 try-catch-finally 구조를 중첩해서 사용해야한다.
    딱 봐도 한눈에 안 들어오고 코드가 복잡해보이는데 이런 문제를 쉽게 해결하기 위해 try-with-resources문을 사용해보자.

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

동일한 코드인데 코드가 아주 간결해졌고 가독성도 좋아진 것을 확인할 수 있다.
try() 안에 생성된 객체는 따로 close()를 호출하지 않더라도 try 블럭을 벗어나는 순간 자동적으로 close()가 호출된다.

try-with-resource문에 의해 자동으로 객체의 close()가 호출될 수 있으려면,
클래스가 AutoCloseable 이라는 인터페이스를 구현한 것이어야만 한다.

9-2. 자바가 제공하는 예외 계층 구조 / RuntimeException과 RE가 아닌 것의 차이는?

자바에서 실행 시 발생할 수 있는 오류(Exception과 Error)를 클래스로 정의하였다.

모든 예외의 최고 조상은 Exception 클래스며, 상속계층도를 Exception 클래스로부터 도식화하면 다음과 같다.

  1. Exception 클래스와 그 자손들
  2. RuntimeException 클래스와 그 자손들 (회색부분)

예외 클래스들은 위와 같이 두 그룹으로 나뉠 수 있다.

RuntimeException 클래스들은 주로 프로그래머의 실수로 인해서 발생될 수 있는 예외들로
자바 프로그래밍 요소들과 관계가 있다.

Ex)

  • ArrayIndexOutOfBoundsException - 배열의 범위를 벗어남
  • NullPointerException - 값이 null인 참조변수의 멤버를 호출
  • ClassCastException - 클래스간의 형변환을 잘못함
  • ArithmeticException - 정수를 0으로 나눔
  • NumberFormatException - 숫자 포맷이 아닌경우

Exception 클래스들은 주로 외부의 영향으로 발생할 수 있는 것들로, 프로그램의 사용자들의 동작에 의해서
발생하는 경우가 많다.

Ex)

  • FileNotFoundException - 존재하지 않는 파일의 이름을 입력
  • ClassNotFoundException - 클래스 이름을 잘못 적음
  • DataFormatException - 입력한 데이터 형식이 잘못된 경우

checked 예외

public class ExceptionEx10 {
    public static void main(String[] args) {
        throw new Exception();
    }
}

위의 예제에서 컴파일 에러가 발생한다.
'Exception 클래스들(Exception과 그 자손들)' 은 발생할
가능성이 있는 문장들에 예외처리를 해주지 않으면 컴파일이 실패하기 때문이다.

unchecked 예외

public class ExceptionEx11 {
    public static void main(String[] args) {
        throw new RuntimeException();
    }
}

그러나 위의 예제는 성공적으로 컴파일 된다. 그 이유는 'RuntimeException 클래스와 그 자손클래스'
해당하는 예외는 프로그래머에 의해 실수로 발생하는 것들이므로 예외처리를 강제하지 않는다.

9-3. Exception과 Error의 차이는?

자바에서는 실행 시(runtime) 발생할 수 있는 프로그램 오류를 '에러(error)''예외(exception)'
두 가지로 구분하였다.

에러(error) : 프로그램 코드에 의해서 수습될 수 없는 심각한 오류
예외(exception) : 프로그램 코드에 의해서 수습될 수 있는 다소 미약한 오류

에러는 예를 들어 메모리 부족, 스택오버플로우와 같이 발생하면 복구할 수 없는 심각한 오류이고,
예외는 발생하더라도 수습될 수 있어서 비교적 덜 심각하다.

9-4. 커스텀 예외 만드는 방법

필요에 따라 프로그래머가 새로운 예외 클래스를 정의하여 사용할 수 있다.
보통 Exception 클래스 또는 RuntimeException 클래스로부터 상속받아 클래스를 만든다.

💡 가능하면 새로운 예외 클래스를 만들기보다 기존의 예외클래스를 활용할 것

class MyException extends Exception {
    MyException(String msg) {   // 문자열을 매개변수로 받는 생성자
        super(msg); // 조상인 Exception 클래스의 생성자를 호출
    }
}
public class CustomException extends Exception {
    private final ErrorCode code;

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

    public ErrorCode getErrorCode() {
        return this.code;
    }
}

커스텀 예외를 던지기 전에 표준예외를 캐치하는 케이스가 많다. 캐치된 예외에는 발생한 오류를
분석하기 위한 중요한 정보가 들어있으므로, 설정하지 않으면 이 정보는 손실될 수 있다.

Exception과 RuntimeException은 예외의 원인을 기술하고 있는 Throwable을 받을 수 있는 생성자를 제공한다.
따라서, 커스텀한 예외도 위의 예제처럼 발생한 Throwable을 파라미터로 받는 생성자를
구현하고 수퍼클래스에 Throwable을 전달한다.

public void test() {
    try {
        calc();
    } catch (CustomException e) {
        log.error(e);
    }
}
private void calc() throws CustomException {
    try {
        int x = 0/0;
    } catch (ArithmeticException e) {
        throw new CustomException("custom error.", e, ErrorCode.INVALID_CALCULATION);
    }
}

위의 커스텀 에외를 사용한 예제이다.

9-5. 연결된 예외(chained exception)

어떤 예외가 다른 예외를 발생시킬 수 있다.
예를 들어 예외 A가 예외 B를 발생시켰다면, A를 B의 '원인 예외(cause exception)'라고 한다.

Throwable initCause (Throwable cause) 지정한 예외를 원인 예외로 등록
Throwable getCause() 원인 예외를 반환

원인 예외를 지정할 수 있게 된 이유는 여러가지 예외를 하나의 큰 분류의 예외로 묶어서 다루기 위함이다.

try {
    startInstall();         // SpaceException 발생
    copyFiles();
} catch (InstallException e) {  // InstallException은
    e.printStackTract();    // SpaceException과 MemoryException의 조상
}

위의 예제처럼 InstallException이 SpaceException과 MemoryException의 조상이라면,
실제로 발생한 예외가 어떤 것이닞 알 수 없는 문제가 발생한다. 그래서 생각한 것이 예외가 원인 예외를
포함할 수 있게 한 것이다.

또 다른 이유로는 checked예외를 unchecked예외로 바꿀 수 있게 하기 위해서다.
checked예외를 unchecked예외로 바꾸면 예외처리가 선택적이 되므로 억지로 예외처리를 하지 않아도 된다.

static void startInstall() throws SpaceException, MemoryException {
    if (!enoughSpace())
        throw new SpaceException("설치할 공간 부족");
    if (!enoughMemory())
        throw new MemoryException("메모리 부족");
}
static void startInstall() throws SpaceException {
    if (!enoughSpace())
        throw new SpaceException("설치할 공간 부족");
    if (!enoughMemory())
        throw new RuntimeException(new MemoryException("메모리 부족"));
}

첫번째 코드를 보면 MemoryException은 Exception의 자손이므로 반드시 예외처리를 해야해서 MemoryException 예외를 던지고 있다.
두번째 코드를 보면 RuntimeException으로 감싸버렸기 때문에 unchecked 예외가 되었다.
더이상 메소드 선언부에 MemoryException을 선언하지 않아도 된다.

💡initCause() 대신 RuntimeEXception 생성자를 이용

References

profile
얍얍 개발 펀치

0개의 댓글