목차
<예외 처리>
소프트웨어를 사용하다 보면 여러 상황의 오류를 마주하게 된다.
잘 접속되던 사이트가 접속이 안된다거나, 스마트폰 앱이 갑자기 종료되는 경우가 그런 경우.
아무리 잘 만든 소프트웨어라도 이런 상황은 언제든 발생할 수 있는데, 이런 오류가 발생해도 갑자기 종료되는 상황이 일어나지 않도록 '예외 처리' 방법에 대해 알아보자.
<예외 클래스>
오류란?
프로그램에서 오류가 발생하는 상황은 두 가지다.
- 프로그램 코드 작성 중 실수로 발생하는 '컴파일 오류(compile error)'
- 실행 중인 프로그램이 의도하지 않은 동작을 하거나, 프로그램이 중지되는 '실행 오류(runtime error)'
실행 오류 중 프로그램을 잘못 구현하여 의도한 바와 다르게 실행되어 생기는 오류를 '버그(bug)'라고 한다.
'컴파일 오류'는 개발 환경(IDE)에서 대부분 원인을 알 수 있다. 발생한 컴파일 오류를 모두 수정해야 프로그램이 정상적으로 실행되므로, 문법적으로 오류가 있다는 것을 바로 알 수 있다.
하지만 프로그램 실행중에 발생하는 '런타임 오류'는 예측하기 어려운 경우가 많고, 프로그램이 비정상 종료되면서 갑자기 멈춰 버린다.
실제 서비스를 제공하고 있는 프로그램의 경우 오류가 생기면 서비스가 중지되므로 문제가 심각해진다. 또한 실행중에 오류가 발생하면 그 상황을 재현하여 테스트를 해야 하는데, 실제 시스템이나 서비스가 운영 중인 경우에는 쉽지 않다.
따라서 '로그(log)분석'을 통해 원인을 찾을 수 있도록 프로그램을 개발할 때 로그를 정확하게 남기는 것이 중요!
자바는 이러한 비정상 종료를 최대한 줄이기 위해 다양한 예외 처리 방법을 가지고 있다.
예외 처리의 목적은 프로그램이 비정상 종료되는 것을 방지하기 위함이다. 그리고 예외가 발생했을 때 로그를 남기면 예외 상황을 파악하고 버그를 수정하는데 도움을 받을 수 있기 때문.
오류와 예외
'실행 오류'는 크게 두 가지가 있다.
- 자바 가상 머신에서 발생하는 시스템 오류(error)
- 예외(exception)
시스템 오류의 예로는 사용 가능한 동적 메모리가 없는 경우나, 스택 메모리의 오버플로우가 발생한 경우 등. 이러한 시스템 오류는 프로그램에서 제어할 수 없다.
반면 '예외(exception)'는 프로그램에서 제어가 가능하다. 예를 들어 프로그램에서 파일을 읽어 사용하려는데 파일이 없거나, 네트워크로 데이터를 전송하려는데 연결에 실패한 경우, 배열 값을 출력하는데 배열 요소가 없는 경우 등이 있다.
자바에서 제공하는 오류에 대한 전체 클래스를 간단히 나타내면 다음과 같다.
- 오류 클래스는 모두 Thowable 클래스에서 상속받는다.
- Error 클래스의 하위 클래스는 시스템에서 발생하는 오류를 다루며 프로그램에서 제어하지 않는다.
- Exception 클래스와 그 하위에 있는 예외 클래스는 프로그램에서 제어하는 부분이다.
예외 클래스의 종류
프로그램에서 처리하는 예외 클래스의 최상위 클래스는 Exception 클래스다.
다음 그림은 Exception 하위 클래스 중 사용 빈도가 높은 클래스 위주로 계층도로 표현했다.
Exception 클래스 하위에는 이 외에도 많은 클래스가 있다.
- 'IOException' 클래스는 입출력에 대한 예외를 처리한다.
- 'RuntimeException'은 프로그램 실행 중 발생할 수 있는 오류에 대한 예외를 처리.
이클립스 같은 IDE에서는 예외가 발생하면 대부분 처리하라는 컴파일 오류 메시지를 띄운다. 그러므로 뒤에서 다룰 try-catch문을 사용해 예외처리를 해야한다.
그러나, Exception 하위 클래스 중 RuntimeException은 try-catch문을 사용하여 예외 처리를 하지 않아도 컴파일 오류가 발생하지 않는다.
예를 들어 RuntimeException 하위 클래스 중 ArithmeticException은 산술 연산 중 발생할 수 있는 예외, 즉 '0으로 숫자를 나누기'같은 경우에 발생하는 예외다. 이러한 컴파일러에 의해 체크되지 않는 예외는 프로그래머가 알아서 처리해야하므로 주의해야 한다.
이제 자바에서 어떻게 예외 처리를 하는지 보자.
<예외 처리하기>
try-catch문
예외 상황을 어떻게 처리해야 하는지 알아보자.
예외를 처리하는 가장 기본적인 문법인 'try-catch문'의 형식은 다음과 같다.
try {
예외가 발생할 수 있는 코드 부분
} catch(처리할 예외 타입 e) {
try 블록 안에서 예외가 발생했을 때 예외를 처리하는 부분
}
- try블록에는 예외가 발생할 가능성이 있는 코드를 작성한다.
만약 try블록 안에서 예외가 발생하면 catch블록이 수행된다. - catch문의 괄호()안에 쓰는 예외 타입은 예외 상황에 따라 달라진다.
try-catch문 사용하기
예제로 예외가 발생하는 상황을 만들고, 그에 따른 예외 처리를 해보겠다.
요소가 5개인 정수형 배열을 만들고, 요소에 0~4를 대입하는 코드. 배열 크기가 5이므로 정수 값을 5개 저장할 수 있다.
여기서 i<5를 i<=5로 바꿔본다.
int[] arr = new int[5]
for(int i = 0; i < 5; i++){
arr[i] = i
System.out.println(arr[i]);
}
↓ i<5를 i<=5로 변경하면
int[] arr = new int[5]
for(int i = 0; i <= 5; i++){
arr[i] = i
System.out.println(arr[i]);
}
변경한 코드는 0부터 5까지 총 6개 숫자를 배열에 넣기 때문에 다음과 같은 예외 상황이 발생한다.
배열에 저장하려는 값의 개수가 배열 범위를 벗어났기 때문에 예외가 발생.
참고로 이 예외는 RuntimeException의 하위 클래스인 ArrayIndexOutOfBoundsException으로 처리하는데, 이 클래스는 예외 처리를 하지 않아도 컴파일 오류가 발생하지 않는다. 따라서 프로그래머가 직접 예외 처리를 하지 않으면 예외가 잡히지 않아서 예외가 발생하는 순간에(i가 5가 되는 순간) 프로그램이 멈추게 된다.
그러므로 예외가 발생한 순간 프로그램이 비정상 종료되지 않도록 예외 처리를 해줘야 한다.
예외가 발생한다는 가정하에 다음과 같이 예외 처리를 해보자.
package exception;
public class ArrayExceptionHandling {
public static void main(String[] args) {
int[] arr = new int[5];
//예외가 발생할 수 있으므로 try블록에 작성
try {
for(int i = 0; i <= 5; i++){
arr[i] = i;
System.out.println(arr[i]);
}
//try에서 예외가 발생하면 catch블록 수행
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println(e);
System.out.println("예외 처리 부분");
}
System.out.println("프로그램 종료");
}
}
- 배열 번위가 유효한 값 4까지는 배열에 저장되어 출력되고, 그다음 값을 배열에 넣으려 할 때 예외가 발생한다.
- 발생한 예외는 catch블록에서 처리하므로 System.out.println("프로그램 종료")문장까지 수행하고 프로그램이 정상 종료된다.
- 만약 예외 처리가 되지 않아 프로그램이 비정상 종료 되었다면 System.out.println("프로그램 종료")문장을 수행하지 못하고 종료가 될 것이다.
이처럼 예외 처리는 프로그램이 비정상 종료되는 것을 방지할 수 있으므로 매우 중요하다.
컴파일러에 의해 예외가 체크되는 경우
앞에서 살펴 본 예제는 예외 처리하지 않아도 컴파일 오류가 나지 않지만, 자바에서 제공하는 많은 예외 클래스들은 컴파일러에 의해 처리된다. 이런 경우 자바에서는 예외 처리를 하지 않으면 컴파일 오류가 계속 남는다.
그러면 예외를 처리해야 하는 파일 입출력과 관련된 예제를 보도록 하겠다.
파일 입출력에서 발생하는 예외 처리하기
자바에서 파일을 읽고 쓰는 데 스트림(stream) 객체를 사용한다. 스트림 종류는 여러 가지가 있지만, 여기에서는 파일에서 데이터를 바이트 단위로 읽어 들이는 FileInputStream을 사용하겠다.
main()함수 안에 FileInputStream 선언 코드를 작성한다.
FileInputStream fis = new FileInputStream("a.txt");
지금까지 클래스를 생성할 때 사용하던 코드와 다르지 않다.
위 코드는 a.txt파일에서 데이터를 읽어 들이기 위해 스트림 객체를 생성한다는 의미.
이렇게 코드를 작성하면 new FileInputStream("a.txt");부분에 다음처럼 오류가 발생한다.
'FileNotFoundException이 처리되지 않았다'는 메시지가 나타나고 그 아래에 있는 두 옵션중 하나를 선택하라고 보인다.
이 코드는 a.txt파일을 열어 읽으려고 FileInputStream 클래스를 생성한 경우인데, 이 경우 a.txt파일이 존재하지 않는 오류가 발생할 수 있다는 것.
읽으려는 파일이 없는 경우에 자바 가상 머신에서는 FileNotFoundException 예외 클래스가 생성된다. 따라서 위 오류 메시지는 이러한 예외 상황에 대비한 예외 처리를 해야 한다는 뜻.
try-catch문으로 감싼다는 Surround with try/catch를 선택한다.
- 그러면 예외가 발생할 위험이 있는 코드가 try블록으로 감싸진다.
앞에서 본 배열과 마찬가지로 try블록이 먼저 수행되고, 예외가 발생하면 catch블록을 수행한다. - try문으로 감싸진 부분에서 발생할 수 있는 예외는 FileNotFoundException이고, 변수 이름은 e로 선언된다.
- 그리고 어디에서 예외가 발생했는지 따라 가는 printStackTrace()메서드가 호출된 것을 알 수있다.
우리는 a.txt파일이 없으므로 이 상황에서는 당연히 이 상황에서는 당연히 다음처럼 예외가 발생한다.
코드를 실행해 보면 결과 화면에 예외 이름과 그 내용이 보인다. 결과 화면에서 ExceptionHandling1.java:10을 클릭하면 예외가 발생한 코드 위치로 이동한다.
try-catch문을 사용해 코드를 완성해보자.
package exception;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
public class ArrayExceptionHandling1 {
public static void main(String[] args) {
try {
FileInputStream fis = new FileInputStream("a.txt");
} catch (FileNotFoundException e) {
System.out.println(e); //예외 클래스의 toString()메서드 호출
}
//예외 처리 후 정상적으로 출력
System.out.println("예외 처리 완료");
}
}
- 예외가 발생했을 때 FileNotFoundException e의 toString()메서드가 호출되도록 코드를 작성했다. 출력 결과를 살펴보면 첫 번째 줄은 e의 출력 내용이다.
- 만약 여기서 비정상 종료된다면 다른 수행이 일어나지 않았을 것이다.
하지만 두 번째 줄의 문자열이 출력되었으므로 예외 처리 후 프로그램이 계속 수행되었음을 알 수 있다.
예외 처리를 한다고 해서 프로그램의 예외 상황 자체를 막을 수는 없다. 하지만 예외 처리를 하면 예외 상황을 알려주는 메시지를 확인할 수 있고, 프로그램이 비정상 종료되지 않고 계속 수행되도록 만들 수 있다.
try-catch-finally문
프로그램에서 사용한 리소스는 프로그램이 종료되면 자동으로 해제된다.
예를 들어 네트워크가 연결되었을 경우 채팅 프로그램이 종료될 때 연결도 닫힌다. 하지만 끝나지 않고 계속 수행되는 서비스인 경우 리소스를 여러 번 반복해서 열기만 하고 닫지 않는다면 문제가 발생하게 된다.
시스템에서 허용하는 자원은 한계가 있기 때문이다.
따라서 사용한 시스템 리소스는 사용 후 반드시 close() 메서드로 닫아줘야 한다.
그러면 앞에서 사용한 FileInputStream 클래스를 다시 살펴보자.
열어 놓은 파일 리소스를 닫는 코드를 다음처럼 추가한다.
try {
FileInputStream fis = new FileInputStream("a.txt");
if (fis != null) {
try {
fis.close(); //try블록에서 파일 리소스를 닫는 close()메서드 호출
} catch (IOException e) {
e.printStackTrace();
}
}
} catch (FileNotFoundException e) {
System.out.println(e);
}
현재는 try블록에서만 파일 리소스를 닫았다. 그런데 프로그램이 정상적으로 종료된 경우에도 열어 놓은 파일 리소스를 닫아야 하고, 비정상 종료된 경우에도 리소스를 닫아야한다. 따라서 try블록 뿐 아니라 catch블록에도 close()메서드를 사용해야 한다.
만약 try블록 안에서 발생할 수 있는 예외 상황이 여러 개라면 catch블록을 예외 상황 수만큼 구현해야한다.
그런데 한번 열어 놓은 리소스를 해제하는 코드를 try-catch-catch-... 처럼 각 블록에 모두 작성해야 한다면 굉장히 번거로울 것이다.
이때 사용하는 블록이 finally블록이다.
finally를 사용하는 형식은 아래와 같다.
try {
예외가 발생할 수 있는 부분
} catch(처리할 예외 타입 e) {
예외를 처리하는 부분
} finally {
항상 수행되는 부분
}
일단 try블록이 수행되면 finally블록은 어떤 경우에도 반드시 수행된다. (try나 catch문에 return문이 있어도 수행된다.)
따라서 try-catch-catch-... 각 블록마다 리소스를 해제하지 않고, finally블록에서 한 번만 해제해주면 된다.
이 내용을 코드로 정리하면.
package exception;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
public class ArrayExceptionHandling3 {
public static void main(String[] args) {
FileInputStream fis = null;
try {
fis = new FileInputStream("a.txt");
} catch (FileNotFoundException e) {
System.out.println(e);
return;
} finally {
if(fis != null) {
try {
fis.close(); //파일 입력 스트림 닫기
} catch (IOException e) {
e.printStackTrace();
}
}
System.out.println("finally문은 항상 수행된다");
}
System.out.println("예외 처리 후 수행된다");
}
}
입력받은 파일이 없는 경우에 대해 try-catch문을 사용해 FileNotFoundException예외 처리를 했다.
프로그램을 실행하면 a.txt파일이 없으므로 예외가 발생하여 catch블록이 수행되어 예외가 출력되고, 강제로 return;을 했다.
하지만 출력 결과를 보면 return문과 상관없이 finally 블록이 수행되어 'finally문은 항상 수행된다.' 문자열이 출력된 것을 알 수 있다.
이 finally블록에서 파일 리소스를 닫는 코드를 구현했다.
fis.close()문장에서도 예외가 발생할 수 있으므로 예외 처리를 해야 한다.
try-with-resorces문
앞에서 보았듯 시스템 리소스를 사용하고 해제하는 코드는 다소 복잡하다.
자바 7부터 try-with-resorces문을 제공해 close() 메서드를 명시적으로 호출하지 않아도 try블록 내에서 열린 리소스를 자동으로 닫도록 할 수 있다.
try-with-resorces문법을 사용하려면 해당 리소스가 AutoCloseable인터페이스를 구현해야 한다.
AutoCloseable인터페이스에는 close()메서드가 있고 이를 구현한 클래스는 close()를 명시적으로 호출하지 않아도 close()메서드 부분이 호출된다.
앞에서 사용했던 FileInputStream을 JavaDoc에서 찾아보면 AutoCloseable 인터페이스를 구현하고 있는 것을 확인할 수 있다.
FileInputStream 클래스는 closeable과 AutoCloseable 인터페이스를 구현했다. 따라서 자바 7부터는 try-with-resorces문법을 사용하면 FileInputStream을 사용할 때 close()를 명시적으로 호출하지 않아도 정상인 경우와 예외가 발생한 경우 모두 close()메서드가 호출된다.
FileInputStream 이외에 네트워크(socket)와 데이터베이스(connection)관련 클래스도 AutoCloseable 인터페이스를 구현하고 있다.
AutoCloseable 인터페이스
AutoCloseable 인터페이스를 직접 구현한 클래스를 만들고 프로그램이 정상적으로 수행됐을 때와 예외가 발생했을 때 각각 close()메서드 부분이 잘 호출되는지 보자.
먼저 프로그램이 정상적으로 수행되는 경우.
package exception;
public class AutoCloseObj implements AutoCloseable{
@Override
//close() 메서드 구현
public void close() throws Exception {
System.out.println("리소스가 close() 되었습니다");
}
}
AutoCloseable 인터페이스는 반드시 close() 메서드를 구현해야 한다.
시스템 리소스인 경우에는 파일 스트림을 닫거나 네트워크 연결을 해제하는 코드를 작성해야겟지만, 여기서는 close()메서드가 제대로 호출되는지 알아보기 위해 출력문을 남기도록 했다.
이제 AutoCloseTest 클래스를 만들어 테스트하자.
package exception;
public class AutoCloseTest {
public static void main(String[] args) {
try(AutoCloseObj obj = new AutoCloseObj();) { //사용할 리소스 언언
} catch (Exception e) {
System.out.println("예외 부분입니다");
}
}
}
try-with-resorces문을 사용할 때 try문의 괄호()안에 리소스를 선언한다.
- 위 예제는 예외 발생 없이 정상 종료되는데
출력 결과를 보면 close()메서드가 호출되어 출력문이 실행됨을 알 수 있다. - 리소스를 여러개 생성해야한다면 세미콜론(;)으로 구분한다.
try(A a = new A(); B b = new B()) {
...
} catch(Exception e) {
...
}
이제 예외가 발생되어 종료되는 경우도 close()메서드가 잘 호출되는지 확인해보자.
throw new Exception()문장을 사용하면 프로그램에서 강제로 예외를 발생시켜 catch블록이 수행되도록 구현할 수 있다.
try블록 안에 다음처럼 코드를 작성하자.
package exception;
public class AutoCloseTest {
public static void main(String[] args) {
try(AutoCloseObj obj = new AutoCloseObj();) {
throw new Exception();
} catch (Exception e) {
System.out.println("예외 부분입니다");
}
}
}
try블록에서 강제로 예외를 발생시키면 catch블록이 수행된다.
- 출력 결과를 보면 리소스의 close()메서드가 먼저 호출되고 예외 블록 부분이 수행되는 것을 알 수 있다.
이처럼 try-with-resorces문을 사용하면 close() 메서드를 명시적으로 호출하지 않아도 정상 종료된 경우와 예외가 발생한 경우 모두 리소스가 잘 해제된다.
향상된 try-with-resorces문 (자바 9이상)
자바 7에서 제공하기 시작한 try-with-resorces문의 예외 처리 방법은 자바 9로 업그레이드 되면서 조금 더 향상되었다. 자바 7에서는 AutoCloseable 인터페이스를 구현한 리소스의 변수 선언을 try문 괄호 안에서 해야했지만,
자바 9부터는 다음 try문의 괄호 안에다가 외부에서 선언한 변수를 쓸 수 있다.
기존 try-with-resorces문
AutoCloseObj obj = new AutoCloseObj();
try(AutoCloseObj obj2 = obj) { //다른 참조 변수로 다시 선언해야 함.
throw new Exception();
} catch (Exception e) {
System.out.println("예외 부분입니다");
}
향상된 try-with-resorces문 : try문에 외부에서 선언한 변수를 쓸 수 있다.
AutoCloseObj obj = new AutoCloseObj();
try(obj) { //다른 참조 변수로 다시 선언해야 함.
throw new Exception();
} catch (Exception e) {
System.out.println("예외 부분입니다");
}
이 문법은 자바 9에서 추가되어 자바 8이하에서는 오류가 발생한다.
<예외 처리 미루기>
예외 처리를 미루는 throws
FileInputStream을 생성했을 때 예외 처리 방법은 두 가지였다.
Surround with try/catch는 살펴보았으니 이제 Add throws declaration에 대해 알아본다.
그대로 번역하면 throws선언을 추가한다는 의미다.
예외를 해당 메서드에서 처리하지 않고 미룬 후 메서드를 사용하는 부분에서 예외를 처리하는 방법이다.
다음 예제를 보자.
package exception;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
public class ThrowsException {
public Class loadClass(String fileName, String className) throws FileNotFoundException, ClassNotFoundException { //두 예외를 미룸
FileInputStream fis = new FileInputStream(fileName); //FileNotFoundException 발생 가능
Class c = Class.forName(className); //ClassNotFoundException 발생 가능
return c;
}
public static void main(String[] args) {
ThrowsException test = new ThrowsException();
test.loadClass("a.txt", "java.lang.String"); //메서드가 호출할 때 예외를 처리함.
}
}
위 코드에서 정의한 loadClass()메서드는 FileInputStream을 열고 Class를 동적으로 로딩하여 반환한다.
파일을 열 때는 FileNotFoundException이 발생할 수 있고,
클래스를 로딩할 때는 ClassNotFoundException이 발생할 수 있다.
하지만 처리를 미루겠다는 throws를 메서드의 선언부에 추가했다.
그럼 이 두 가지 예외는 어디에서 처리될까?
throws를 활용해 예외 처리 미루기
예외를 처리하지 않고 미룬다고 선언하면, 그 메서드를 호출해 사용하는 부분에서 예외를 처리해야 한다.
main()함수에서 loadClass()를 호출하는 부분이 있다. 여기서 오류가 발생했는데 메시지를 확인하면
세 가지 옵션 중 하나를 선택하여 오류를 처리할 수 있다.
- Add throws declaration은 main()함수 선언 부분에 throws FileNotFoundException과 ClassNotFoundException을 추가하고 예외 처리를 미룬다는 뜻이다.
main()함수에서 미룬 예외 처리는 main()함수를 호출하는 자바 가상 머신(JVM)으로 보내진다.
즉 예외를 처리하는 것이 아니라 대부분의 프로그램이 비정상 종료된다. 따라서 다른 두 옵션 중 하나를 사용하는 것이 좋다. - Surround with try/multi-catch를 선택하면 다음 코드가 생성된다.
이 옵션은 하나의 catch문에서 여러 예외를 한 문장으로 처리하겠다는 뜻.public static void main(String[] args) { ThrowsException test = new ThrowsException(); // Surround try/multi-catch를 선택하여 생성됨. try { test.loadClass("a.txt", "java.lang.String"); // 여러 예외를 한 문장으로 처리함 } catch (FileNotFoundException | ClassNotFoundException e) { // TODO Auto-generated catch block e.printStackTrace(); } }
그렇지 않고 각 상황마다 예외 처리를 하려면 세 번째 옵션 Surround with try/catch를 선택하면 된다.
public static void main(String[] args) { ThrowsException test = new ThrowsException(); //Surround with try/catch를 선택해 생성됨 try { test.loadClass("a.txt", "java.lang.String"); //각 예외 상황마다 다른 방식으로 처리함. } catch (FileNotFoundException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (ClassNotFoundException e) { // TODO Auto-generated catch block e.printStackTrace(); } }
그러면 예외 상황의 수만큼 catch문이 생성된다. 각 예외 상황마다 다른 방식으로 처리해야 하고 로그도 다르게 남겨야 하는 경우라면 이 옵션이 적절하다고 할 수 있다.
예외가 발생한 메서드에서 그 예외를 바로 처리할 것인지, 아니면 미루어 그 메서드를 호출하여 쓰는 부분에서 처리할 것인지는 만드려는 프로그램의 상황에 따라 다를 수 있다.
만약 어떤 메서드가 다른 여러 코드에서 호출되어 사용된다면 호출하는 코드 상황에 맞게 로그를 남기거나 예외 처리를 하는 것이 더 좋다. 따라서 이런 경우에는 메서드를 호출하는 부분에서 예외 처리를 하도록 미루는 것이 합리적이다.
다중 예외 처리
여러 catch문을 한꺼번에 사용하는 경우에 각 catch 블록은 각각의 예외 처리를 담당한다.
그런데 문법적으로 반드시 예외 처리를 해야 하는 경우 이외에도 예외 처리를 해야 할 때가 있다.
예를 들어 배열을 사용할 때 배열의 크기보다 큰 위치, 즉 요소가 존재하지 않는 위치로 접근하는 경우에 RuntimeException중 ArrayIndexOutofBoundsException이 발생하는데, 이 예외는 컴파일러에 의해 체크가 되지 않는다.
이렇게 어떤 예외가 발생할지 미리 알 수 없지만 모든 예외 상황을 처리하고 자한다면 맨 마지막 부분에 Exception 클래스를 활용하여 catch 블록을 추가한다.
다음 예제를 보자.
package exception;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
public class ThrowsException {
public Class loadClass(String fileName, String className) throws FileNotFoundException, ClassNotFoundException {
FileInputStream fis = new FileInputStream(fileName);
Class c = Class.forName(className);
return c;
}
public static void main(String[] args) {
ThrowsException test = new ThrowsException();
try {
test.loadClass("a.txt", "java.lang.String");
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
//Exception 클래스로 그 외 예외 상황 처리
} catch (Exception e) {
e.printStackTrace();
}
}
}
Exception 클래스는 모든 예외 클래스의 최상위 클래스다.
다른 catch블록에 선언한 것 이외의 예외가 발생하더라도 Exception 클래스로 자동 형 변환된다.
(※Exception 클래스를 기본(default)예외 처리라고도 부른다.)
다중 예외 처리에서 주의 사항
예외는 catch문을 선언한 순서로 검사한다.
따라서 맨 위에 catch(Exception e)문장을 쓰면 발생하는 모든 예외 클래스는 Exception 상위 클래스로 자동 형 변환되어 오류가 발생한다.
Exception 클래스의 아래에 있는 FileNotFoundException과 ClassNotFoundException에 오류가 발생했다.
오류 메시지를 확인해보면,
기본(default)(Exception으로)예외 처리를 하는 경우,
모든 예외가 처리되므로 FileNotFoundException이나 ClassNotFoundException문장에는 예외가 도달할 일이 없어 컴파일 오류가 발생한다.
따라서 기본 예외 처리를 하는 Exception 클래스 블록은 여러 예외 처리 블록의 가장 아래에 놓여야 한다.
<사용자 정의 예외>
자바에서 제공하는 예외 처리 클래스 이외에 개발하는 프로그램에 따라 다양한 예외 상황이 발생할 수 있다.
예를 들어 어떤 사이트에 회원 가입을 할 때 입력하는 아이디 값이 null이어서는 안 되고, 8자 이상 20자 이하로 만들어야 하는 조건이 필요할 수 있다. 이런 조건을 체크하는 작업을 자바 프로그램에서 한다면 예외 클래스를 직접 만들어 예외를 발생시키고 예외 처리 코드를 구현할 수 있다.
실무에서 프로젝트를 진행할 때에도 예외 클래스를 직접 만들어 사용하는 경우가 종종 있으므로 사용자 정의 예외 클래스를 어떻게 구현하는지 알아두면 좋다.
사용자 정의 예외 클래스 구현
사용자 정의 예외 클래스를 구현할 때는
기존 JDK에서 제공하는 예외 클래스 중 가장 유사한 클래스를 상속받는 것이 좋다.
유사한 예외 클래스를 잘 모르겠다면 가장 상위 클래스인 Exception 클래스를 상속받자.
아이디가 null값이거나 지정 범위를 벗어나는 경우의 예외 처리 클래스를 만들어 보자.
package exception;
public class IDFormatException extends Exception {
public IDFormatException(String message) { //생성자의 매개변수로 예외 상황 메시지 받음.
super(message);
}
}
위 코드는 Exception 클래스에서 상속받아 구현했다.
- 예외 상황 메시지를 생성자에서 입력받는다.
- Exception 클래스에서 메시지 생성자, 멤버 변수와 메서드를 이미 제공하고 있으므로 super(message)를 사용해 예외 메시지를 설정한다.
- 나중에 getMessage()메서드를 호출하면 메시지 내용을 호출할 수 있다.
그럼 예외를 발생시켜 보자.
package exception;
public class IDFormatTest {
private String userID;
public String getUserID() {
return userID;
}
//ID에 대한 제약 조건 구현
public void setUserID(String userID) throws IDFormatException { //ID~Exception 예외를 setUserID()메서드가 호출될 때 처리하도록 미룸
if (userID == null) {
throw new IDFormatException("아이디는 null일 수 없습니다."); //강제로 예외 발생
}
else if (userID.length() < 8 || userID.length() > 20) {
throw new IDFormatException("아이디는 8자 이상, 20자 이하만 가능합니다."); //강제로 예외 발생
}
this.userID = userID;
}
public static void main(String[] args) {
IDFormatTest test = new IDFormatTest();
//ID가 null값인 경우
String userID = null;
try {
test.setUserID(userID);
} catch (IDFormatException e) {
System.out.println(e.getMessage());
}
//ID가 8자 이하인 경우
userID = "1234567";
try {
test.setUserID(userID);
} catch (IDFormatException e) {
System.out.println(e.getMessage());
}
}
}
IDFormatTest클래스에서 setUserID()메서드는 아이디에 대한 제약 조건을 구현한다.
이 제약 조건이 지켜지지 않으면 예외를 발생시킨다.
여기서 발생하는 예외는 자바에서 제공하는 예외가 아니기 때문에 예외 클래스를 직접 생성하여 예외를 발생시켜야 한다.
- setUserID메서드에서 매개변수로 넘어온 userID가 null인 경우에 예외 메시지를 생성자에 넣어 예외 클래스를 생성한 후 throw문으로 직접 예외를 발생시킨다.
ID가 8자 미만, 20자 초과인 경우에도 길이 관련 예외를 발생시킨다. - setUserID()메서드는 IDFormatException 예외 처리를 해야한다.
이 예외는 메서드를 호출하는 부분에서 처리하도록 throws 예약어를 선언해준다. - main()함수에서 예외 상황을 만들기 위해 ID값에 null을 대입.
setUserID()메서드에서 IDFormatException 예외가 발생하고 setUserID()메서드는 이 예외를 미뤘으므로 catch블록에서 처리된다.
ID가 8자 이하인 예외에서도 마찬가지.
이처럼 프로그램 개발 상황에서 필요에 따라 사용자 정의 예외 클래스를 직접 만들고 이를 발생시켜 예외 처리를 할 수 있음을 확인할 수 있다.
예외 처리를 할 때는 로그를 잘 남기자
회사에서 어떤 시스템을 개발하여 구축했다. 그런데 고객으로부터 오류가 발생했다는 연락이 왔을 때 개발자는 어떤 조치를 취할 수 있을까?
어떤 상황에서 오류가 발생했는지, 또 시스템에서 어떤 메서드를 호출하고 어떻게 매개변수를 전달했는지 오류 현상만 보고는 알 수 없다.
따라서 프로그램을 개발할 때는 로그(log)를 남기는 것이 매우 중요하다. 오류가 발생했을 때 로그를 보고 오류가 발생하는 코드를 순서대로 따라가며 확인할 수 있고 원일을 찾을 수 있다.
로그는 정보의 의미에 따라 레벨을 나누어 관리한다.
간단한 정보 의미를 가진 로그부터~심각한 예외가 발생했을 때의 로그까지 여러 레벨이 존재할 수 있다. 이러한 로그를 체계적이고 의미 있게 남겨서 시스템에서 오류가 발생했을 때 그 원인을 유추해 볼 수 있어야 한다.
연습:
비밀번호 예외 클래스 만들기 |
위에서 실습한 사용자 정의 예외를 응용해 PasswordException을 만들어 보자. 예외 상황은 비밀번호가 null인 경우, 문자열로만 이루어진 경우, 5자 이하인 경우다. (※문자열로만 이루어졌는지 알아보려면 String의 matches() 메서드를 사용하면 된다.) |
String클래스의 matches()사용법
String pass = new String("abc");
System.out.println(pass.matches("[a-zA-Z]+")); //true
String pass2 = new String("abc1");
System.out.println(pass2.matches("[a-zA-Z]+")); //false
정답:
PasswordException 클래스 - 예외 처리 클래스
package alone;
public class PasswordException extends Exception {
public PasswordException (String message) {
super(message);
}
}
PasswordTest 클래스
package alone;
public class PasswordTest {
String password;
public String getPassword() {
return password;
}
public void setPassword(String password) throws PasswordException {
if(password == null) {
throw new PasswordException("비밀번호는 null일 수 없습니다");
}
else if(password.matches("[a-zA-Z]+")) {
throw new PasswordException("비밀번호는 문자열로만 생성할 수 없습니다");
}
else if(password.length()<5) {
throw new PasswordException("비밀번호는 5자 이하로 생성할 수 없습니다");
}
this.password = password;
}
public static void main(String[] args) {
PasswordTest test = new PasswordTest();
String password = null;
try {
test.setPassword(password);
System.out.println("오류없음1");
} catch (PasswordException e) {
System.out.println(e.getMessage());
}
password = "aaaa";
try {
test.setPassword(password);
System.out.println("오류없음2");
} catch (PasswordException e) {
System.out.println(e.getMessage());
}
password = "aaa1";
try {
test.setPassword(password);
System.out.println("오류없음3");
} catch (PasswordException e) {
System.out.println(e.getMessage());
}
password = "aaaㅁㅇㄴㅇ1";
try {
test.setPassword(password);
System.out.println("오류없음4");
} catch (PasswordException e) {
System.out.println(e.getMessage());
}
}
}