공부내용공유

java 예외 처리에 관하여 (feat: 이펙티브 자바 chapter 10 / 예외) 본문

Server/Java

java 예외 처리에 관하여 (feat: 이펙티브 자바 chapter 10 / 예외)

forfun 2024. 9. 1. 16:59

서론


 

현재 진행중인 이펙티브 자바 스터디에서 예외 처리에 관련한 파트를 공부하게 되었다, 스터디를 준비하기위해 여러 예외, 예외 처리 관련한 글을 찾아보고 또 개발을 하면서 경험한 내용들을 정리하고자 이 글을 작성하였다.

 

 

 

본론


 

해당 글의 목차는

  • 예외 처리의 중요성
  • 커스텀 예외에 관하여
  • 계층별 예외의 분리

으로 구성될 예정이다.

 

 

예외 처리의 중요성

 

프로그램을 만들다 보면 시스템 내부, 사용자의 잘못된 요청, 기능상으로 프로그램의 정상적인 흐름을 멈춰야 할 때가 있다, 이 중에는 코드상으로는 고칠 수 없는 케이스(java 에서는 error로 분류되는)와 코드상으로 재시도를 하거나 어떠한 처리를 하고 다시 이전의 흐름으로 돌리는 케이스 등이 있다.

 

 

이 정상적인 흐름을 멈추고 처리하는데 있어서는 여러가지 방법이 있겠으나 해당 글에서는 예외를 통한 흐름 제어, 특히 자바에서 제공하는 런타임 예외를 어떻게 해야 잘 사용할 수 있고 어떤 점을 생각해야 하는지를 다룰 예정이다.

 

 

예외 처리는 왜 중요할까? 내가 지금까지 개발을 진행하면서 적절한 예외 처리를 통해 얻을 수 있는 장점은

  1. 클라이언트 (api 사용자 혹은 어플리케이션 사용자)에게 해당 기능을 정상적으로 사용할 수 없는 이유에 대해 메세지 전달이 가능하다.
  2. 같이 개발을 하는 팀원에게 (혹은 미래의 나에게) 이 로직의 제한되는 부분, 발생할 수 있는 케이스들을 알려줄 수 있다.
  3. 운영중에 특정 상황에서 문제가 생겼을 경우, 문제를 빠르게 파악하고 대응하는데 도움이 된다.

 

이정도가 있었다. 당연히 너무 과한 예외처리는 코드의 가독성을 떨어트리고 개발 속도에도 큰 영향을 주기에 적절한 예외 처리를 해야한다.

 

 

팀의 컨벤션이 있다면 그에 맞춰야 하지만 컨벤션을 만들어야 하거나 큰 틀만 있고 그 안에서 적절히 예외 처리를 해야한다면 아래 사항들을 한번쯤은 생각해보면 좋을 것 같다.

 

 

커스텀 예외에 관하여

 

 

 

위 구조도와 같이 java에서는 기본적으로 제공하는 예외 클래스들이 있다. Runtime Exception에는 대표적으로

  • NullPointerException
  • IllegalArgumentException
  • ArrayIndexOutBoundException

등등이 있다.

 

 

개발을 진행하다 보면 언어 차원에서, 프레임워크 차원에서 제공해주는 예외로는 표현할 수 없는 상황이 나오기에 직접 예외를 정의해야 하기도 하고 애초에 회사 혹은 팀의 컨벤션을 위한 기본 커스텀 예외가 있을 수 있다. (대부분 java에서 제공하는 RuntimeException을 기반으로 커스텀 예외를 정의할 거라고 생각한다.)

 

 

 

커스텀 예외는 어떤 장점이 있을까?

  1. 클래스의 이름을 직접 정의함으로 클래스 이름만으로 예외 상황을 표현할 수 있고 이는 코드의 좋은 가독성으로 이어진다.
  2. 커스텀 예외로 계층 구조를 만들어서 효율적으로 예외 처리가 가능하다.
  3. 위에서 언급했던 것처럼 팀내 컨벤션을 더 잘 정의하고 지키게 할 수 있다. (예외 전달시 메세지 포멧이나 기타 등등...)

 

 

우리 팀도 적극적으로 커스텀 예외를 사용하고 있고 나도 큰 고민 없이 사용하다가 이펙티브 자바 item 72를 보면서 무조건 커스텀 예외를 만들어서 사용하는게 맞을까? 라는 생각이 들었다.

 

 

간단한 예시로 보자.

