멍두의 개발새발

[스프링] @RestControllerAdvice 리팩토링 본문

Programming/Spring

[스프링] @RestControllerAdvice 리팩토링

멍두 2024. 4. 14. 01:46
반응형

@RestControllerAdvice

@ResponseBody와 @ControllerAdvice를 결합한 Annotaiton으로 전역으로 에러를 처리하고 자동으로 http body에 예외 정보를 넣어준다.

 

@Slf4j
@RestControllerAdvice
public class MainExceptionResolverController {

    @ExceptionHandler(IllegalArgumentException.class)
    public ResponseEntity<ExceptionDto> illegalArgumentExceptionHandler(IllegalArgumentException ex){
        ExceptionDto exceptionDto = new ExceptionDto("IllegalArgumentException", ex.getMessage());
        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(exceptionDto);
    }

}

이런 식으로 작성하면 IllegalArgumentException이 터졌을 때 사용자는 400 code와 함께

{ "exceptionName": "IllegalArgumentException", "message": "잘못된 요청입니다" }
로 응답을 받을 수 있다.

 

😵 그런데 점점 exception이 다양해지면서 중복되는 코드가 많아졌다.

 

💻 MainExceptionResolverController

@Slf4j
@RestControllerAdvice
public class MainExceptionResolverController {
    @ExceptionHandler(AiConnectionException.class)
    public ResponseEntity<ExceptionDto> aiConnectionExceptionHandler(AiConnectionException ex){
        ExceptionDto exceptionDto = new ExceptionDto("AiConnectionException", ex.getMessage());
        return ResponseEntity.status(HttpStatus.NOT_FOUND).body(exceptionDto);
    }

    @ExceptionHandler(UserNotFoundException.class)
    public ResponseEntity<ExceptionDto> userNotFoundExceptionHandler(UserNotFoundException ex){
        ExceptionDto exceptionDto = new ExceptionDto("UserNotFoundException", ex.getMessage());
        return ResponseEntity.status(HttpStatus.NOT_FOUND).body(exceptionDto);
    }
    @ExceptionHandler(IllegalArgumentException.class)
    public ResponseEntity<ExceptionDto> illegalArgumentExceptionHandler(IllegalArgumentException ex){
        ExceptionDto exceptionDto = new ExceptionDto("IllegalArgumentException", ex.getMessage());
        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(exceptionDto);
    }

    @ExceptionHandler(RuntimeException.class)
    public ResponseEntity<ExceptionDto> runtimeExceptionHandler(RuntimeException ex){
        ExceptionDto exceptionDto = new ExceptionDto("IllegalArgumentException", ex.getMessage());
        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(exceptionDto);
    }
}

으악 내눈 👎

 

이 코드를 리팩토링 해보자!

  1. 최대한 중복을 없애보자
  2. 추후에 에러를 추가할 때를 고려하여 확장이 편한 코드로 만들어보자
  3. 가독성을 높히자

 

중복을 없애기 위해 아래 부분을 메소드로 추출하자

ExceptionDto exceptionDto = new ExceptionDto("IllegalArgumentException", ex.getMessage());
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(exceptionDto);

 

이때 메소드마다 필요한 className과 httpStatus이다.

httpStatus는 인자로 받고 className은 exception.getClassName()으로 추출하자.

 

💻 extractClassName

className을 추출하는 함수

    protected String extractClassName(String fullClassName){
        String[] classNameParts = fullClassName.split("\\.");
        return classNameParts[classNameParts.length - 1];
    }
  • 파라미터 String fullClassName = exception.getClassName().toString()
    • ex ) com.potato.balbambalbam.main.cardList.exception.CategoryNotFoundException
  • 이때 exception이름만 추출하기 위해 String을 . 기준으로 split하고 제일 마지막 index를 가져온다.

 

💻 exceptionHandler

    protected ResponseEntity<ExceptionDto> exceptionHandler(Exception ex, HttpStatus httpStatus){
        String className = extractClassName(ex.getClass().toString());
        String exMessage = ex.getMessage();

        log.info("[ERROR] ["+ className + "]:" + exMessage);

        return ResponseEntity.status(httpStatus).body(new ExceptionDto(className, exMessage));
    }
  • 이제 exception은 모두 이 함수로 처리해주면 된다.
  • log처리 + resposneEntity 생성

 

