2022.07.28/자바 정리/예외를 다루는 방법

Jimin·2022년 7월 29일
0

비트캠프

목록 보기
12/60

예외를 다루는 방법

예외처리

  • 메서드를 실행하는 중에 예외가 발생했을 때 호출자에게 알려주는 문법
  • 메서드를 호출하는 중에 예외를 받았을 때 처리하는 문법
  • 예외를 호출자에게 알려주는 문법
throw [Throwable 객체];
throw new String("예외가 발생하였습니다"); // 컴파일 오류
throw new RuntimeException("예외가 발생하였습니다.");

예외처리 문법의 의미

  • 메서드 실행 중에 예외 상황을 만났을 때 리턴 값으로 알려주는 방식의 한계를 극복하기 위해
  • 예외가 발생하더라도 시스템을 멈추지 않고 적절한 조치를 취한 후 계속 실행하기 위해

예외 던지기 - 예외 상황을 호출자에게 알려주기

  • 예외가 발생한 즉시, 보고를 하므로 예외 이후의 코드는 실행되지 않는다!
public class Exam0111 {

  static void m() {
    throw new RuntimeException("예외가 발생했습니다!");
  }

  static void test() {
    m();
    // m() 메서드가 던진 예외를 받지 않으면?
    // 즉시 현재 메서드의 실행을 멈추고 호출자에게 예외 처리를 위임한다.
    // m() 메소드로부터 받은 예외 객체를 이 메서드 test()를 호출한 호출자에게 넘겨 버린다.

    // 따라서 다음 출력 코드는 실행되지 않는다.
    System.out.println("test() 호출됨!");
  }

  public static void main(String[] args) {

    // 예외를 받았을 때 처리하는 문법
    try {
      test();
    } catch (RuntimeException e) {
      // 예외가 발생하면 catch 블록이 실행된다.
      // 코드에서 던진 예외 객체는 catch의 파라미터가 받는다.
      // catch 블록에는 예외에 대한 적절한 조치를 수행하는 코드를 둔다.
      // 예) 다음과 같이 예외가 발생된 이유를 간단히 출력할 수 있다.
      System.out.println(e.getMessage());
    }

    System.out.println("시스템을 종료합니다.");

  }

}

오류 구분

  • Error: JVM에서 발생시키는 예외 (메모리 부족, 스레드 종료, ...)
    → 이런 예외는 application에서 제어할 수 없다.
    ⇒ 적절하게 기록(Log)를 남긴 후 application을 종료하는 수밖에 없다.
  • Exception: Application에서 발생시키는 예외
    → App에서 제어 가능하다.
    ⇒ 적절하게 조치를 취한 후 계속 실행하게 할 수 있다.
  • RuntimeException: 예외 표시 없이 메서드에서 사용 가능하다.
    → 프로그램의 편의성 때문에 등장했다.
    ⇒ 예외처리를 요구하지 않는다.
  • Throwable에는 두 부류의 서브 클래스가 있다.

1. java.lang.Error (시스템 오류)

  • JVM에서 발생된 오류이다.
  • 따라서 이 오류는, 개발자가 사용하는 클래스가 아니다.
  • 이 오류가 발생하면 현재의 시스템 상태를 즉시 백업하고, 실행을 멈춰야 한다.
  • JVM에서 오류가 발생한 경우에는 계속 실행해봐야 소용이 없다.
  • 근본적으로 문제를 해결할 수 없다.
  • 오류의 예: 스택 오버 플로우 오류, VM 관련 오류, AWT 윈도우 관련 오류, 스레드 종료 오류 등

2. java.lang.Exception (애플리케이션 오류)

  • 애플리케이션에서 발생시킨 오류이다.
  • 개발자가 사용하는 클래스이다.
  • 적절한 조치를 취한 후 계속 시스템을 실행하게 만들 수 있다.
  • 오류의 예: 배열의 인덱스가 무효한 오류, I/O 오류, SQL 오류, Parse 오류, 데이터 포맷 오류 등
  • 오류를 던진다면 반드시 메서드 선언부에 어떤 오류를 던지는지 선언해야 한다.
  • 메서드 호출자에게 알려주는 것이다.
import java.io.FileNotFoundException;

public class Exam0210 {
  
