공부방/JAVA

예외처리 , 그거 어떻게 하는건데

EVO. 2023. 9. 12. 21:47

프로그램을 실행 중 어떤 원인에 의해서 오작동하거나 비정상적으로 종료되는 경우가 있다. 이를 발생 시점에 따라 '컴파일 에러' 와 '런타임에러'로 나눌 수 있다. 컴파일 에러는 소스코드를 컴파일하면 에러를 잡아준다. 예를들면 코드 오타가 있거나 자료형을 잘못 적었거나 등 이런것은 개발자가 쉽게 바로 에러잡아주는 곳에서 고치고 다시 실행하면 된다. '컴파일에러'는 IDE에서 많이 도와주고 있으니 이번 글에서 다룰 내용이 아니다. 

 

반면 '런타임 에러'는 상황이 다르다. 프로그램 실행 도중 발생되는 에러라 컴파일러가 에러를 잡아줄 수가 없다. 즉 개발자인 우리가 이에 대한 적절한 코드를 미리 작성해놓음으로써 프로그램의 비정상적인 종료를 막을 수 있다. 예외처리는 바로 '런타임에러'를 잡기 위한 방식이다.

"컴파일시점에 예외처리" 이런 말은 상당히 잘못된 말이니 꼭 유념하자 (나한테 하는 말)

 

자바에서 예외처리 하는 방법

1. try-catch 문

try 블럭 내에서 예외가 발생한 경우 (위의 코드와 같이 throw로 예외를 고의로 발생시킬 수 있지만 JAVA API에서 설정한 예외도 있다. 이는 좀있다 설명하겠다) 발생한 예외와 일치하는 catch블럭이 있는지 확인한다. 

일치하는 catch블럭을 찾게 되면, 그 catch블럭 내의 문장들을 수행하고 try-catch문을 빠져나가서 그 다음 문장을 계속해서 수행한다. 

예외가 발생하지않으면 당연히 catch블럭은 거치지 않는다.

 

2. 멀티 catch 블럭

JDK 1.7부터 여러 catch블럭을 '|'기호를 이용해서 하나의 catch블럭으로 합칠 수 있게 했으며 중복된 코드를 줄일 수 있는 장점이 있다. 이를 쓸때 하나의 catch블럭에 '|'을 연결하는데 그 서로간에 조상과 자손의 관계만 아니면 된다. 

 

3. 메서드에 예외 선언하기 

키워드 throws를 사용해서 메서드 내에서 발생할 수 있는 예외를 적어주는 것이다. 이는 try-catch문처럼 해당 클래스 내에서 예외를 처리하는 것이 아닌 이 메서드를 호출한 메서드에게 예외를 떠맡기는 것이다. 

 

메서드에 예외를 선언하는 것은 매우 직관적이기에 개발자는 어떤 예외를 처리해야되는 지 바로 알 수 있는 장점이 있다. 

 

해당 예외처리에 대해 직접 실행해보고 싶으면

https://github.com/minsang-alt/java/tree/main/src/main/java/com/example/javaspring/exception 에서 간단하게 실행해보자 

 

예외를 처리하다보면 어느 경우에는 예외처리를 하지않아도 컴파일 에러 없이 잘 실행되고 어느경우엔 위에 처럼 예외를 처리해야만 되는 경우도 있다. 이를 이해하기 위해서는 UncheckedException 과 CheckedException의 구별 할 필요가 있으며 먼저 예외 클래스의 계층구조부터 알아보자 

 

자바가 제공하는 예외 계층 구조 

위의 다이어그램보다 사실 자바에서 제공하는 클래스는 훨씬 더 많다 그중 일부만 표현했다.

https://docs.oracle.com/javase/8/docs/api/java/lang/Exception.html

 

그리고 위에 박스 색깔로 분홍색은 CheckedException, 파란색은 UnCheckedException으로 구별을 하였다. Exception을 상속받는 자손클래스들중 RuntimeException와 그를 상속받는 자손들을 제외한 모든 자손들을 CheckedException, 그외 RuntimeException과 그의 자손들을 UnCheckedException이라 한다. 

 

CheckedException vs UnCheckedException

CheckedException은 컴파일할때 예외에 대한 처리를 강제하고 UncheckedException은 예외에 대한 처리를 강제하지 않는다. 

 

