본문 바로가기
☕ Java 웹 프로그래밍/Java

[프로그래머스] Java(자바) 입문 | Part 9. 예외처리

by 일단연 2023. 5. 9.

 Exception 

오류와 예외

  • 컴퓨터 시스템이 동작하는 도중에 예상하지 못한 사태가 발생해 실행 중인 프로그램이 영향을 받는 것을 오류(error)와 예외(exception)로 구분할 수 있음
  • 오류(error)
    • 시스템 레벨에서 프로그램에 심각한 문제를 야기하여 실행 중인 프로그램을 종료시킴
    • 개발자가 미리 예측해 처리할 수 없는 것이 대부분이므로, 오류에 대한 처리는 불가능
  • 예외(Exception)
    • 프로그램 실행 중 예기치 못한 사건
    • Exception이 발생한 지점에서 프로그램이 비정상적으로 종료됨
    • But, 오류와 달리 발생할 수 있는 상황을 미리 예측해 처리할 수 있음
    • 예외 처리: 예외 상황을 미리 예측하고 처리
public class ExceptionExam {
  public static void main(String[] args) {
    int i = 10;
    int j = 5;
    int k = i / j;
    System.out.println(k);
    System.out.println(main 종료!!);
  }
}
  • 위 코드에서 j를 0으로 바꾸면 Excption 발생
    • j를 0으로 바꾸면 ArithmeticException이 발생하면서 프로그램이 종료됨
    • Java는 정수를 정수로 나눌 때 0으로 나누면 안 됨. 0으로 나누면 오류가 발생
  • 예외 처리
    • 프로그래머는 j라는 변수에 0이 들어올지도 모르는 예외 상황을 미리 예측하고 처리할 수 있음

 

예외처리하는 문법: try-catch-finally

  • 예외 처리문은 중괄호( { } )를 생략할 수 없음
try {
    예외를 처리하길 원하는 실행 코드;
} catch (예외클래스 변수1) {
    변수1 예외가 발생할 경우에 실행될 코드;
} catch (예외클래스 뱐수2) {
    변수2 예외가 발생할 경우에 실행될 코드;
}
...
finally {
    예외 발생 여부와 상관없이 무조건 실행될 코드;
}
  • try 블록
    • 기본적으로 맨 먼저 실행되는 코드로 여기에서 발생한 예외는 catch 블록에서 처리
  • catch 블록
    • try 블록에서 발생한 예외 코드나 예외 객체를 인수로 전달받아 그 처리를 담당
    • 발생한 오류와 catch 블록에 선언한 오류가 맞아떨어져야 catch 블록이 실행됨
    • catch 블록의 매개변수는 (예외클래스 변수)
      • 예: catch(ArithmeticException e){ }
      • 어떤 Exception이 발생할지 모를 때는 catch(Exception e)와 같이 Exception클래스를 이용
  • finally 블록
    • try 블록에서 예외 발생 여부와 관계없이 맨 마지막에 무조건 실행됨
  • catch 블록과 finally 블록은 생략 가능
  • 만들 수 있는 try 구문
    • try / catch
    • try / finally
    • try / catch / ... / finally
  • 예외클래스변수명.toString( ): 예외의 정보를 출력해주는 메소드
public class ExceptionExam {
  public static void main(String[] args) {
    int i = 10;
    int j = 0;
    try{
      int k = i / j;
      System.out.println(k);
    }catch(ArithmeticException e){
      System.out.println("0으로 나눌 수 없습니다. : " + e.toString());
    }finally {
      System.out.println("오류가 발생하든 안하든 무조건 실행되는 블록입니다.");
    }
  }
}
  • 실행결과
  • 0으로 나눌 수 없습니다. : java.lang.ArithmeticException: / by zero main end!
  • Exception을 처리하지 않았을 때는 프로그램이 강제 종료되었지만, catch라는 블록으로 Exception을 처리하니 강제종료되지 않고 잘 실행됨
  • try블록에서 여러 종류의 Exception이 발생한다면 catch라는 블록을 여러 개 둘 수 있음
  • Exception클래스들은 모두 Exception클래스를 상속받으므로, 예외클래스에 Exception을 두게 되면 어떤 오류가 발생하든 하나의 catch블록에서 모든 오류를 처리할 수 있음

 