  static void m1() throws Throwable {
    throw new Throwable(); // OK!
    // 예외를 던질 때 Throwable 클래스를 직접 사용하지 말라!
    // 그 하위 클래스를 사용하라.
    // 특히 애플리케이션 오류를 의미하는 Exception 클래스를 사용하라.
  }

  // 여러 개의 오류를 던지는 경우 메서드 선언부에 그대로 나열하라.
  static void m2() throws FileNotFoundException, RuntimeException {
    int a = 100;
    if (a < 0)
      throw new FileNotFoundException(); // OK!
    else
      throw new RuntimeException(); // OK!
  }

  public static void main(String[] args) {}

}

RuntimeException

  • RuntimeException은 밑의 사진과 같이 Throwable과 Exception을 상속받는다.
  • Exception의 서브 클래스임에도 불구하고 RuntimeException 객체를 던질 경우, 메서드 선언부에 예외를 던진다고 표시하지 않아도 된다.
  • 왜? ⇒ 스텔스 모드(비유!) 를 지원하기 위해 만든 예외이다.
public class Exam0220 {

  static void m() throws RuntimeException {
    throw new RuntimeException(); // OK!
  }
  static void m2() {
    throw new RuntimeException();
  }
  public static void main(String[] args) {}

}

메서드 선언부에 던지는 오류 표시하기

  • 소보로빵, 단팥빵, ... 을 모두 통칭해서 빵을 던진다고 할 수 있당
  • 근데 빵으로 통칭해서 던지지 말고 구체적으로 알려주는 것이 좋당

- 메서드에서 발생되는 예외는 메서드 선언부에 모두 나열해야 한다.

static void m(int i) throws Exception, RuntimeException, SQLException, IOException {
    if (i == 0)
      throw new Exception();
    else if (i == 1)
      throw new RuntimeException();
    else if (i == 2)
      throw new SQLException();
    else
      throw new IOException();
  }

  public static void main(String[] args) {}

}

- 공통 분모를 사용하여 퉁치는 방법

  • 메서드에서 발생하는 예외의 공통 수퍼 클래스를 지정하여, 여러 개를 나열하지 않을 수 있다.
  • 그러나 호출자에게 어떤 오류가 발생하는지 정확하게 알려주는 것이 유지보수에 도움이 된다.
    ⇒ 따라서 가능한 그 메서드에서 발생하는 예외는 모두 나열하라!

던지는 예외 받고 처리하기

예외처리 안하면 오류

  • 예외를 던질 수 있다고 선언된 메서드를 호출할 때, 그 예외 상황에 대한 처리를 하지 않으면 컴파일 오류가 발생한다.
throw new Exception();

public static void main(String[] args) {
    m(1); → 컴파일 오류 발생
 }

예외 처리 방법 1: 예외 처리 책임을 상위 호출자에게 위임

  • 예외를 처리하고 싶지 않다면 상위 호출자에게 책임을 떠넘길 수 있다.
  • 컴파일 오류는 발생하지 않지만,
    • main() 호출자는 JVM이고, JVM은 main()에서 던지 예외를 받는 순간 즉시 실행을 멈춘다.
      그래서 main()의 호출자에게 책임을 떠넘기는 것은 바람직하지 않다.
    • main()은 예외 처리의 마지막 보루이다.
  • main()에서 마저 예외 처리를 하지 않으면 프로그램은 멈.춘.다!
 public static void main(String[] args) throws Exception {
    m(1);
  }

예외 처리 방법 2: try ~ catch ~

  • try ~ catch 를 사용하여 코드 실행 중에 발생된 예외를 중간에 가로챈다.
  • 메서드가 던지는 예외 개수 만큼 catch 블록을 선언하면 된다.
public class Exam0430 {

  static void m(int i) throws Exception, RuntimeException, SQLException, IOException {
    if (i == 0)
      throw new Exception();
    else if (i == 1)
      throw new RuntimeException();
    else if (i == 2)
      throw new SQLException();
    else if (i == 3)
      throw new IOException();

  }

  public static void main(String[] args) {
    try {
      m(4);
      System.out.println("실행 성공!");
    } catch (IOException e) {
      System.out.println("IOException 발생");
    } catch (SQLException e) {
      System.out.println("SQLException 발생");
    } catch (RuntimeException e) {
      System.out.println("RuntimeException 발생");
    } catch (Exception e) {
      System.out.println("기타 Exception 발생");
    }
  }
}

