티스토리 뷰

Java/개념정리

[Java] 예외 처리

통통푸딩 2021. 10. 6. 23:27
반응형

자바 프로그램을 사용하다가 프로그램이 비정상적으로 종료될 때가 있다.
이런 원인을 에러(Error)라고 한다.


에러는 컴파일 에러런타임 에러로 구분할 수 있다.
컴파일 에러는 컴파일 과정에서 발생하는 에러, 런타임 에러는 실행(런타임) 과정에서 발생하는 에러를 말한다.

컴파일 에러는 기본적으로 자바 컴파일러가 문법 검사를 통해 에러를 잡아내주기 때문에 , 그 에러를 수정하면 프로그램을 실행할 수 있다.

런타임 에러는 컴파일 시에 에러가 발생하지 않더라도 실행(런타임) 과정에서 에러가 발생하는 것을 말한다.
런타임 에러를 막기 위해서는 프로그램 실행 과정에서 일어날 수 있는 에러의 경우의 수를 모두 고려해 방지해야 한다.



자바에서 런타임 에러를 에러(Error)예외(Exception) 두가지로 구분한다.

에러(Error)의 패키지는 java.lang.error 이다. 에러는 스택오버플로우(StackOverFlowError)나 메모리 부족(OutOfMemoryError) 와 같이 JVM이나 하드웨어 등의 기반 시스템 문제로 발생하는 것을 말한다.
이런 에러는 개발자가 복구할 수 없다. 프로그램이 비정상적으로 종료되기 때문에 애초에 발생하지 않도록 하는 것이 최선이다.

예외(Exception)의 패키지는 java.lang.Exception 이다. 예외는 NullPointerException 과 같은 사용자의 잘못된 조작이나 개발자의 코드 작성이 잘못되었을 경우 발생한다. 이런 예외는 개발자가 복구할 수 있다. 미리 적절한 코드를 작성해서 프로그램이 비정상적으로 종료되지 않도록 예외 처리를 할 수 있다.



1. 자바 예외 처리 방법

try - catch

try 블록에는 여러 개의 catch 블록이 올 수 있다.
try 블록에서 예외가 발생할 경우 바로 catch 블록으로 넘어간다.
발생한 예외(Exception)의 종류와 일치하는 하나의 catch 블록만 수행된다.

try {
    // 예외가 발생할 가능성이 있는 코드
} catch (Exception1 e1) {
    // Exception1이 발생했을 때, 이를 처리하기 위한 코드
} catch (Exception2 e2) {
    // Exception2가 발생했을 때, 이를 처리하기 위한 코드
} catch (ExceptionN eN) {
    // ExceptionN이 발생했을 때, 이를 처리하기 위한 코드
}

첫번째 catch 블록에서 Exception1은 예외 클래스, e1은 예외 클래스의 인스턴스를 가리키는 참조 변수이다.
예외 클래스는 자바에 있는 Exception과 Error의 superClass인 Throwable을 상속한 클래스 중 하나여야 한다.

아래 예제처럼 참조변수를 통해서 예외에 대한 정보에 접근할 수 있다.

public class ExceptionExample {
   public static void main(String[] args){
      int a = 10;
      int b = 0;

     try {
            // 10을 0으로 나눴으므로 예외 발생
            int c = a / b;

      } catch (IllegalArgumentException e) {
          System.out.println(e.getClass().getName());
          System.out.println(e.getMessage());
      } catch (ArithmeticException e) {
          System.out.println(e.getClass().getName());
          System.out.println(e.getMessage());
      } catch (NullPointerException e) {
          System.out.println(e.getClass().getName());
          System.out.println(e.getMessage());
      }
   }
}

/* 실행 결과

Task :ExceptionExample.main()
java.lang.ArithmeticException
/ by zero

*/

발생한 예외(ArithmeticException)가 속한 클래스의 catch 블록만 실행된 것을 볼 수 있다.
두번째 블록이 실행되었고, 참조변수를 통해서 해당 예외 정보들을 출력했다.
catch 블록에서는 예외 메세지를 출력하는 것 뿐 아니라 복구 작업을 할 수 있다.

자바 7 이후부터는 아래처럼 하나의 catch 블록에서 하나 이상의 Exception 타입을 작성할 수 있다.
이 때, 나열된 예외 클래스들이 부모-자식 관계라면 오류가 발생한다.

