[Java/객체지향] Exception

양성욱·2023년 9월 19일
0
post-thumbnail

이 글은 프로그래머스 - 실무 자바 개발을 위한 OOP와 핵심 디자인 패턴 강의를 정리한 내용입니다.

예외는 프로그래밍 학습에 있어 굉장히 필수적이고 중요한 내용 중 하나입니다. 이미 예외에 대해 문법적으로 잘 알고있다고 생각할 수 있지만, 생각보다 잘 모르거나 잘못이해하고 있는 경우가 많습니다.

이번에는 예외의 문법적인 내용과 더불어 자바의 예외 처리 전략에 대해 알아보겠습니다.

Checked Exception VS Unchecked Exception

다음 질문에 명확하게 답변할 수 있으신가요?

❓ Checked Exception과 Unchecked Exception은 무슨 차이인가요?

자바를 사용해서 개발한다면 위 내용은 반드시 알고 넘어가야하는 개념입니다.

혹시 속으로 이렇게 답변하고 있으신가요?

🤓 Checked Exception은 컴파일타임에 발생하는 예외이고, Unchecked Exception은 런타임에 발생하는 예외입니다!

위 답변은 잘못된 답변입니다. 예외는 모두 런타임 시점에만 발생하기 때문입니다. 컴파일 시점에는 예외가 발생하지 않습니다. 컴파일 시점에 발생하는건 문법적인 오류때문에 발생하는 컴파일 에러입니다.

그럼 뭐라고 답변해야할까요? 정답은 이렇습니다.

컴파일 시 Checked Exception은 예외에 대한 처리를 강제하고, Unchecked Exception은 예외에 대한 처리를 강제하지 않습니다.

예외 처리를 강제하지 않는다는게 이해가 되지 않을 수 있습니다. 예외 처리를 강제한다는 의미는 예외가 발생했을때 try-catch문으로 감싸거나, throws로 예외를 메서드 밖으로 던지는걸 강제한다는 의미입니다.

Checked Exception

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은 반드시 잡거나, 밖으로 던지라는 내용이 포함되어 있는걸 확인할 수 있습니다.

Unchecked Exception

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 ExceptionChecked Exception과 다르게 컴파일 시점에 예외 처리를 강제하지 않는다는 점입니다.

Exception 상속 관계

결론적으로

  • Checked Exception을 정의하고 싶다 -> Exception 상속
  • UnChecked Exception을 정의하고 싶다 -> RuntimeException 상속

Java 예외 객체의 상속 관계를 정리하면 위와 같은 구조가됩니다.

RuntimeExceptionException을 상속받고 있는건 맞지만 문법적으로 Unchecked Exception으로 정의되어 있습니다.

그리고 RuntimeException을 상속받는 예외들은 Unchecked Exception이 되기 때문에 Unchecked Exception은 런타임에, Checked Exception은 컴파일타임에 발생한다고 오해할 수 있습니다.

지금까지 얘기했듯 두 예외의 차이는 예외 처리를 컴파일 시점에 강제하는지에 대한 여부입니다. 꼭 기억하세요!!

Error

상속 관계도를 보면 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 ExceptionFileNotFoundException을 throws합니다. 따라서 이 생성자를 호출하는 코드는 예외 처리가 강제됩니다.

첫 번째 코드처럼 발생한 예외를 다시 throws를 통해 메서드 밖으로 던지거나, 두 번째 코드처럼 try-catch문으로 처리해주는 로직을 작성해야합니다. 만약 두 번째 코드처럼 예외를 잡아서 다시 메서드 밖으로 RuntimeException을 던질것이라면 차라리 Unchecked Exception으로 정의해주는게 나을것입니다.

그리고 첫 번째 코드처럼 throws로 예외를 메서드 밖으로 던지게 되면 또다른 문제가 생깁니다. 이 메서드를 호출하는 메서드에서도 다시 throws로 예외를 밖으로 던져줘야합니다.


메서드를 호출한곳에서 예외를 처리할 방법이 없다면(처리하기 귀찮으면)throws로 예외를 밖으로 던지게 되고, 이걸 호출한 또 다른 메서드에게 예외가 전이됩니다.

결국 계속 무의미하게 예외 throws가 지속될 수 밖에 없습니다. 그리고 이건 객체지향 관점에서도 문제가 있습니다.


readFileFileNotFoundException을 던지는 것이 외부에 알려지는것은 캡슐화를 위반하는 코드가됩니다. 왜냐하면 내부에서 FileNotFoundException을 던질만한 메서드를 사용하고 있다는 사실이 던져지는 예외를 통해서 알려지기 때문입니다.

😵‍💫 위 내용은 다른 포스트에서 좀 더 자세하게 다루겠습니다. 지금은 Checked Exception을 던지는게 캡슐화를 위반하는 코드가 될 수도 있다고만 생각하시면됩니다.

정리

예외를 도중에 처리하고 싶다면 Unchecked Exceptiontry-catch문을 활용하여 충분히 처리할 수 있습니다.

결론적으로 캡슐화를 위반하지 않으면서, 필요할때만 예외를 처리할 수 있는 Unchecked Exception을 사용하는게 더 적절합니다.

profile
개발의 신이시여... 제게 집중할 수 있는 ㅎ... 네? 맥주요?

0개의 댓글