catch 블럭의 순서

  • 앞에서 이미 빵을 받는다고 선언해버리면 나중에 소보루 빵, 단팥빵 받는다고 해도 인식하지 못한당.
  • 여러 개의 예외를 받을 때 예외를 구체적으로 구분해서 처리하고 싶다면, 수퍼 클래스 변수로 먼저 받지 말라!
  • 그러면 그 클래스의 모든 서브 클래스 객체도 다 받게 된다.
  • 즉 서브 클래스의 변수에서 받을 기회조차 없다.
  • 컴파일러 에러가 나게 된다.
    ⇒ 예외 객체를 정확하게 받고 싶다면 Exam0430.java 처럼 서브 클래스 예외부터 받아라.
  • 즉, 통쳐서 처리하고 싶을 때, 예외를 구분할 필요 없는 경우에는, 그냥 Exception으로 다 받아주면 된다는 뜻이다.
public class Exam0440 {

  static void m(int i) throws Exception, RuntimeException, SQLException, IOException {
    if (i == 0)
      throw new Exception();
    else if (i == 1)
      throw new RuntimeException();
    else if (i == 2)
      throw new SQLException();
    else if (i == 3)
      throw new IOException();
  }

  public static void main(String[] args) {
    try {
      // try 블록에서 예외가 발생할 수 있는 메서드를 호출한다.
      m(1);

    } catch (Exception e) {
    
    } catch (IOException e) {
    → 여기부터 컴파일러 에러 난다.
    } catch (SQLException e) {

    } catch (RuntimeException e) {
    
    }
  }

Throwable 변수로 예외를 받지 말라!

  • Throwable 변수는 Exception뿐만 아니라, Error까지 받아 버린다.
  • 가능한한, Error 계열의 시스템 예외를 받지 말라!
  • 혹 받더라도 현재 프로그램을 종료하기 전에 필수적으로 수행해야 하는 마무리 작업만 수행하도록 하라.
  • 왜?
    • 시스템 예외는 당장 프로그램을 정상적으로 실행할 수 없는 상태일 때 발생한다.
    • 정상적인 복구가 안되는 예외이다.
    • 따라서 이 예외를 처리하려 해서는 안된다.
    • 시스템을 멈출 수 밖에 없다.

예외처리 후 마무리 작업 - finally 블록

  • 예외가 발생하면 그 코드 줄에서 그 즉시 실행을 멈추고 즉시 자신을 호출한 곳으로 오류를 보고한다. 마지막 호출지는 JVM이다.
  • 주의, close()를 호출하기 전에 예외가 발생한다면, Scanner 자원이 해제되지 못한다.
  • 해결책?
    ⇒ 정상적으로 실행되든, 예외가 발생하든지 간에 무조건 close()가 실행되게 만들라!
  • 어떻게?
    ⇒ finally {}에 자원 해제 시키는 코드를 담아라!

정상적으로 실행하든, 아니면 예외가 발생하여 catch 블록을 실행하든 finally 블록은 무조건 실행한다.

  • 즉 try ~ catch ~ 블록을 나가기 전에 반드시 실행한다.
  • 그래서 이 블록에는 try 에서 사용한 자원을 해제시키는 코드를 주로 둔다.
    • 자원? 파일, DB 커넥션, 소켓 커넥션, 대량의 메모리 등

try-finally 블럭

try 블록을 나가기 전에 무조건 실행해야 할 작업이 있다면, catch 블록이 없어도 finally 블록만 사용할 수 있다!

  • 이렇게 catch 블록이 없는 try ~ finally ~ 블록을 작성하는 상황:
    → 예외가 발생하면 그 처리는 호출자에게 맡기게 되는데,
    메서드를 호출하는 동안 사용한 자원을 이 메서드를 종료하기 전에 해제시켜야 하는 경우!
public static void main(String[] args) throws Exception {
    try {
      m(1);
      // m()에서 발생된 예외는 try 블록에서 받지 않는다.
      // 따라서 main() 호출자에게 위임한다.
      // => 물론 main() 메서드 선언부에 위임할 예외의 종류를 표시해야 한다.
    } finally {
      System.out.println("마무리 작업 실행!");
    }
  }

finally 블록과 자원 해제

