Kim VamPa

[JAVA 스터디] 9주차 과제 : 예외 처리 본문

스터디/자바 스터디

[JAVA 스터디] 9주차 과제 : 예외 처리

Kim VamPa 2021. 1. 16. 03:17
728x90
반응형

목차 

1. Exception과 Error의 차이

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

   2.1 자바 예외 구조

   2.2 UncheckedException vs CheckedException

3. 자바에서 예외 처리 방법

   3.1 예외가 발생하면 어떻게 되는가?

   3.2 예외 처리란?

   3.3 try-catch문

      3.3.1 try-catch 흐름

      3.3.2 다중 catch

      3.3.3 멀티 catch

   3.4 try-catch-finally

   3.5 try-catch-resources

   3.6 throw

   3.7 thorws

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

 

 

 

1. Exception과 Error의 차이

Error : 프로그램 코드에 의해서 수습될 수 없는 심각한 오류
Exception : 프로그램 코드에 의해서 수습될 수 있는 다소 미약한 오류

 

 Error란 개발자의 잘못이 아닌 개발한 애플리케이션의 구동 환경에 의해서 발생하는 오류입니다. 해당 오류는 일단 발생하면 개발자가 직접 복구할 수 없고 비정상적으로 종료가 됩니다. 자바에서 뜨는 대표적인 Error의 예로는 StackOverflowError, OutofMemoryError이 있습니다. 그 외에도 애플리케이션이 구동되는 운영체제에 문제가 생겨서 멈추는 경우도 Error라고 할 수 있습니다.

 Exception은 개발자가 작성한 코드가 의도한 것과 다른 상황에 직면했을 때 발생하는 오류입니다. 이 오류도 발생을 하게 되면 비정상적으로 종료가 되지만 Error와 다르게 개발자가 개입하여 복구가 가능합니다. 더불어 Exception이 발생하기 전에 개발자가 적절한 코드를 미리 작성해놓는다면 애플리케이션의 비정상적인 종료도 막을 수 있습니다. 정수를 0으로 나누거나, 파일을 읽는 코드를 작성하였지만 살행 할 땐 읽을 파일이 없는 경우 등이 Exception의 예입니다.

 

 

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

2.1 자바 예외 구조

 Java에서는 실행 시 발생할 수 있는 예외(Exception)와 에러(Error)들을 클래스(Class)들로 정의를 하였습니다. 정의된 에러, 예외 클래스들 간의 관계가 계층 구조입니다. Java에서 정의해놓은 에러와 예외 클래스들의 구조는 아래 그림과 같습니다.

 

그림 1

 

 정의된 여러 에러(error) 관련 클래스들은 Error 클래스를 상속받고, 여러 예외(exception) 관련 클래스들은 Exception 클래스를 상속받습니다. Error 클래스와 Exception 클래스는 Throwable 클래스를 상속받습니다. 따라서, 모든 에러, 예외 클래스들의 최상위 클래스는 Throwalbe 클래스입니다. (Throwable 클래스를 최상위 클래스라고 표현한 이유는 모든 에러, 예외 클래스들의 틀 안에서 최상위 클래스라는 의미입니다. 왜냐하면 모든 클래스들의 조상은 Object이기 대문에 실질적 최상위 클래스는 Object 클래스입니다.)

 

 우리가 자세히 알고자 하는 클래스들은 예외(Exception) 클래스 이기 때문에 예외 클래스를 좀 더 자세히 살펴보겠습니다. Exception 클래스의 상속계층도는 다음과 같습니다.( 예외 클래스들 중에서 몇 개의 중요 클래스들만을 나열하겠습니다.)

 

그림 2

 

 그림 2를 보면 exception 클래스를 상속받은 여러 예외 클래스들이 세분화된 것을 볼 수 있습니다. Exception 클래스를 상속받는 예외 클래스들은 'RuntimeException 클래스와 그 자손 클래스들'과 'RuntimeException 클래스 이외의 예외 클래스들'로 크게 두 그룹으로 분류할 수 있습니다.

 

 RuntimeException 클래스와 그 자손 클래스들은 주로 프로그래머의 실수에 의해서 발생될 수 있는 예외입니다. 주요 예외로는 ArrayIndexOutOfBoudsEception(배열의 범위 벗어남), NullPointerException(Null인 참조 변수 호출), ClassCastException(잘못된 클래스 형 변환), ArithemeticException(정수를 0으로 나눔)이 있습니다.

 

 RuntimeException 이외의 클래스들은 주로 외부의 영향으로 발생할 수 있는 예외입니다. 주로 사용자들의 행동에 의해서 발생하는 경우가 많습니다. 해당 주요 예외로는 FileNotFoudException(존재하지 않는 파일의 이름 입력), ClassNotFoundException(실수로 클래스의 이름을 잘못 작성), DataFormatException(잘못 입력된 데이터 형식)이 있습니다.

 

 두 그룹은 좀 더 정확히는 Unchecked Exception(RuntimeException 클래스와 그 자손 클래스)과 CheckedException(RuntimeException 클래스 이외의 예외 클래스)로 불립니다.  다음 순서에서 왜 Chekced와 Unchecked가 불리는지와 각 그룹의 특징에 대해서 좀 더 자세히 알아보겠습니다.

 

 

