본문 바로가기
spring

mvc 1 - 스프링 MVC

by 오우지 2022. 2. 24.

스프링 MVC의 동작 원리에 대해 알아보자.

 

FrontController Pattern으로 만든 수제 MVC와 실제 구현 되어 있는 MVC 구조는 동일하다.

 

DispacherServlet 서블릿 등록

 

DispacherServlet도 부모 클래스에서 HttpServlet을 상속 받아서 사용하고 서블릿으로 동작한다.

1. 스프링 부트는 DispacherServlet을 서블릿으로 자동으로 등록하면서 모든 경로에 대해서 매핑한다.

- 더 자세한 경로가 우선순위가 높기 때문에 기존에 등록한 서블릿도 함께 동작한다.

 

2. 서블릿이 호출되면 HttpServlet이 제공하는 service()가 호출된다.

스프링 MVC는 DispatcherServlet의 부모인 FrameworkServlet에서 service()를 오버라이딩 해뒀다.

FrameworkServlet.service()를 시작으로 여러 메서드가 호출되면서 핵심인 DispacherServlet.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 {
        ModelAndView mv = null;
        Exception dispatchException = null;

        try {
            processedRequest = checkMultipart(request);
            multipartRequestParsed = (processedRequest != request);

            // Determine handler for the current request.
            mappedHandler = getHandler(processedRequest);(1)
            if (mappedHandler == null) {
                noHandlerFound(processedRequest, response);
                return;
            }

            // Determine handler adapter for the current request.
            HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());(2)

            // Process last-modified header, if supported by the handler.
            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;
            }

            // Actually invoke the handler.
            mv = ha.handle(processedRequest, response, mappedHandler.getHandler());(3)

            if (asyncManager.isConcurrentHandlingStarted()) {
                return;
            }

            applyDefaultViewName(processedRequest, mv);
            mappedHandler.applyPostHandle(processedRequest, response, mv);
        }
        catch (Exception ex) {
            dispatchException = ex;
        }
        catch (Throwable err) {
            // As of 4.3, we're processing Errors thrown from handler methods as well,
            // making them available for @ExceptionHandler methods and other scenarios.
            dispatchException = new NestedServletException("Handler dispatch failed", err);
        }
        processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);(4)
    }
    catch (Exception ex) {
        triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
    }
    catch (Throwable err) {
        triggerAfterCompletion(processedRequest, response, mappedHandler,
                new NestedServletException("Handler processing failed", err));
    }
    finally {
        if (asyncManager.isConcurrentHandlingStarted()) {
            // Instead of postHandle and afterCompletion
            if (mappedHandler != null) {
                mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
            }
        }
        else {
            // Clean up any resources used by a multipart request.
            if (multipartRequestParsed) {
                cleanupMultipart(processedRequest);
            }
        }
    }
	}

1. handler를 꺼내는 부분이다.

2. handler를 넘겨서 handler adaptor를 찾는다.

적용할 수 있는 어댑터가 많은 것을 알 수 있다.

3. 핸들러 어댑터가 핸들러가 반환하는 정보를 ModelAndView 변환해서 반환

4. processDispatchResult에서 render를 타면 render에서 뷰네임으로 진짜 뷰 객체를 찾고 렌더한다.

 

 

스프링 MVC의 큰 강점은 DispatcherServlet의 코드 변경 없이 원하는 기능을 변경하거나 확장할 수 있다는 점이다.

인터페이스를 구현해서 DispatcherServlet에 등록하면 나만의 컨트롤러를 만들 수도 있다.

 

 

핸들러 매핑과 핸들러 어댑터

핸들러 매핑과 핸들러 어댑터가 어떤 것들이 사용되는지 알아볼 것이다.

과거에 스프링이 제공해서 사용했던 컨트롤러로 핸들러 매핑과 어댑터를 이해할 수 있다.

public interface Controller {
ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse 
response) throws Exception;
}

과거에는 이런 형식의 컨트롤러를 사용했었는데 이를 구현하려면

@Component("/springmvc/old-controller")
public class OldController implements Controller {

    @Override
    public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
        System.out.println("OldController.handleRequest");
        return new ModelAndView("new-form");
    }

}

 

 

물론 이렇게는 구현 못했겠지만 우선 이렇게 등록하면 이 컨트롤러는 빈의 이름으로 URL을 매핑할 것이다.

@Component를 통해서 스프링 빈의 이름은 url로 맞춰놨다.

 

