ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [springboot 예외 처리 안티패턴 정리] Exception Handling Worst Practice
    Spring Boot 2024. 12. 30. 01:05
    • 개발자로 회사를 4곳을 거치며 다양한 예외처리 방식을 경험했다.
    • 예외처리 방식의 정답은 없지만 틀린 답은 있었다. 해서는 안되는 안티패턴을 정리 해본다.
    • 선행 지식 java error : https://yeoon.tistory.com/135

    1. 클라이언트에게 검증 위임하기 (X)

    • 서버의 입력 유효성 검사는 반드시 서버에서 이루어 져야한다.
    • 실무에서 클라이언트에서 검증을 이미 진행한다는 이유로, 서버에서 입력 값 검증을 하지 않는 경우가 있었다.
      • 유효성 검사를 넣으면, 개발 속도가 느려진다는 이유 였는데 이는 굉장히 위험하다.
      • 사용자는 반드시 실수한다. 사용자는 알고도 잘못된 파라미터를 보낸다.

    2. Exception, RuntimeException, Throwable, Error 직접 사용하기 (X)

    • Java에서 제공하는 Error 객체를 그대로 사용하면 안된다.

    Effective Java 3판 - p397
    java exception 계층 - jetbrains 제공

    • Exception 선언은 하위 RuntimeException, CheckedException이 섞이며, IDE에서 식별해야할 중요한 에러를 놓치게 만드는 안티패턴이다. (Throwable도 마찬가지)
    // 메서드 레벨에 throws Exception이 선언 == 컴파일러가(IDE) checked exception 구분 못하게 만듬 
    @PostMapping("/products")
    public void createProduct(@RequestBody CreateProductRequest request) throws Exception {
        productService.productRegister(request);
    }
    • Checked Exception은 모던 개발에서 잘 사용하지 않는다. 
      • 분산환경에서 외부시스템이나 수백, 수천의 라이브러리의 checked exception이 구현 계층 메서드에 throws, try ~ catch 같은 의존성을 만드는데, 무수히 많은 checked exception으로 관리 불가
      • 정말 필요한 경우에만 사용한다. 또는 RuntimeException으로 치환하고 공통 error handler을 구성한다. (ex. ControllerAdvice)

    책 - 클린코드

    • Error 객체는 JVM의 low-level exception으로 시스템을 멈추게 한다. 구현 레벨에서 사용할 일이 없다. (ex. OutOfMemoryError, StackOverflowError)
    • RuntimeException 역시 그대로 사용하지 않는다.
      • HttpStatus 500으로 자동으로 치환된다.
      • 어떤 문맥에서의 error인지, call stack 정보 추적이 어려운 문제가 있다.
    • 사용할때는 추상 클래스로써 상속하여 사용하자.
    class BusinessException(
        val errorCode: ErrorCode,
        val httpStatus: HttpStatus
    ) : RuntimeException(errorCode.message)

     

    3. Exception 연쇄로 사용하지 않기 (x)

    • Checked Exception을 Unchecked Exception을 치환 구현할때 필요시 cause를 넘기도록 만든다.
    • 그래야 dedug할때 caused by … 로 코딩에 중요한 정보를 식별할 수 있다.
    • 에러가 중요하다면 아래처럼 cause를 넘기는 것을 잊지 말자.

    Effective Java 3판

     

    4. HttpStatus 200으로 에러 알림하기 (x)

    • REST API로 Error 상황에 httpStauts 200 OK로 응답하는 것은 최악의 실수 이다.
    • (통신 자체는 성공이니 200으로 응답하고, error response로 error 정보를 return하는 방식)
    • 프론트, 앱과 결합도가 생기며 변경하지 못하는 안티패턴이다.

    대용량 아키텍처와 성능 튜닝 - 조대협

    • 200 OK와 함께 body error 객체를 넘기는 순간부터 HttpStatus를 활용하지 못한다.
      • 2xx, 3xx, 4xx, 5xx 코드별 알림 보내기, 지표 집계 불가능 
        • 5xx 에러, CPU, 메모리 문제 같은 비즈니스에 영향 주는 애러, 배치 시스템 에러
        • 비즈니스 지표에 이상이 생긴 경우 알림하기 어렵다.(ex, 주문에서 400대 error 임계점 초과시 알림 같은 것 구분 불가)
      • 알림을 보내고자 한다면 보낼 수는 있겠지만, Advice 한곳에 작업하면 되는 코드를 수십, 수백곳에 적성해야한다.
    • 적어도 아래 5개의 HttpStatus Code는 구분해서 API를 설계하자. (HttpStatus Code 참고 : https://www.rfc-editor.org/rfc/rfc9110#section-15)

    대표적으로 많이 쓰는 HttpStatus code

     

    5. Exception 응답 형식으로 null, -9999, boolean 사용하기 (x)

    200 OK
    {
      "success": false, # 성공과 실패 여부만 알려주는 불친절한 flag. httpCode로 표현가능
      "detail": null, # null을 응답하는 것은 프론트에서 null 처리를 하게 만드는 안티패턴 (과거 프로그래밍 언어의 유산)
      "errorCode": -9999, # int 상수로 error에 대한 표준을 정하는 방식, 코드 정하는 시간이 추가적으로 들어가고 무엇을 의미하는지 가독성이 없다(표준화가 가능한 대기업만 적절한 방식)  
    }

     

    • int, boolean 대신에 Enum을 사용해서 가독성 좋은 Error 응답을 구성한다. (enum이라 프론트에서 받아 처리하기도 좋다.)
    400 Bad Request
    {
      "errorCode": "BAD_REQUEST", # Enum으로 구현 -9999 같은 int 상수 보다 가독성이 훨씬 좋다.
      "detail": "Bad Request",
      "errors": [ // 마틴파울러의 Notification 패턴
        {
          "field": "password",
          "value": "",
          "reason": "password is required"
        },
    ...
    • (구현 참고)
    // Service 계층에서 RuntimeException 사용
    val member = memberRepository.findByLoginId(loginId) 
       ?: throw BusinessException(ErrorCode.MEMBER_NOT_FOUND, HttpStatus.NOT_FOUND)
    
    // RuntimeException 상속하여 만든 BusinessException class
    class BusinessException(
        val errorCode: ErrorCode,
        val httpStatus: HttpStatus
    ) : RuntimeException(errorCode.message) // 예시 exception은 cause가 필요 없어서 생략
    
    // ErrorCode Enum
    enum class ErrorCode(val message: String) {
        BAD_REQUEST("Bad Request"),
        UNAUTHORIZED("Unauthorized"),
        FORBIDDEN("Forbidden"),
        MEMBER_NOT_FOUND("Member not found"),
        // Add other error codes as needed
    }
    

    책 - 실전 자바 소프트웨어 개발 (Real-World Software Development) - 2020

     

    결론

    • 예외 처리에 정답은 없다. 
    • 입력 값은 반드시 검증하자. (값, type, range)
    • int 상수, boolean 대신에 Enum을 클라이언트에게 넘겨서 가독성 좋고, 분기하기 좋게 만들자.
    • 중요한 예외는 재귀적으로 cause를 넘기도록 만든다. 되어있지 않아면 root exception 식별 도움주는 도구를 도입하자.
    • 표준을 만들고 따르자.
    반응형
Designed by Tistory.