Study/Spring

[Spring] Spring MVC - 구조 이해 및 기본 기능

sw_develop 2025. 4. 19. 15:44

프론트 컨트롤러

  • 프론트 컨트롤러 패턴 특징
    • 프론트 컨트롤러를 담당하는 서블릿 하나로 클라이언트의 모든 요청을 받음
      • 요청이 들어오는 입구를 하나로 만들어 공통 처리 가능함
    • 프론트 컨트롤러가 요청에 맞는 컨트롤러를 찾아서 호출
    • 프론트 컨트롤러를 제외한 나머지 컨트롤러는 서블릿을 사용하지 않아도 됨
      • HttpServletRequest, HttpServletResponse 등 서블릿과 관련된 코드를 사용하지 않아도 된다는 의미
      • 프론트 컨트롤러에서 호출하기 때문
  • Spring Web MVC와 프론트 컨트롤러
    • 스프링 웹 MVC의 핵심도 바로 ‘프론트 컨트롤러’
    • 스프링 웹 MVC의 ‘DispatcherServlet’이 프론트 컨트롤러 역할을 수행하는 것

 

스프링 MVC 전체 구조

1) Dispatcher Servlet 구조 살펴보기

  • `org.springframework.web.servlet.DispatcherServlet`
    • 스프링 MVC도 프론트 컨트롤러 패턴으로 구현되어 있음
    • 스프링 MVC의 프론트 컨트롤러가 바로 ‘DispatcherServlet’임
  • DispatchServlet 서블릿 등록
    • 부모 클래스에서 HttpServlet을 상속 받아서 사용하고, 서블릿으로 동작함
      • DispatcherServlet -> FrameworkServlet -> HttpServletBean -> HttpServlet
    • 스프링부트는 DispatcherServlet을 서블릿으로 자동으로 등록하면서 모든 경로(urlPatterns=” /”)에 대해서 매핑함
  • 요청 흐름
    • 서블릿이 호출되면 HttpServlet이 제공하는 service()가 호출됨
      • 스프링 MVC는 DispatcherServlet의 부모인 FrameworkServlet에서 service()를 오버라이딩 해두었음
    • FrameworkServlet.service()를 시작으로 여러 메서드가 호출되면서 DispatchServlet.doDispatch()가 호출됨

 

참고) Dispatcher.doDispatch()

더보기
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
        HttpServletRequest processedRequest = request;
        HandlerExecutionChain mappedHandler = null;
        boolean multipartRequestParsed = false;
        WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);

        try {
            try {
                ModelAndView mv = null;
                Exception dispatchException = null;

                try {
                    processedRequest = this.checkMultipart(request);
                    multipartRequestParsed = processedRequest != request;
                    
                    // 1. 핸들러 조회
                    mappedHandler = this.getHandler(processedRequest);
                    if (mappedHandler == null) {
                        this.noHandlerFound(processedRequest, response);
                        return;
                    }

					// 2. 핸들러 어댑터 조회 - 핸들러를 처리할 수 있는 어댑터
                    HandlerAdapter ha = this.getHandlerAdapter(mappedHandler.getHandler());
                    String method = request.getMethod();
                    boolean isGet = HttpMethod.GET.matches(method);
                    if (isGet || HttpMethod.HEAD.matches(method)) {
                        long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
                        if ((new ServletWebRequest(request, response)).checkNotModified(lastModified) && isGet) {
                            return;
                        }
                    }

                    if (!mappedHandler.applyPreHandle(processedRequest, response)) {
                        return;
                    }

					// 3. 핸들러 어댑터 실행 -> 4. 핸들러 어댑터를 통해 핸들러 실행 -> 5. ModelAndView 반환
                    mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
                    if (asyncManager.isConcurrentHandlingStarted()) {
                        return;
                    }

                    this.applyDefaultViewName(processedRequest, mv);
                    mappedHandler.applyPostHandle(processedRequest, response, mv);
                } catch (Exception ex) {
                    dispatchException = ex;
                } catch (Throwable err) {
                    dispatchException = new ServletException("Handler dispatch failed: " + String.valueOf(err), err);
                }

                this.processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
            } catch (Exception ex) {
                triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
            } catch (Throwable err) {
                triggerAfterCompletion(processedRequest, response, mappedHandler, new ServletException("Handler processing failed: " + String.valueOf(err), err));
            }

        }
    ...
}