이 컨트롤러가 호출되는데 2가지 항목이 필요하다.

1. HandlerMapping

- 스프링 빈의 이름으로 핸들러를 찾을 수 있는 핸들러 매핑이 필요하다.

 

2. HandlerAdapter

- 핸들러 매핑을 통해서 찾은 핸들러를 실행할 수 있는 핸들러 어댑터가 필요하다.

 

근데 이건 다 구현돼있다. 그래서 우선순위나 알아보자

0 = RequestMappingHandlerMapping: 애노테이션 기반의 컨트롤러인 @RequestMapping에서 사용
1 = BeanNameUrlHandlerMapping: 스프링 빈의 이름으로 핸들러를 찾는다.

이제 핸들러 어댑터를 찾을 것이다. 핸들러 어댑터를 찾는 우선순위는 다음과 같다.

0 = RequestMappingHandlerAdapter: 애노테이션 기반의 컨트롤러인 @RequestMapping에서 사용
1 = HttpRequestHandlerAdapter: HttpRequestHandler 처리
2 = SimpleControllerHandlerAdapter: Controller 인터페이스(애노테이션X, 과거에 사용)

 

순서를 다시 정리해보면

1. 핸들러 매핑으로 핸들러 조회

- HandlerMapping을 순서대로 실행해서 핸들러를 찾는다.

 

2. 핸들러 어댑터 조회

- HandlerAdapter의 supports()를 순서대로 호출한다.

 

3. 핸들러 어댑터 실행

- 디스패쳐 서블릿이 조회한 SimpleControllerAdapter를 실행하면서 핸들러 정보도 함께 넘겨준다.

- OldController를 내부에서 실행하고 그 결과를 반환한다.

 

여기서 HttpRequestHandler를 살펴보면 이는 서블릿과 가장 유사한 형태의 핸들러다.

@Component("/springmvc/request-handler")
public class MyHttpRequestHandler implements HttpRequestHandler {

    @Override
    public void handleRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        return;
    }
}

다음과 같이 구현할 수 있다.

 

위의 코드도 똑같이 실행된다. 조금 더 코드레벨로 보자면

mappedHandler = getHandler(processedRequest);(1)
if (mappedHandler == null) {
    noHandlerFound(processedRequest, response);
    return;
}
// Determine handler adapter for the current request.
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

// Actually invoke the handler.
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());(3)

위에 doDispatch에 이런 부분이 있었다. 이 부분은 핸들러를 찾아서 핸들러가 존재하는지 확인하는 부분이다.

@Nullable
protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
    if (this.handlerMappings != null) {
        for (HandlerMapping mapping : this.handlerMappings) {
            HandlerExecutionChain handler = mapping.getHandler(request);
            if (handler != null) {
                return handler;
            }
        }
    }
    return null;
}

getHandler는 다음과 같이 구현돼있다.

만약 noHandlerFound가 실행된다면

protected void noHandlerFound(HttpServletRequest request, HttpServletResponse response) throws Exception {
    if (pageNotFoundLogger.isWarnEnabled()) {
        pageNotFoundLogger.warn("No mapping for " + request.getMethod() + " " + getRequestUri(request));
    }
    if (this.throwExceptionIfNoHandlerFound) {
        throw new NoHandlerFoundException(request.getMethod(), getRequestUri(request),
                new ServletServerHttpRequest(request).getHeaders());
    }
    else {
        "response.sendError(HttpServletResponse.SC_NOT_FOUND);"
    }
}

SC_NOT_FOUND(404)가 실행되는 것을 알 수 있다.

 

정상 실행됐다는 가정 하에 핸들러 어댑터를 찾는 getHandlerAdapter(mappedHander.getHandler());로 넘어가보자.

