본문 바로가기
Java

Java의 Error, Exception, 그리고 Checked와 Unchecked

by Taler 2022. 11. 22.

자바는 오류가 발생하거나 발생할 여지가 있는 여러 상황에 대한 예외를 예비해두었다. IllegalArgumentException이나 NullPointerException 등이 이 예외의 예시이다. 또한, Try - catch를 통해 예외를 처리할 수 있음을 우리는 알고 있다.

 

필자는 보통 어떻게 코드를 구성해야 클린하게 예외를 처리할지에 집중했지, 예외 그 자체에 대해서는 간과하곤 했다. 하지만 여러 경험을 통해 다양한 Exception을 마주하게 됐고, 오류나 예외에 대해서도 공부할 필요성을 느꼈다. 그렇게 정리한 내용을 함께 봐보자.

 

 

Error와 Exception의 차이


Error와 Exception은 비슷한 것 같지만 다르다. 혼용하기도 하고, 다른 줄 몰랐던 사람도 있을 것이다. 차이점은 문제의 심각성에서 나온다.

 

Error는 시스템이 종료되어야 할 수준의 수습할 수 없는 심각한 문제를 의미한다. 개발자가 미리 예측해 방지할 수 없는 문제로써, 예비할 수 없기 때문에 Error가 된 것이다.

 

Exception개발자가 구현한 로직에서 발생한 실수나 사용자의 영향에 의해서 발생한다. 오류와 달리 개발자가 미리 예측해 방지할 수 있으며, 이 예외처리(Exception Handling)는 프로그램의 신뢰성을 위해서 필수적인 작업이다.

 

이 Error와 Exception, Object에 대한 관계를 살펴보면 아래와 같다.

 

오류와 예외의 상속 관계

 

오류와 예외 모두 자바의 최상위 클래스인 Object를 상속받는다. 하지만 그 사이에는 Throwable 클래스가 먼저 상속되어 있다. 해당 클래스에 대한 문서를 읽어보면, 이 클래스의 객체에 오류나 예외에 대한 메시지를 담는 클래스라고 나오게 된다. 예외가 연결될 때 (Chained exception) 연결된 예외의 정보를 기록하는 용도로 사용된다고도 한다. (Stacktrace)

 

이 Throwable 객체가 가진 정보와 할 수 있는 행위는 getMessage()와 printStackTrace()라는 메서드로 구현되어 있으며, 해당 메서드를 통해 Throwable 클래스가 어떤 역할을 하는지 까지도 알 수 있다. 당연히 이를 상속받은 Error와 Exception에서 두 메서드를 사용한다. 우리가 실제로 보는 에러 코드들은 이 두 메서드를 이용해서 콘솔상에 나타나게 되는 것.

 

즉, Error와 Exception은 심각도의 차이가 있긴 하지만, 그 뿌리가 같아서 예외나 에러가 던져졌을 경우 결국엔 같은 Throwable의 메서드들을 이용하게 된다는 것까지 알게 됐다.

 

이제 본격적으로 오류와 예외 각각에 대해서 알아보자.

 

 

오류


오류는 기본적으로 시스템 메모리 부족과 같이 예측이나 처리가 어려운 문제를 말한다. Exception과 달리 미리 대비하는 것이 어렵고, 개발 당시 신경을 쓰는 방법밖에 없다.

 

그런 오류에도 아래와 같이 여러 종류가 있다. (신경 쓸게 많아진다.)

 

Error(오류)의 상속 관계도

 

  • StackOverFlowError: 호출의 깊이가 깊어지거나 재귀가 지속되어 프로세스에 할당된 Stack을 초과한 경우, 즉 Stack Overflow 발생 시 던져지는 오류
  • OutOfMemoryError: JVM이 프로세스에게 할당한 heap 메모리의 부족으로 더 이상 새로운 객체를 할당할 수 없을 때 던져지는 오류

 

이런 익숙한 용어들이 바로 ‘오류’이다. 해결하는 방법은 간단하다. StackOverFlow는 재귀를 사용할 때는 주의하고, loop로 대체하는 방법을 통해 피할 수 있으며, OutOfMemory는 새는 메모리를 직접 차단해주는 식으로 최대한 피해야 한다.

 