1차 리팩토링

@Slf4j
@RestControllerAdvice
public class MainExceptionResolverController {

    @ExceptionHandler(AiConnectionException.class)
    public ResponseEntity<ExceptionDto> aiConnectionExceptionHandler(AiConnectionException ex){
        return exceptionHandler(ex, HttpStatus.NOT_FOUND);
    }

    @ExceptionHandler(UserNotFoundException.class)
    public ResponseEntity<ExceptionDto> userNotFoundExceptionHandler(UserNotFoundException ex){
        return exceptionHandler(ex, HttpStatus.NOT_FOUND);
    }
    @ExceptionHandler(IllegalArgumentException.class)
    public ResponseEntity<ExceptionDto> illegalArgumentExceptionHandler(IllegalArgumentException ex){
        return exceptionHandler(ex, HttpStatus.BAD_REQUEST);
    }

    @ExceptionHandler(RuntimeException.class)
    public ResponseEntity<ExceptionDto> runtimeExceptionHandler(RuntimeException ex){
        return exceptionHandler(ex, HttpStatus.BAD_REQUEST);
    }

    protected ResponseEntity<ExceptionDto> exceptionHandler(Exception ex, HttpStatus httpStatus){
        String className = extractClassName(ex.getClass().toString());
        String exMessage = ex.getMessage();

        log.info("[ERROR] ["+ className + "]:" + exMessage);

        return ResponseEntity.status(httpStatus).body(new ExceptionDto(className, exMessage));
    }

    protected String extractClassName(String fullClassName){
        String[] classNameParts = fullClassName.split("\\.");
        return classNameParts[classNameParts.length - 1];
    }
}

중복되는 코드가 거의 사라졌으나 여전히 httpStatus가 동일하면 중복되는 코드가 보인다.

떼잉! 😞

하나의 @ExceptionHandler로 여러 exception을 처리하자

@ExceptionHandler({excpeiton1.class, exception2.class, ... })

 

 

리팩토링 성공!

@Slf4j
@RestControllerAdvice
public class MainExceptionResolverController {

    @ExceptionHandler({AiConnectionException.class, UserNotFoundException.class, CategoryNotFoundException.class, CardNotFoundException.class})
    public ResponseEntity<ExceptionDto> notFoundExceptionHandler(Exception ex){
        return exceptionHandler(ex, HttpStatus.NOT_FOUND);
    }

    @ExceptionHandler({IllegalArgumentException.class, RuntimeException.class})
    public ResponseEntity<ExceptionDto> badRequestExceptionHandler (IllegalArgumentException ex){
        return exceptionHandler(ex, HttpStatus.BAD_REQUEST);
    }

    protected ResponseEntity<ExceptionDto> exceptionHandler(Exception ex, HttpStatus httpStatus){
        String className = extractClassName(ex.getClass().toString());
        String exMessage = ex.getMessage();

        log.info("[ERROR] ["+ className + "]:" + exMessage);

        return ResponseEntity.status(httpStatus).body(new ExceptionDto(className, exMessage));
    }

    protected String extractClassName(String fullClassName){
        String[] classNameParts = fullClassName.split("\\.");
        return classNameParts[classNameParts.length - 1];
    }
}

리팩토링의 목표였던

  1. 최대한 중복을 없애보자
  2. 추후에 에러를 추가할 때를 고려하여 확장이 편한 코드로 만들어보자
  3. 가독성을 높히자

를 모두 성공했다!

 

httpStatus에 따라 함수 이름을 짓고 해당 code를 반환하는 error exception들은 모두 @ExceptionHandler안에 넣어주기만 하면 된다.

 

굳!

 


💬 이글을 작성한 이유

작동하는 코드를 짜기 급급해서 이런 리팩토링은 처음이다

내 코드가 보기에도 에러를 추가할 때도 너무 불편해서 리팩토링을 해보니 짜릿했다

뭔가 내가 뚝딱뚝딱 만든게 두 눈에 바로 보이는 느낌🔨
특히 확장이 편한 코드로 만든 부분이 젤 만족스러웠다.

 

이기분을 잊지않기 위해 이 글을 쓴다

 

반응형