protected HandlerAdapter getHandlerAdapter(Object handler) throws ServletException {
        if (this.handlerAdapters != null) {
            for (HandlerAdapter adapter : this.handlerAdapters) {
                if (adapter.supports(handler)) {
                    return adapter;
                }
            }
        }

위의 핸들러 찾는 로직과 비슷하다.

인터셉터나 다른 로직은 제외하면 handle이 실행된다.

 

여기까지는 로직의 이해를 위한 부분이었고 @RequestMapping이 실제로는 가장 많이 쓰이는 애노테이션 기반의 컨트롤러를 지원하는 매핑과 어댑터다.

 


뷰 리졸버 동작 방식

스프링 부트가 자동 등록하는 뷰 리졸버

1 = BeanNameViewResolver : 빈 이름으로 뷰를 찾아서 반환한다. (예: 엑셀 파일 생성 기능에 사용)
2 = InternalResourceViewResolver : JSP를 처리할 수 있는 뷰를 반환한다.

이것 말고도 매우 많다.

 

1. 핸들러 어댑터 호출

- 핸들러 어댑터를 통해 new-form이라는 논리 뷰 이름을 획득한다.

 

2. ViewResolver 호출

- new-form이라는 뷰 이름으로 viewResolver를 순서대로 호출한다.

- InternalResourceViewResolver가 호출된다.

 

3. InternalResourceViewResolver

이 뷰 리졸버는 InternalResourceView를 반환한다.

@Override
protected AbstractUrlBasedView buildView(String viewName) throws Exception {
    InternalResourceView view = (InternalResourceView) super.buildView(viewName);
    if (this.alwaysInclude != null) {
        view.setAlwaysInclude(this.alwaysInclude);
    }
    view.setPreventDispatchLoop(true);
    return view;
}

 

4. 뷰 - InternalResourceView

InternalResourceView는 JSP처럼 forward()를 호출해서 처리할 수 있는 경우에 사용하고 일반적으론 view.render가 호출된다.

 

 


스프링MVC 시작하기

 

스프링은 애노케이션을 활용한 매우 유연하고 실용적 컨트롤러 @RequestMapping 컨트롤러를 만들었다.

앞에서 본 것 같이 우선순위가 가장 높은 핸들러 매핑과 어댑터는 RequestMappingHandlerMapping, RequestMappingHandlerAdaptor다. 

 

@Controller:

- 스프링이 자동으로 스프링 빈으로 등록한다.(내부에 @Component 기능이 있다.)

- 스프링 MVC에서 애노테이션 기반 컨트롤러로 인식한다

 

@RequestMapping: 요청 정보를 매핑한다. 해당 URL이 호출되면 이 메서드가 호출된다. 애노테이션을 기반으로 동작하기 때문에 메서드 이름은 임의로 지어도 된다.

 

* ReqeustMapping 또는 @Controller가 클래스 래벨에 붙어 있는 경우에 매핑 정보로 인식한다.

 

ModelAndView: 모델과 뷰 정보를 담아서 반환하면 된다.

 

 

@Controller
@RequestMapping("/springmvc/v3/members")
public class SpringMemberControllerV3 {

    private MemberRepository memberRepository = MemberRepository.getInstance();

//    @RequestMapping(value = "/new-form", method = RequestMethod.GET)
    @GetMapping("/new-form")
    public String newForm(){
        return "new-form";
    }

//    @RequestMapping(method = RequestMethod.GET)
    @GetMapping
    public String save(@RequestParam("username") String username, @RequestParam("age") int age, Model model) {

        Member member = new Member(username, age);
        memberRepository.save(member);

        model.addAttribute("member", member);
        return "save-result";
    }

//    @RequestMapping(value = "/save", method = RequestMethod.POST)
    @PostMapping("/save")
    public String members(Model model) {
        List<Member> members = memberRepository.findAll();

        model.addAttribute("members", members);
        return "members";
    }
}

결론은 이렇게 쓰면 된다.


Spring MVC 기본 기능

로깅

로깅 알아보기

운영 시스템에서는 sout보다는 로깅 라이브러리를 사용한다.

SLF4J 라이브러리는 로그 라이브러리를 통합해서 인터페이스로 제공하는 것이고 그 구현체는 Logback, Log4J, Log4J2등등이 있는데 그냥 스프링 부트가 기본으로 제공하는 Logback을 사용하면 된다.

 

로그 레벨은 TRACE > DEBUG > INFO > WARN > ERROR 순으로 나오는데

개발서버는 debug, 운영서버는 info 등으로 관리할 수 있다.

 

설정은 application.properties에서 설정하면 된다.

logging.level.root=info
logging.level.hello.springmvc=debug

 

로그를 사용할 때는 

log.debug("data="+data)
log.debug("data={}", data)

두 가지 모두 사용할 수 있는데 위에것은 + 연산 과정에서 리소스가 사용되고 아래 것은 연산이 따로 일어나지 않는다. 

 

로그 사용시 장점

- 쓰레드 정보, 클래스 이름 같은 부가 정보를 함께 볼 수 있고, 출력 모양을 조정할 수 있다.

- 로그 레벨에 따라 개발 서버에서는 모든 로그를 출력하고 운영서버에서는 출력하지 않는 등 로그를 상황에 맞게 조절할 수 있다.

- 시스템 아웃 콘솔에만 출력하는게 아니라 파일이나 네트워크 등 로그를 별도의 위치에 남길 수 있다. 특히 파일로 남길 떄는 로그 분할도 가능하다. 

- 성능도 System.out보다 좋다.

 

 

매핑 정보

@Controller: 반환값이 String이면 뷰 이름으로 인식된다.

@RestController: 반환값으로 뷰를 찾는 것이 아니라 HTTP 메시지 바디에 바로 입력한다. 결과로 ok메시지를 받을 수 있다.

@RequestMapping("/hello-"): 해당 URL 호출이 오면 이 메서드가 실행되도록 매핑한다. 대부분의 속성을 배열로 제공해 다중 설정이 가능하다.

 

최근 HTTP API는 리소스 경로에 식별자를 넣는 스타일을 선호한다. @RequestMapping은 URL 경로를 템플릿화 할 수 있는데 @PathVariable()을 사용하면 매칭되는 부분을 편리하게 조회할 수 있다.

@GetMapping("/mapping/users/{userId}/orders/{orderId}")
public String mappingPath(@PathVariable String userId, @PathVariable Long orderId) {
    log.info("mappingPath userId={}, orderId={}", userId, orderId);
    return "ok";
}

 

특정 헤더 조건 매핑

/**
*특정 헤더로 추가 매핑
   * headers="mode",
   * headers="!mode"
   * headers="mode=debug"
   * headers="mode!=debug" (! = )
   */
  @GetMapping(value = "/mapping-header", headers = "mode=debug")
  public String mappingHeader() {
      log.info("mappingHeader");
      return "ok";
  }

파라미터 매핑과 비슷하지만, HTTP 헤더를 사용한다.

 

미디어 타입 조건 매핑 - HTTP 요청 Content-type, consume

/**
* Content-Type 헤더 기반 추가 매핑 Media Type * consumes="application/json"
* consumes="!application/json"
* consumes="application/*"
* consumes="*\/*"
* MediaType.APPLICATION_JSON_VALUE
*/
  @PostMapping(value = "/mapping-consume", consumes = "application/json")
  public String mappingConsumes() {
      log.info("mappingConsumes");
      return "ok";
  }

Content-Type 헤더를 기반으로 미디어 타입으로 매핑한다. 안맞으면 415

 

/**
* Accept 헤더 기반 Media Type * produces = "text/html"
* produces = "!text/html" * produces = "text/*"
* produces = "*\/*"
*/
  @PostMapping(value = "/mapping-produce", produces = "text/html")
  public String mappingProduces() {
      log.info("mappingProduces");
      return "ok";
  }

HTTP 요청의 Accept 헤더를 기반으로 미디어 타입으로 매핑 406을 반환

 

헤더 조회

@Slf4j
@RestController
public class RequestHeaderController {
    @RequestMapping("/headers")
    public String headers(HttpServletRequest request,
HttpServletResponse response,
HttpMethod httpMethod,
Locale locale,
@RequestHeader MultiValueMap<String, String> headerMap,
@RequestHeader("host") String host,
@CookieValue(value = "myCookie", required = false)String cookie
){
	log.info("request={}", request);
    log.info("response={}", response);
    log.info("httpMethod={}", httpMethod);
    log.info("locale={}", locale);
    log.info("headerMap={}", headerMap);
    log.info("header host={}", host);
    log.info("myCookie={}", cookie);
    return "ok";
}

HttpServletRequest
HttpServletResponse
HttpMethod
: HTTP 메서드를 조회한다. org.springframework.http.HttpMethod

Locale : Locale 정보를 조회한다.
@RequestHeader MultiValueMap<String, String> headerMap

모든 HTTP 헤더를 MultiValueMap 형식으로 조회한다.

@RequestHeader("host") String host

- 특정 HTTP 헤더를 조회한다.

- 속성: 필수 값 여부: required,  기본 값 속성: defaultValue
@CookieValue(value = "myCookie", required = false) String cookie

특정 쿠키를 조회한다.

- 속성 필수 값 여부: required, 기본 값: defaultValue

MultiValueMap

map 과 유사한데 하나의 키에 여러 값을 받을 수 있다.

HTTP header, HTTP 쿼리 파라미터와 같이 하나의 키에 여러 값을 받을 때 사용한다.

 

 

 

 

클라이언트에서 서버로 요청 데이터를 전달할 때 주로 3가지 방법을 사용한다.

1. GET 쿼리 파라미터

2. POST 메시지 바디에 쿼리 파라미터 형식 전달

3. HTTP message body에 데이터를 직접 담아서 요청

 

파라미터를 통해 값을 받는 방법에서 스프링에서 사용할 수 있는 여러가지 방법이 있다. 

 

@RequestParam

 

@RestController = @Controller + @ResponseBody라고 생각하면 된다.

@ResponseBody
@RequestMapping("/request-param-v5")
public String requestParamRequired(
              @RequestParam(required = true) String username,
              @RequestParam(required = false) Integer age){
      log.info("username={}, age={}", username, age);
      return "ok";
}

@RequestParam은 넘겨주는 변수와 이름이 같다면 굳이 안써도 된다. 또 만약 변수명을 바꿔서 받아주고 싶다면 RequestParam 안에 ("username")등과 같이 받는 이름을 달아주고 뒤의 String memberName과 같이 새 변수명을 달아주면 된다.

required를 통해 필요 여부도 정해서 충족하지 않는다면 400 bad request 에러를 띄울 수도 있다.

 

@ResponseBody
@RequestMapping("/request-param-v6")
public String requestParamDefalut(
              @RequestParam(defaultValue = "guest") String username,
              @RequestParam(required = false) Integer age){
      log.info("username={}, age={}", username, age);
      return "ok";
}

값이 없으면 디폴트를 넣어주는 방법도 있다. 빈 문자인 경우에도 설정한 기본값이 들어간다.

 

@ResponseBody
@RequestMapping("/request-param-map")
public String requestParamMap(@RequestParam Map<String, Object> paramMap){
        log.info("username={}, age={}", paramMap.get("username"), paramMap.get("age"));
        return "ok";
}

Map을 통해서 받아주는 방법도 있는데 파라미터의 값이 1개가 확실하다면 Map을 사용해도 되지만 그렇지 않다면 MultiValueMap을 사용해야 한다.

 

@ModelAttribute

 

@Data 적용시

Getter, Setter, ToString, EqualsAndHashCode, RequiredArgsConstructor를 자동 적용

 

@ResponseBody
@RequestMapping("/model-attribute-v1-1")
public String modelAttributeV1(@ModelAttribute HelloData helloData){
        log.info("username={}, age={}", helloData.getUsername(), helloData.getAge());

        return "ok";
}

위와 같이 객체도 파라미터를 객체로 받는 @ModelAttribute를 생략 가능하다

 

http 메시지 바디를 통해 데이터가 넘어오는 경우 @RequestParam, @ModelAttribute를 사용할 수 없다. 그 대신 InputStream으로 받아줄 수 있다. 

 

@RequestBody

 

HttpEntity

Http header, body  정보를 편리하게 조회하는데 쓰인다.

메시지 바디 정보를 조회하는데 위에서 지금까지 하던 요청 파라미터를 조회하는 기능과는 관계 없다.

응답에도 사용 가능하며 헤더 정보도 포함할 수 있다. 단 view조회는 불가능하다.

 

하지만 세개 중 가장 편한건 @RequestBody다.

 

@ResponseBody
@PostMapping("/request-body-string-v4")
public String requestBodyStringV4(@RequestBody String messageBody) throws IOException {
        log.info("messageBody={}", messageBody);

        return "ok";
}

여기서 @RequestBody를 생략할 수 있을까?

정답은 못한다. 스프링은 단순타입은 @RequestParam, 나머지는 @ModelAttribute로 반환하기 때문에 HTTP 메시지 바디가 아니라 요청 파라미터를 처리하는 로직을 따르게 된다.

 

HTTP 요청 메시지 JSON

HttpServletRequest를 사용해서 직접 HTTP 메시지 바디에서 데이터를 불러와 문자로 변환한다. JSON 데이터를 Jackson 라이브러리인 ObjectMapper를 사용해서 자바 객체로 변환한다.

 

/**
* @RequestBody 생략 불가능(@ModelAttribute 가 적용되어 버림)
* HttpMessageConverter 사용 -> MappingJackson2HttpMessageConverter (content-
*  type: application/json)
*
*/
    @ResponseBody
    @PostMapping("/request-body-json-v3")
    public String requestBodyJsonV3(@RequestBody HelloData data) {
        log.info("username={}, age={}", data.getUsername(), data.getAge());
        return "ok";
    }

@RequestBody에 직접 만든 객체를 지정할 수 있다. @ResponseBody를 이용해서 JSON 반납, @RestController 적용시 생략가능

 

스프링은 @ModelAttribute, @RequestParam 생략시 다음과 같은 규칙을 적용한다.

String, int, Integer같은 단순 타임 = @RequestParam

나머지 = @ModelAttribute

 

 

 

HTTP 메시지 컨버터

뷰 템플릿으로 HTML을 생성하는게 아니라 HTTP API처럼 JSON데이터를 HTTP 메시지 바디에서 직접 읽거나 쓰는 경우 HTTP메시지 컨버터를 사용하면 편리하다.

 

@ResponseBody 사용 원리

HTTP의 BODY 문자 내용을 직접 반환

viewResolver 대신에 HttpMessageConverter가 동작

- 기본 문자처리: StringHttpMessageConverter

- 기본 객체 처리: MappingJackson2HttpMessageConverter

- byte 처리 등등 기타 여러 HttpMessageConverter가 기본으로 등록되어 있다.

 

스프링 MVC는 다음의 경우에 HTTP 메시지 컨버터를 적용한다.

- HTTP 요청: @RequestBody, HttpEntity(RequestEntity)

- HTTP 응답: @ResponseBody, HttpEntity(ResponseEntity)

 

HTTP 메시지 컨버터는 요청, 응답 둘 다 사용된다.

canRead(), canWrite(): 메시지 컨버터가 해당 클래스, 미디어 타입을 지원하는지 체크

read(), write(): 메시지 컨버터를 통해서 메시지를 읽고 쓰는 기능

더 있지만 중요한 컨버터 위주로 알아보면

 

1. ByteArrayHttpMessageConverter: byte() 데이터를 처리한다.

클래스 타입: byte[], 미디어 타입: */*

요청 예) @RequestBody byte[] data

응답 예) @ResponseBody return byte[] 쓰기 미디어타입 application/octet-stream

 

2. StringHttpMessageConverter: String 문자로 데이터를 처리한다.

클래스 타입: String, 미디어 타입: */*

요청 예) @RequestBody String data

응답 예) @ResponseBody return "ok" 쓰기 미디어타입 text/plain

 