  • 정상적으로 실행되든 예외가 발생하든 상관없이 자원해제 같은 일은 반드시 실행해야 한다.
  • 항상 자원을 사용한 후 해제시키는 것을 습관적으로 해야 한다.
public class Exam0620 {
  public static void main(String[] args) {
    Scanner keyScan = null;
    try {
      keyScan = new Scanner(System.in);
      System.out.print("입력> ");
      int value = keyScan.nextInt();
      System.out.println(value * value);
    } finally {
      keyScan.close();
      System.out.println("스캐너 자원 해제!");
    }
  }
}

try-with-resources

try-with-resources 라는 문법을 사용하면 굳이 finally 블록에서 close()를 직접 호출할 필요가 없다.
⇒ 자동으로 처리한다.

  • 단 java.lang.AutoCloseable 구현체에 대해서만 가능하다!
  • 문법
try (java.lang.AutoCloseable 구현체) {...(구현체 사용)}
  • close() 해줘야 하는 AutoCloseable 자원(구현체)를 try() 괄호 안에 넣어 선언해 준다.
    → 그래야 컴파일러 에러가 발생하지 않는다.
  • 만든 자원 변수는 {} 괄호 안에서 사용한다.
  • 중괄호는 원래의 try 블럭이다. 자원 선언보다 먼저 실행된다.
  • 예시
static void m() throws Exception {
    try (
    	Scanner keyScan = new Scanner(System.in);
        // FileReader 클래스도 java.lang.AutoCloseable 구현체이다.
        FileReader in = new FileReader("Hello.java"); // OK!
        ) {
      System.out.print("입력> ");
      int value = keyScan.nextInt();
      System.out.println(value * value);
    }
  }

- implement의미

A implements B{
...
}

A 클래스가 B인터페이스의 규칙을 이행한다.

인터페이스 == 규칙

 static class C implements AutoCloseable {
    @Override
    public void close() throws Exception {
      System.out.println("C 클래스의 자원을 해제하였습니다.");
    }
  }

예외에 대해 의미를 부여하기 - 사용자 정의 예외 만들고 사용하기

  • RuntimeException 계열의 예외는 굳이 throws 문장을 선언하지 않아도 되지만, 예외가 존재하는 메소드를 호출하는 개발자에게 어떤 예외가 발생할 수 있는지 명확하게 제시해주는 것이 유지보수에 도움이 되기 때문에 메서드 선언부에 발생되는 예외를 명시하는 것이 좋다.
    → 그런데 조금 아쉬움이 있는 점은 예외가 존재하는 메서드를 사용하는 개발자가 이 메서드에서 RuntimeException을 던진다는 의미에 대해 직관적으로 이해하기는 어렵다.
    ⇒ 그냥 RuntimeException을 던진다고 하니, 예외를 던진다는 것은 이해하지만, 그 예외가 의미하는 바가 무엇인지 즉시 알아보기 힘들다는 얘기다.
  • 예외 클래스 이름을 보면 오류의 원인이 상세하게 나와 있지만,
    전체적인 그림에서 어느 객체에서 발생된 예외인지 직관적으로 알 수 없다.

실무에서는 개발자에게 예외의 의미를 직관적으로 전달하기 위해,
RuntimeException 같은 평범한, 의미가 모호한 이름의 클래스를 사용하지 않고
대신에 기존 예외를 상속 받아 의미있는 이름으로 서브 클래스를 정의한 다음에 그 예외 클래스를 던지도록 프로그래밍한다.

  • 예외가 발생되면 원본 예외를 그대로 던지지 말고, 예외의 의미를 직관적으로 파악할 수 있는 예외 객체를 던진다.
  • 즉 게시물 관리 작업을 하다가 오류가 발생했음을 직관적으로 알게 한다.
  • 어떤 방법?
    → 게시물 예외를 직관적으로 알 수 있는 클래스를 만든다.
    → 그 클래스가 BoardException 이다.
import java.sql.Date;
import java.util.Scanner;

public class Exam0130 {
  
