본문 바로가기
프로그래밍/Spring

[Spring] problem spring web을 이용해 Exception Hadling을 단순화해보자 - MVC 편

by 사바라다 2022. 2. 26.

안녕하세요. 오늘은 spring에서의 exception handling을 쉽게 사용할 수 있게하는 problem-spring-web 라이브러리를 사용해 보도록 하겠습니다. 오늘 포스팅에서는 Web MVC와 Webflux를 모두 알아보도록 하겠습니다.

application/problem+json

알고 있으셨나요 ? 저는 몰랐습니다. http 에러 응답을 json으로 상세하게 반환하는 별도의 Http MediaType이 존재하는 사실을. 이 MediaType의 이름은 application/problem+json입니다. application/problem+json 스키마의 설명은 아래와 같습니다. 새로운 오류 응답 형식을 정의할 필요를 피하기 위해 RFC에서 정한 규약중 하나입니다. 무조건 이런 규약을 따라야하는 것은 아니지만 최대한 지키는게 확장성 및 재활용 가능성 측면에서 좋을 것이라는 사실은 모두들 이해하실거라고 생각합니다.

This document defines a "problem detail" as a way to carry machine-readable details of errors in a HTTP response to avoid the need to define new error response formats for HTTP APIs.

problem spring web

problem spring web은 Spring 프레임워크 기반의 application에서 application/problem+json 응답을 쉽게 만들어주는 라이브러리입니다. Spring Web MVC Exception handling과 Spring WebFlux Exception Handling을 구별 없이 모두 지원합니다. 이 라이브러리를 통해서 개발자는 예외를 Client에게 전달하는 부분에 있어서 보일러플레이트 코드를 줄일 수 있습니다.

Spring Web MVC 설정

먼저 Spring Web MVC를 사용하는 환경에서 적용하는 방법에 대해서 알아보도록 하겠습니다.

의존성

가장 먼저 라이브러리를 import하도록 하겠습니다. 현재 기준으로 가장 최신 버전은 0.27.0 버전이기 때문에 저도 해당 버전을 베이스로 진행하도록 하겠습니다. 아래 라이브러리는 spring boot를 사용하실 때 import하면 되는 라이브러리입니다. 만약 spring boot를 사용하고 있으시지 않으시다면 org.zalando:problem-spring-web을 같은 버전으로 import하시면 됩니다.

implementation("org.zalando:problem-spring-web-starter:0.27.0")

Config(환경) 설정

라이브러리가 정상적으로 import 되었으면 다음은 Config 설정을 진행하도록 하겠습니다. preblem spring web을 사용하기 위해서는 ProblemModule과 ConstraintViolationProblemModule을 bean으로 만들어주셔야합니다.

@Configuration
public class ProblemSpringWebConfig {

    @Bean
    public ProblemModule problemModule() {
        return new ProblemModule();
    }

    @Bean
    public ConstraintViolationProblemModule constraintViolationProblemModule() {
        return new ConstraintViolationProblemModule();
    }
}

그리고 추가적으로 objectMapper를 커스터마이징 해주셔야합니다. response를 json으로 만들어서 반환하는데 jackson을 사용하는데 이때 objectMapper를 이용합니다. 여기에 위에서 만들었던 bean을 주입해 주시면 response가 정상적으로 원하는 형식으로 반환되게 됩니다.

@Bean
@Primary
public ObjectMapper objectMapper() {

    ObjectMapper objectMapper = new ObjectMapper();
    objectMapper.registerModules(
            problemModule,
            constraintViolationProblemModule
    );

    return objectMapper;
}

아래는 properties 설정입니다. exception은 spring Context 외부인 filter 에서도 발생할 수 있기 때문에 아래와 같은 설정도 상황에 따라서 넣어주셔야할 수도 있습니다.

spring:
  resources:
    add-mappings: false
  mvc:
    throw-exception-if-no-handler-found: true
  main:
    allow-bean-definition-overriding: true # 본인 프로젝트의 버전 및 bean 중복 여부에 따라서 없어도됨