2.2 ChekcedException vs UncheckedException

컴파일러가 예외처리 강요 유무에 따라 CheckedException과 UncheckedException으로 구분

 

  Checked Exception Unchecked Exception
예외처리 강요 유무 반드시 예외를 처리해야 함 예외처리를 강제하지 않음
예외 확인 시점 컴파일 단계 실행단계
예외발생시 
트랜잭션 처리
roll-back 하지 않음 roll-back 함

 

 CheckedException은 RuntimeException과 그 자손 클래스들을 제외한 모든 예외 클래스를 의미하고, Unchecked Exception은 RumtimeException과 그 자손 예외 클래스를 의미합니다. Checked와 Unchecked를 구분하는 기준은 '컴파일러가 예외처리를 강요하느냐'입니다. CheckedException이 발생할 가능성이 있는 메서드를 호출할 경우 컴파일러는 해다으 메서드에 try/catch, 혹은 throws로 던져서 처리하도록 경고 표시와 같은 것을 통해서 강요를 합니다. 반대로 UncheckedException의 경우 예외처리를 하지 않아도 아무런 문제가 없습니다.

 

그림 3

 

 CheckedException은 앞 순서에서 설명하였듯이 보통 애플리케이션의 사용자와 같이 외적인 요인들로 인해서 발생합니다. 외적인 요인들은 개발자가 관리를 할 수 있거나 예상을 할 수 있는 범주가 아니기 대문에 필연적으로 예외상황을 일으킬 가능성이 매우 큽니다. 따라서 개발자가 발생할 것이라고 예상이 가능한 예외입니다. 자바 언어 설계자들은 체계적으로 어떠한 코드(메서드)들이 어떠한 종류의 예외들을 일으킬 가능성이 큰지를 정리하였고 이를 바탕으로 컴파일러에서 발생 가능성이 큰 예외를 체크하고 예외처리를 강요하도록 설계를 하였습니다. 

 

 반면 UncheckedException의 경우 개발자의 부주의로 인해서 발생합니다. 개발자가 언제 어디에서 어떻게 실수를 할지 모르기 때문에 해당 예외들을 예상을 할 수가 없습니다. 따라서 CheckedException과 다르게 컴파일러가 예외를 찾아낼 수 없고, 직접 코드를 실행함으로써 예외를 발견할 수 있습니다.  컴파일러에서 체크할 수 없고 직접 실행해야만 예외를 발견할 수 있기 때문에 UncheckedException, RuntimeEception이라고 불립니다.

 

 또 하나의 CheckedException과 UncheckedException의 큰 차이점은 예외 발생 시 트랜잭션의 롤백 유무입니다. ChekcedException의 경우 롤백을 하지 않습니다. 반대로 UncheckedException의 경우 롤백을 합니다. 이렇게 되도록 설계된 이유를 추측해보면 CheckedException의 경우 앞서 말했듯이 예상이 가능한 예외이고, UncheckedException의 경우 예상이 불가능한 예외이기 때문에 롤백이 되도록 설계되지 않았을까 생각합니다. 해당 사례에 대해 관심이 잇으시다면 아래의 포스팅 글을 한번 읽으셔도 좋을 거 같습니다.

 

woowabros.github.io/experience/2019/01/29/exception-in-transaction.html

 

응? 이게 왜 롤백되는거지? - 우아한형제들 기술 블로그

이 글은 얼마 전 에러로그 하나에 대한 호기심과 의문으로 시작해서 스프링의 트랜잭션 내에서 예외가 어떻게 처리되는지를 이해하기 위해 삽질을 해본 경험을 토대로 쓰여졌습니다. 스프링의

woowabros.github.io

 

3. 자바에서 예외 처리 방법

3.1 예외가 발생하면 어떻게 되는가?

 

 예외처리에 알아보기 앞서서 코드가 실행될 때 예외가 발생하면 어떠한 현상이 일어나는 지를 보겠습니다. 아래의 코드는 숫자를 순서대로 출력하는 간단한 코드입니다. 숫자 3이 출력되는 코드에 3을 0으로 나누어서 'ArithemeticException' 고의로 발생시키도록 하였습니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class ExceptionApp {
 
    public static void main(String[] args) {
    
        System.out.println(1);
        System.out.println(2);
        System.out.println(3/0);
        System.out.println(4);
        System.out.println(5);
        System.out.println(6);
    
    }
 
}

 