catch (IOException | SQLException ex){
    //에러 처리 코드
}

 

finally

finally는 try-catch 블록과 함께 예외 발생 여부와 상관없이 항상 코드를 실행하기 위함이다.
심지어 try,catch 블록에 return문이 있더라도 finally 블록은 실행된다.

finally 블록은 선택적으로 작성할 수 있고, catch 블록 뒤에 작성한다.
finally 블록은 클린업 코드를 넣어서 resource leak를 막을 용도로 사용하는게 좋다.

예외가 발생한 경우 : try→catch→finally 순으로 실행
예외가 발생하지 않은 경우 : try→finally 순으로 실행

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

     try {
        System.out.println("try 블록");
      } catch (ArithmeticException e) {
        System.out.println("catch 블록");
      } finally {
        System.out.println("finally 블록");
      }
   }
}

/* 실행 결과

Task :ExceptionExample.main()
try 블록
finally 블록

*/

 

try-with-resources

자바 7부터 자원 해제를 자동으로 해주는 try-with-resources문이 추가되었다.
원래 File input이나 DB connection 생성 같이 시스템 자원을 사용하는 경우 finally블록에서 자원을 close 하는 형태로 작성했다.

try-with-resources 문은
try 블록에서 사용할 resource를 선언하는 식으로 사용한다.
finally 블록에서 자원 반환 코드를 작성하지 않아도 자원(resource)을 자동으로 반환하기 위해 사용한다.
여기서 말하는 자원들은 java.lang.AutoClosable이나 java.io.Closable을 구현한 오브젝트를 말하며 반드시 close 되어야 하는 자원이다.

/* try-catch-finally문

public static void main(String args[]) throws IOException {
    FileInputStream fis = null;
    BufferedInputStream bis = null;
    try {
        fis = new FileInputStream("file.txt");
        bis = new BufferedInputStream(fis);

    } catch (IOException e) {
        // 에러 처리
    } finally {
        // 어떤 경우에도 반드시 자원을 닫아야함
        if (fis != null) fis.close();
        if (bis != null) bis.close();
    }
}

*/

//try-with-resources문
public static void main(String args[]) {
    try (
        FileInputStream fis = new FileInputStream("file.txt");
        BufferedInputStream bis = new BufferedInputStream(fis)
    ) {
        //do something
    } catch (IOException e) {
        // 에러처리
    }
}

try 옆에 괄호를 열고 자원을 할당받으면 해당 자원은 블록 수행 이후에 자동으로 반환이 된다.

throw

때로 예외를 발생시켜야 할 때가 있는데, throw 키워드로 고의로 예외를 발생시킬 수 있다.
이런 발생시킬 수 있는 예외는 Exception의 최상위 클래스인 Throwable class의 하위 클래스라면 모두 가능하다.
그래서 Throwable class를 상속받은 사용자 정의 클래스를 만들어서 예외를 발생시킬 수도 있다.
예를 들어서 사용자가 "멍청이"라는 아이디를 사용하지 못하게 하고 싶으면 throw로 예외를 발생시킬 수 있다.

public class ExceptionExample {

    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);

        System.out.println(“아이디를 입력하세요.”);
        String userId= scanner.nextLine();

        try {
            if (userId.equals(“멍청이”)) {
                throw new IllegalArgumentException(“부적절한 아이디입니다.”);
            }
        } catch (IllegalArgumentException e) {
            System.out.println(e.getMessage());
        }
    }
}

/* 실행 결과

Task :ExceptionExample.main()
아이디를 입력하세요.
멍청이
부적절한 아이디입니다.

*/
throw new IllegalArgumentException(“부적절한 아이디입니다.”);

/* 위 코드는 아래 코드와 같다. */
IllegalArgumentException e = new IllegalArgumentException(“부적적한 아이디입니다.”);
throw e;

 

throws

catch 블록으로 예외를 처리하는 것도 좋지만, 때로는 현재 메소드가 아닌
Call Stack의 상위 메소드에서 예외를 처리하게 두는 것이 좋을 때도 있다.
그 때 사용하는 키워드가 throws 이다.
메소드 선언부에 예외를 선언할 수 있고, 여러 개 예외를 쉼표로 구분해서 선언할 수 있다.