즉, 피할 수만 있다. 의식적으로 주의를 기울여야 피할 수 있는 문제점들이 오류이고, 이들은 일단 발생했다면 돌이키기가 힘들다. 이런 문제들은 미리 대처하는 방안을 생각해두기란 쉽지 않다. 웹 서비스를 제작했는데, 생각보다 더 많은 사용자가 접근해 그들 모두가 복잡한 로직을 수행하고 있다면 오류는 금방 발생할 수 있다.

 

 

예외


오류와 달리 예외는 던져지거나, 직접 던질 수 있다(Throw).

 

예외는 앞서 말했듯, 개발자가 구현한 로직에서 발생한 실수나 사용자의 영향에 의해서 발생한다. 개발자가 작성하는 대부분의 로직에 대해 예상된 문제 상황들을 나타내며, 오류보다는 덜 심각한 문제들로 단지 무시하거나 일정한 조치를 취함으로써 해결될 수 있다. 때문에 신뢰할 수 있는 프로그램을 작성하고자 한다면 예외처리가 필수적인 것이다.

 

필자가 작성한 우테코 프리코스 4주차의 InputValidator 코드. IllegalArgumentException을 직접 던지는 모습을 볼 수 있다.

 

예외 또한 오류와 같이 모아둔 도식도가 있다. 해당 다이어그램에 적히지 않은 예외들은 여기에 포함된 예외를 상속하는 하위 예외이다.

 

예외(Exception)의 상속 관계도

 

위 그림에서 볼 수 있듯 예외는 RuntimeException을 상속하는 예외들과 그렇지 않은 예외로 나눌 수 있다. 이 차이를 알아보기 위해서 Exception을 Checked Exception, Unchecked Exception으로 나누어 살펴보자.

 

 

Checked vs UnChecked Exception


Checked ExceptionCompile Exception이라고도 하며 Exception을 바로 상속받는다. 즉, RuntimeException을 상속하지 않는, 나란히 서있는 예외들이 바로 Checked Exception이다. 이들은 컴파일 시점에 예외를 Catch 하는지 정적으로 확인하며, 만약 컴파일 시점에 예외에 대한 처리(try-catch)가 없다면 컴파일 에러를 발생시킨다.

 

반면 Unchecked Exception은 RuntimeException을 상속받는다. 컴파일 시점에 예외를 Catch하는지 확인하지 않으며, 때문에 컴파일 시점에 해당 예외가 발생할지 그 여부를 판단할 수 없다. 확인하지 않기 때문에 명시적으로 try-catch를 통한 예외 처리를 강제하지 않는다는 것이 특징이다. (물론 처리하는 것이 좋다.)

 

참고로 예외에 대해서 찾아보다 보면, Checked Exception은 트랜잭션을 롤백하지 않고 Unchecked Exception은 트랜잭션을 롤백한다는 이야기가 많다. 이는 정확한 정보라고 할 수 없는데, 기본적으로 트랜잭션에는 Checked는 롤백하지 않는다, Unchecked Exception은 롤백한다 하는 규칙은 없다. 단지 스프링 트랜잭션에서 기본 설정 값이 그렇다는 것이다. 이는 아래와 같이 Transaction의 옵션으로 직접 설정을 할 수 있다.

 

@Transactional(rollbackFor = {NoSuchMethodException.class})
public void saveModel(Model model) throws Exception {
  dao.save(model);
 
  ArrayList<Integer> arr = null;
  int size = getArraySize(arr);
}

.

 

해당 코드는 NoSuchMethodException에 대해서는 rollback을 하라고 명시한다. 이와 같이 Checked Exception, Unchecked Exception에 관계없이 보통의 transaction들은 어떤 예외에 대해서 rollback 하고 안 할지를 명시할 수 있게 만들어져 있다.

 

https://www.youtube.com/watch?v=_WkMhytqoCc

위 영상을 보면 뭐가 문제인지 알 수 있다.

 

 

바람직한 사용법


