TIL(Today I Learned)

[Today I Learned - 10] 400 Bad Request With Spring

lazy man 2024. 1. 22. 11:47

1. 어떤 문제가 있었나


 현재 근무중인 회사에서 API 를 운영하고 있는데 고객과 연동 테스트를 진행하던 도중 발생한 문제에 대해 알아보려고 한다. 문제 상황은 운영중인 API 에서 GET 메소드를 지원하고 필요한 데이터를 BODY에 담아서 요청하도록 설계되어 있는데 업체에서 GET 요청을 보내도 오류가 발생한다고 문의가 왔다. 

(GET 메소드에 BODY를 담는다는 것에 대해서는 오해의 소지(?)가 있을 수 있다고 생각하는데 이 부분에 대한 나의 생각은 기회가 된다면 다른 포스팅으로 정리할 생각이다)

 

 우선 고객이 정확한 에러 로그를 첨부하지 않은 상태였기 때문에 시스템 로그부터 확인하려고 했는데 특이한 로그가 있어서 이 부분에 대해 확인해보려고 한다. (로그는 Filter 와 Interceptor 에서 출력하고 있다. )

 

# 로그 기록
1. GET 방식으로 요청한 것 확인(Filter)
2. 인터셉터로 GET 방식의 요청이 도착한 것 확인(Interceptor)
3. 컨트롤러가 아닌 에러 URL(/error) 이동하는 로그 확인(Interceptor)

 

 

2. 어떤 시도를 했나


우선 API 정의서대로 내부 테스트 해봤을 때 정상적으로 동작하는 것을 확인할 수 있었다. 그런데 왜 갑자기 에러 URL로 넘어가는지 알 수 없어서 다양한 조합(METHOD 변경, BODY 변경 등..)으로 내부 테스트를 하면서 고객이 남긴 로그와 동일하게 동일한 로그를 출력하는 상황을 찾을 수 있었는데 그 상황은 바로 GET 메소드에 BODY가 없는 상황이다. 

 

즉 메소드는 GET으로 API 정의서와 맞췄지만 API 정의서에서 요구하는 BODY 정보가 아예 비어있을 때 동일한 문제가 발생하고 있었다.

 

 

3. 원인 및 해결방법


포스트맨으로 확인해보았을 땐 400 Bad Request가 발생했는데 왜 시스템에서는 Filter 또는 Interceptor에서 에러 로그를 출력하지 않았을까? 우선 인터셉터의 코드를 보았을 때 예외가 발생하는 경우 밖으로 던지도록 되어있었다.

(문제를 해결한 시점에서 보자면 내부 로그 설정(logback)을 찾아보니 루트 logger를 콘솔로만 출력하고 파일로 저장하지 않고 있었다. 이유는 모르겠지만 이와 같은 상황때문에 왜 에러 로그를 출력하지 않았는지를 추적하는 과정을 포스팅한 것이다)

@Override
public boolean preHandle(HttpServletRequest request,HttpServletResponse response,Object object)
	throws Expcetion {
    // 로깅
    return trun;
}

 

그렇다면 Interceptor 내에서 발생한 예외가 밖으로 던져지면서 이러한 상황이 만들어졌을까? 이와 같은 가정이면 인터셉터에서 로그가 2번 찍히는 상황이 설명되지 않는다. 그래서 컨트롤러를 찾는 과정에서 예외가 발생했고 이로 인해 에러 URL로 이동하면서 인터셉터가 한번 더 호출된 것이 아닐까라는 생각을 하게 되었다. 

 

클라이언트의 요청을 처리하는 동작 순서에 대해 다시 한번 생각해보자. 

클라이언트가 서버로 요청을 보내면 필터(was)를 거친 후 디스패처 서블릿(스프링)에서 요청을 처리한다. 디스패처 서블릿은 핸들러 매핑을 통해 적절한 컨트롤러를 찾고 실행 체인에 등록된 인터셉터를 처리한 후 컨트롤러에게 요청을 위임한다.

 