결과는 아래와 같습니다.

 

그림 4

 

 예외가 발생이 되어 나타난 현상은 다음 두 가지입니다.

1. 발생한 코드에서 비정상적으로 종료되어 예외가 발생한 코드 아래의 코드는 실행이 되지 않는다.
2. console창에 예외의 발생 원인과 위치를 출력

 

 

3.2 예외 처리란?

예외처리 : 프로그램 실행 시 발생할 수 있는 예외에 대비한 코드를 작성하는 것

 3.1에서 보았듯이 예외가 발생하게 되면 프로그램이 비정상적으로 종료되게 됩니다. 하지만 예외가 발생하는 코드에 예외처리를 하게 된다면 예외에 대비해 작성해둔 코드가 실행이 되면서 프로그램의 비정상적 종료를 막고 정상적인 실행상태를 유지할 수 있습니다. 예외처리를 하면 처리하지 않을 때와 어떻게 다른지 직접 코드를 실행해서 알아보겠습니다. 위에서 사용한 예제에서 예외가 발생시키는 코드에 try-catch문을 사용하여 예외 처리하였습니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class ExceptionApp {
 
    public static void main(String[] args) {
    
        System.out.println(1);
        System.out.println(2);
        try {
            System.out.println(3/0);
        } catch(ArithmeticException e) {
            System.out.println("예외가 발생하였습니다.");
        }        
        System.out.println(4);
        System.out.println(5);
        System.out.println(6);
    
    }
 
}
 

 

결과는 아래와 같습니다.

 

그림 5

 

 결과를 보시면 3.1의 결과와는 다르게 예외가 발생한 코드에서 예외에 대비해서 작성해둔 코드가 출력이 되었고, 예외가 발생한 코드에서 종료가 되지 않고 그 아래의 코드들도 정상적으로 실행이 된 것을 볼 수 있습니다.

 

3.3 try-catch문

 try-catch문의 구조와 작성방법은 아래와 같습니다.

 

1
2
3
4
5
6
7
 
    try {
        // 에외가 발생할 가능성이 잇는 문장들
    } catch(Exception1 e) {    // Exception1 => 예외가 발생할 수 있는 예외클래스
        // Exception1이 발생했을 경우, 이를 처리하기 위한 문장
    }
 

 

- try 블록 {} 에는 예외가 발생할 가능성이 있는 문장들을 작성합니다.

- catch 괄호()에는 발상핼 가능성이 있는 예외(exception) 클래스 타입의 참조 변수를 선언합니다.

- catch 블록 {} 에는 catch 괄호()에서 선언한 참조 변수 타입의 예외가 발생했을 때, 이를 처리하기 위한 문장을 작성합니다. 

 

 try-catch문에서 try블럭에 예외가 발생할 코드를 작성할 때 블록에 포함시킬 코드의 범위를 신중히 선택해야 합니다. try 블록에서 예외가 발생한 위치 이후에 있는 try 블록의 문장들은 실행되지 않기 때문입니다. 아래의 예제 코드를 보겠습니다. try 블록에 3,4,5를 서대로 출력되도록 작성하였고 3과 4 사이에 예외를 발생시키는 코드를 작성하였습니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
 
public class ExceptionApp {
 
    public static void main(String[] args) {
    
        System.out.println(1);
        System.out.println(2);
        try {
            System.out.println(3);
            System.out.println(3/0);
            System.out.println(4);
            System.out.println(5);
        } catch (ArithmeticException e) {
            System.out.println("예외가 발생");
        }
        System.out.println(6);
        System.out.println(7);
        
    }
 
}
 

 

그림 6   결과

 

 결과를 보시면 try 블록에 포함되었더라도 예외가 발생을 하게 되면 발생한 위치 이후의 try 블록 내의 문장들은 실행이 되지 않습니다.

 

3.3.1 try-catch문의 흐름

 try-catch문의 전체적인 흐름은 다음과 같습니다.

 

