Java 예외 처리

Copyright (c) 2015-, All Rights Reserved by Kwanghoon Choi
(아래 자바 프로그래밍 강의 교재의 내용을 자유롭게 이용하되 다른 웹 사이트 등을 통한 배포를 금합니다.)

1. Java 예외 처리

  • 예외 클래스
  • 예외 발생 (throw)
  • 예외 처리 (try/catch 블록, throws 예외 명세 선언)
  • 예외 전파

프로그램을 작성할 때 정상적인 입력을 받아 의도한대로 처리하고 출력하는 정상적인 경우 외에도 다양한 비정상적인 상황도 고려해야 한다. 널(Null)을 담고 있는 변수를 참조하고, 0으로 나누려고 시도하고, 의도한 입력이 들어오지 않고, 읽거나 쓰려고 하는 파일을 열 수 없고, 심지어는 객체를 생성하는데 메모리가 부족할 수도 있다.

1.1 예외 클래스

비정상적인 상황을 Java에서 예외 클래스로 표현한다.

  • NullPointerException (널 변수 참조)
  • ArithmeticException (0으로 나누기)
  • InputMismatchException (입력 오류), NumberFormatException ("1.23"을 Integer 변환을 시도할 때), IllegalArgumentException (메소드 인자가 유효하지 않을 때 즉, String 타입이지만 의도한 형식을 갖추고 있지 않은 문자열을 인자로 받는 경우)
  • IOException (입출력 예외)
  • OutOfMemoryError (메모리 부족)

 

모든  Java 예외 클래스의 시조는 Throwable 클래스이다.

Object
 +--- Throwable
       +--- Exception
       |     +--- RuntimeException
       |     +--- ...
       |
       +--- Error
             +--- ...

예외 클래스를 새로 정의하려면 표현하고자 하는 에러 상황과 관련있는 예외 클래스를 선택하고 그 예외 클래스를 상속 받아 새로운 클래스를 선언하면 된다.

class MaziException extends Exception {
...
}

1.2 예외 발생

예외를 발생시키는 방법은 예외 클래스로 객체를 만들고 던지는(throw) 것이다.

if (x != null) x.m();
else throw new NullPointerException("x is null");

 

1.3 예외 처리 방법 try/catch 블록과 예외 명세 선언

발생한 예외를 처리하는 방법은 try/catch 블록을 사용한다. 아래의 예제에서 지정한 파일을 열고 읽으려 할 때 발생할 수 있는 예외, FileNotFoundException과 IOException을 try/catch 블록으로 처리한다.

void readFile() {
  try {
     File file = new File("C:\abc.txt");
     InputStreamReader isr = new InputStreamReader(new FileInputStream(file)); 
     ...
     isr.close();
  } catch (FileNotFoundException e) {
     ...
  } catch (IOException e) {
     ...
  }
}

[참고] C++의 예외 처리에서 예외 객체를 만들어 던지고 try/catch 문으로 처리하는 것과 동일하다.

예외를 처리하는 (try/catch 블록을 사용하는 방법과) 다른 방법은 해당 코드를 감싸는 메소드에 throws 예외 명세를 선언하는 것이다. 아래 예제의 readFile 메소드와 예외 명세의 의미는, readFile() 메소드를 호출하면 정상적으로 파일을 읽어 정상적으로 처리하거나, 또는 FileNotFoundException이나 IOException 클래스의 예외를 발생시키는 것이다.

void readFile() throws FileNotFoundException, IOException {
   File file = new File("C:\abc.txt");
   InputStreamReader isr = new InputStreamReader(new FileInputStream(file)); 
     ...
   isr.close();
}

 

1.4 예외 전파

프로그램 실행 중 예외가 발생하면 그 지점을 감싸는 try/catch 블록에서 이 예외를 처리할 catch 문을 찾는다. 예외 발생 지점을 감싸는 메소드 안에서 그러한 catch 문을 찾지 못하면 이 메소드를 호출한 메소드로 예외를 전달한다. 이 과정을 예외 전파 (exception propagation)라고 부른다.

void process() {
   try {
     readFile();
   } catch (FileNotFoundException e) {
     ...
   } catch (IOException e) {
     ...
   }
}

위의 예제에서 process() 메소드에서 readFile() 메소드를 호출한다. 이때 호출한 메소드의 예외 명세에 의하면 FileNotFoundException과 IOException 예외가 발생할 수 있으므로 이 점으로 고려하여 readFile() 메소드 호출을 감싸는 예외 처리 코드를 작성할 수 있다.

여기서 try/catch 블록을 사용하지 않고 process() 메소드의 예외 명세에 이 두가지 예외를 선언함으로써 예외 처리를 대신할 수도 있다. 이 경우 process() 메소드를 호출한 메소드에서 예외 처리를 고려해야 한다.

[참고] C++의 throws 예외 명세와 다른점은, C++의 경우 메소드의 예외 명세에 나열한 예외에 대해서만 예외 전파를 하고 나열하지 않은 예외가 메소드 안에서 발생하면  바로 비정상 종료하도록 예외 처리의 의미가 정의되어 있다.

 

