자바14 예외 처리
2021년08월10일오류란
프로그램에서 오류가 발생하는 상황은 두 가지이다. 하나는 프로그램 코드 작성 중 실수로 발생하는 컴파일 오류(compile error)이고, 다른 하나는 실행 중인 프로그램이 의도하지 않은 동작을 하거나 프로그램이 중지되는 실행 오류(runtime error)이다. 실행 오류 중 프로그램을 잘못 구현하여 의도한 바와 다르게 실행되어 생기는 오류를 버그라고 한다. 컴파일 오류는 개발 환경에서 대부분 원인을 알 수 있다. 발생한 컴파일 오류를 모두 수정해야 프로그램이 정상적으로 실행되므로, 문법적으로 오류가 있음을 알 수 있다. 하지만 버그는 예측하기 어렵고 프로그램이 비정상 종료되면서 갑자기 멈춰 버린다. 실제 서비스가 이러하면 서비스가 중지되는 것이므로 문제가 심각해진다. 또한 실행 중에 오류가 발생하면 그 상황을 재현하여 테스트해야 하는데, 실제 시스템이나 서비스가 운영 중인 경우에는 쉬비 않다. 따라서 로그 분석을 통해 원인을 찾을 수 있도록 프로그램을 개발할 때 로그를 남기는것이 중요하다. 자바는 이런 비정상 종료를 최대한 줄이기 위해 다양한 예외에 대한 처리 방법을 가지고 있다. 예외 처리를 하는 목적은 일단 프로그램이 비정상 종료되는 것을 방지하기 위한 것이다. 그리고 예외가 발생했을 때 로그를 남겨두면 예외 상황을 파악하고 버그를 수정하는데 도움이 된다.
오류와 예외
실행 오류는 크게 두 가지가 있는데, 하나는 자바 가상 머신에서 발생하는 시스템 오류이고 다른 하나는 예외(exception)이다. 시스템 오류의 예로는 사용 가능한 동적 메모리가 없는 경우나 스택 메모리의 오버플로가 발생한 경우이다. 이러한 시스템 오류는 프로그램에서 제어할 수 없는 반면 예외는 프로그램에서 제어할 수 있다. 가령, 프로그램에서 파일을 읽어 사용하려는데 파일이 없는 경우, 네트워크로 데이터를 전송하려는데 연결이 안 된 경우, 배열 값을 출력하려는데 배열 요소가 없는 경우 등이다.
자바에서 제공한은 오류에 대한 전체 클래스는 이러하다.
Throwable <- Error
<- Exception
오류 클래스는 모두 Throwable 클래스에서 상속받는다. Error 클래스의 하위 클래스는 시스템에서 발생하는 오류를 다루며 프로그램에서 제어하지 않는다. 프로그램에서 제어하는 부분은 Exception 클래스와 그 하위에 있는 예외 클래스이다.
예외 클래스의 종류
프로그램에서 처리하는 예외 클래스의 최상위 클래스는 Exception 클래스이다. 그리고 이를 상속받는 구성은 대략 이러하다.
Exception <- IOException(입출력 예외 처리) <- FileNotFoundException, SocketException
<- RuntimeException(실행 오류 예외 처리) <- ArithmeticException, IndexOutofBoundsException
예외는 다음과 같은 방식으로 처리한다.
try-catch
try {
예외가 발생할 수 있는 코드 부분
} catch(처리할 예외 타입 e) {
try 블록 안에서 예외가 발생했을 때 예외를 처리하는 부분
}
// 실사용
public class ArrayExceptionHandling {
public static void main(String[] args) {
int[] arr = new int[5];
try {
for(int i=0; i<=5; i++) {
arr[i] = i;
System.out.println(arr[i]);
}
} catch(ArrayIndexOutOfBoundsException e) {
System.out.println(e);
System.out.println("예외 처리 부분");
}
}
}
위 예제에서는 배열을 5개 만들었는데, 인덱스를 그 이상으로 접근하여 예외가 발생하는 것이다.
try-catch-finally
프로그램에서 사용한 리소스는 프로그램이 종료되면 자동으로 해제된다. 예를 들어 네트워크가 연결되었을 경우에 채팅 프로그램이 종료될 때 연결도 닫힌다. 하지만 끝나지 않고 계속 수행되는 서비스 같은 경웨 리소스를 여러 번 반복해서 열기만 하고 닫지 않는다면 문제가 발생한다. 시스템에서 허용하는 자원은 한계가 있기 때문이다. 따라서 앞에서 사용한 시스템 리소스는 사용 후 반드시 close() 메서드로 닫아 주어야 한다. try인 경우나 비정상 종료된 경우에도 닫아주어야 하는데 이럴 때 사용하는게 finally 문이다. finally는 이름 그대로 최종적으로 실행해주는 것이다. 즉 try나 catch 이후에 무조건 실행되는 것이다.
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();
}
}
}
}
try-with-resources
위처럼 하면 시스템 리소스를 사용하고 해제하는 코드는 다소 복잡해보인다. 자바7부터는 try-with-resources문을 제공하여 close() 메서드를 명시적으로 호출하지 않아도 try블록 내에서 열린 리소스를 자동으로 닫도록 만들 수 있다. try-with-resources문법을 사용하려면 해당 리소스가 AutoCloseable인터페이스를 구현해야 한다. FileInputStream은 Closeable과 AutoCloseable인터페이스를 구현했기에 이 문법을 사용할 수 있다.
public class AutoCloseObj implements AutoCloseable {
@Override
public void close() throws Exception {
System.out.println("close()");
}
}
public class AutoCloseTest {
public static void main(String[] args) {
try(AutoCloseObj obj = new AutoCloseObj()) {
throw new Exception()
} catch(Exception e) {
System.out.println("예외");
}
}
}
// 자바9
AutoCloseObj obj = new AutoCloseObj();
try(obj) {
throw new Exception();
} catch(Exception e) {
System.outn.print("exception");
}
try-with-resources문을 사용할 때 try문의 괄호() 안에 리소스를 선언한다. 그러면 자동으로 close()를 불러준다. 자바 7에서는 괄호안에서 다른 참조 변수로 선언해주어야 하는데, 자바 9부터는 위에서 선언한 참조변수를 넣을 수 있다.
예외 처리 미루기 (throws)
FileInputStream을 생성했을 때 예외 처리 방법은 두 가지가 있었다. 하나는 try/catch이고 다른 하나는 throws 선언이다. 이것은 예외를 해당 메서드에 처리하지 않고 미룬 후 메서드를 호출하여 사용하는 부분에서 예외를 처리하는 방법이다.
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();
test.loadClass("a.txt", "java.lang.String"); // 메서드를 호출할 때 예외를 처리함
}
}
이렇게 하면 loadClass에서 생기는 예외는 main()에서 처리를 해야하는데, main()에서도 throws를 하게되면 자바 가상 머신으로 보내지는데, 이러면 비정상 종료가 된다. 다른 방법으론 main()에 try/catch문을 써주면 된다.
ThrowsException test = new ThrowsException();
try {
test.loadClass("a.txt", "java.lang.String");
} catch (FileNotFoundException | ClassNotFoundException e) {} // 하나의 문장으로 처리
// 또는
try {
test.loadClass("a.txt", "java.lang.String");
} catch (FileNotFoundException e) {
} catch (ClassNotFoundException e) {}
다중 예외 처리
여러 catch문을 한꺼번에 사용하는 경우에 각 catch 블록은 각각의 예외를 담당한다. 그런데 문법적으로 반드시 예외 처리를 해야 하는 경우 이외에도 예외 처리를 해야 할 때가 있다. 그럴땐 마지막 부분에 Exception 클래스를 활용하여 처리해버린다.
try {
test.loadClass("a.txt", "java.lang.String");
} catch (FileNotFoundException e) {
} catch (ClassNotFoundException e) {
} catch (Exception e) {}
이때 주의해야할게 catch문은 선언한 순서대로 검사한다. 따라서 맨 위에 Exception 문장을 쓰면 발생하는 모든 예외 클래스는 Exception 상위 클래스로 자동 형 변환되어 오류가 발생한다.
사용자 정의 예외
자바에서 제공하는 예외 처리 클래스 이외에 개발하는 프로그램에 따라 다양한 예외 상황이 발생할 수 있다. 예를 들어 어떤 사이트에 회원 가입을 할 때 입력하는 아이디 값이 null이어서는 안 되고 8자 이상 20자 이하로 만들어야 하는 조건이 필요할 수 있다. 이런 조건을 체크하라는 작업을 자바 프로그램에서 한다면 예외 클래스를 직접 만들어 예외를 발생시키고 예외 처리 코드를 구현할 수 있다. 사용자 정의 예외 클래스를 구현할 때 JDK에서 제공하는 가장 유사한 클래스를 상속받는 것이 좋다. 만약 모르겠다면 Exception클래스를 상속받자.
public class IDFormatException extends Exception {
public IDFormatException(String message) {
super(message);
}
}
위와 같이 Exception을 상속받고 생성자를 만들고 super(message)로 예외 메시지를 설정한다. 나중에 getMessage()메서드를 호출하면 메시지 내용을 볼 수 있다.
public lcass IDFormatTest {
private String userID;
public String getUserID() {
return userID;
}
public void setUserID(String userID) throws IDFormatException {
if(userID == null) {
throw new IDFormatException("ID can't be null");
} else if (userID.length() < 8 || userID.length() > 20) {
throw new IDFormatException("use ID length from 8 to 20");
}
this.userID = userID;
}
public static void main(String[] args) {
IDFormatTest test = new IDFormatTest();
String userID = null;
try {
test.setUserID(userID);
} catch (IDFormatException e) {
System.out.println(e.getMessage());
}
userID = "1234567";
try {
test.setUserID(userID);
} catch (IDFormatException e) {
System.out.println(e.getMessage());
}
}
}