...


private void processDispatchResult(HttpServletRequest request, HttpServletResponse response, @Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv, @Nullable Exception exception) throws Exception {
    ...

    if (mv != null && !mv.wasCleared()) {
    	// 뷰 렌더링 호출
        this.render(mv, request, response);
        
        ...
    } else if (this.logger.isTraceEnabled()) {
        this.logger.trace("No view rendering, null ModelAndView returned.");
    }

    ...
}

...

protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception {
    ...
    
    View view;
    if (viewName != null) {
		// 6. 뷰 리졸버를 통해 뷰 찾기 -> 7. View 반환
        view = this.resolveViewName(viewName, mv.getModelInternal(), locale, request);
        if (view == null) {
            String var10002 = mv.getViewName();
            throw new ServletException("Could not resolve view with name '" + var10002 + "' in servlet with name '" + this.getServletName() + "'");
        }
    } else {
        view = mv.getView();
        if (view == null) {
            String var9 = String.valueOf(mv);
            throw new ServletException("ModelAndView [" + var9 + "] neither contains a view name nor a View object in servlet with name '" + this.getServletName() + "'");
        }
    }

    ...

    try {
        if (mv.getStatus() != null) {
            request.setAttribute(View.RESPONSE_STATUS_ATTRIBUTE, mv.getStatus());
            response.setStatus(mv.getStatus().value());
        }

		// 8. 뷰 랜더링
        view.render(mv.getModelInternal(), request, response);
    } catch (Exception var8) {
        if (this.logger.isDebugEnabled()) {
            this.logger.debug("Error rendering view [" + String.valueOf(view) + "]", var8);
        }

        throw var8;
    }
}

 

2) Spring MVC 구조

  1. 요청 전달 : HTTP 요청이 DispatcherServlet으로 전달됨
  2. 핸들러 조회 : 핸들러 매핑을 통해 요청 URL에 매핑된 핸들러(컨트롤러)를 조회함
  3. 핸들러 어댑터 조회 : 핸들러를 실행할 수 있는 핸들러 어댑터를 조회함
  4. 핸들러 어댑터 실행 : 핸들러 어댑터를 실행함
  5. 핸들러 실행 : 핸들러 어댑터가 실제 핸들러를 실행함
  6. ModelAndView 반환 : 핸들러 어댑터는 핸들러가 반환하는 정보를 ModelAndView로 변환해서 반환함
  7. viewResolver 호출 : 뷰 리졸버를 찾고 실행함
  8. View 반환 : 뷰 리졸버는 뷰의 논리 이름을 물리 이름으로 바꾸고, 렌더링 역할을 담당하는 뷰 객체를 반환함
  9. 뷰 렌더링 : 뷰를 통해서 뷰를 렌더링함

 

* 스프링의 핸들러 매핑과 핸들러 어댑터

  • 스프링은 어노테이션을 활용한 유연하고, 실용적인 컨트롤러를 만들었는데 이것이 바로 @RequestMapping 어노테이션을 사용하는 컨트롤러
  • @RequestMapping을 사용할 때 가장 우선순위가 높은 핸들러 매핑과 핸들러 어댑터는 RequestMappingHandlerMapping, RequestMappingHandlerAdapter
    • 이것이 스프링에서 주로 사용하는 어노테이션 기반의 컨트롤러를 지원하는 핸들러 매핑과 어댑터임

 

