Spring Boot가 요청을 처리하는 방법

도비·2024년 9월 15일
0

Spring Boot

목록 보기
13/13
post-thumbnail

목표 ⭐️

Spring Boot에서 어떻게 요청을 컨트롤러에 매핑하는지에 대해 알아보자!

Spring의 Handler 등록 과정

습관처럼 우리는 요청을 받을 Controller 클래스를 @RestController 를 붙여 사용한다. 이렇게 요청을 정의하는 방식에도 여러 방식이 있지만, 보통 아래와 같은 방식을 통해 요청을 정의한다.

@RestController
public class TestController {
	
    @GetMapping("/test")
    public String test() {
    	return "test";
    }
}

그런데 이렇게 정의해놓은 클래스를 어떻게 Handler라고 인식할 수 있는 것일까?

우선, 스프링은 컨트롤러와 메서드 정보를 관리하고 요청이 왔을 때 어느 컨트롤러가 이 요청을 처리해야하는지 확인하고 위임하는 과정을 거친다. 이 방법에 대해 자세히 알아보자.

스프링 어플리케이션이 처음 가동 되면, RequestMappingHandlerMapping 클래스의 빈이 생성된다. 이 RequestMappingHandlerMapping이 컨트롤러와 메서드 정보를 관리하고, 요청을 처리하는 대상 클래스이다.

이 RequestMappingHandlerMapping은 AbstractHandlerMethodMapping이라는 추상 클래스를 상속받는다.

AbstractHandlerMethodMapping 클래스는 내부에 MappingRegisty 클래스를 가지고 있다. 실제로 요청 매핑 정보가 이곳에서 관리된다.

어플리케이션이 구동되는 시점에 MappingRegistry 내부 registry에 Map<T, MappingRegistration<T>> 형태로 저장된다. (이 방식은 아래에서 더 살펴보겠다.)

이 때 Key 값은 RequestMappingInfo로, HTTP 메서드 + URI + 헤더 + 파라미터 등을 가지고 있고, Value 값은 해당 요청을 처리하는 빈과, 요청을 처리할 빈 내부 메서드(HandlerMethod)를 담고 있다.

그럼 이제 어떻게 Controller가 MappingRegistry에 등록되는지 살펴보자.

RequestMappingHandlerMapping 클래스의 processCandidateBean 메서드를 통해 빈으로 등록되어 있는 클래스 중 Handler를 등록한다.

이 때, 아래 isHandler 메서드에서 클래스가 Controller 어노테이션을 포함하고 있는지 확인하는 과정을 통해 handler 여부를 확인한다.

이 후, detectHandlerMethod를 통해 아래와 같이 MappingRegistry에 컨트롤러 메서드를 등록한다.

여기까지 @RestController와 @RequestMapping으로 정의되어 있는 것들을 어떻게 Spring이 등록하는지 살펴보았다.

✔️ 정리

  • 스프링부트 어플리케이션이 가동되면 컨트롤러와 메서드 정보를 관리하고 요청을 처리하는 RequestMappingHandlerMapping 클래스의 빈이 생성된다.

  • RequestMappingHandlerMapping 클래스의 부모 클래스인 AbstractHandlerMethodMapping 클래스 내부에 있는 MappingRegistry 클래스에 요청 매핑 정보가 (Map 형태로) 관리된다.

  • RequestMappingHandlerMapping 클래스의 processCandidateBean 메서드를 통해 빈으로 등록된 클래스 중 컨트롤러를 detectHandlerMethod를 통해 MappingRegistry에 등록한다.

이제는 요청이 들어왔을 때 스프링이 이것을 어떻게 처리하는지 살펴보자.

Spring의 요청 처리

프론트 컨트롤러라고도 불리는 DispatcherServlet에서 HTTP 프로토콜로 들어오는 모든 요청을 받아 적합한 컨트롤러에 위임한다.

DispatcherServlet은 요청이 오면 요청 정보를 기반으로 MappingRegistry에서 찾은 후, HandlerMethod로 요청을 위임하는 것이다.

이 과정에서 DispatcherServlet이 컨트롤러로 요청을 직접 위임하는 것이 아니라, HandlerAdapter를 통해 컨트롤러에 위임한다. Controller를 구현하는 방식은 여러가지가 있기 때문에 Controller가 어떤 방식으로 구현되어 있던 요청을 적절하게 위임할 수 있게 어댑터 패턴을 통해 요청을 위임하는 것이다.

그러면 이렇게 요청을 위임할 때 JSON이나 Path Parameter과 같은 요청들을 어떻게 객체 값에 바인딩하는 것일까?

Argmument Resolver

DispatcherServlet에서 Adapter를 거쳐 컨트롤러로 요청을 전달할 때 컨트롤러에서 필요로 하는 객체를 만들고 값을 바인딩하기 위해 Argument Resolver가 사용된다.

이 때, 이 ArgumentResolver는 아래의 어노테이션에 붙은 요청에 대해 동작한다

  • @RequestParam
  • @ModelAttribue
  • @CookieValue
  • @RequestHeader
  • @RequestBody

이 때, JSON으로 들어온 객체를 RequestBody로 변환해주는 작업은 RequestResponseBodyMethodProcessor에서 담당한다.

예시

우리가, 아래와 같은 RequestBody 객체와 Controller를 만들었다고 생각해보자.

public record Request(
	@Min(1)
    int number,
    
    @NotBlank
    String name
)
...(생략) 

@PostMapping("/test)
public void postTest(@RequestBody @Valid Request req) {
	...(생략)
}

이렇게 될 경우 요청이 들어왔을 때 ArgumentResolver를 상속받은RequestResponseBodyMethodProcessor가 작동할 것이다. 그런데, @Valid는 어떻게 작동하는 것일까?

Processer 내부에서 resolveArgument 메서드를 통해 요청으로 들어온 JSON 값을 객체로 바인딩 해주는데, 이 과정에서 Validation이 일어난다.

resolveArgument 메서드 내부에서 사용되는 validateIfApplicable 함수를 살펴보면, 아래와 같이 validation 어노테이션을 찾고 parameter를 validate 하는 것을 알 수 있다.

따라서 이 과정을 통해 @Valid 어노테이션을 적용한 RequestBody에 대해 제약 조건을 위반했을 때 MethodArgumentValidException이 발생하는 것이다.

✔️ 정리

  • 클라이언트에서 요청이 들어오면 DispatcherServlet이 요청을 받아 HandlerAdapter에게 요청을 위임한다.
  • 요청을 위임받은 Adapter가 Interceptor를 거친 후 요청을 객체로 바인딩해주기 위해 Resolver를 호출한다.
  • Resolver를 통해 Request의 Validation과 Binding를 거친다. (이 과정에서 Valid에 실패할 경우 MethodArgumentValidException이 발생한다. )
  • 이 후, Adapter에서 Controller로 요청을 최종적으로 위임한다.

마무리

Spring 개발을 시작한지 꽤 되었는데 이제서야 Spring이 요청을 처리하는 전반적인 방식에 대해 알아보았다. 언제나 기본부터 시작할 것!

참고! @Validated의 경우 AOP 방식으로 작동한다!

profile
하루에 한 걸음씩

0개의 댓글