  static Board read() throws BoardException {
    try (Scanner keyScan = new Scanner(System.in)) {
      Board board = new Board();

      System.out.print("번호> ");
      board.setNo(Integer.parseInt(keyScan.nextLine()));

      System.out.print("제목> ");
      board.setTitle(keyScan.nextLine());

      System.out.print("내용> ");
      board.setContent(keyScan.nextLine());

      System.out.print("등록일> ");
      board.setCreatedDate(Date.valueOf(keyScan.nextLine()));

      return board;
    } catch (Exception 원본오류) {
      throw new BoardException("게시물 입력 도중 오류 발생!", 원본오류);
    }
  }

  public static void main(String[] args) {
    try {
      Board board = read();
      // read() 메서드의 선언부를 보면, BoardException을 던진다고 되엉 있다.
      System.out.println("---------------------");
      System.out.printf("번호: %d\n", board.getNo());
      System.out.printf("제목: %s\n", board.getTitle());
      System.out.printf("내용: %s\n", board.getContent());
      System.out.printf("등록일: %s\n", board.getCreatedDate());
    }catch(BoardException ex) {
      ex.printStackTrace();
    }
  }

}
  • BoardException 클래스는 생성자가 호출될 때 그와 대응하는 수퍼 클래스의 생성자를 호출하는 일 외에는 다른 작업을 수행하지 않는다.
  • 아니, 기능을 추가할 것도 아니면서 왜 RuntimeException을 상속 받았는가?
    → 이 클래스는 기존의 예외 클래스 기능을 확장하기 위함이 아니라,
    의미있는 이름을 가진 예외 클래스를 만드는 것이 목적이다.
    ⇒ 즉, 예외가 발생했을 때 클래스 이름으로 어떤 예외인지 쉽게 추측할 수 있도록 하기 위함이다.
    일종의 분류표로서 사용한다.

사용자 정의 예외를 다루는 방법

board-app project

ObjectList에서 발생하는 예외에 대해 사용자 정의 예외를 던진다.

1단계 - ObjectList에서 발생하는 예외 상황을 표현할 사용자 정의 예외를 만든다.

  • ListException 클래스 생성
    • get()과 remove() 에서 예외가 발생하면 이 클래스의 객체를 만들어 던진다.
    • Exception을 상속 받으면, ListException 클래스 상속 받을 때 마다 throws 해줘야하는데 이는 매우매우 귀찮기 때문에 오류 표시 안해줘도 되는 런타임을 상속 받아 준다!
    • RuntimeException을 상속 받고 나면, RuntimeException에게 값을 넘겨줄 ListException의 생성자들을 만들어 줘야하는데, 이는 밑의 사진과 같이 내가 일일이 코드로 안치고 Gernerate Constructors from uperclass 라는 메뉴를 클릭해서 자동으로 이클립스에게 만들라고 시킬 수 있다.
public class ListException extends RuntimeException{
  private static final long serialVersionUID = 1L ; // 나중에 설명...

  public ListException() {
    super();
    // TODO Auto-generated constructor stub
  }

  public ListException(String message, Throwable cause, boolean enableSuppression,
      boolean writableStackTrace) {
    super(message, cause, enableSuppression, writableStackTrace);
    // TODO Auto-generated constructor stub
  }

  public ListException(String message, Throwable cause) {
    super(message, cause);
    // TODO Auto-generated constructor stub
  }

  public ListException(String message) {
    super(message);
    // TODO Auto-generated constructor stub
  }

  public ListException(Throwable cause) {
    super(cause);
    // TODO Auto-generated constructor stub
  } 

}

2단계 - 사용자 정의 예외(ListException)를 적용한다.

- ObjectList 클래스 변경

  • get(), remove() 메서드에서 예외를 발생시킬 때 ListException을 발생시킨다.

- BoardList 클래스 변경

  • ObjectList 변경에 맞춰 예외 코드 변경

- MemberList 클래스 변경

  • ObjectList 변경에 맞춰 예외 코드 변경

-- get() in ObjectList class

  /*
   * 목록에서 인덱스에 해당하는 항목을 찾아 리턴한다.
   * @param index 목록에 저장된 항목의 인덱스
   * @return 목록에 저장된 항목의 인덱
   * @throws ListException 인덱스가 무효함.
   * */
  public Object get(int index) throws ListException{  
    if(index <0 || index >= size) {

      throw new ListException("인덱스가 무효합니다!");
    }
    return elementData[index];
  }
  • 개발자가 예외 클래스 이름만 보고도 어떤 작업을 하다가 예외를 발생했는지 직관적으로 알 수 있도록, 사용자 정의 예외를 던진다!!!!!! ⇒ 아 , 얘 목록 다루다가 오류 떴구나!!!
  • RuntimeException계열의 예외는 메서드선언부에 표시할 필요가 없다.

-- remove() in ObjectList class