HTTP 응답 - HTTP API, 메시지 바디에 직접 입력

  • HTTP API를 제공하는 경우에는 HTML이 아니라 데이터를 전달해야 하므로, HTTP 메시지 바디에 JSON 같은 형식으로 데이터를 실어 보냄
    • HTML이나 뷰 템플릿을 사용해도 HTTP 응답 메시지 바디에 HTML 데이터가 담겨서 전달됨
    • 여기서 설명하는 내용은 정적 리소스나 뷰 템플릿을 거치지 않고, 직접 HTTP 응답 데이터를 전달하는 경우를 말함

 

응답 반환 방식

더보기
package hello.springmvc.basic.response;

import hello.springmvc.basic.HelloData;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Controller
public class ResponseBodyController {
	
    // 1)
    @GetMapping("/response-body-string-v1")
    public void responseBodyV1(HttpServletResponse response) throws IOException {
    	response.getWriter().write("ok");
    }
    
    // 2)
    @GetMapping("/response-body-string-v2")
    public ResponseEntity<String> responseBodyV2() {
    	return new ResponseEntity<>("ok", HttpStatus.OK);
    }
    
    // 3)
    @ResponseBody
    @GetMapping("/response-body-string-v3")
    public String responseBodyV3() {
    	return "ok";
    }
    
    // 4)
    @GetMapping("/response-body-json-v1")
    public ResponseEntity<HelloData> responseBodyJsonV1() {
    	HelloData helloData = new HelloData();
        helloData.setUsername("userA");
        helloData.setAge(20);
        
        return new ResponseEntity<>(helloData, HttpStatus.OK);
    }
    
    // 5)
    @ResponseStatus(HttpStatus.OK)
    @ResponseBody
    @GetMapping("/response-body-json-v2")
    public HelloData responseBodyJsonV2() {
    	HelloData helloData = new HelloData();
        helloData.setUsername("userA");
        helloData.setAge(20);
        
        return helloData;
    }
}

 

1) 서블릿을 직접 다룰 때처럼 HttpServletResponse 객체를 통해서 HTTP 메시지 바디에 직접 ok 응답 메시지를 전달함

2) ResponseEntity는 HttpEntity를 상속 받았는데, HttpEntity는 HTTP 메시지의 헤더, 바디 정보를 가지고 있음

  • ResponseEntity는 여기에 더해 HTTP 응답 코드를 설정할 수 있음

3) @ResponseBody나 ResponseEntity를 사용하면 View를 사용하지 않고, HTTP 메시지 컨버터를 통해 HTTP 메시지를 직접 입력할 수 있음

4) ResponseEntity를 반환함. HTTP 메시지 컨버터를 통해 JSON 형식으로 변환되어서 반환됨

5) ResponseEntity는 HTTP 응답 코드를 설정할 수 있는데, @ResponseBody를 사용하면 이런 것을 설정하기 까다로움

  • 대신, @ResponseStatus(HttpStatus.OK) 어노테이션을 사용하면 응답 코드도 설정할 수 있음
  • 어노테이션이기 때문에 응답 코드를 동적으로 변경할 수는 없음

 

Q. @RestController은?

  • @Controller 대신 @RestController 어노테이션을 사용하면, 해당 컨트롤러에 모두 @ResponseBody가 적용되는 효과가 있음
    • @RestController는 합성 어노테이션으로 @Controller와 @ResponseBody를 포함하고 있음
  • @Controller는 @Component를 포함하고 있으므로 컴포넌트 스캔 대상이 되어 스프링 빈 자동 등록 대상이 됨
  • @ResponseBody는 HTTP 메시지 컨버터를 사용해 데이터가 HTTP 메시지 바디에 직접 쓰여지므로 View 템플릿을 사용하지 않고 JSON 타입의 응답 데이터를 반환할 수 있음

 