그림 7

 

 try-catch문에서 좀 더 세부적으로 어떻게 동작하는지를 알아보겠습니다.(예외가 발생한 상황을 중점으로) try 블록{} 문장이 실행이 먼저 됩니다. 만약 try블록{} 안의 문장에서 예외가 발생을 하게 되면 예외가 발생한 코드에서 실행이 멈추고 발생한 예외에 해당하는 예외 클래스의 인스턴스가 자동적으로 생성되게 됩니다. (Arithemetic 예외가 발생을 하면 ArithemeticException 인스턴스가 생성됩니다.)

 그다음 생성된 예외 클래스 인스턴스가 catch 괄호에 선언된 예외 클래스 타입의 참조 변수와 타입이 같은지 체크를 합니다. 만약 catch문이 여러 개일 경우에는 첫 번째 catch문부터 순차적으로 내려가면서 catch의 참조 변수 타입을 체크하게 됩니다.

 타입의 일치 여부 검사를 확인하는 수단'instanceof 연산자'를 통해서입니다. 연산자의 결과값이 ture인 경우 해당 catch문의 블록에 작성한 코드를 실행하게 되고 try-catch문을 빠져나가게 됩니다. 하지만 연산 결과값이 false인 경우 그다음 catch문의 참조 변수 타입과 일치 여부를 검사하게 되고, 작성된 catch의 참조 변수들 중 일치하는 타입을 찾지 못하게 되면 예외처리를 하지 않았을 때처럼 예외가 발생합니다.

 

3.3.2 다중 catch

 tyr-catch문에서는 하나 이상의 catch 블록을 작성할 수 있습니다. try블록에서 예외가 발생했을 때 모든 catch문이 실행이 되는 것이 아니라, 발생한 예외와 종류가 일치하는 예외 클래스 타입의 참조 변수를 선언한 catch블록의 코드를 실행하게 됩니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
try {
        // 에외가 발생할 가능성이 잇는 문장들
    } catch(Exception1 e) {    // Exception1 => 예외가 발생할 수 있는 예외클래스
        // Exception1이 발생했을 경우, 이를 처리하기 위한 문장
    } catch(Exception2 e) {    
        // Exception2이 발생했을 경우, 이를 처리하기 위한 문장
    } catch(Exception3 e) {    
        // Exception3이 발생했을 경우, 이를 처리하기 위한 문장
    } catch(Exception4 e) {    
        // Exception4이 발생했을 경우, 이를 처리하기 위한 문장
    }
 

 

※ 다중 Catch문의 예외 클래스 타입 참조 변수의 순서는 상속관계를 고려하여 배치해야 합니다.

 다중 catch에서 선언하는 참조 변수의 예외 클래스 타입이 서로 상속 관계가 아니라면 순서를 고려 할 필요는 없습니다. 하지만 참조 변수의 예외 클래스 타입이 서로 상속 관계에 있다면 좀 더 신중히 배치를 해야 합니다. 조상 예외 클래스 타입의 참조 변수를 선언하는 catch문을 제일 상단에 배치를 하게 되면 문제가 생기게 되기 때문입니다. 예를 통해 이해해보겠습니다.

 

1
2
3
4
5
6
7
8
9
10
11
 
        try {
            // ...     
        } catch (Exception e) {
            
        } catch (ArithmeticException e) {
            
        } catch (ArrayIndexOutOfBoundsException e) {
            
        }
 

 

 예외 클래스의 참조변수 타입으로 Exception, ArithmeticException, ArrayIndexOutOfBoundsException을 사용하였습니다. 작성을 하였다는 것은 해당 유형의 예외들이 맞는지 각각 한 번씩 체크를 해보고 싶다는 의도가 있을 것입니다. 

 하지만 위와 같이 작성을 하게 된다면 문제가 생깁니다. 우리는 3.3.1에서 catch문이 여러 개일 경우 제일 상단의 catch문부터 순차적으로 발생한 예외와 일치하는 것인지를 체크한다고 하였습니다. 그런데 위의 코드는 제일 첫 번째 catch에 선언된 참조 변수 타입이 나머지 다른 catch에 선언된 참조 변수 타입의 조상 클래스를 사용하고 있습니다. 따라서 첫 번째 catch문에서 작성한 예외 클래스 참조 변수 타입이 발생한 예외와 같다고 판단하여 바로 첫 번째 catch 블록의 코드를 실행하게 될 것입니다. 결과적으로 아래의 catch문들은 실행 기회 자체를 잃게 됩니다. 더불어 해당 코드는 컴파일러에서 방금 설명한 것과 같은 이유로 문제가 있다는 에러를 표시합니다. 

 

그림 9

 

 이러한 상황을 안 나게 하려면 다중 catch문을 사용할 때 각 catch문에 선언하는 참조 변수 타입들 간의 관계를 파악하여 자손 예외 클래스 타입의 참조 변수를 선언하는 catch문은 앞 순서에, 조상 예외 클래스 타입의 참조 변수를 선언하는 catch문은 뒷 순서에 배치해야 합니다. 

 

 

