본문 바로가기

Backend/Spring

Spring Webflux ExceptionHandler Functional Endpoints

Webflux에서 Annotated Controllers 패턴에서는 주로 MVC와 비슷하게 @ControllerAdvice 어노테이션과 @ExceptionHandler 어노테이션을 이용하는데요.

Functional Endpoints 패턴에서는 주로 ErrorWebExceptionHandler을 활용합니다. 해당 인터페이스를 구현한 AbstractErrorWebExceptionHandler를 사용해볼거에요.

 

AbstractErrorWebExceptionHandler

해당 생성자가 Deprecated 되었습니다. (Deprecated since 2.4.0 for removal in 2.6.0 in favor)

 

@Deprecated
public AbstractErrorWebExceptionHandler(ErrorAttributes errorAttributes,
		org.springframework.boot.autoconfigure.web.ResourceProperties resourceProperties,
		ApplicationContext applicationContext) {
	this(errorAttributes, (Resources) resourceProperties, applicationContext);
}

 

많은 예제들이 위의 생성자를 이용하였는데요.

2.4.0부터는 새로 추가된 생성자를 이용하는 것이 좋겠습니다.

 

public AbstractErrorWebExceptionHandler(ErrorAttributes errorAttributes, Resources resources,
		ApplicationContext applicationContext) {
	Assert.notNull(errorAttributes, "ErrorAttributes must not be null");
	Assert.notNull(resources, "Resources must not be null");
	Assert.notNull(applicationContext, "ApplicationContext must not be null");
	this.errorAttributes = errorAttributes;
	this.resources = resources;
	this.applicationContext = applicationContext;
	this.templateAvailabilityProviders = new TemplateAvailabilityProviders(applicationContext);
}

 

이제 해당 클래스를 활용하여 Global ExceptionHandler를 만들어볼게요.

예제 코드는 코틀린으로 작성하였습니다.

 

ExceptionHandler

@Component
@Order(-2)
class ExceptionHandler(
  errorAttributes: ErrorAttributes,
  resources: WebProperties.Resources,
  applicationContext: ApplicationContext,
  serverCodecConfigurer: ServerCodecConfigurer,
) : AbstractErrorWebExceptionHandler(
  errorAttributes, resources, applicationContext
) {
  init {
    super.setMessageReaders(serverCodecConfigurer.readers)
    super.setMessageWriters(serverCodecConfigurer.writers)
  }

  override fun getRoutingFunction(errorAttributes: ErrorAttributes?): RouterFunction<ServerResponse> =
    RouterFunctions.route(RequestPredicates.all(), this::handleError)

  fun handleError(request: ServerRequest): Mono<ServerResponse> {
    return when (val throwable = super.getError(request)) {
      is ServiceException -> {
        val serviceMessage = throwable.serviceMessage
        ServerResponse.status(serviceMessage.status)
          .bodyValue(
            ErrorResponse(
              name = serviceMessage.name,
              message = serviceMessage.message,
            )
          )
      }
      else -> {
        ServerResponse.status(HttpStatus.INTERNAL_SERVER_ERROR)
          .bodyValue(
            ErrorResponse(
              name = throwable.javaClass.simpleName,
              message = throwable.message,
            )
          )
      }
    }
  }
}

 

 

ServiceException과 ServiceMessage는 커스텀으로 사용하는 예외처리 클래스입니다.

아래 샘플 코드가 있으며 자신이 사용하고 있는 Exception으로 대체해주시면 됩니다.

특정 Exception에 대해 별도로 처리하고 싶으면 when 문 내에 is *Exception 추가하여 별도로 코딩하시면 됩니다.

개인적으로 Exception 종류가 너무 많아진다하면 ServerResponse build 하는 부분을 모두 별도 파일로 분리하고 있어요.

ServiceException

class ServiceException(val serviceMessage: ServiceMessage) : RuntimeException()

ServiceMessage

enum class ServiceMessage(
  val status: HttpStatus,
  val message: String,
) {
  NOT_FOUND_BOARD(HttpStatus.NOT_FOUND, "Board not found."),
  ;

  fun exception(): ServiceException {
    return ServiceException(this)
  }

  fun <T> error(): Mono<T> {
    return Mono.error(this.exception())
  }
}