이 말은 즉 컨트롤러는 찾았는데 컨트롤러에게 요청을 위임할 수 없다는 뜻인데 그럼 디스패처 서블릿은 컨트롤러를 어떻게 찾고 위임하는지 알아야했다. 참고했던 포스팅을 통해 URL과 Method를 통해 컨트롤러를 찾아 호출하는 것을 확인할 수 있었다. 현재 상황에서 URL과 Method는 API 정의서와 일치하기 때문에 컨트롤러를 찾는 것은 문제가 없을 것이고 컨트롤러를 호출할 때 문제가 발생할 가능성이 높았기 때문에 이 부분에 집중해서 원인을 분석했다.

 

 포스트맨으로 확인했던 HttpMessageNotReadableException은 DefaultHandlerExceptionResolver에서 발생하고 있었다. DefaultHandlerExceptionResolver 는 HandlerExceptionResolver를 구현하는데 HandlerExceptionResolver는 스프링 내에서 발생하는 예외에 따라 적절한 HTTP 상태 코드를 설정해주는 역할을 수행한다. 아래의 코드를 보면 doResolveException 을 통해 예외 종류별로 HTTP 상태 코드 값을 설정하고 있는 것을 확인할 수 있다.

@Nullable
protected ModelAndView doResolveException(HttpServletRequest request, HttpServletResponse response, 
	@Nullable Object handler, Exception ex) {
	// ...
	if (ex instanceof HttpMessageNotReadableException theEx) {
		return this.handleHttpMessageNotReadable(theEx, request, response, handler);
	}
}

protected ModelAndView handleHttpMessageNotReadable(HttpMessageNotReadableException ex, 
	HttpServletRequest request, HttpServletResponse response, @Nullable Object handler) throws IOException {
    response.sendError(400);
    return new ModelAndView();
}

 

 그럼 doResolveException 메서드는 언제 호출되느냐? 참고했던 포스팅을 통해 컨트롤러 밖으로 던져지는 예외는 디스패처 서블릿이 ExceptionResolver 를 통해 예외 처리를 시도하고 Default ModelAndView 객체를 반환한다. 그리고 스프링부트는 에러 처리를 위한 기본적인 ErrorController가 존재하고 별도 설정을 하지 않으면 기본 PATH(/error로) 매핑된다. (ModelAndView 이후 ErrorController를 호출하기까지의 과정은 나중에...)

 

컨트롤러가 아닌 에러 URL(/error) 이동하는 로그 확인(Interceptor)

 

여기서 의문의 로그가 찍혔던 이유를 확인할 수 있다. ErrorController 컨트롤러로 이동하기 위해서도 결국 디스패처 서블릿을 통해 Interceptor → Controller 순으로 이동하기 때문에 위와 같은 로그가 찍히게 된 것이다.

 

 

4. 배운점


  • 디스패처 서블릿은 컨트롤러를 어떻게 찾고 있는가?
    • 핸들러 매핑을 통해 URL과 HTTP METHOD를 기준으로 핸들러를 매핑한다
  • HandlerExceptionResolver란 무엇인가?
    • 스프링에서 발생하는 예외를 HTTP 응답 코드로 매핑하는 역할을 수행한다.

 

 

5. 출처


[핸들러 매핑 동작 원리 1, 2]

https://velog.io/@hsw0194/Spring-MVC-HandlerMapping%EC%9D%98-%EB%8F%99%EC%9E%91%EB%B0%A9%EC%8B%9D-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0-1%ED%8E%B8

 

Spring MVC - HandlerMapping의 동작방식 이해하기 1편

HandlerMapping은 어떻게 동작할까? request에 맞는 handler만 어떻게 가져올 수 있을까?

velog.io

https://velog.io/@hsw0194/Spring-MVC-HandlerMapping%EC%9D%98-%EB%8F%99%EC%9E%91%EB%B0%A9%EC%8B%9D-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0-2%ED%8E%B8

 

Spring MVC- HandlerMapping의 동작방식 이해하기 2편

HandlerMapping은 어떻게 동작할까? request에 맞는 handler만 어떻게 가져올 수 있을까?

velog.io

 

 

[HandlerExceptionResolver 동작원리]

https://velog.io/@jeong_woo/Spring-API-Exception-with-Spring

 

Spring API Exception with Spring

스프링 부트가 기본으로 제공하는 ExceptionResolver 는 다음과 같다. HandlerExceptionResolverComposite 에 다음 순서로 등록.ExceptionHandlerExceptionResolver:@ExceptionHandler 을 처리한다

velog.io