3.3.3 멀티 catch

 JDK1.7부터 사용이 가능한 기능입니다. 여러 개의 catch 블록을 '|'을 사용하여 하나의 catch 블록으로 합칠 수 있게 되었습니다. 이를 통해 중복되던 코드를 줄일 수 있습니다. 

 예제를 통해 멀티 catch 기능을 살펴보겠습니다. 아래의 예제는 try-catch문이며 다중 catch를 상요하고 있습니다. 모든 catch 블록{}에는 예외발생 당시 호출 스택(Call Stack)에 있었던 메서드의 정보와 예외 메시지를 화면에 출력시키는 printStackTrace() 메서드를 작성하였습니다. 

 

1
2
3
4
5
6
7
8
9
10
11
12
try {
        ...
    } catch(Exception1 e) {    
        e.printStackTrace();
    } catch(Exception2 e2) {    
        e2.printStackTrace();
    } catch(Exception3 e3) {    
        e3.printStackTrace();
    } catch(Exception4 e4) {    
        e4.printStackTrace();
    }
 

 

 printStackTrace()가 현재 catch 블럭 수만큼 작성이 되어 잇습니다. 멀티 catch 기능을 사용하면 아래의 코드와 같이 단 한 번의 catch문을 작성하면 됩니다.

 

1
2
3
4
5
6
try {
        ...
    } catch(Exception1 | Exception2  | Exception3 | Exception4  e) {    
        e.printStackTrace();
    }
 

 

 멀티 catch에서 '|'기호로 연결할 수 있는 예외 클래스의 개수는 제한이 없습니다. 하지만 멀티 catch를 사용함에 있어 2가지를 인지하고 고려하여 사용해야 합니다.

 

첫 번째, 멀티 catch에서 선언하는 예외 클래스들은 상속관계가 아니어야 한다.

 '|'로 연결된 예외 클래스가 서로 상속관계에 있다면 컴파일 에러가 발생합니다. 두 예외 클래스가 멀티 catch에서 참조 변수로 선언되었는데 두 예외 클래스가 조상과 자손의 관계에 있다면, 이는 조상 클래스만 써주는 것과 똑같습니다. 따라서 불필요한 코드이기 때문에 제거하라는 의미에서 에러가 발생합니다.

 

 

두 번째, 멀티 catch에 사용된 예외 클래스들의 공통된 조상 예외 클래스의 멤버만 사용할 수 있습니다.

 멀티 catch에서 여러 예외 클래스를 작성을 하지만 실질적으로 'or' 연산을 의미하는 '|'로 연결된 하나의 참조 변수만을 선언한 것입니다.  따라서 try 블록{}에서 예외가 발생했을 때 멀티 catch의 참조 변수가 가리키는 인스턴스가 어느 예외 클래스의 인스턴스인지 알 수 없습니다. 예를 통해서 이해해보겠습니다. 아래의 코드처럼 멀티 catch에 사용된 예외 클래스가 ExceptionA, ExceptionB, ExceptionC가 있다고 가정하겠습니다. 

 

1
2
3
4
5
6
try {
        ...
    } catch(ExceptionA | ExceptionB  | ExceptionC e) {   
       e.methodB();
    }
 

 

 위의 코드는 멀티 catch에 작성된 예외 클래스는 ExceptionA, ExceptionB, ExceptionC입니다. try블록{}에서 ExcetionA타입의 예외가 발생했다고 가정하겠습니다. 하지만 개발자는 ExceptionB 타입의 예외를 예상하여 catch블록{}에 ExcetinoB클래스의 메서드를 호출한다면 값이 존재하지 않을 것입니다.

 그렇기 때문에 공통된 조상의 메서드만을 호출하거나, instanceof 연산자를 사용하여 정확한 어느 예외의 인스턴스인지를 판단하여 형 변환(캐스팅) 후 해당 메서드를 사용할 수 있습니다. 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
try {
        ...
    } catch(ExceptionA | ExceptionB  | ExceptionC) {    
        if(e instanceof ExceptionA){
            ExceptionA e1 = (ExceptionA)e;
            e1.methodA();    
        } else if(e instanceof ExceptionB){
            ExceptionB e2 = (ExceptionA)e;
            e2.methodB();                
        } else {
            ExceptionC e3 = (ExceptionA)e;
            e3.methodC();                
        }
    }
 

 

 위의 코드는 instanceof 연산자를 사용하여 정확히 어느 예외의 인스턴스인지를 판단한 뒤 형 변환 후 메서들 사용할 수 있도록 한 코드입니다. 멀티 catch를 사용하고자 했던 목적이 중복되는 코드를 줄이고자 하는 거였는데 위의 코드는 보기에도 복잡하고 매우 많은 코드를 작성하여야 합니다. 따라서 instanceof 연산자를 사용해야 할 상황이라면 다중 catch문을 사용하는 것이 더 현명한 선택입니다.

 다만 다중 catch 블록을 멀티 catch 블록으로 변환하는 경우 대부분이 코드를 간단히 하는 정도의 수준이기 때문에 언급한 2가지 고려사항을 너무 고민하지 않아도 됩니다.

 

 