이제 다음에 나올 코드는 OOP 강좌 ( https://school.programmers.co.kr/app/courses/17778/curriculum/lessons/205377 ) 에서 자세히 볼 수 있다. 

 

CheckedException

보면 다음과 같이 throws 키워드를 사용하거나 try-catch문으로 예외처리를 하였다. 만약 이렇게 안하면 어떻게 될까

 

이런식으로 IDE에서 직접 예외처리를 하라고 알려주기도 하고 만약 이 내용을 무시하고 컴파일을 하려고 하면 

라고 컴파일 에러가 발생한다. 

CheckedException은 예외처리를 강제해야한다는 점을 알 수 있고 하지않으면 컴파일때 하라고 에러를 발생시킨다. 

 

UnCheckedException

이번에는 UncheckedException을 예외처리를 안해도 에러가 발생하지 않고 try-catch문으로 예외처리를 강제해도 어떠한 컴파일 에러가 발생하지 않는다. 

 

그래서 개발자들은 어떤 예외를 주로 사용할까 라고 하면 주로 UncheckedException을 사용한다고 한다. CheckedException이 예외처리를 강제하고 있으니 더 좋은 거 아니야? 라고 반문을 할 수가 있다. 하지만 Unchecked를 사용하는 이유는 다음과 같다. 

 

보통 예외는 예외가 발생한 해당 로직에서 해당 예외를 처리를 할 수가 없다. 다른 계층에서 처리하도록 예외를 넘긴다. 그런데 만약 CheckedException으로 사용한다면 항상 예외처리를 해줘야 한다. 

출처 : https://school.programmers.co.kr/app/courses/17778/curriculum/lessons/205377

FileNotFoundException은 대표적인 CheckedException이라 두개의 예제처럼 예외처리를 해줘야 하는데 아래의 예제처럼 예외를 받자마자 다시 예외를 던진다는 것은 매우 비효율적인 작업임을 우리는 알 수가 있다. 그리고 첫번째 예제처럼 예외를 CheckedException을 메소드 바깥으로 던지면 해당 메소드를 호출한 메소드 역시 예외처리를 해야하는데 이는 모든 메서드가 나는 해당 예외처리를 하고 있다는 캡슐화 원칙을 깨트리는 순간이다. 

 

따라서 말하고 싶은 것은 UncheckedException을 던지기 위해 커스텀예외처리를 하면 된다.

 

커스텀 예외 만드는 법

너무나도 간단하다 그냥 RuntimeException을 상속받으면 된다. 

UncheckedException은 예외처리도 하고 싶으면 할 수 있기 때문에 꼭 이걸 쓰도록 하자  

 

 

Exception과 Error의 차이

Exception클래스는 위 다이어그램에서 봤듯이 Throwable 클래스를 상속받는다. Error 클래스 역시 Throwable 클래스를 상속받는 데 단, 다른 점은 Error클래스는 예외처리를 하는 것이 아니라 에러가 발생하지 않도록 코드를 구성하는 것이 더 맞는 표현이다. 

 

다시 이 에러는 예외처리로 해당 에러를 잡을려고 시도하면 안되고 절대로 발생되지 않아서야 할 에러이므로 꼭 로직을 통해 에러가 발생되지 않도록 해야한다. 

 

대표적인 것은 테스트코드를 구성할 때 AssertionError가 있다 조건이 참이어야 하는데 거짓인 경우 프로그램을 종료시키는 에러이다.

또는 흔히들 알고 있는 스택오버플로우 OutOfMemory 이런 에러가 발생하면 프로그램이 종료되므로 반드시 애플리케이션을 실행하기 전에 해당 에러를 잡아야 한다.

이것말고도 https://docs.oracle.com/javase/8/docs/api/java/lang/Error.html 에서 더 많은 에러를 볼 수가 있다. 

 

기본적인 설명은 이제 끝났다. 이제 좋은 예외 컨밴션(사실 향로님이 말씀해주신 https://jojoldu.tistory.com/734 ) 을 간단하게 정리해보고 실제 프로젝트 코드에 적용시켜보도록 하자

 

좋은 예외 처리

  • Error는 프로그램이 종료되는 오류로 로그레벨을 error로 두고, 로그에서는 에러 트레이스를 남긴다
  • 예외는 시스템 외적인 요소로 발생하는 치명적이지 않은 오류로 로그레벨을 warn으로 둔다.
  • 실패한 코드의 의도를 파악하려면 호출 스택 + 오류메세지에 실패한 작업의 이름과 실패 유형, 어떠한 값을 사용하다 실패했는지 담겨야한다. 이때 Exception에 담길 내용(사용자에게 노출됨) Logger를 통해 남길 내용 의 분리가 필요하다.  
  • 예외의 네이밍은 예외의 원인과 내용을 정확하게 반영해야 한다. 코드를 읽는 사람이 예외 이름만 보고도 해당 예외가 왜 발생했는 지 어느정도 추측할 수 있어야 한다. 
  • 각 계층에 맞는 예외를 던져야 한다. 즉 예외를 받고 다시 던지는 식으로 해서 가능한 가장 늦은 위치에서 에러를 처리하는 것인데 만약 아무작업이 그냥 잡고 던지는 것은 해서는 안되는 일이다. 좀 더 자세히 말하면 각 계층은 크게 세 부분으로 나눠져 있다. 
    - 데이터 액세스 계층
    - 비즈니스 로직 계층
    - 프리젠테이션 계층 
    이 원칙의 기본 아이디어는, 하위 계층에서 발생한 예외가 상위 계층으로 전파되면서 각 계층에서 적절하게 변환되거나 추가 정보가 포함되어야 한다는 것이다. 그래서 최종적으로 사용자에게 보여지는 에러메시지나 로그메시지 등은 가장 높은 수준의 문맥 정보를 포함할 수 있게 된다. 

출처 : https://jojoldu.tistory.com/734

  • 정상적인 흐름에서는 Catch 금지 => 무분별한 예외 처리는 코드의 가독성을 해칠 수 있으며 예외 분석하기가 매우 힘들어진다. 
  • Exception을 throw 하자마자 잡지 않는다. 가능한 가장 최상위 계층에서 처리를 하며 이 말은 무조건 글로벌 핸들러에서 처리한다는 의미가 아닌 여러 계층 중 가장 멀리 떨어진 계층에서 처리한다. 
    글로벌 핸들러에서 예외를 처리하면 훨씬 코드가 깔끔해지는 장점이 있다. (중복코드 제외할 수 있는 장점) 

 

프로젝트에 적용해보기

 

 

 

 

 

 

 

 

 

출처