기본적으로 Checked Exception은 복구가 가능하다는 메커니즘을 가지고 있다. 예를 들어 아래의 예시와 같이 특정 이미지 파일을 찾아서 전송해주는 함수에서 이미지를 찾지 못했을 경우 기본 이미지를 전송한다.

 

public void sendFile(String fileName){

    File file;
    try {
        file = FileFindService.find(fileName);
    } catch (FileNotFoundException e){ // FileNotFoundException은 Checked exception
        // 파일을 못찾았으니 기본 파일을 찾아서 전송 한다
        file = FileFindService.find("default.png");
    }

    send(file);
}

.

 

하지만 우리가 일반적으로 Checked Exception 예외가 발생했을 경우 복구 전략을 갖고 그것을 복구할 수 있는 경우는 그렇게 많지 않다. 유니크해야 하는 이메일 값이 중복돼서 SQLException이 발생하는 경우 유저가 압력을 가했던 이메일 + 난수를 입력해서 insert 시키는 것도 가능하다.

 

하지만 현실에서는 이런 복구 전략을 사용하는 것보다는 그냥 RuntimeException을 발생시키고 입력을 다시 유도하는 것이 현실적이다.

 

여기서 중요한 것은 해당 Exception을 발생시킬 때 명확하게 어떤 예외가 발생해서 Exception이 발생했는지 정보를 전달해주는 것이다. 위 같은 경우에는 DuplicateEmailException (Unchecked Exception)을 발생 시는 것이 바람직하다.

 

예외가 발생할 여지가 있는 블록을 호출자가 예외를 활용해 추가적인 작업(복구 전략)을 수행하도록 하고 싶다면 Checked Exception을 활용할 수 있다. 예외를 반드시 처리하도록 만들어 책임을 강제하는 것.

반면, 호출자가 예외 상황이나 문제를 해결할 수 없다면, Unchecked Exception을 활용하자. 호출된 메서드에서 예외를 발생시켜 개발자나 사용자가 그것을 대응하도록 하는 것이다.

 

Checked Exception을 사용하는 경우에 피호출 메소드에서 호출자로 예외를 던진다고 정리할 수 있다. 이 던짐은 해당 예외를 처리할 수 있는 메서드까지 던져지게 된다. 하지만, 이런 Throw의 무분별한 사용은 Call Stacktrace를 어지럽게 만들고 코드 가독성을 떨어트림과 동시에 그래서 어디에서 해당 예외가 발생했는지 찾기 어렵게 만든다.

 

이 경우 Try-catch를 사용해 Checked Exception을 던지는 메서드를 감싸고, 만약 Checked Exception이 발생한다면 이를 Unchecked Exception으로 전환해 던져주는 방법이 있다. 즉, Checked Exception을 만나면 더 구체적인 Unchecked Exception을 발생시켜 정확한 정보를 전달하고 로직의 흐름을 끊어야 한다.

 

예를 들자면 이런 것.

public String writeValueAsString(Object obj) {
    try {
            return objectMapper.writeValueAsString(object);
    } catch (JsonProcessingException e) {
            throw new JsonSerializeFailed(e.getMessage());
    }
}

public <T> T readValue(String json, Class<T> clazz) {
    try {
	      return objectMapper.readValue(json, clazz);
    } catch (IOException e) {
	      throw new JsonDeserializeFailed(e.getMessage());
    }
 }

.

이렇게 구성한다면, 예외가 발생한 메서드에서 예외를 처리하거나 개발자 혹은 사용자에게 해당 처리를 위임할 수 있다.

이 경우에는 예외가 발생하는 곳에서 예외가 던져지기에 예외를 추적하기 용이하다는 장점이 있다.

 

위와 같이 구현했을 경우 예외가 로그로 남거나 클라이언트에 응답을 전달할 수 있다. 클라이언트는 예외 응답을 받아서 사용자를 위해 알림과 같은 방법으로 표시할 수 있다. 그리고 사용자는 다시 올바른 입력을 시도할 것이다.

 

 

Reference


https://cheese10yun.github.io/checked-exception/

https://madplay.github.io/post/java-checked-unchecked-exceptions

https://toneyparky.tistory.com/40

댓글