3.4 try-catch-finally

 try-catch-finally문은 예외 발생여부와 상관없이 실행되어야 할 코드를 포함시킬 목적으로 사용됩니다. try-catch문에 선택적으로 finally 블록을 덧붙여 사용할 수 있습니다. 아래의 try-catch-fianlly를 사용한 코드를 보면

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class ExceptionApp {
 
    public static void main(String[] args) {
        
        System.out.println(1);
        System.out.println(2);
        try {
            System.out.println(3);
            System.out.println(3/0);
        } catch (ArithmeticException e) {
            System.out.println("예외가 발생");
        } finally {
            System.out.println(4);
            System.out.println(5);
        }
        System.out.println(6);
        System.out.println(7);
        
    }
 
}
 

 

 위의 코드는 3.3에서 사용한 예제 코드를 활용하여 작성하였습니다. 기존 3.3 예제 코드에서는 예외가 발생을 하게 되면 숫자 3,4는 출력되지 못했습니다. 그렇지만 이번 코드에서는 숫자 3,4를 예외가 발생하더라도 출력시키기 위해서 3,4를 출력하는 문장을 finally 블록{}에 작성을 하였습니다.  아래의 그림 10은 코드의 실행 결과입니다.

 

그림 10

 

 결과를 보시면 예외가 발생 했음에도 finally 블록{}에 작성한 3,4를 출력하는 코드가 정상적으로 실행이 됐음을 볼 수 있습니다.

 

* try-catch-finally 문이 메서드 내에 작성된 상황에서, try 블록{} 혹은 catch블록{}에 return문장이 있도라도 finally 블록{} 안에 있는 문장이 실행된 뒤에 메서드가 종료됩니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class ExceptionApp {
 
    public static void main(String[] args) {
        
        methodA(0);
        System.out.println("-------------");
        methodA(1);
        System.out.println("-------------");
        methodA(2);
    }
    
    static void methodA(int a) {
        try {
            if(a == 1) {            // 1 : try문에서 리턴하는 경우
                System.out.println("try블록에서 리턴");
                return;
            } else if(a == 2) {     // 2 : catch문에서 리턴하는 경우
                System.out.println(2/0);
            } else {                // 이외의 숫자 : 메서드 문 끝까지 실행하는 경우
                System.out.println("메서드 끝까지 실행");
            }
        } catch(Exception e) {
            System.out.println("catch블록에서 리턴");
            e.printStackTrace();
            return;
        } finally {
            System.out.println("finally실행");
        }
        System.out.println("try-catch문 종료");
        return;
    }
 
}
 

 

그림 11

 

 

3.5 try-catch-resources

 JDK1.7부터 추가되었으며 try-catch문의 변형시킨 명령어입니다. try-catch-finally문을 활용하여 입출력(I/O) 클래스를 사용하게 될 때 생기는 문제점을 보완하기 위해 생겨난 기능입니다.

 

기존 어떠한 문제점이 있었는가?

 아래는 try-catch-finally문을 활용하여 입출력 클래스를 사용한 예제 코드로, 메모장 파일을 생성하고 그 내용을 수정하는 코드입니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class ExceptionApp {
 
    public static void main(String[] args) {
        
        FileWriter f = null
        try {
            f = new FileWriter("hello.txt");
            f.write("Hello");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                f.close();
            } catch(Exception e) {
                e.printStackTrace();
            }
            
        } // finally 븡록 종료
        
    }// main 메소드 종료
 
}
 
cs

 

그림 12

 

 위의 코드처럼 try-catch-finally문을 활용하여 입출력(I/O) 클래스를 사용하는 코드를 작성하게 되면 두 가지 문제점이 생겼습니다.

 

첫 번째, 코드가 매우 복잡해집니다.

 위의 코드를 보시다시피 코드가 매우 복잡해집니다. finally블록에는 입출력 클래스를 사용하기 때문에 close() 메서드를 작성하였는데, 해당 메서드 자체가 CheckedException을 유발할 수 있는 문장이라서 try-catch문을 하나 더 추가해주다 보니 더욱 복잡해졌습니다.

 

두 번째, try 블록과 finally 블록 모두 예외가 발생을 하면 try 블록의 예외처리는 무시가 됩니다.

 try 블록이나 finally 블록 중 한 곳만 예외가 발생을 한다면 문제가 안되지만 동시에 발생을 하게 되면 finally블록의 예외처리만 실행이 됩니다. 앞의 문제점보다 해당 문제점이 가장 치명적입니다.

 

 try-catch-resources문을 사용하여 작성을 한다면 위의 두 문제점을 해결할 수 있습니다.

 