void method() throws Exception1, Exception2, Exception3 {
    // ...
}


throws는 해당 메소드에서 예외를 처리하지 않고, 메소드를 사용할 때 사용하는 쪽에서 예외를 처리하도록 책임을 전가하는 것이다.

public class ExceptionExample {

    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);

        System.out.println(“아이디를 입력하세요.”);
        String userId= scanner.nextLine();

        try {
            checkId(userId);
        } catch (IllegalArgumentException e) {
            System.out.println(e.getMessage());
        }
    }

   static void checkId(String userId) throws IllegalArgumentException {
    if (userId.equals(“멍청이”)) {
           throw new IllegalArgumentException(“부적절한 아이디입니다.”);
        }
   }
}

checkId 메소드 내에서 해당 예외에 대한 처리를 하지 않고 throws로 예외를 선언하기만 했다.
이렇게 하면, 이 메소드를 사용하는 사람들이 각자 상황에 맞게 예외를 처리할지 선택하여 구현 할 수 있다.




2. 자바가 제공하는 예외 계층 구조

자바에서 런타임 에러(Exception, Error)를 클래스로 정의하고 있다.
아래 사진은 예외와 에러의 계층 구조이다.

출처:  https://madplay.github.io/post/java-checked-unchecked-exceptions

Exception과 Error는 Throwable클래스를 상속받고 있다.
이런 계층 구조를 아는 것은 유용하다.
catch 블록을 작성할 때 잡을 예외클래스를 상위 클래스로 작성한다면 하위클래스의 예외클래스들도 catch 할 수 있기 때문이다.

위에서 Unchecked Exception은 컴파일 시점에서 확인될 수 없는 예외, Checked Exception은 컴파일 시점에서 확인될 수 있는 예외이다.

Unchecked Exception

자바에서 Exception에서 RuntimeException 클래스를 상속받은 하위 클래스, Error를 말한다.
컴파일 시점에서 확인되지 않기 때문에, 일반적으로 발생을 예측할 수 없는 예외다.
주로 프로그래밍 에러나 외부 API 사용 때문에 발생한다.
이 예외들은 컴파일러가 예외를 처리하거나 선언하도록 강제하지 않기 때문에 프로그래머가 알아서 처리를 해야한다.

Checked Exception

만약 코드 내에서 Checked Exception을 발생시킨다면, 그 예외는 반드시 처리되거나 메소드의 선언부에서 throws 해줘야한다.
그렇지 않으면 컴파일 자체가 안된다.

출처 :  https://wisdom-and-record.tistory.com/46

 

왜 Unchecked Exception과 Checked Exception을 나눠놓았을까?

이것에 관해서 오라클 공식 문서에서 설명하고 있다.
https://docs.oracle.com/javase/tutorial/essential/exceptions/runtime.html

 

Unchecked Exceptions — The Controversy (The Java™ Tutorials >                     Essential Java Classes > Exceptions)

The Java Tutorials have been written for JDK 8. Examples and practices described in this page don't take advantage of improvements introduced in later releases and might use technology no longer available. See Java Language Changes for a summary of updated

docs.oracle.com

메소드를 호출하는 쪽은 그 메소드가 어떤 예외를 발생시킬 수 있는가에 반드시 알아야 한다.
그래서 자바는 checked exception을 통해 그 메소드가 발생시킬 수 있는 예외를 명세하도록 강제하고 있다고 볼 수 있다.


Runtime Exception은 왜 예외를 강제로 명세하지 않아도 되도록 했을까?
Runtime Exception은 프로그램 코드의 문제로 발생하는 예외이다.
즉, 코드를 작성한 개발자의 잘못 때문에 발생한다.
따라서 메소드를 호출하는 쪽에서 이를 복구하거나 대처할 수 있을 거라고 예상하긴 어렵다.

또 Runtime Exception은 프로그램 어디서나 매우 빈번히 발생할 수 있다.
모든 Runtime Exception을 메소드에 명시하도록 강제하는 것은 프로그램의 명확성을 떨어뜨릴 수 있다.

그래서 클라이언트에서 exception을 적절히 대처할 수 있을 것이라고 예상되는 경우 checked exception으로 만들고, 그렇지 않은 경우 unchecked exception으로 만드는 것이 좋다.