3. MappingJackson2HttpMessageConverter: application/json

클래스 타입: 객체 또는 HashMap, 미디어타입 application/json관련

요청 예) @RequestBody HelloData data

응답 예) @ResponseBody return helloData 쓰기 미디어타입 application/json관련

 

위의 타입 둘 다 맞지 않으면 처리가 안된다.

 


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

그래서 HTTP 메시지 컨버터는 MVC 어디에서 사용되는가

비밀은 @RequestMapping을 처리하는 핸들러 어댑터인 RequestMappingHandlerAdapter에 있다.

애노테이션 기반의 컨트롤러는 매우 유연한데 모두 ArgumentResolver 덕분이다. 애노테이션 기반 컨트롤러를 처리하는 RequestMappingHandlerAdaptor는 이 ArgumentResolver를 호출해서 컨트롤러가 필요로 하는 다양한 파라미터 값을 생성한다. 그렇게 파라미터 값이 모두 준비되면 컨트롤러를 호출하면서 값을 넘겨준다.

 

정확히는 HandlerMethodArgumentResolver다.

supportParameter()를 호출해 체크하고 지원하면 resolveArgument()를 호출해서 실제 객체를 생성한다.

 

ReturnValueHandler return은 이걸로 한다.

 

HTTP 메시지 컨버터를 사용하는 @RequestBody도 컨트롤러가 필요로 하는 파라미터 값에 사용된다. @ResponseBody의 경우도 컨트롤러 반환 값을 이용한다. 이를 확장해서 사용할 수 있는데 실제 기능이 필요할 때 WebMvcConfigurer를 상속 받아서 스프링 빈으로 등록하면 된다. 

 

 

 

 

 

 

 

'spring' 카테고리의 다른 글

mvc2 - 타입컨버터  (0) 2022.03.02
API 예외 처리  (0) 2022.03.01
mvc2 - 필터, 인터셉터, 예외처리  (0) 2022.02.21
mvc1- 서블릿, JSP, MVC  (0) 2022.02.19
HTTP 웹 기본지식-3 헤더  (0) 2022.02.17