HTTP 메시지 컨버터

  • 뷰 템플릿으로 HTML을 생성해서 응답하는 것이 아니라, HTTP API처럼 JSON 데이터를 HTTP 메시지 바디에서 직접 읽거나 쓰는 경우 HTTP 메시지 컨버터를 사용하면 편리함
  • 스프링 MVC는 다음의 경우에 HTTP 메시지 컨버터를 적용
    • HTTP 요청 : @RequestBody, HttpEntity(RequestEntity)
    • HTTP 응답 : @ResponseBody, HttpEntity(ResponseEntity)

 

1) HTTP 메시지 컨버터 인터페이스

코드)

더보기
package org.springframework.http.converter;

public interface HttpMessageConverter<T> {

    boolean canRead(Class<?> clazz, @Nullable MediaType mediaType);
    boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType);

    List<MediaType> getSupportedMediaTypes();

    default List<MediaType> getSupportedMediaTypes(Class<?> clazz) {
        return !this.canRead(clazz, (MediaType)null) && !this.canWrite(clazz, (MediaType)null) ? Collections.emptyList() : this.getSupportedMediaTypes();
    }

    T read(Class<? extends T> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException;

    void write(T t, @Nullable MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException;
}
  • HTTP 메시지 컨버터는 HTTP 요청, HTTP 응답 둘 다 사용
    • canRead(), canWrite() : 메시지 컨버터가 해당 클래스, 미디어 타입을 지원하는지 체크
    • read(), write() : 메시지 컨버터를 통해서 메시지를 읽고 쓰는 기능

 

2) 스프링부트 기본 메시지 컨버터

0 = ByteArrayHttpMessageConverter
1 = StringHttpMessageConverter
2 = MappingJackson2HttpMessageConverter

// 일부 생략
  • 스프링부트는 다양한 메시지 컨버터를 제공하고, 대상 클래스 타입과 미디어 타입 둘을 체크해 사용 여부를 결정
  • 만약 만족하지 않으면 다음 메시지 컨버터로 넘어감

 

주요 메시지 컨버터

더보기
  • ByteArrayHttpMessageConverter: byte[] 데이터 처리
    • 클래스 타입 : byte[], 미디어타입 : */* (all)
    • 예)
      • 요청 : @RequestBody byte[] data
      • 응답 : @ResponseBody return byte[], 쓰기 미디어 타입 : application/octet-stream
  • StringHttpMessageConverter : String 문자로 데이터 처리
    • 클래스 타입 : String, 미디어 타입 : */* (all)
    • 예)
      • 요청 : @RequestBody String data
      • 응답 : @ResponseBody return "ok", 쓰기 미디어 타입 : text/plain
  • MappingJackson2HttpMessageConverter
    • 클래스 타입 : 객체 또는 HashMap, 미디어 타입 : application/json 관련
    • 예)
      • 요청 : @RequestBody HelloData data
      • 응답 : @ResponseBody return helloData, 쓰기 미디어 타입 : application/json 관련
    •  

 

3) HTTP 요청/응답 처리

1. HTTP 요청 데이터 읽기

  • HTTP 요청이 오고, 컨트롤러에서 @RequestBody or HttpEntity 파라미터를 사용
  • 메시지 컨버터가 메시지를 읽을 수 있는지 확인하기 위해 canRead()를 호출
    • 대상 클래스 타입을 지원하는가
      • ex) @RequestBody를 명시한 변수의 타입이 byte[], String, 객체 또는 HashMap인지 확인
    • HTTP 요청의 Content-Type 미디어 타입을 지원하는가
      • ex) text/plain, application/json, */*
  • canRead() 조건을 만족하면 read()를 호출해서 객체 생성 후 반환

 

2. HTTP 응답 데이터 생성

  • 컨트롤러에서 @ResponseBody, HttpEntity로 값이 반환
  • 메시지 컨버터가 해당 메시지를 쓸 수 있는지 확인하기 위해 canWrite()를 호출
    • 대상 클래스 타입을 지원하는가
      • ex) return의 대상 클래스 타입 확인 (byte[], String, 객체 또는 HashMap)
    • HTTP 요청의 Accept 미디어 타입을 지원하는가 (정확히는 @RequestMapping의 produces)
      • ex) text/plain, application/json, */*
  • canWrite() 조건을 만족하면 write()를 호출해서 HTTP 응답 메시지 바디에 데이터를 생성

 