자바 기본 Exception

1) Arithmetic exception
산술 연산에서 예외 조건이 발생했을 때 발생한다.

2) ArrayIndexOutOfBounds Exception
잘못된 인덱스로 Array에 접근했을 때 발생한다. 인덱스가 배열 크기의 범위를 벗어났을 때다. 즉 , 인덱스가 음수이거나 배열 크기보다 크거나 같을때다.

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

4) FileNotFoundException
파일에 접근할 수 없거나 열리지 않을 때 발생한다.

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

6) InterruptedException
스레드가 watiting, sleeping 또는 어떤 처리중일 때 interrupt가 되면 발생한다.

7) NoSuchMethodException
찾을 수 없는 메소드에 접근할 때 발생한다.

8) NullPointerException
null 객체의 멤버를 참조할 때 발생한다.

9) NumberFormatException
메소드가 문자열을 숫자 형식으로 변환할 수 없는 경우 예외가 발생한다.
예를 들어서, "hello"라는 문제를 숫자 형식으로 변환하려할 때.

10) StringIndexOutOfBoundsException
문자열에 접근하는 인덱스가 문자열의 크기 범위에 벗어나는 인덱스일 때 발생한다.




3. 사용자 정의 예외 만들기

자바에서 기존에 정의되어있는 예외 클래스 말고도 사용자의 필요에 따라 새로운 예외를 정의할 수 있다.
Exception 클래스나 알맞은 예외 클래스를 상속받아 만들면 된다.

public class OutOfMemberNumException extends Exception{
    public OutOfMemberNumException (){} // 기본 생성자
    public OutOfMemberNumException (String message){
        super(message); // 에러메세지 생성자
    }
}
public void addMember() throws OutOfMemberNumException {
    if(memberNum > 100){
        throw new OutOfMemberNumException("멤버 번호가 100을 초과할 수 없습니다.");
    }
    else memberNum++;
}

멤버를 추가할 때 멤버 수가 100을 초과하면 예외를 발생시킬 수 있도록 throw해주었다.
위의 경우 Checked Exception에서 커스텀 예외를 만든 것이다. 그래서 Exception 클래스를 상속받았다.
오류가 런타임에서 감지된다면 필요에 따라 Unchecked Exception 에서 커스텀 예외를 만들면 된다.
이 때는 RuntimeException 클래스를 상속받아야 한다.




4. Chained Exception

체인처럼 한 예외가 다른 예외를 발생시킬 수도 있다.
예외 A가 예외 B를 발생시켰다면 A를 B의 원인 예외(cause exception)라고 한다.
원인 예외는 initCause() 로 지정할 수 있고, 이 함수는 Throwable 클래스에 정의되어있어서 모든 예외 클래스에서 사용할 수 있다.

public class ExceptionExample {

    public static void main(String[] args) {
        try {
            methodExample(0);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    static void methodExample(int num) throws IOException{
        try {
            if (num == 0) {
                throw new IllegalArgumentException();
            }
        } catch (IllegalArgumentException e) {
            IOException ioException = new IOException();

        // IOException의 예외를 IllegalArgumentException으로 지정
        ioException.initCause(e);

        throw ioException;
        }
    }
}

이 방식으로 원인 예외를 지정하여 예외를 발생시키면 여러가지 예외를 하나의 큰 분류의 예외로 묶어서 다룰 수 있다.
연결되는 예외는 상속 관계가 아니어도 연결할 수 있다.

이런 방식을 이용하여 Checked Exception을 Unchecked Exception으로 바꿀 수도 있다.

static void methodExample(int num){
    try {
        if (num == 0) {
            throw new IllegalArgumentException();
        }
    } catch (IllegalArgumentException e) {
    //RuntimeException(Throwable cause) -> 원인 예외를 등록하는 생성자
        throw new RuntimeException(e); 
    }
}
반응형

'Java > 개념정리' 카테고리의 다른 글

[Java] enum(열거형)  (0) 2021.10.14
[Java] 멀티쓰레드 프로그래밍  (0) 2021.10.14
[Java] 인터페이스(interface)  (0) 2021.10.06
[Java] 자바 package  (0) 2021.09.28
[Java] 상속  (2) 2021.09.27
댓글
반응형
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday