ABOUT ME

Today
Yesterday
Total
  • [Spring] Spring MVC - 구조 이해 및 기본 기능
    Study/Spring 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 사용

     

    참고

    댓글

Designed by Tistory.