  public boolean remove(int index) /*throws ListException*/ {
    if(index <0 || index >= size) {
      throw new ListException("인덱스가 무효합니다!");
    }
    for(int i=index+1;i<size;i++) {
      elementData[i-1]=elementData[i];
    }
    elementData[--size] = null;
    return true;
  }
  • RuntimeException계열의 예외는 메서드선언부에 표시할 필요가 없다.
  • get() 처럼 throws해줘도 되고, remove처럼 throws 안 해줘도 된다.

-- get(), remove() in BoardList class

  • throws Throwable → throws ListException 혹은 생략한다.
  • ObjectList의 get()에서 던지는 예외를 현재 클래스의 get() 메서드에서 처리하지 않고 호출자(BoardHandler class)에게 처리를 위임한다.
  • ListException은 Runtime 계열이기 때문에 메서드 선언부에 표시해도 되고, 표시하지 않아도 된다.
  • 생략이 가능하기 때문에, Exception 계열의 예외를 다루는 것보다 덜 번거롭다.
  • Throwable을 throws 하면 컴파일 에러가 나는 이유:
    Throwable클래스가 ListException, RuntimeException 클래스의 상위 클래스여도, 수퍼클래스인 ObjectList 클래스가 에러 안던지니까 이 클래스의 메소드를 오버라이딩한 BoardList 클래스의 get()도 에러를 던지면 안된다!
  @Override
  public Board get(int boardNo)throws ListException{
    for (int i = 0; i < size(); i++) {
      Board board = (Board)super.get(i);
      if (board.no== boardNo) {
        return board;
      }
    }
    return null;
  }

  @Override
  public boolean remove(int boardNo) {
    for (int i = 0; i < size(); i++) {
      Board board = (Board)super.get(i);
      if (board.no == boardNo) {
        return super.remove(i);
      }
    }
    return false;
  }

3단계 - XxxList를 사용하는 XxxHandler 클래스를 변경한다.

- BoardHandler클래스 변경

  • BoardList 변경에 맞춰 예외 코드 변경

- MemberHandler 클래스 변경

  • MemberList 변경에 맞춰 예외 코드 변경

- App 클래스 변경

  • Throwable 대신 애플리케이션 예외인 Exception으로 교체한다.
  • 즉, 시스템 예외인 Error 계열 예외는 다루지 않는다.

- BoardHandler클래스 변경

  • execute() 메소드에서 Throwable로 받아주는 대신, Exception으로 오류를 받아준다.
try {
        int menuNo = Prompt.inputInt("메뉴를 선택하세요[1..5](0: 이전) ");
        displayHeadline();

        switch (menuNo) {
          case 0: return;
          case 1: this.onList(); break;
          case 2: this.onDetail(); break;
          case 3: this.onInput(); break;
          case 4: this.onDelete(); break;
          case 5: this.onUpdate(); break;
          default: System.out.println("메뉴 번호가 옳지 않습니다!");
        }

        displayBlankLine();

      }catch(Exception ex) {
        System.out.printf("예외 발생: %s\n", ex.getMessage());
      

- App 클래스 변경

  • execute() 메소드에서 Throwable로 받아주는 대신, Exception으로 오류를 받아준다.
try {
        int mainMenuNo = Prompt.inputInt("메뉴를 선택하세요[1..6](0: 종료) ");

        switch (mainMenuNo) {
          case 0: break loop;
          case 1: // 게시판
            boardHandler.execute();
            break;
          case 2: // 독서록
            readingHandler.execute();
            break;
          case 3: // 방명록
            visitHandler.execute();
            break;
          case 4: // 공지사항
            noticeHandler.execute();
            break;
          case 5: // 일기장
            diaryHandler.execute();
            break;
          case 6: // 회원
            memberHandler.execute();
            break;
          default: System.out.println("메뉴 번호가 옳지 않습니다!");
        } // switch
      }catch(Exception ex) {
        System.out.println("입력 값이 옳지 않습니다!");
      }
profile
https://github.com/Dingadung

0개의 댓글