요청 매핑 핸들러 어댑터 구조

  • 스프링은 어노테이션 기반의 컨트롤러를 사용하고, @RequestMapping이 이를 가능하게 해줌
  • @RequestMapping을 처리하는 핸들러 어댑터인 RequestMappingHandlerAdapter가 요청/응답 데이터를 처리해줌

 

1) RequestMappingHandlerAdapter 동작 방식

요약

  • 요청값 변환 및 처리 : ArgumentResolver, 응답값 변환 및 처리 : ReturnValueHandler

 

 

ArgumentResolver

  • 어노테이션 기반의 컨트롤러는 매우 다양한 메서드 파라미터를 사용할 수 있음
  • 이렇게 파라미터를 유연하게 처리할 수 있는 이유가 바로 ArgumentResolver 덕분임
  • 어노테이션 기반 컨트롤러를 처리하는 RequestMappingHandlerAdapter는 ArgumentResolver를 호출해서 컨트롤러(핸들러)가 필요로 하는 다양한 파라미터의 값(객체)을 생성하고, 파라미터의 값이 모두 준비되면 컨트롤러를 호출하면서 값을 넘겨줌

 

동작 방식

코드)

더보기
package org.springframework.web.method.support;

public interface HandlerMethodArgumentResolver {
    boolean supportsParameter(MethodParameter parameter);

    @Nullable
    Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception;
}
  • ArgumentResolver의 supportsParameter()를 호출해서 대상 ArgumentResolver 구현체가 해당 파라미터를 지원하는지 체크하고, 지원하면 resolveArgument()를 호출해서 실제 객체를 생성
  • 생성된 객체가 컨트롤러 호출 시 전달

 

ReturnValueHandler

  • ArgumentResolver와 비슷하고, 대신 응답값을 변환하고 처리함
    • 컨트롤러에서 String으로 뷰 이름만 반환해도 동작하는 이유가 바로 ReturnValueHandler가 처리해주기 때문임

 

코드)

더보기
package org.springframework.web.method.support;

public interface HandlerMethodReturnValueHandler {
    boolean supportsReturnType(MethodParameter returnType);

    void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception;
}

 

Q. 그렇다면 HTTP 메시지 컨버터는 스프링 MVC 구조의 어디에서 사용되는 것일까?

요약

  • 요청 : @RequestBody, HttpEntity 타입의 파라미터를 처리하는 ArgumentResolver가 HTTP 메시지 컨버터를 호출해 필요한 객체를 생성
  • 응답 : @ResponseBody, HttpEntity 타입의 반환값을 처리하는 ReturnValueHandler가 HTTP 메시지 컨버터를 호출해 응답 결과를 만듦

 

HTTP 메시지 컨버터 위치

  • 요청의 경우
    • @RequestBody와 HttpEntity를 처리하는 각각의 ArgumentResolver가 있음
    • ArgumentResolver들이 HTTP 메시지 컨버터를 사용해 필요한 객체를 생성한 후 핸들러(컨트롤러)에게 전달함
  • 응답의 경우
    • @ResponseBody와 HttpEntity를 처리하는 각각의 ReturnValueHandler가 있음
    • ReturnValueHandler가 HTTP 메시지 컨버터를 호출해 응답 결과를 만듦
  • 스프링 MVC는 ArgumentResolver, ReturnValueHandler를 둘 다 구현한 구현체를 사용함
    • @RequestBody와 @ResponseBody가 있으면, RequestResponseBodyMethodProcessor 사용
    • HttpEntity가 있으면, HttpEntityMethodProcessor 사용

 

참고