아래 설정 처럼 SpringBootApplication 에서 autoConfiguration을 빼주시는 것도 필요합니다. 이것을 뺌으로써 기본적으로 처리하는 exceptionHandler를 사용하지 않게 처리하는 것입니다.

@SpringBootApplication(
        exclude = ErrorMvcAutoConfiguration.class
)

위에서는 환경 설정이었으며 실제로 사용은 아래와 같이 하시면 됩니다. 기본적으로 사용하기 위해서는 @ControllerAdvice 어노테이션을 클래스에 붙여주시며 ProblemHandling 인터페이스를 implements 받는 것 만으로 적용이 됩니다.

@ControllerAdvice
class ExceptionHandling implements ProblemHandling {

}

HandlerNotFoundException를 발생시켜 보도록 하겠습니다. 그러면 아래와 같은 결과를 얻어보실 수 있습니다.

{
    "title": "Not Found",
    "status": 404,
    "detail": "No handler found for POST /v1/live-room/list/search/region"
}

기본 설정

기본 설정은 아래와 같습니다. 위와 같이 problem spring web을 기본적으로 세팅하면 아래와 같은 Exception에 대해서 자동으로 exception 문구가 변경되어 client에게 전달되게 됩니다.

Customizing

2개의 커스터마이징을 진행해보도록 하겠습니다. 첫번째는 기본적으로 정의되어있는 NoHandlerFoundException를 커스터마이징 하는 방법입니다. 커스터마이징 하는 방법은 아래와 같습니다. 위에서 우리는 ProblemHandling을 의존받았었습니다. 이 안을 들여다보면 이미 정의되어있는 예외에 대한 처리가 interface default 메서드로 있습니다. 따라서 이를 아래 코드처럼 Override 하여 재정의하면 됩니다.

@ControllerAdvice
class ExceptionHandling implements ProblemHandling {

    @Override
    public ResponseEntity<Problem> handleNoHandlerFound(NoHandlerFoundException exception, NativeWebRequest request) {

        ThrowableProblem problem = Problem.builder()
                .withTitle("Handler 못찾음")
                .withStatus(Status.NOT_FOUND)
                .withDetail(exception.getMessage())
                .with("parameter", "add Parameter")
                .build();

        return create(exception, problem, request);
    }

}

재정의한 결과로 반환되는 응답은 아래와 같습니다.

{
    "title": "Handler 못찾음",
    "status": 404,
    "detail": "No handler found for POST /v1/live-room/list/search/region",
    "parameter": "add Parameter"
}

두번째는 새로운 Exception에 대한 정의를 만드는 방법입니다. 이는 새로운 Interface를 만들고 default 메서드를 만드는 방법으로 구현이 가능합니다. @Exceptionhandler에 속성으로 특정 예외일때만 처리할 수 있도록 할 수 있으니 참고해주시기 바랍니다.

public interface SabaradaTrait extends AdviceTrait {

    @ExceptionHandler
    default ResponseEntity<Problem> handleException(RuntimeException e, ServerWebExchange request) {

        ThrowableProblem problem = Problem.builder()
                .withTitle("런타임 에러 발생")
                .withStatus(Status.INTERNAL_SERVER_ERROR)
                .withDetail(e.getMessage())
                .with("parameter", "add Parameter")
                .build();

        return create(e, problem, (NativeWebRequest) request);
    }
} 

그리고 이렇게 만든 Interface는 @ControllerAdvice를 받는 클래스에 상속을 받으면 됩니다.

@ControllerAdvice
class ExceptionHandling implements ProblemHandling, SabaradaTrait {
    [...중략...]
}
{
    "title": "런타임 에러 발생",
    "status": 500,
    "detail": "unexpected error",
    "parameter": "add Parameter"
}

마무리

오늘은 이렇게 problem spring web을 이용하여 Spring MVC에 적용하고 사용하는 방법에 대해서 알아보았습니다.

다음 시간에는 이어서 Webflux에 적용하는 방법에 대해서 알아보도록 하겠습니다.

감사합니다.

참조

github_problem-spring-web

baeldung_problem-spring-web

webconcepts_problem+json

댓글