사용방법

 전체적인 구조는 try catch문과 똑같습니다. try블록에 예외가 발생할 가능성 있는 코드를 작성하고, catch 괄호엔 발생할 가능성이 잇는 예외 클래스 참조 변수 선언, catch 블록에는 예외가 발생했을 때 실행이 될 코드를 작성합니다. 다른 점은 try에 괄호()가 생겼다는 점입니다.  해당 괄호에는 close() 메서드가 반드시 필요한 객체를 생성하는 문장을 작성합니다.  try-catch-resources문을 실행을 하게 되면 try 괄호 안에 작성한 객체가 따로 close() 객체를 호출하지 않더라도 try블록을 벗어나는 순간 자동으로 close()를 호출해줍니다. 단, try 괄호에 생성하는 객체 클래스는 AutoClosealbe이라는 인터페이스를 구현한 것이어야만 한다는 조건이 붙습니다.

 

 위에서는 try-catch-finally문을 활용하여 메모장 파일을 생성하고 수정하였는데 이번에는 try-catch-resoureces문을 사용하여 메모장을 생성하고 수정하는 코드를 작성해 보겠습니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class ExceptionApp {
 
    public static void main(String[] args) {
        
        
        try(FileWriter f = new FileWriter("hello2.txt")){
            f.write("Hello2");
        } catch(Exception e) {
            e.printStackTrace();
        }
        
        
    }// main 메소드 종료
 
}
 

 

그림 13

 

 코드를 보시면 try-catch-finally를 사용했을대와 다르게 코드가 매우 단순화돼서 한눈에 들어오고, close() 메서드를 사용하지 않은 것을 확인할 수 있습니다. 

 

 

3.6 throw

 개발자가 고의로 예외를 일으키기 위해 사용 하는 키워드입니다. 사용방법은 다음과 같습니다.

1. 먼저 발생시키고자하는 예외 클래스를 new를 이용해서 인스턴스를 생성하고, 해당 예외 클래스 타입의 참조 변수를 선언하여 인스턴스의 주소를 저장합니다.

2. 키워드 throw를 이용해서 예외를 발생시킵니다.

 위의 사용방법대로 RuntimeException을 고의로 일으키기 위해선 아래의 코드와 같이 작성해야합니다.

 

1
2
3
4
5
 
RuntimeException e = new RuntimeException();
 
throw e;
 

 

 위의 코드가 실제로 예외를 일으키는지 try-catch문을 이용해서 확인해보았습니다. 아래는 실행 코드와 그 결과입니다. console창을 보시면 의도대로 RuntimeException이 발생한 것을 볼 수 있습니다. 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
 
public class ExceptionApp {
 
    public static void main(String[] args) {
        
        try {
            RuntimeException e = new RuntimeException();
            throw e;
        } catch(RuntimeException e) {
            e.printStackTrace();
            System.out.println("에러메시지 : " + e.getMessage());
        }
        
        
    }
 
}

 

그림 14

 

 

 Exception 인스턴스를 생성할 때, 생성자에 String을 입력하면, 입력한 String이 Exception 인스턴스에 메시지로 저장됩니다. 해당 메시지는 getMessage() 메서드를 이용해서 출력시킬 수 있습니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
 
public class ExceptionApp {
 
    public static void main(String[] args) {
        
        try {
            // 생성자에 String을 입력하면 예외 클래스의 인스턴스에 메시지로 저장됩니다.
            RuntimeException e = new RuntimeException("커스텀 메시지 작성");    
            throw e;
        } catch(RuntimeException e) {
            e.printStackTrace();
            System.out.println("에러메시지 : " + e.getMessage());
        }
        
        
    }
 
}
 

 

그림 15

 

 

3.7 throws

 throws는 메서드에서 사용하는 예외처리 키워드입니다. 사용 방법은 메서드의 선언부에 thorws 사용하여 메서드 내에서 발생할 수 있는 예외를 입력하기만 하면 됩니다. 예상되는 예외가 여러 개일 경우에는 쉼표로 구분해서 추가해주면 됩니다.

 