//커스텀 예외
public class UserNameTooLongException extends RuntimeExcpetion {
    pulbic UserNameTooLongException(String message) {}
}

//자바 기본 제공 예외
public class IllegalArgumentException extends RuntimeException {
    /**
     * Constructs an {@code IllegalArgumentException} with no
     * detail message.
     */
    public IllegalArgumentException() {
        super();
    }
}

 

 

사실 커스텀 예외의 장점중 제일 크다고 생각하는 이름으로 예외의 목적을 명시함은 표준 예외의 메세지를 통하여도 어느정도 해결이 가능하다.

 

 

 

커스텀 예외를 만든다고 하면 이러한 모든 잘못된 request 값에 대하여 예외 클래스를 만들어야 하는데 이는 메세지로도 커버가 가능한데 이렇게까지 부수적인 코드를 만들어야 할까? 라는 의문을 들게 한다.

 

 

 

이에 대해서는 나는 IllegalArgumentException 같은 명확하고 여러 곳에서 사용하는 표준 예외의 경우 사용을 하거나 표준 예외와 똑같되 컨벤션에 맞는 커스텀 예외를 정의하여 메세지를 적극 사용하고 이외의 커스텀 예외가 필요할 때도 계층 구조를 잘 살려서 최소한의 클래스만 만들고 내부에서 메세지를 통해 구분을 하는 방법이 괜찮지 않나 생각중이다.

 

public class NotFoundException extends RuntimeException {
    String message // 
}

User user = userRepository.findUser.orElseThrow(() -> new NotFoundException("USER_NOT_FOUND"));
Setting setting = settingRepository.findSetting.orElseThrow(() -> new NotFoundException("SETTING_NOT_FOUND"));

 

 

이런식으로 말이다. 이 상황에서도 Custom 예외의 장점을 볼 수 있는데 만약 너무 여러곳에서 저런 예외들이 사용되면 메세지를 별도로 정의한 커스텀 예외 안으로 넣어 재사용성을 높이고 각 예외를 사용하는 곳에서 메세지를 다르게 정의하는 불상사를 예방할 수 있다.

 

 

계층별 예외의 분리

 

대부분의 아키텍쳐는 계층을 구분함으로 (controller, domain, persistence...) 각 계층의 책임과 역할을 몰아넣는다. 이때 당연히 예외도 각 계층에 맞게 존재해야 한다.

 

 

 

이를테면 몽고 db를 사용한다고 하면 아래 와 같은 라이브러리에서 지원하는 이러한 런타임 예외가 있고 라이브러리에서 checked exception 도 있을 수 있다.

public class MongoBulkWriteException extends MongoServerException {
}

 

 

 

이렇게 persistence에 관련된 예외는 오로지 persistence 계층에서만 던지고 적당한 처리를 해야한다. 만약 직접 예외를 처리하는 클래스나 (java의 경우 globalExceptionResolver, express 나 nest의 경우 예외 처리를 하는 미들웨어?) 있다면 당연히 해당 클래스는 예외들을 알아도 된다.

 

 

 

커스텀 예외 클래스를 만들 때도 패키지 위치를 해당 예외가 어떤 계층의 예외인지에 따라 결정을 해야한다.

- app
    - domain
        - item
            - presentation
            - serivce
                - exception
                    - UnAuthorizException
                    - MongoBulkException
                    - ItemNameTooLongException
            - persistence

 

이런식으로 한 계층에 예외를 몰아두면 추후 분리가 굉장히 힘들지기 때문이다.   

  

 

 

이렇게 명확하게 계층이 달라지는 예외의 경우에는 고민할 여지가 없지만 NotFoundExcpetion과 같은 컨트롤러와 도메인 영역에 걸쳐있는 예외의 경우에는 조금 애매할 수 있다.  

  

 

 

이전에 헥사고날 아키텍쳐에서 예외 클래스의 위치에 대한 글을 본적이 있는데 도메인 패키지에 도메인 관련 예외는 당연하고 common 예외를 위치하게 하여 controller, persistence 관련 예외가 해당 예외들을 상속하거나 자신들의 예외를 받아 도메인 예외를 던지게 하는 방식을 제안하고 있었다.  

  

 

결론


예외 처리에 대해 이전보다는 더 많은 경험을 하고 여러 자료를 보면서 조금 더 나만의 기준이 생겼다, 앞으로도 개발을 하면서 꾸준히 나의 기준에 살을 붙여나갈 것이다.