1.5  Java 예외 처리의 장점 (프로그래밍언어에서 예외 처리를 예외 처리를 지원하는 것의 장점)

(장점1) 일반 코드와 에러 처리 코드를 분리

예외 처리를 사용하지 않은 에러 처리 코드는 일반 코드와 섞인 "스파게티" 형태로 뒤죽박죽 될 수 있다. 예외 처리 특징을 통해 이 점을 방지할 수 있다.

파일을 열어 읽는 readFile 메소드의 슈도코드(Pseudo code)를 예로 살펴보자.

readFile {
  파일 열기;
  파일에 저장된 데이터 크기 계산;
  그 크기만큼 메모리 할당;
  할당한 메모리에 파일 데이터를 읽어 저장;
  파일 닫기;
}

이 메소드를 실행할 때 에러가 발생할 수 있는 여러가지 상황이 있다.

  • 파일을 열 수 없다면
  • 파일에 저장된 데이터 크기를 알 수 없다면
  • 충분한 메모리를 확보할 수 없다면
  • 파일을 읽지 못하면
  • 파일을 닫을 수 없다면

이러한 상황도 처리하면 원래 슈도코드에 조건식을 사용한 에러 처리 코드를 추가 작성할 수 있다.

errorCodeType readFile {
  errorCode = 0; // 에러 발생하지 않음으로 초기화

  파일 열기;
  if ( 파일 열기가 성공하면 ) {
      파일에 저장된 데이터 크기 계산;
      if ( 파일 크기 계산을 성공하면 ) {
         그 크기만큼 메모리 할당;
         if ( 메모리를 할당했다면 ) { 
            할당한 메모리에 파일 데이터를 읽어 저장;
            if ( 파일 데이터 읽기 실패 ) {
               errorCode = -1;
            }
         }  else {
            errorCode = -2;
         }
      } else {
         errorCode = -3;
      }

      파일 닫기;
      if ( 파일 닫기 에러 && errorCode == 0 ) {
         errorCode = -4;
      } else {
         errorCode = errorCode와 -4; // 두가지 에러 발생
   } else {
      errorCode = -5;
   }
}

위와 같이 모든 에러를 처리하는 코드를 작성한 점은 좋지만 코드 구조를 보면 정상적인 실행과 에러 발생시 실행되는 과정이 조건문을 가지고 뒤섞여 있음을 알 수 있다. 예외 처리를 사용하면 일반 코드와 에러 처리 코드를 분명히 구분하여 모든 에러를 처리하면서도 읽고 이해하기 더 좋은 코드 구조를 유지할 수 있다.

readFile {
  try {
    파일 열기;
    파일에 저장된 데이터 크기 계산;
    그 크기만큼 메모리 할당;
    할당한 메모리에 파일 데이터를 읽어 저장;
    파일 닫기;
  } catch ( 파일 열기 실패 ) {
    ...
  } catch ( 파일 데이터 크기 얻기 실패 ) {
    ...
  } catch ( 메모리 할당 실패 ) { 
    ...
  } catch ( 파일 읽기 실패 ) {
    ...
  } catch ( 파일 닫기 실패 ) {
    ...
  }
}

 

(장점2) 메소드 호출 순서의 역순으로 예외를 전파

에러가 발생하면 메소드를 호출해온 순서의 역순으로 전달하는 과정이 예외 처리에서 자동으로 지원하는 장점이 있다. 예외 전파가 없었다면 프로그램에서 에러 코드를 리턴하는 것을 직접 작성했어야 할 것이다.

method1 {
   call method2;
}
method2 {
   call method3;
}
method3 {
   call readFile;
}

예외 처리를 사용하지 않는다고 가정하자. 위의 일반 코드에 다음과 같이 변형하여 에러를 처리할 수 있다.

method1 {
   errorCodeType error;
   error = call method2;
   if ( error ) 
      에러 처리;
   else
      그 다음 작업 진행;
}
method2 {
   errorCodeType error;
   error = call method3;
   if ( error ) 
      에러 처리;
   else
      그 다음 작업 진행;
}
method3 {
   errorCodeType error;
   error = call readFile;;
   if ( error ) 
      에러 리턴;
   else
      그 다음 작업 진행;

}
method1 {
   try {
      call method2;
   } catch (exception e) {
      에러 처리;
   }
}

method2 throws exception {
   call method3;
}

method3 throws exception {
   call readFiel;
}

 

(장점3) 예외 클래스의 상속 관계를 활용하여 유사한 에러를 그룹화해서 처리

일반 클래스 처럼 예외 클래스도 상속 관계를 가지고 정의한다. 따라서 catch문에 지정한 예외 클래스에 따라서 한가지 이상의 예외를 처리할 수 있다. 예를 들어, FileNotFoundException을 지정하면 파일 찾기 실패 예외만 처리하지만,  IOException을 지정하면 모든 I/O 관련 예외를 처리할 수 있다.

 

 

Leave a Reply

Your email address will not be published. Required fields are marked *