1
2
3
4
5
 
    void method() thorws Exception1, Exception2, ... ExceptionN {
        // 메서드 
    }    
 

 

 throws는 try-catch와 같이 예외가 발생했을 때 대체되는 코드가 실행이 되는 것이 아니라, 메서드에서 발생하는 예외를 호출한 메서드에게 넘겨주는 역할을 합니다. 쉽게 말해 본래는 예외가 발생하는 메서드 내에서 try-catch문을 사용하여 예외처리를 하여야 하지만, 예외처리를 하지 않고 호출한 메서드에게 발생한 예외를 처리하도록 떠넘기는 것입니다.

 하나의 메서드에 throw 예외를 일으켜보겠습니다. 그러면 아래와 같이 throw 문장을 예외 처리해주어야 한다는 경고가 뜹니다. 

 

 

 throw 문장을 try-catch문으로 감싸주지 않고 메서드에 throws Exception을 추가해주면 경고 표시가 사라집니다. 대신 throws 문장을 추가해준 메서드를 호출하는 코드가 예외를 처리해주어야 한다는 경고가 뜹니다. 즉, method3()에서 처리했어야 할 예외를 method3()를 호출한 method2()에서 떠넘긴 것입니다.

 

그림 17

 

 

 이와 같이 메서드를 작성할 때 throws를 사용하여 발생할 가능성이 있는 메서드를 선언부에 명시할 때의 장점은  해당 메서드를 사용하는 쪽에서는 어떠한 예외들이 처리되어야 하는지 쉽게 알 수 있으며, 명시된 예외에 대한 처리를 하도록 강요하므로 보다 견고한 프로그램 코드를 작성할 수 있습니다.

 

 

 thorws를 통해서 예외를 전달받은 메서드는 또다시 자신의 선언부에 thorws를 사용하여 자신을 호출한 메서드에게 전달할 수 있습니다. 이런 식으로 계속 호출 스택에 있는 메서드들을 따라 전달할 수 있으며 main 메서드까지 예외가 전달되게 됩니다. 그렇지만 main메서드 또한 메서드이기 때문에 thorws를 사용할 수 있기 때문에 main 메서드에서조차 예외를 처리하지 않는다면, main 메서드까지 종료되어 프로그램 전체가 종료됩니다.  아래의 코드가 해당 예이며 console창을 보면 java.lang.Exception이 발생하여 비정상적으로 종료했다는 것과 예외가 발생했을 때 호출 스택(call stack)의 내용을 알 수 있습니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
 
public class ExceptionApp {
 
    public static void main(String[] args) throws Exception {    
        method1();
    }
    
    static void method1() throws Exception{
        method2();
    }
    
    static void method2() throws Exception {
        method3();
    }
    
    static void method3() throws Exception {
        throw new Exception();
    }
 
}
 

 

그림 18

 

 

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

 상속을 사용해서 자신만의 예외클래스를 만들 수 있습니다. 주로 상속할때 사용하는 조상 클래스는 Exception과 RuntimeException 입니다. 두 클래스 각각 UncheckedException와 CheckedExcetption의 최상위 조상 예외 클래스이기 때문입니다. 오버라이딩을 통하여 메서드를 자신의 의도대로 수정을 하거나, 필요하다면 새로운 멤버 변수나 메서드를 추가할 수 있습니다.

  Exception 클래스는 생성 시에 String값을 받아서 메시지를 저장 할 수 있습니다. 만약 우리가 만든 예외클래스도 메시지를 저장하고 싶다면, Stirng 매개변수로 받는 생성자를 추가해주어야만 합니다.

 

 

 

 RuntimeException을 상속 받아서 커스텀한 예외클래스를 만들어보겠습니다. 해당 예외클래스에 새로운 멤버 변수와 메서드를 추가하고, 메세지를 저장 할 수 있는 생성자를 추가합니다. 정상적으로 제가 작성한 예외클래스가 작동하는지 try-catch문을 통해서 테스트해보겠습니다. 

 

* 커스텀 예외 클래스

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
 
public class CustomException extends RuntimeException {
    
    CustomException(){                // 기본생성자
        super();
    }
    
    CustomException(String msg){    // 문자열을 매개변수로 받는 생성자
        super(msg);
    }
    
    String CustomVar = "새로추가한 변수";
    
    void CustomMethod() {
        System.out.println("새로 추가한 메서드");
    }
    
}
 

 

아래그림에서 보이는 메서드들을 오버라이딩을 통해서  자신만의 메서드로 변환 시킬 수 있습니다.

 

그림 19

 

 

* 커스텀 예외 클래스 테스트(trhow / try-catch 사용) 및 결과

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
 
public class ExceptionApp {
 
    public static void main(String[] args) {
        try {
            throw new CustomException("커스텀생성자 메시지 저장");
        } catch(CustomException e) {
            System.out.println("메시지 : " + e.getMessage());
            System.out.println(e.CustomVar);
            e.CustomMethod();
        }
        
    } // 메인 종료
 
}
 

 

그림 20

 

 

Reference

 

Date

  • 2021-01-16

 

728x90
반응형

'스터디 > 자바 스터디' 카테고리의 다른 글

[JAVA 스터디] 8주차 과제 : 인터페이스  (0) 2021.01.09
Comments