본문 바로가기

개발

[Spring] Spring MVC 구조에 대한 고민과 이해

스프링 MVC 1편 - 요청 매핑 핸들러 어댑터 구조 을 듣고 감명 받아서 공유합니다.

 

 

스프링을 공부해오면서

  • 컨트롤러 클래스의 메서드 파라미터는 누가 주입해줄까❓
  • 리턴 값들은 어떻게 찰떡같이 반환이 잘 이뤄지는걸까❓
  • 그래서 HTTP 메시지 컨버터는 도대체 언제 사용될까❓

라는 궁금증이 계속 제 머리를 멤돌았어요. 그래서 여러 곳에 질문을 남기기도 했어요.

 

 

그런데 드디어 그 궁금증을 해결하게 되었어요. 모든 비밀은 애노테이션 기반의 컨트롤러(@RequestMapping)을 처리하는 핸들러 어댑터(RequestMappingHandlerAdapter)에 있었습니다. 🤭

 

 


Spring MVC 구조

Server-side를 애노테이션 기반의 컨트롤러로 구성했다고 가정해요.

 

클라이언트의 HTTP 요청이 들어오면

 

1. Handler 조회

 

  • DispatcherServlet은 handler mapping을 통해 요청된 URL에 매핑된 handler(컨트롤러)를 조회한다.

2. HandlerAdapter 조회

  • Handler를 실행할 수 있는 HandlerAdapter를 조회한다.

3. RequestMappingHandlerAdapter 호출

  • handle(handler)를 통해 RequestMappingHandlerAdapter를 호출한다.

4. ArgumentResolver(HandlerMethodArgumentResolver) 호출

5. HttpMessageConverter 실행

  • canRead()canWrite()를 통해 조건이 만족되는 HttpMessageConverter 구현체를 호출하여 요청에 맞는 객체를 생성한다.

6. Handler 실행

  • ArgumentResolver에서 파라미터의 값이 모두 준비되면, RequestMappingHandlerAdapter가 해당되는 실제 handler(컨트롤러)를 실행한다.

7. ReturnValueHandler(HandlerMethodReturnValueHandler) 호출

8. HttpMessageConverter 실행

  • canRead()canWrite()를 통해 조건이 만족되는 HttpMessageConverter 구현체를 호출하여 응답에 맞는 객체를 생성한다.

9. Return 데이터 반환

  • RequestMappingHandlerAdapter는 ReturnValueHandler가 전달하는 return 데이터를 DispatcherServlet에게 반횐한다.

10. viewResolver 호출

  • DispatcherServlet은 viewResolver를 찾고 호출한다.

11. view 반환

  • viewResolver는 view 논리 이름을 물리 이름으로 바꾸고, 랜더링 역할을 담당하는 view 객체로 만들어 DispatcherServlet에게 반환한다.

12. view 랜더링

  • DispatcherServlet은 render(model)을 통해 view를 랜더링한다.

 


 

❗️: 추가적으로, 스프링은 아래를 모두 인터페이스를 제공하므로, 필요하다면 언제나 기능을 확장할 수 있다고 해요. 즉, OCP를 지킬 수 있는거죠.

  • HandlerMethodReturnValueHandler
  • HandlerMethodArgumentResolver
  • HttpMessageConverter

 

❗️: 기능 확장이 필요할 때는 WebMvcConfigurer를 상속 받아서 스프링 빈으로 등록하면 된다고 해요. 빈 default 메서드로 이뤄진 인터페이스이기 때문에 필요한 부분만 오버라이딩하여 사용할 수 있어요. 빈 메서드로 만든 이유는 아래 이슈의 1-2-b. 메서드가 비어있는 것이 왜 편리함을 주는 것일까❓를 보면 좋을 것 같아요.

public interface WebMvcConfigurer {

    default void configurePathMatch(PathMatchConfigurer configurer) { }
    default void configureContentNegotiation(ContentNegotiationConfigurer configurer) { }
    default void configureAsyncSupport(AsyncSupportConfigurer configurer) { }
         ...
}

 

 


 

여담이긴 하지만, Spring framework의 극한의 객체지향과 유지보수성을 보여주는 사례가 찾아서 공유해요. 위의 WebMvcConfigurer는 사실 Java 8 이후에 나온 인터페이스라고 해요. 그 전에는 WebMvcConfigurerAdapter를 대신 사용했다고 해요. 그래서인지 @Deprecated를 볼 수 있어요.

 

@Deprecated
public abstract class WebMvcConfigurerAdapter implements WebMvcConfigurer {
    @Override
    public void configurePathMatch(PathMatchConfigurer configurer) { }
         ...
}

 

이 내용을 보고 WebMvcConfigurerAdapter를 그대로 사용해도 됐을텐데, 왜 WebMvcConfigurer를 새롭게 만들었지❓라는 궁금증이 생겼어요. 왜냐하면 그것도 다 비용이잖아요? 뭔가 이점을 볼 수 있는 게 있으니, 수고를 했다고 생각이 들었거든요.

 

잠시 생각하고 깨달은 점은 WebMvcConfigurer 인터페이스가 default 메서드로만 구성되어 있다는 점이에요. default 메서드를 통해서 기존에 존재하던 인터페이스에 새로운 기능을 추가하는 것과 동시에 하위 호환성을 유지할 수 있기 때문이에요. 미래에 다른 기능이 추가될 수 있으니까 원활한 유지보수를 위해서 WebMvcConfigurer를 수고스럽게 만들었던 거에요.

 

'아... 정말 이게 유연한 설계가 아닐까?' 라는 생각이 들었어요. 프레임워크 코드를 보고 고민하면 많은 걸 배울 수 있는 것 같아요. 이래서 경력의 개발자들이 무급으로 오픈 소스에 기여하는 게 아닐까? 라는 생각도 동시에 들었어요. 갑자기 일기가 됐지만.. 저도 지속적으로 오픈 소스에 기여할 수 있는 개발자가 되고 싶네요.

 

생각해보니 이전에 알아보고 default 메서드에 대해 이슈를 남긴 적이 있더라구요. 헷갈리는 분들은 아래 이슈의 인터페이스에서 다이아몬드 문제가 발생하는 경우 - 1를 보면 될 것 같아요.