실습

  • 문제 설명
    • 다음 코드에서는 길이 10인 배열에서 인덱스가 20인 값을 읽어오려고 하기 때문에 Exception이 발생하게 됩니다. Exception이 발생하는 부분을 try/catch문으로 감싸서 처리해보세요.
  • 해설
    • array[20] = 5;를 try 블록에 넣고, catch 블록에서 오류 발생 메시지와 오류 상세 정보를 출력
      • 여기서 발생하는 오류는 ArrayIndexOutOfBoundsException
    • 어떤 Exception이 발생할지 모를 때는 catch(Exception e)와 같이 Exception클래스를 이용
  • ExceptionExam클래스
public class ExceptionExam{
  public static void main(String []args){
    int [] array = new int[10];
    try{
      array[20] = 5;  
    }catch(Exception e){
      System.out.println("오류가 발생했습니다. : " + e.toString());
    }
  }
}


Java의 예외 처리 메커니즘 (참고

* TCPschool의 예외 처리 자료를 보고 정리했습니다.

 

1. try 블록에 도달한 프로그램의 제어는 try 블록 내의 코드를 실행
    이때 만약 예외가 발생(throw)하지 않고, finally 블록이 존재하면 프로그램의 제어는 바로 finally 블록으로 이동


2. try 블록에서 예외가 발생하면 catch 핸들러는 다음과 같은 순서로 적절한 catch 블록을 찾음
    2-1. 스택에서 try 블록과 가장 가까운 catch 블록부터 차례대로 검사
    2-2. 만약 적절한 catch 블록을 찾지 못하면, 바로 다음 바깥쪽 try 블록 다음에 위치한 catch 블록을 차례대로 검사
    2-3. 이러한 과정을 가장 바깥쪽 try 블록까지 계속 검사
    2-4. 그래도 적절한 catch 블록을 찾지 못하면, 예외는 처리되지 못함


3. 만약 적절한 catch 블록을 찾게 되면, throw 문의 피연산자는 예외 객체의 형식 매개변수로 전달됨


4. 모든 예외 처리가 끝나면 프로그램의 제어는 finally 블록으로 이동


5. finally 블록이 모두 처리되면, 프로그램의 제어는 예외 처리문 바로 다음으로 이동

  • 만약 ①번 try 블록에서 예외가 발생하지 않고, 바깥쪽 try 블록에서도 예외가 발생하지 않으면, ⑥번 finally 블록이 바로 실행됨
  • 하지만 ①번 try 블록에서 예외가 발생하면, ②번과 ③번 catch 블록에서 해당 예외를 처리할 수 있는지 검사
  • 만약 적절한 catch 블록을 찾지 못하면, 바깥쪽 try 블록의 ④번과 ⑤번 catch 블록도 차례대로 검사
  • 이때 해당 예외를 처리할 수 있는 catch 블록을 찾게 되면, 해당 catch 블록을 실행한 후 ⑥번 finally 블록을 실행
  • 하지만 모든 catch 블록이 해당 예외를 처리할 수 없으면, 예외는 처리되지 못한 채 해당 프로그램은 강제 종료됨

 


 

 Throws 

예외가 발생했을 때 예외를 호출한 쪽에서 처리하도록 던져줌(떠넘김)

  • 문법
    • 접근제한자 리턴타입 메소드명( ) throws 예외클래스명{ }
    • 메소드 선언부 뒤에 throws 예외클래스명을 적어주면 됨
  • Exception클래스가 가장 최상위 예외클래스이기 때문에 예외클래스 자리에 Exception을 적어주면 모든 오류를 대변할 수 있음
  • 예외클래스를 여러 개 적을 수 있음
    • 예: public static int divide(int i, int j) throws ArithmeticException, ClassCastException{ }
public class ExceptionExam2 {   
  public static void main(String[] args) {
    int i = 10;
    int j = 0;
    int k = divide(i, j);
    System.out.println(k);
  }

  public static int divide(int i, int j){
    int k = i / j;
    return k;
  }
}
  • 정수를 매개변수로 2개를 받아들인 후 나눗셈을 한 후 그 결과를 반환하는 divide메소드
  • main메소드에서는 divide메소드를 호출
  • 다음과 같이 divide 메소드를 수정
    • public static int divide(int i, int j) throws ArithmeticException{ }
public class ExceptionExam2 {

  public static void main(String[] args) {
    int i = 10;
    int j = 0;
    int k = divide(i, j);
    System.out.println(k);
  }

  public static int divide(int i, int j) throws ArithmeticException{
    int k = i / j;
    return k;
  }
}
  • 메소드 선언 뒤에 throws ArithmeticException 이 적혀있는 것을 알 수 있음
  • 이렇게 적어놓으면 divide메소드는 ArithmeticException이 발생하니 divide메소드를 호출하는 쪽(main 메소드)에서 오류를 처리하라는 뜻
package javaStudy;
public class ExceptionExam2 {

  public static void main(String[] args) {
    int i = 10;
    int j = 0;
    try{
      int k = divide(i, j);
      System.out.println(k);
    } catch(ArithmeticException e){
      System.out.println("0으로 나눌 수 없습니다.");
    }
  }

  public static int divide(int i, int j) throws ArithmeticException{
    int k = i / j;
    return k;
  }
}

 

실습

  • 문제 설명
    • ExceptionExam클래스의 get50thItem메소드에서는 매개변수로 받은 array의 50번째 값을 return합니다. 만약 array의 크기가 50보다 작을 경우에는 ArrayIndexOutOfBoundsException이라는 예외가 발생하는데요. get50thItem이 ArrayIndexOutOfBoundsException를 throw하도록 정의해 보세요.
  • 해설
    • ExceptionExam클래스의 get50thItem메소드 뒤에 throws 예외클래스를 적고, ExamExam클래스의 ExceptionExam 호출부에 try-catch문을 적어줌
  • ExceptionExam클래스
public class ExceptionExam{
  public int get50thItem(int []array) throws ArrayIndexOutOfBoundsException{
    return  array[49];
  }
}
  • ExamExam클래스
public class ExamExam{
  public static void main(String[]args){
    try{
      ExceptionExam ex = new ExceptionExam();
    }catch(ArrayIndexOutOfBoundsException e){
      System.out.println("array의 크기는 50보다 작을 수 없습니다. : " + e.toString());
    }
  }
}


 

 Exception 발생시키기 

강제로 오류를 발생시키는 throw

  • throw는 오류를 떠넘기는 throws와 보통 같이 사용됨
public class ExceptionExam3 {   
  public static void main(String[] args) {
    int i = 10;
    int j = 0;
    int k = divide(i, j);
    System.out.println(k);
  }    
   
  public static int divide(int i, int j){
    int k = i / j;
    return k;
  }   
}
  • divide메소드는 2번째 파라미터( j )의 값이 0일 경우 나누기를 할 때 Exception이 발생

 

위의 코드를 에러가 발생하지 않게 수정

public class ExceptionExam3 {
  public static void main(String[] args) {
    int i = 10;
    int j = 0;
    int k = divide(i, j);
    System.out.println(k);      
  }

  public static int divide(int i, int j){
    if(j == 0){
      System.out.println("2번째 매개변수는 0이면 안됩니다.");
      return 0;
    }
    int k = i / j;
    return k;
  }
}
  • j가 0일 경우 안 된다는 메시지를 출력하고 k에 0을 리턴하도록 divide메소드를 수정
    • 이렇게 할 경우, main메소드의 k변수는 0값을 가지게 되고 0을 출력
  • 0으로 나눈 결과는 0이 아님. 0을 리턴하면 더 큰 문제가 발생할 수도 있음

 

에러도 발생하지 않고, 올바르지 않은 결과를 리턴하지 않도록 수정

  • throw로 Exception을 직접 발생시킴
    • 문법: throw new 예외클래스명( );
public class ExceptionExam3 {
  public static void main(String[] args) {
    int i = 10;
    int j = 0;
    int k = divide(i, j);
    System.out.println(k);
  }      
 
  public static int divide(int i, int j) throws IllegalArgumentException{
    if(j == 0){
      throw new IllegalArgumentException("0으로 나눌 수 없습니다.");
    }
    int k = i / j;
    return k;
  }   
}
  • j가 0일 경우, new연산자를 통해 IllegalArgumentException 객체가 만들어짐
  • new 앞의 throw는 해당 라인에서 Exception이 발생한다는 의미
    • 즉 그 줄에서 “0으로 나눌 수 없습니다.”라는 오류가 발생했다는 것
  • Exception클래스 이름을 보면 argument(인자)가 잘못되었기 때문에 발생한 오류라는 것을 알 수 있음

 

divide 메소드를 호출한 쪽에서의 오류 처리

  • divide 메소드 뒤의 throws IllegalArgumentException은 해당 오류는 divide를 호출한 쪽에서 처리를 해야 한다는 것을 의미
public class ExceptionExam3 {   
  public static void main(String[] args) {
    int i = 10;
    int j = 0;
    try{
      int k = divide(i, j);
      System.out.println(k);
    }catch(IllegalArgumentException e){
      System.out.println("0으로 나누면 안됩니다.");
    }           
  }

  public static int divide(int i, int j) throws IllegalArgumentException{
    if(j == 0){
      throw new IllegalArgumentException("0으로 나눌 수 없어요.");
    }
    int k = i / j;
    return k;
  }   
}
  • 결과
    • java.lang.IllegalArgumentException: 0으로 나눌 수 없습니다.

 

예외 처리 방법 정리

  • 1.  오류가 발생하는 메소드에서 예외 발생시키기
    • 문법: throw new 예외클래스명( );
  • 2.  직접 처리하거나 메소드 호출부에서 처리
    • 2-1. 직접 try-catch-finally문 이용
    • 2-2. 메소드가 내부적으로 처리하지 않고 메소드를 호출해 사용하는 쪽에서 try-catch-finally문 이용
      • 메소드 수정: 접근제한자 리턴타입 메소드명( ) throws 예외클래스명{ }
      • 호출부 수정: try-catch-finally 사용

 

실습

  • 문제 설명
    • ExceptionExam클래스의 get50thItem메소드에서는 매개변수로 받은 array의 50번째 값을 return합니다. 만약 array의 크기가 50보다 작을 경우에는 0을 return하고 있는데요. 0을 리턴하는 대신에 IllegalArgumentException을 throw하도록 만들어 보세요.
  • 해설
    • 0을 리턴하면 발생하는 문제를 예방하기 위해 get50thItem메소드에 일부러 Exception 발생시킴
      • if(array.length < 50){ throw new IllegalArgumentException("0으로 나누면 안 됩니다."); }
    • get50thItem메소드에서 직접 Exception을 처리하지 않고 호출부에서 처리하게 함
      • 메소드 뒤에 throws 예외클래스 적고
        • public int get50thItem(int []array)throws IllegalArgumentException{ { }
      • ExamExam클래스의 호출부에 try-catch문 적용
        • try{ ExceptionExam ex = new ExceptionExam( ); }catch(IllegalArgumentException e){ System.out.println(e.toString()); }
  • ExceptionExam클래스
public class ExceptionExam{
  public int get50thItem(int []array)throws IllegalArgumentException{
    if(array.length < 50){
      throw new IllegalArgumentException("array의 크기가 50부다 작아선 안 됩니다.");
    }
  return  array[49];
  }
}
  • ExamExam클래스
//아래는 실행을 위한 코드입니다. 수정하지 마세요.
public class ExamExam{
    public static void main(String[]args){
        ExceptionExam ex = new ExceptionExam();
    }
}


 

 사용자 정의 Exception 

Exception이나 Exception(RuntimeException 등)의 후손을 상속받아 만들어진, 사용자가 직접 정의해 사용하는 클래스

  • 클래스의 이름만으로 어떤 오류가 발생했는지 알려주어 코드의 직관성을 높임 (Exception클래스 자체의 특별한 기능은 없음)
  • 문법: public class 클래스명 extends 또는 RuntimeException{ }

  • 사용자 정의 Exception의 종류
    • Exception 클래스를 상속받아 정의한 checked Exception
      • 반드시 Exception을 처리해야만 하는 Exception
      • Exception을 처리하지 않으면 컴파일 오류를 발생시킴
    • RuntimeException 클래스를 상속받아 정의한 unChecked Exception
      • Exception을 처리하지 않아도 컴파일 시에는 오류를 발생시키지 않음 (그래도 적절히 처리해주는 게 좋음)

 

RuntimeException을 상속받은 BizException객체

  • BizException 클래스는 unChecked Exception
  • 클래스명은 알아보기 좋게 업무 수행 시 발생한 Exception이라는 의미로 BizException으로 정함
  • 사용자 정의 Exception 클래스에서는 생성자만 지정해주면 됨
    • String msg: 어떤 오류가 발생했는지 String 값으로 받음
    • super(msg): 메시지를 가지고 들어왔을 때 부모클래스에 메시지를 보냄
    • Exception ex: Exception을 받아들임
    • super(ex): 부모클래스에 Exception을 넘겨줌
  • 이미 부모클래스에 같은 기능을 하는 생성자를 가지고 있기 때문에 사용자 정의 Exception은 해당 생성자를 부모의 생성자에게 전달만 해주면 됨
public class BizException extends RuntimeException {
  public BizException(String msg){   //문자열로 된 오류메시지를 담는 생성자
    super(msg);
  }   
  public BizException(Exception ex){ //실제 발생할 Exception을 담는 생성자
    super(ex);
  }
}

 

BizService클래스는 업무를 처리하는 메소드를 가지고 있다고 가정

  • bizMethod: 업무를 처리하는 메소드
  • public void bizMethod(int i) throws BizException{ }
    • Exception이 발생했을 때 해당 Exception을 호출한 곳이 Exception을 처리하도록 떠넘김
  • throw new BizException(” “)
    • 의도적으로 Exception을 발생시켜서 BizException클래스로 String값을 전달
public class BizService {
  public void bizMethod(int i) throws BizException{
    System.out.println("비즈니스 로직이 시작합니다.");
    if(i < 0){
      throw new BizException("매개변수 i는 0 이상이어야 합니다.");
    }
    System.out.println("비즈니스 로직이 종료됩니다.");
  }
}

 

앞에서 만든 BizService를 이용하는 BizExam클래스

  • RuntimeException을 상속받는 메소드를 throws하고 있기 때문에 Exception 처리를 하지 않아도 컴파일 시 에러가 발생하진 않음. 그래도 처리해주는 게 좋음.
  • 매개변수 값이 0보다 작을 때는 Exception이 발생하기 때문에 try-catch블록으로 처리
  • e.printStackTrace( ): 에러 메세지의 발생 근원지를 찾아서 단계별로 에러를 출력
    • javaStudy.BizException: 출력문구 at javaStudy.BizService.bizMethod(BizService.java:7) at javaStudy.BizExam.main(BizExam.java:9)
public class BizExam {  
  public static void main(String[] args) {
    BizService biz = new BizService();
    biz.bizMethod(5);
    try{
      biz.bizMethod(-3);
    }catch(Exception e){
      e.printStackTrace();
    }
  }
}

 

실행결과

  • 비즈니스 로직이 시작합니다. 비즈니스 로직이 종료됩니다. 비즈니스 로직이 시작합니다. javaStudy.BizException: 매개변수 i는 0 이상이어야 합니다. at javaStudy.BizService.bizMethod(BizService.java:7) at javaStudy.BizExam.main(BizExam.java:9)

 

실습 1

  • 문제 설명
    • MyCheckedException클래스가 Checked Exception이 되도록 만들어 보세요.
  • 해설
    • MyCheckedException클래스를 Checked Exception로 만드려면 Exception클래스를 상속받아야 함
  • MyCheckedException클래스
public class MyCheckedException extends Exception{
  public MyCheckedException(){
    super();
  }
}
  • CheckedExam클래스
//아래는 실행을 위한 코드입니다. 수정하지 마세요.
public class CheckedExam{
  public static void main(String[]args){
    MyCheckedException ex = new MyCheckedException();
  }
}

 

실습 2

  • 문제 설명
    • 다음 코드를 실행하면 error: unreported exception MyCheckedException; must be caught or declared to be thrown 이라는 에러메시지가 나옵니다. get50thItem에서 Checked exception을 throw하는데 try/catch문으로 처리되고 있지 않기 때문입니다. 코드의 6번째줄을 try/catch문으로 처리해 보세요.
  • 해설
    • get50thItem메소드를 호출하는 ExceptionExam클래스에서 try-catch문으로 Exception을 처리해야 함
  • ExceptionExam클래스
public class ExceptionExam{
  public static void main(String[] args){
    ExceptionExam exam = new ExceptionExam();
    int[] array = new int[10];
    try{
      exam.get50thItem(array);
    } catch(Exception e){
      System.out.println(e.toString());
    }  
  }
    
  public int get50thItem(int []array) throws MyCheckedException{
    if(array.length < 50){
      throw new MyCheckedException();
    }
    return array[49];
  }
}
  • MyCheckedException클래스
public class MyCheckedException extends Exception{
    
}


 

 예외(Exception) 클래스 

Exception 클래스

  • 자바에서 모든 예외의 조상 클래스가 되는 Exception 클래스
  • Exception 클래스의 종류
    • RuntimeException 클래스
    • 그 외의 Exception 클래스의 자식 클래스(CheckedException)

  • UnCheckedException
    • RuntimeException 클래스를 상속받는 자식 클래스들은 주로 치명적인 예외 상황을 발생시키지 않는 예외들로 구성됨 > try-catch 문을 사용하기보다는 프로그램을 작성하면서 예외가 발생하지 않도록 주의를 기울이는 편이 좋음
  • CheckedException
    • Exception 클래스를 상속받는 자식 클래스들은 치명적인 예외 상황을 발생시키므로, 반드시 try / catch 문을 사용하여 예외를 처리해야만 함

 

예외 처리의 계층 관계

  • 예외가 발생하면, try 블록과 가장 가까운 catch 블록부터 순서대로 검사
  • 여러 개의 catch 블록을 사용할 때는 Exception 클래스의 계층 관계에도 주의를 기울여야 함
try {
  System.out.write(list);
} catch (Exception e) {
  e.printStackTrace();
} catch (IOException e) {
  e.printStackTrace();
}
  • IOException이 발생하면, 자바는 첫 번째 catch 블록부터 순서대로 해당 예외를 처리할 수 있는지를 검사함
  • 그런데 IOException은 Exception의 자식 클래스이므로, IOException을 비롯한 Exception 클래스의 자식 클래스에 해당하는 예외가 발생하면, 언제나 첫 번째 catch 블록에서만 처리됨
  • = catch 블록의 순서를 위의 예제처럼 작성하면, 두 번째 catch 블록은 영원히 실행되지 못함
try {
  System.out.write(list);
} catch (IOException e) {
  e.printStackTrace();
} catch (Exception e) {
  e.printStackTrace();
}
  • IOException만을 따로 처리하고자 한다면, 다음 예제처럼 catch 블록의 순서를 변경해야 함

 

여러 예외 타입의 동시 처리

try {
  this.db.commit();
} catch (IOException e) {
  e.printStackTrace();
} catch (SQLException e) {
  e.printStackTrace();
}
  • Java SE 7부터는 '|' 기호를 사용하여 하나의 catch 블록에서 여러 타입의 예외를 동시에 처리할 수 있음
try {
  this.db.commit();
} catch (IOException | SQLException e) {
  e.printStackTrace();
}

 

Throwable 클래스

  • 모든 예외의 조상이 되는 Exception 클래스와 모든 오류의 조상이 되는 Error 클래스의 부모 클래스
  • Throwable 타입과 이 클래스를 상속받은 서브 타입만이 자바 가상 머신(JVM)이나 throw 키워드에 의해 던져질 수 있음
메소드  설명
String getMessage( ) 해당 throwable 객체에 대한 자세한 내용을 문자열로 리턴
void printStackTrace( ) 해당 throwable 객체와 표준 오류 스트림(standard error stream)에서 해당 객체의 스택 트레이스(stack trace)를 출력
String toString( ) 해당 throwable 객체에 대한 간략한 내용을 문자열로 리턴