벌써 목요일이다.
그래서 그런지 좀 피곤하게 시작한다.
에러와 예외는 심각도에 따라 분류된다
직접 프로그램이나 로직을 만들 때는 나의 의도와 다른 입력 또는 출력 값이 나올 수 있다.
물론, 모든 예외를 예상하고 완벽한 로직을 만든다면 좋겠지만,
원래 인생이란 생각대로 되지 않는 법.
예외가 발생하는 경우가 대부분이다.
그럼 이 예외를 처리하는 방법을 알아보자.
예외 발생 시 프로그램의 비 정상 종료를 막고 정상 실행 상태로 유지하는 것이다.
만약, 우리가 카카오톡으로 친구들과 연락을 하고 있는데, 어딘가에 발생한 예외로 계속 카카오톡이 종료된다면
진짜 너무 불편한 상황이 올 것이다. 그래서 종료를 막고 해당 행동을 차단하면서 실행을 유지하는 것이 예외 처리 이다.
try ~ catch
try{
// 예외가 발생할 수 있는 구문 또는 로직
} catch(XXException e){
// 파라미터에 대한 예외가 발생 했을 때, 처리할 코드
}
이렇게 try-catch 구문으로 예외를 처리할 수 있다.
public class SimpleException {
public static void main(String[] args) {
int[] intArray = { 10 };
try {
System.out.println(intArray[2]);
} catch (Exception e) {
System.out.println(e.getMessage());
}
System.out.println("프로그램 종료합니다.");
}
}
이처럼, try-catch문이 없었다면, 런타임 시 에러가 발생하고 강제로 종료되었을 텐데,
예외를 처리하면서 예외 발생 메세지는 출력 되었지만, 정상적으로 종료되는 것을 알 수 있다.
Throwable의 주요 메서드를 알아보자.
이처럼 런타임 단계에서 발생한 예외도 있지만, 컴파일 단계에서 발견하고는 예외도 존재한다.
public class CheckedExceptionHandling {
public static void main(String[] args) {
Class<?> myClass = Class.forName("com.ssafy.day09.a_basic.SimplException");
System.out.println(myClass.getName());
// END
System.out.println("프로그램 정상 종료");
}
}
이 코드를 그대로 작성하면, 빨간줄과 함께 컴파일이 안된다.
이 또한, try-catch 문으로 해결이 가능하다.
위의 내용처럼 런타임, 컴파일 단계에서 발생하는 예외가 각각 다르다.
이를 하나로 정리한 트리가 있다.
우리는 복잡한 로직도 구현하기 때문에, 발생할 수 있는 예외의 종류도 여러 개일 가능성이 높다.
이를 처리하는 방법은 다중 catch문을 사용하거나, catch할 예의를 |
연산자를 통해 여러 Exception을 동시에 처리할 수 있다.
public class MultiExceptionHandling {
@SuppressWarnings("resource")
public static void main(String[] args) {
// TODO: 다음에서 발생하는 예외를 처리해보자.
Class.forName("abc.Def"); // ClassNotFoundException
new FileInputStream("Hello.java"); // FileNotFoundException
DriverManager.getConnection("Hello"); // SQLException
// END
System.out.println("프로그램 정상 종료");
}
}
// 예외 처리 ↓
public class MultiExceptionHandling {
@SuppressWarnings("resource")
public static void main(String[] args) {
try {
Class.forName("abc.Def");
new FileInputStream("Hello.java");
DriverManager.getConnection("Hello");
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (SQLException e) {
e.printStackTrace();
}
// END
System.out.println("프로그램 정상 종료");
}
}
예외 처리한 코드를 보면 3개의 메서드와 각 메서드에 해당하는 예외 처리를 했다.
그럼 catch문을 작성할 때, 순서는 상관없을까?
물론 컴파일이나 런타임에 문제는 없겠지만, 보다 정확하게 예외를 처리하기 위해서는
자식
-> 부모
순으로 상속 관계를 갖는 catch문을 작성해야 한다.
try {
Class.forName("abc.Def");
new FileInputStream("Hello.java");
DriverManager.getConnection("Hello");
} catch (Exception e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (SQLException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
이처럼 제일 위에 Exception을 받는 catch문이 있다면, 아래의 나머지 catch문은 의미가 없어진다. 그리고 컴파일 에러까지 남 ㄷㄷ
Unreachable catch block for ClassNotFoundException
try-catch문에 추가로 finally 블럭을 넣을 수 있다.
그럼 try-catch-finally문이 된다.
try{
...
} catch {
...
} finally{
...
}
그럼 finally
는 무슨 역할일까? 뜻만 본다면 마지막으로, 결국에 등등의 뜻이 된다.
즉, try-catch문이 다 수행된 후 마지막에 반드시 수행하는 것을 뜻한다.
어느 정도의 강도냐면, try문 안에 return이 있어도 반드시 finally를 실행하고 빠져 나온다.
그럼 코드의 중복을 줄이는 코드를 작성할 수 있다.
InstallApp app = new InstallApp();
try {
app.copy();
app.install();
app.delete();
} catch (Exception e) {
app.delete();
e.printStackTrace();
}
↓
InstallApp app = new InstallApp();
try {
app.copy();
app.install();
} catch (Exception e) {
e.printStackTrace();
} finally {
app.delete();
}
열 수 있는 파일이 정해져 있는 경우를 위해 리소스를 반납 해야 함. 즉, close처리를 해줘야 함.
public void useStream() {
FileInputStream fileInput = null;
try {
fileInput = new FileInputStream("abc.txt");
fileInput.read();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (fileInput != null) {
try {
fileInput.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
이와 같이 try 문 안에 리소스 close가 필요한 객체가 있을 때, close를 해야 하는 시점을 정해야 한다. 그래서 위와 같이 코드의 depth가 깊어지고 지저분해진다.
그래서 코드의 가독성을 높이기 위해 try-with-resources라는 기법을 쓴다.
try(FileInputStream fileInput = new FileInputStream("abc.txt")){
fileInput.read();
} catch (IOException e) {
e.printStackTrace();
}
이처럼 try문에 ()를 만들고 거기에 리소스를 선언해주면 try 구문 안에서만 사용하다가 자동으로 close가 된다.
단, 모든 객체들이 가능한 것은 아니다.
AutoClosealbe
인터페이스를 구현한 객체들만 가능하다.
ex) 각종 I/O stream, socket, connection 등
또한, 리소스 객체는 try 블록 안에서 재 할당할 수 없다. 즉, final 변수로 취급한다.
예외를 처리하는 목적으로 try-catch문을 사용한다면
발생한 예외를 상위로 넘기는 것을 throws를 통해 사용할 수 있다.
예외를 전달받은 메서드는 다시 상위로 전달하거나 처리를 해야한다.
앞서 예외 계층에서 나온 것처럼 예외는 컴파일 단계와 런타임 단계에서 발생하는 계층으로 나눌 수 있다.
컴파일 단계에는 명시적으로 try-catch문을 사용해야 컴파일이 가능했는데, throws 역시 예외가 발생하는 메서드를 호출하는 모든 상위 메서드에 throws를 명시 해야 한다.
반대로 unchecked exception은 굳이 throws를 명시할 필요는 없다.
물론, 예외를 처리할 메서드에는 try-catch문으로 처리해야 한다.
이제 예외를 처리까지 알아봤으니
어떤 예외가
뭐 때문에
어디서
발생했는지 알아볼 차례다.
우선, Throwable을 상속 받는 모든 객체는 printStackTrace()라는 메서드를 통해 예외 정보를 볼 수 있다.
그럼 왜 굳이 throws로 예외 처리를 넘기는 걸까? 그냥 바로 예외를 처리하면 안될까?
그 이유는 사전에 예외가 발생할 수 있음을 선언부에 명시하고 프로그래머가 이를 인지 및 대처할 수 있게 하기 위함이다.
즉, 상황에 맞게 프로그래밍 할 수 있게 도와주기 위해.
메서드 재정의 시 부모 클래스가 던지는 예외보다 더 큰 예외를 던질 수 없다.
무슨 뜻인지 코드로 확인해 보자.
class Parent{
void methodA() throws IOException{}
void methodB() throws ClassNotFoundException{}
}
public class Child extends Parent{
@Override
void methodA() throws FileNotFoundException{...}
@Override
void methodB() throws Exception{...}
}
Child라는 클래스는 Parent라는 클래스를 상속 받고 각 메서드를 재정의하고 있다.
이때, methodB는 Parent 클래스에서 정의한 methodB의 예외 클래스의 상위 예외로 재정의 하고 있다. 결국 컴파일에러가 발생하게 된다.
즉, 부모가 다루지 않는 예외를 자식이 다루게 되니 논리상 어긋나게 된다.
추가로
자식 계층에서 던진 예외를 부모 계층에서 받을 때, 부모 계층에 맞는 예외로 바꿔서 보내야 할 때도 있다.
코드 동작에 에러는 없지만, 하위 계층에서 발생한 예외를 상위 계층의 메서드 성격에 맞게 처리가 필요할 경우 이러한 과정이 요구되기도 한다.
점점 정리할 부분이 많아져서 최대한 핵심을 정리하려고 노력하지만, 과연 이게 핵심인지 정리할 수록 생략한 부분이 아쉽게 느껴진다.