이 글은 프로그래머스 - 실무 자바 개발을 위한 OOP와 핵심 디자인 패턴 강의를 정리한 내용입니다.
예외는 프로그래밍 학습에 있어 굉장히 필수적이고 중요한 내용 중 하나입니다. 이미 예외에 대해 문법적으로 잘 알고있다고 생각할 수 있지만, 생각보다 잘 모르거나 잘못이해하고 있는 경우가 많습니다.
이번에는 예외의 문법적인 내용과 더불어 자바의 예외 처리 전략에 대해 알아보겠습니다.
다음 질문에 명확하게 답변할 수 있으신가요?
❓ Checked Exception과 Unchecked Exception은 무슨 차이인가요?
자바를 사용해서 개발한다면 위 내용은 반드시 알고 넘어가야하는 개념입니다.
혹시 속으로 이렇게 답변하고 있으신가요?
🤓 Checked Exception은 컴파일타임에 발생하는 예외이고, Unchecked Exception은 런타임에 발생하는 예외입니다!
위 답변은 잘못된 답변입니다. 예외는 모두 런타임 시점에만 발생하기 때문입니다. 컴파일 시점에는 예외가 발생하지 않습니다. 컴파일 시점에 발생하는건 문법적인 오류때문에 발생하는 컴파일 에러입니다.
그럼 뭐라고 답변해야할까요? 정답은 이렇습니다.
컴파일 시 Checked Exception
은 예외에 대한 처리를 강제하고, Unchecked Exception
은 예외에 대한 처리를 강제하지 않습니다.
예외 처리를 강제하지 않는다는게 이해가 되지 않을 수 있습니다. 예외 처리를 강제한다는 의미는 예외가 발생했을때 try-catch
문으로 감싸거나, throws
로 예외를 메서드 밖으로 던지는걸 강제한다는 의미입니다.
public static void main(String[] args) {
try {
throw new CheckedException();
} catch (CheckedException e) {
// 예외에 대한 적절한 처리
}
}
위 코드는 예외를 try-catch
문으로 감싸고 있는 형태이고
public static void main(String[] args) throws CheckedException {
throw new CheckedException();
}
위 코드는 throws
로 예외를 메서드 밖으로 던지는 형태입니다.
Checked Exception
은 이런식으로 예외에 대한 처리를 컴파일 시점에 강제합니다.
확인해볼까요?
CheckedException
public class CheckedException extends Exception {
}
Client
public class Client {
public void throwsCheckedExceptionMethod() throws CheckedException {
throw new CheckedException();
}
public void tryCatchCheckedExceptionMethod() {
try {
throw new CheckedException();
} catch (CheckedException e) {
// 예외에 대한 적절한 처리
e.printStackTrace();
}
}
Main
public class CheckedExceptionExampleMain {
public static void main(String[] args) {
Client client = new Client();
try {
client.throwsCheckedExceptionMethod();
} catch (CheckedException e) {
// 예외에 대한 적절한 처리
}
client.tryCatchCheckedExceptionMethod();
}
}
Client
에서 CheckedException
예외에 대해 예외 처리를 해주고 있는 로직을 다음과 같이 제거하겠습니다.
public class Client {
public void throwsCheckedExceptionMethod() {
throw new CheckedException();
}
public void tryCatchCheckedExceptionMethod() {
throw new CheckedException();
}
}
이제 위 코드를 실행해보면 다음과 같은 결과가 출력되는걸 확인할 수 있습니다.
java: unreported exception study.ooppractice.part01.exception.checked_exception.CheckedException; must be caught or declared to be thrown
java: unreported exception study.ooppractice.part01.exception.checked_exception.CheckedException; must be caught or declared to be thrown
컴파일 에러가 발생하였습니다. 에러 메시지를 유심히 살펴보면 CheckedException은 반드시 잡거나, 밖으로 던지라는 내용이 포함되어 있는걸 확인할 수 있습니다.
Checked Exception
은 예외 처리를 강제하는 반면, Unchecked Exception
은 예외에 대한 처리를 강제하지 않습니다.
public static void main(String[] args) {
throw new UncheckedException();
}
따라서 위 코드처럼 try-catch
문이나 throws
같은 예외 처리 로직이 없어도 정상적으로 컴파일이됩니다.
이번에도 코드로 직접 확인해보겠습니다.
UncheckedException
public class UncheckedException extends RuntimeException {
}
참고로 RuntimeException
을 상속받았을때 Unchecked Exception
이 됩니다.
Client
public class Client {
public void throwsUncheckedExceptionMethod() {
throw new UncheckedException();
}
public void tryCatchUncheckedExceptionMethod() {
throw new UncheckedException();
}
}
Client
코드에는 UncheckedException
에 대한 어떠한 예외 처리 로직도 존재하지 않습니다.
Main
public class UncheckedExceptionExampleMain {
public static void main(String[] args) {
Client client = new Client();
try {
client.throwsUncheckedExceptionMethod();
} catch (UncheckedException e) {
// 예외에 대한 적절한 처리
}
client.tryCatchUncheckedExceptionMethod();
}
}
이제 코드를 실행하면 다음과 같은 결과를 확인할 수 있습니다.
Exception in thread "main" study.ooppractice.part01.exception.unchecked_exception.UncheckedException
at study.ooppractice.part01.exception.unchecked_exception.Client.tryCatchUncheckedExceptionMethod(Client.java:9)
at study.ooppractice.part01.exception.unchecked_exception.UncheckedExceptionExampleMain.main(UncheckedExceptionExampleMain.java:13)
따로 UncheckedException
에 대한 예외 처리를 해주지 않았기 때문에 당연히 예외 로그가 출력됩니다. 하지만 CheckedException
과 달리 정상적으로 컴파일 된다는 것을 확인할 수 있습니다.
다시한번 강조하지만 중요한 포인트는 UnChecked Exception
은 Checked Exception
과 다르게 컴파일 시점에 예외 처리를 강제하지 않는다는 점입니다.
결론적으로
Checked Exception
을 정의하고 싶다 -> Exception
상속UnChecked Exception
을 정의하고 싶다 -> RuntimeException
상속Java 예외 객체의 상속 관계를 정리하면 위와 같은 구조가됩니다.
RuntimeException
이 Exception
을 상속받고 있는건 맞지만 문법적으로 Unchecked Exception
으로 정의되어 있습니다.
그리고 RuntimeException
을 상속받는 예외들은 Unchecked Exception
이 되기 때문에 Unchecked Exception
은 런타임에, Checked Exception
은 컴파일타임에 발생한다고 오해할 수 있습니다.
지금까지 얘기했듯 두 예외의 차이는 예외 처리를 컴파일 시점에 강제하는지에 대한 여부입니다. 꼭 기억하세요!!
상속 관계도를 보면 Exception
말고 Error
객체도 존재하는걸 확인할 수 있습니다.
Exception
을 예외라고 번역하듯, Error
는 오류라고 번역합니다.
try-catch
문으로 처리를 위한 로직을 작성할 수 있지만오류에 대표적인 예시로는 다음 두 가지가 있습니다.
OutOfMemoryError
StackoverflowError
이 둘 모두 애초에 발생하지 않도록 코드를 작성해야한다는 느낌이 올 것입니다.
그럼 앞서 살펴본 두 예외 중 무엇을 사용해야할까요? 물론 이미 자바에서 정의되어 있는 예외는 그대로 사용해야하지만, 새로운 커스텀 예외를 정의할 경우 어떤 예외로 만들어야할지 고민이 될 것입니다.
🤓 예외에 대한 처리를 강제하는
Checked Exception
으로 만드는게 더 적절하지 않을까요?! 예외가 발생했으면 당연히 처리해야지, 그냥 밖으로 던지는건 영 찝찝한데요?
그러나 실제로는 그 반대입니다. 우린 앞으로 예외를 정의할 때 Checked Exception
이 아니라 Unchecked Exception
으로 정의해야합니다.
그 이유는 대부분의 예외가 로직에서 해결할 수 경우이기 때문입니다.
예시를 들어보겠습니다.
사용자에게 파일 이름을 받아서 매칭되는 파일을 읽어와 사용자에게 제공하는 서비스가 있다고 가정해보겠습니다.
그런데 만약 사용자가 잘못된 파일 이름을 입력해준다면? 서버에서는 당연히 파일을 찾을 수 없습니다. 그럼 이때 서버는 사용자에게 어떤 동작을 해줘야할까요?
일반적으로는 사용자가 파일 이름을 다시 입력하도록 예외 메시지를 응답해줄 것입니다.
자바 코드에서 파일을 열지 못했을 때 발생하는 예외는 FileNotFoundException
입니다. 이 예외는 대표적인 Checked Exception
입니다.
그리고 FileNotFoundException
이 발생하는 코드는 다음과 같이 작성해볼 수 있습니다.
// throws로 예외를 메서드 밖으로 던지는 경우
public void readFile(String fileName) throws FileNotFoundException {
FileReader fileReader = new FileReader(fileName);
}
// try-catch문으로 예외를 메서드 안에서 처리해주는 경우
public void readFile(String fileName) {
try {
FileReader fileReader = new FileReader(fileName);
} catch (FileNotFoundException e) {
throw new RuntimeException(e);
}
}
FileReader
의 생성자는 Checked Exception
인 FileNotFoundException
을 throws합니다. 따라서 이 생성자를 호출하는 코드는 예외 처리가 강제됩니다.
첫 번째 코드처럼 발생한 예외를 다시 throws
를 통해 메서드 밖으로 던지거나, 두 번째 코드처럼 try-catch
문으로 처리해주는 로직을 작성해야합니다. 만약 두 번째 코드처럼 예외를 잡아서 다시 메서드 밖으로 RuntimeException
을 던질것이라면 차라리 Unchecked Exception
으로 정의해주는게 나을것입니다.
그리고 첫 번째 코드처럼 throws
로 예외를 메서드 밖으로 던지게 되면 또다른 문제가 생깁니다. 이 메서드를 호출하는 메서드에서도 다시 throws
로 예외를 밖으로 던져줘야합니다.
메서드를 호출한곳에서 예외를 처리할 방법이 없다면(처리하기 귀찮으면) 또 throws
로 예외를 밖으로 던지게 되고, 이걸 호출한 또 다른 메서드에게 예외가 전이됩니다.
결국 계속 무의미하게 예외 throws가 지속될 수 밖에 없습니다. 그리고 이건 객체지향 관점에서도 문제가 있습니다.
readFile
이 FileNotFoundException
을 던지는 것이 외부에 알려지는것은 캡슐화를 위반하는 코드가됩니다. 왜냐하면 내부에서 FileNotFoundException
을 던질만한 메서드를 사용하고 있다는 사실이 던져지는 예외를 통해서 알려지기 때문입니다.
😵💫 위 내용은 다른 포스트에서 좀 더 자세하게 다루겠습니다. 지금은
Checked Exception
을 던지는게 캡슐화를 위반하는 코드가 될 수도 있다고만 생각하시면됩니다.
예외를 도중에 처리하고 싶다면 Unchecked Exception
도 try-catch
문을 활용하여 충분히 처리할 수 있습니다.
결론적으로 캡슐화를 위반하지 않으면서, 필요할때만 예외를 처리할 수 있는 Unchecked Exception
을 사용하는게 더 적절합니다.