Lined Notebook

[스프링 MVC - 백엔드 웹 개발 기술] 11. 스프링 MVC 구조의 이해

by ymkim

01. 스프링 MVC 전체 구조

  • 직접 만든 MVC Framework와 스프링 MVC를 비교해보자

01-1. 직접 만든 MVC 프레임워크 구조

01-2. 스프링 MVC 구조

직접 만든 프레임워크 → 스프링 MVC와 비교

  1. FrontController → DispatcherServlet (FrontController Pattern)
    1. DispatcherServlet이 스프링에서 가장 중요한 개념 중 하나
    2. FrontController Pattern을 구현한 FrontController를 DispatcherServlet이라 지칭함
  2. handlerMappingMap → HandlerMapping
  3. MyHandlerAdapter → HandlerAdapter
  4. ModelView → ModelAndView
  5. viewResolver → ViewResolver
  6. MyView → View

01-3. DispatcherServlet 구조 살펴보기

org.springframework.web.servlet.DispatcherServlet
  1. 스프링 MVC프론트 컨트롤러 패턴으로 구현되어 있다
  2. 스프링 MVC의 프론트 컨트롤러가 바로 디스패처 서블릿(DispatcherServlet)이다
  3. 디스패처 서블릿이 스프링의 MVC의 핵심이다

01-4. DispatcherServlet 서블릿 등록

  • DispatcherServlet부모 클래스에서 HttpServlet 을 상속받아 사용하고, 서블릿으로 동작한다
    • DispatcherServlet → FrameworkServlet → HttpServletBean → HttpServlet
  • 스프링 부트(Spring Boot)는 DispatcherServlet을 서블릿으로 자동 등록하면서 모든 경로(”urlPatterns=”/”)에 대해서 매핑 한다
  • 참고 : 더 자세한 경로가 우선순위가 높다. 그래서 기존에 등록한 서블릿도 함께 동작
    • DispatcherServlet이 우선순위가 더 낮다

01-5. 요청 흐름

  1. 서블릿 호출 → HttpServlet이 제공하는 service()가 호출 된다
  2. 스프링 MVC는 DispatcherServlet의 부모인 FrameworkServlet에서 service() 를 오버라이드 해두었음
  3. FrameworkServlet.service() 를 시작으로 여러 메서드가 호출되면서, DispatcherServlet.doDispatch() 가 최종적으로 호출된다

지금부터 DispatcherServlet의 핵심인 doDispatch() 코드를 분석해보자.

최대한 간단히 설명하기 위해 예외처리, 인터셉터 기능은 제외함.

01-6. DispatcherServlet.doDispatch()

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
    HttpServletRequest processedRequest = request;
    HandlerExecutionChain mappedHandler = null;
    ModelAndView mv = null;

    // 1. 핸들러 조회
    mappedHandler = getHandler(processedRequest)
    if (mapperHandler == null) {
            noHandlerFound(processedRequest, response);
            return;
    }

    // 2. 핸들러 어댑터 조회 - 핸들러를 처리할 수 있는 어댑터
    HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

    // 3. 핸들러 어댑터 실행 -> 4. 핸들러 어댑터를 통해 실행 -> 5. ModelAndView 실행
    mv = ha.handle(processedRequest, response, mappedHandler, mv, dispatchException);

    processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
private void processDispatchResult(HttpServletRequest request, 
	HttpServletResponse response, HandlerExecutionChain mappedHandler, ModelAndView mv, Exception exception) throws Exception {
		
    ..중략

    // 뷰 렌더링 호출
    render(mv, request, response);

    ..중략
}
protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception {
    View view;
    String viewName = mv.getViewName();

    // 6. 뷰 리졸버를 통해서 뷰 찾기, 7. View 반환
    view = resolveViewName(viewName, mv.getModelInternal(), locale, request);

    // 8. 뷰 렌더링
    view.render(mv.getModelInternal(), request, response);
}

01-7. Spring MVC 구조

동작 순서

  1. 핸들러 조회 : 핸들러 매핑을 통해 요청 URL에 매핑된 핸들러(컨트롤러) 조회
  2. 핸들러 어댑터 : 핸들러를 실행할 수 있는 핸들러 어댑터를 조회
  3. 핸들러 어댑터 실행 : 핸들러 어댑터를 실행한다
  4. 핸들러 실행 : 핸들러 어댑터실제 핸들러(컨트롤러) 실행한다
  5. ModelAndView 반환 : 핸들러 어댑터는 핸들러가 반환하는 정보를 ModelAndView로 변환하여 반환
  6. viewResolver 호출 : 뷰 리졸버를 찾고 실행한다
    1. JSP의 경우 : InternalResourceViewResolver자동으로 등록되고, 사용 된다
  7. View 반환 : 뷰 리졸버논리 이름물리 이름으로 바꾸고, 랜더링 역할을 하는 뷰 객체 반환
    1. JSP의 경우 : InternalResourceView(JstlView) 를 반환하는데, 내부에 forward() 있음
  8. 뷰 랜더링 : 뷰를 통해 뷰를 랜더링

인터페이스 살펴보기

  • 스프링 MVC의 큰 강점은 DispatcherServlet 코드 변경없이, 기능 확장 및 변경이 가능. 지금까지 설명한 대부분을 확장 가능할 수 있게 인터페이스 제공
  • 이 인터페이스만 구현해서 DispatcherServlet에 등록하면 나만의 컨트롤러 생성 가능

주요 인터페이스 목록

  • 핸들러 매핑 : org.springframework.web.servlet.HandlerMapping
  • 핸들러 어댑터 : org.springframework.web.servlet.HandlerAdapter
  • 뷰 리졸버 : org.springframework.web.servlet.ViewResolver
  • : org.springframework.web.servlet.View

정리

  • 스프링 MVC는 코드도 많고, 복잡해서 내부 구조 파악이 쉽지 않음
  • 사실 나만의 컨트롤러를 만들일은 없음, 이미 다 만들어져 있음
  • 지금은 전체적인 구조가 어떻게 되어 있는지만 이해하면 된다

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

  • 핸들러 매핑과 핸들러 어댑터 종류로 어떤 것들이 존재하는지 알아보자
  • 지금은 사용 안하지만, 과거에 스프링이 주로 사용한 컨트롤러로 핸들러 매핑과 어댑터를 이해하자
    • @Controller 어노테이션이 나오기 전에 사용한 방식

02-1. Controller 인터페이스

과거 버전 스프링 컨트롤러

// org.springframework.web.servlet.mvc.Controller
public interface Controller {

	ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception;

}
  • 스프링도 처음에는 위와 같이 딱딱한 형식의 컨트롤러를 제공함
  • Controller 인터페이스는 @Controller 어노테이션과는 전혀 무관함

간단하게 구현 해보자

02-1. OldController

package hello.servlet.web.springmvc.old;

import org.springframework.stereotype.Component;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.Controller;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Component("/springmvc/old-controller") // bean의 이름을 /springmvc/old-controller로 지정
public class OldController implements Controller {

    @Override
    public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
        System.out.println("OldController.handleRequest");
        return null;
    }
}
  • @Component
    • 해당 컨트롤러를 /springmvc/old-controller 라는 이름의 스프링 빈으로 등록
    • 빈의 이름으로 URL을 매핑할 것이다

실행

  • http://localhost:8080/springmvc/old-controller
  • OldController.handleRequest가 출력되면 끝이다

02-2. 해당 컨트롤러가 어떻게 호출이 될까?

해당 컨트롤러가 호출되려면 아래 2가지가 필요함

  • HandlerMapping(핸들러 매핑)
    • 핸들러 매핑에서 이 컨트롤러를 찾을 수 있어야 한다
    • 예) 스프링 빈의 이름으로 핸들러를 찾을 수 있는 핸들러 매핑필요
  • HandlerAdapter(핸들러 어댑터)
    • 핸들러 매핑을 통해 찾은 핸들러(컨트롤러)를 실행할 수 있는 핸들러 어댑터가 필요하다
    • 예) Controller 인터페이스를 실행할 수 있는 핸들러 어댑터를 찾고 실행해야 한다
  • 스프링은 이미 필요한 핸들러 매핑과 핸들러 어댑터를 대부분 구현함
    • 개발자가 직접 만들 이유가 없음
  • 스프링 부트가 자동 등록하는 핸들러 매핑과 핸들러 어댑터

HanlderMapping

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

HandlerAdapter

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

핸들러 매핑, 핸들러 어댑터 모두 순서대로 찾고 만약 없으면 다음 순서로 넘어간다

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

  1. HandlerMapping을 순서대로 실행해서, 핸들러를 찾는다
  2. 이 경우 빈 이름으로 핸들러를 찾아야 하기에 이름 그대로 빈 이름으로 핸들러를 찾아주는 BeanNameUrlHandlerMapping(스프링 빈 이름 기반)가 실행에 성공하고 핸들러인 OldController를 반환한다

2. 핸들러 어댑터 조회

  1. HandlerAdapter의 supports() 를 순서대로 호출한다
  2. @Override public boolean supports(Object handler) { return (handler instanceof ControllerV4); }
  3. SimpleControllerHandlerAdapter가 Controller 인터페이스를 지원하므로 대상이 된다

3. 핸들러 어댑터 실행

  1. 디스패처 서블릿이 조회한 SimpleControllerHandlerAdapter를 실행하면서 핸들러 정보도 함께 넘겨준다
  2. SimpleControllerHandlerAdapter는 핸들러인 OldController를 내부에서 실행 후 결과를 반환한다

정리 - OldController 핸들러매핑, 어댑터

  • OldController 를 실행하면서 사용된 객체는 아래와 같다
    • HandlerMapping = BeanNameUrlHandlerMapping
    • HandlerAdapter = SimpleControllerHandlerAdapter

HttpRequestHandler

package hello.servlet.web.springmvc.old;

import org.springframework.stereotype.Component;
import org.springframework.web.HttpRequestHandler;

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

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

    @Override
    public void handleRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        System.out.println("MyHttpRequestHandler.handleRequest");
    }
}

실행

  • http://localhost:8080/springmvc/request-handler
  • 웹 브라우저에 빈 화면 나오고 MyHttpRequestHandler.handleRequest 나오면 성공

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

  1. HandlerMapping을 순서대로 실행, 핸들러를 찾는다
  2. 이 경우 빈 이름으로 핸들러를 찾아야 하기에 이름 그래도 빈 이름으로 핸들러를 찾아주는 BeanNameUrlHandlerMapping이 실행에 성공하고 핸들러인 MyHttpRequestHandler를 반환한다

2. 핸들러 어댑터 조회

  1. HandlerAdapter의 supports() 를 순서대로 호출한다
  2. HttpRequestHandlerAdapter가 HttpRequestHandler 인터페이스를 지원하므로 대상이 된다

3. 핸들러 어댑터 실행

  1. 디스패처 서블릿이 조회한 HttpRequestHandlerAdapter를 실행하면서 핸들러 정보도 함께 넘긴다
  2. HttpRequestHandlerAdapter는 핸들러인 MyHttpRequestHandler를 내부에서 실행하고, 그 결과를 반환

정리 - MyHttpRequestHandler 핸들러매핑, 어댑터

  • MyHttpRequestHandler 를 실행하면서 사용된 객체는 다음과 같다
    • HandlerMapping = BeanNameUrlHandlerMapping
    • HandlerAdapter = HttpRequestHandlerAdapter

@RequestMapping

  • 조금 뒤에 설명하겠지만, 가장 우선순위가 높은 핸들러 매핑과 핸들러 어댑터는 RequestMappingHandlerMapping, RequestMappingHandlerAdapter이다
  • @RequestMapping 의 앞글자를 따서 만든 이름인데, 이것이 바로 지금 스프링에서 주로 사용하는 어노테이션 기반의 컨트롤러를 지원하는 매핑과 어댑터이다.

03. 뷰 리졸버

이번 시간에는 뷰 리졸버에 대해서 자세히 알아본다.

03-1. OldController - View 조회할 수 있도록 변경

package hello.servlet.web.springmvc.old;

import org.springframework.stereotype.Component;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.Controller;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@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");
    }
}
  • View를 사용할 수 있도록 아래 코드를 추가
    • return new ModelAndView(”new-form”);

실행

  • http://localhost:8080/springmvc/old-controller
  • 웹 브라우저에 Whitelabel Error Page 출력 + OldController.handleRequest 이 출력됨
2023-09-28 18:19:12.070 INFO 10328 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet 
: Initializing Servlet 'dispatcherServlet' 2023-09-28 18:19:12.071 
INFO 10328 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet 
: Completed initialization in 1 ms OldController.handleRequest <- 로깅 정보
  • 실행 하면 컨트롤러는 정상 호출, Whitelabel Error Page 404 오류 발생
  • 로그 상에는 OldController.handleRequest 텍스트가 정상 출력되는 것을 확인 할 수 있다
  • 이러한 404 Not Found 오류를 막기위해서는 아래와 같은 설정이 잡혀야 한다

application.properties

# 404 Not Found를 방지하기 위한 Spring Boot 설정
spring.mvc.view.prefix=/WEB-INF/views/
spring.mvc.view.suffix=.jsp

prefix, suffix 등록 필요

SpringBoot는 application.properties에 등록된 설정 정보(spring.mvc.view…)를 기반으로 InternalResourceViewResolver라는 뷰 리졸버를 자동으로 등록한다

  • 뷰 리졸버 - InternalResourceViewResolver
    • 스프링 부트InternalResourceViewResolver 라는 뷰 리졸버를 자동으로 등록한다
    • 이 때 application.properties에 등록한 설정 정보를 사용하여 등록한다
      • spring.mvc.view.prefix
      • spring.mvc.view.suffix
  • 참고로 권장하지는 않지만 전체 경로를 주어도 동작은 한다
    • return new ModelAndView(”/WEB-INF/views/new-form.jsp”);

내부적으로는 SpringBoot가 다음과 같이 처리한다

@ServletComponentScan // 서블릿 스캔 후 자동 등록
@SpringBootApplication
public class ServletApplication {

    public static void main(String[] args) {
        SpringApplication.run(ServletApplication.class, args);
    }
 	
    @Bean
    InternalResourceViewResolver internalResourceViewResolver() {
        return new InternalResourceViewResolver("/WEB-INF/views/", ".jsp");
    }
}
  • 반환 타입 : InternalResourceViewResolver
  • 반환 내용 : new InternalResourceViewResolver("/WEB-INF/views/", ".jsp");

03-2. 뷰 리졸버의 동작 방식

  1. 기존 방식 메커니즘은 동일(설명 중략..)
  2. ModelAndView를 반환하고 해당 정보를 통해 논리적인 이름(”new-form”)으로 viewResolver를 호출한다
  3. 스프링부트는 자동으로 여러 리졸버를 등록한다

뷰 리졸버 종류는 아래 2가지

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

다음으로는 ViewResolver가 호출되는 과정을 알아보자.
핸들러 어댑터의 호출 → view.render() 의 수행과정은 아래와 같다.

1. 핸들러 어댑터 호출

  • 핸들러 어댑터를 통해 new-form 이라는 논리 뷰 이름 획득
  • 핸들러 어댑터를 호출하게 되면 실제 핸들러를 호출한 후에 해당 데이터(Model)과 논리적인 뷰 이름(”new-form”)을 얻게 된다. 후에 해당 논리 이름을 통해 ViewResolver를 호출한다.

2. ViewResolver 호출

  • new-form 이라는 뷰 이름으로 viewResolver를 순서대로(BeanNameView | Internal.. ) 호출
    • BeanNameViewResolver : 빈 이름으로 view를 찾아 반환
    • InternalResourceViewResolver : JSP를 처리할 수 있는 viewResolver 반환
  • BeanNameViewResolver 는 new-form 이라는 빈 이름으로 등록된 뷰를 찾아야 하는데 없음
  • 여기서는 InternalResourceViewResolver 가 호출된다

3. InternalResourceViewResolver

  • 이 뷰 리졸버가 InternalResourceView를 반환

4. 뷰 - InternalResourceView

  • InternalResourceView 는 JSP 처럼 포워드 forward() 를 호출해서 처리할 수 있는 경우 사용
  • 즉, InternalResourceView 는 forward() 방식으로 내부 로직 처리

5. view.render()

view.render()가 호출되고, InternalResourceView 는 forward() 사용해서 JSP 실행

참고 01. InternalResourceViewResolver 는 JSTL lib가 있으면 InternalResourceView를 상속받은 JstlView를 반환. JstlView → JSTL 태그 사용 → 약간의 부가기능 추가.

참고 02. 다른 뷰는 실제 뷰를 랜더링하지만, JSP의 경우 forward() 통해서 해당 JSP로 이동(실행)해야 랜더링이 됨. JSP 제외 나머지 뷰 템플릿은 forward() 과정 없이 바로 랜더링 한다
참고 03. Thymeleaf 뷰 템플릿을 사용하면 ThymeleafViewResolver 를 등록해야함. 최근에는 lib만 추가하면 스프링 부트가 이런 작업 모두 자동화.

04. 스프링 MVC - 시작하기

  • 스프링제공하는 컨트롤러애노테이션 기반으로 동작, 매우 유연하고 실용적
  • 과거에는 자바 언어에 애노테이션이 없었음, 스프링도 처음부터 이런 유연한 컨트롤러 제공한 것은 아님

04-1. @RequestMapping 등장

  1. 스프링애노테이션을 활용한 유연하고 실용적인 컨트롤러를 만들었음
    1. @RequestMapping 애노테이션을 사용하는 컨트롤러
  2. 과거에는 스프링 F/W가 MVC 부분이 약해서 MVC 기술은 스트럿츠 같은 F/W 사용
  3. @RequestMapping 기반 애노테이션 컨트롤러가 등장하면서 스프링의 완승으로 끝남
  • @RequestMapping
    • 위 어노테이션(@RequestMapping)이 있으면 아래 핸들러 매핑, 어댑터 사용
    • 앞서 보았듯이 우선순위가 가장 높은 핸들러 매핑, 어댑터는 다음과 같다
      • @RequestMappingHandlerMapping
      • @RequestMappingHandlerAdapter
  • 이것이 바로 스프링에서 주로 사용하는 애노테이션 기반의 컨트롤러를 지원하는 핸들렁 매핑, 어댑터
  • 실무에서는 99.9% 위 방식의 컨트롤러 사용

이제 본격적으로 애노테이션(@RequestMapping) 기반의 컨트롤러를 만들어본다.
지금까지 만들었던 F/W에서 사용했던 컨트롤러를 @RequestMapping 기반으로 변경한다.

04-2. SpringMemberFormControllerV1 - 회원 등록 폼

package hello.servlet.web.springmvc.v1;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;

@Controller
public class SpringMemberFormControllerV1 {

    @RequestMapping("/springmvc/v1/members/new-form")
    public ModelAndView process() {
        return new ModelAndView("new-form");
    }
}
  • @Controller
    • 스프링자동으로 스프링 빈으로 등록 ( 내부에 @Component 어노테이션 존재, 자동 컴포넌트 스캔 )
    • 스프링 MVC에서 어노테이션 기반 컨트롤러인식
      • RequestMappingHandlerMapping에서 꺼내 사용할 수 있는 대상이 되도록 표기
  • @RequestMapping
    • 요청 정보 매핑하고, 해당 URL이 호출되면 해당 메서드가 호출 되도록 지정
    • 어노테이션 기반으로 동작하기에 메서드 이름을 임의로 지으면 됨
  • RequestMappingHandlerMapping스프링 빈 중에서 @RequestMapping 또는 @Controller클래스 레벨에 붙어 있는 경우 매핑 정보로 인식한다

RequestMappingHandlerMapping isHandler 함수

/**
 * {@inheritDoc}
 * <p>Expects a handler to have either a type-level @{@link Controller}
 * annotation or a type-level @{@link RequestMapping} annotation.
 */
@Override
protected boolean isHandler(Class<?> beanType) {
	return (AnnotatedElementUtils.hasAnnotation(beanType, Controller.class) ||
			AnnotatedElementUtils.hasAnnotation(beanType, RequestMapping.class));
}
  • 클래스 레벨에 Controller, RequestMapping이 붙어있는 경우만 인식

또한 다음과 같은 코드로 변경하여도 동일하게 동작한다.

@Component와 @RequestMapping 클래스 레벨에서 사용

@Component
@RequestMapping
public class SpringMemberFormControllerV1 {

    @RequestMapping("/springmvc/v1/members/new-form")
    public ModelAndView process() {
        return new ModelAndView("new-form");
    }
}
  • @Component + @RequestMapping 어노테이션 사용

@RequestMapping은 클래스 레벨 + Bean 수동 등록

@RequestMapping
public class SpringMemberFormControllerV1 {

    @RequestMapping("/springmvc/v1/members/new-form")
    public ModelAndView process() {
        return new ModelAndView("new-form");
    }
}
@Bean
TestController testController() {
    return new TestController();
}
  • @Component 어노테이션 제거 후 직접 Bean 등록

@Bean vs @Component

@Bean @Component
메서드 레벨에 사용 클래스 레벨에 사용
개발자가 컨트롤 불가능한 외부 라이브러리 사용시 사용
(ex. PasswordEncoder by Security)
개발자가 직접 컨트롤이 가능한 내부 클래스에 사용
수동 빈 등록 자동 빈 등록

04-3. SpringMemberSaveControllerV1

@Controller
public class SpringMemberSaveControllerV1 {

    private MemberRepository memberRepository = MemberRepository.getInstance();

    @RequestMapping("/springmvc/v1/members/save")
    public ModelAndView process(HttpServletRequest request, HttpServletResponse response) {
        String username = request.getParameter("username");
        int age = Integer.parseInt(request.getParameter("age"));

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

        ModelAndView mv = new ModelAndView("save-result");
        mv.addObject("member", member);
        return mv;
    }
}

04-4. SpringMemberListControllerV1

import java.util.List;

@Controller
public class SpringMemberListControllerV1 {

    private MemberRepository memberRepository = MemberRepository.getInstance();

    @RequestMapping("/springmvc/v1/members")
    public ModelAndView process() {
        List<Member> members = memberRepository.findAll();

        ModelAndView mv = new ModelAndView("members");
        mv.addObject("members", members);
        return mv;
    }
}

05. 스프링 MVC - 컨트롤러 통합

@RequestMapping을 보면 클래스 단위가 아니라 메서드 단위에 적용된 것을 확인 가능.
따라서 컨트롤러 클래스를 유연하게 하나로 통합 가능하다.

결론은 클래스 레벨에 @RequestMapping 선언하여 공통 URL 통합 시키는 부분

05-1. SpringMemberControllerV2

package hello.servlet.web.springmvc.v2;

...

@Controller
public class SpringMemberControllerV2 {

    private MemberRepository memberRepository = MemberRepository.getInstance();

    @RequestMapping("/springmvc/v2/members/new-form")
    public ModelAndView newForm() {
        return new ModelAndView("new-form");
    }

    @RequestMapping("/springmvc/v2/members/save")
    public ModelAndView save(HttpServletRequest request, HttpServletResponse response) {
        String username = request.getParameter("username");
        int age = Integer.parseInt(request.getParameter("age"));

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

        ModelAndView mv = new ModelAndView("save-result");
        mv.addObject("member", member);
        return mv;
    }

    @RequestMapping("/springmvc/v2/members")
    public ModelAndView members() {
        List<Member> members = memberRepository.findAll();

        ModelAndView mv = new ModelAndView("members");
        mv.addObject("members", members);
        return mv;
    }

}
  • @RequestMapping은 메서드 단위로 수행이 가능하기에 연관성을 가지는 API를 하나의 컨트롤러에 구성 가능
  • 하지만 현재 중복이 발생한다. 이러한 코드는 아래와 같이 수정이 가능하다
    • /springmvc/v2/members
@Controller
@RequestMapping("/springmvc/v2/members")
public class SpringMemberControllerV2 {

    private MemberRepository memberRepository = MemberRepository.getInstance();

    @RequestMapping("/new-form")
    public ModelAndView newForm() {
        return new ModelAndView("new-form");
    }

    @RequestMapping("/save")
    public ModelAndView save(HttpServletRequest request, HttpServletResponse response) {
        String username = request.getParameter("username");
        int age = Integer.parseInt(request.getParameter("age"));

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

        ModelAndView mv = new ModelAndView("save-result");
        mv.addObject("member", member);
        return mv;
    }

    @RequestMapping
    public ModelAndView members() {
        List<Member> members = memberRepository.findAll();

        ModelAndView mv = new ModelAndView("members");
        mv.addObject("members", members);
        return mv;
    }
}
  • 기존 중복된 URL 매핑을 메서드 레벨에서 제거하고 클래스 레벨에 지정한다
    • @RequestMapping("/springmvc/v2/members")

06. 스프링 MVC - 실용적인 방식

  • 스프링 MVC는 개발자가 편리하게 개발할 수 있도록 많은 기능을 제공

06-1. SpringMemberControllerV3

package hello.servlet.web.springmvc.v3;

...

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

    private MemberRepository memberRepository = MemberRepository.getInstance();

    @RequestMapping("/new-form")
    public String newForm() {
        return "new-form";
    }

    @RequestMapping("/save")
    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
    public String members(Model model) {
        List<Member> members = memberRepository.findAll();
        model.addAttribute("members", members);
        return "members";
    }

}
  • 스프링 MVC는 ModelAndView를 반환해도 되고, String(문자 view 이름)을 반환해도 된다
  • 여기서는 ModelAndView를 반환 값으로 사용하지 않고 String을 반환 값으로 사용 하면서 Model 객체를 매개변수 값으로 받아서 해당 Model에 값을 등록하여 사용자에게 전달하는 방식을 사용한다

06-2. SpringMemberControllerV3 - Method 지정

@RequestMapping(value = "/new-form", method = RequestMethod.GET)
public String newForm() {
    return "new-form";
}
  • 위와 같이 특정 Method를 지정하여 처리하는 것이 가능
  • 하지만 위와 같은 방식도 일일이 지정을 해야 한다는 불편함이 있음

06-3. GetMapping, PostMapping.. 사용

@GetMapping(value = "/new-form")
public String newForm() {
    return "new-form";
}

@PostMapping(value = "/save")
  • 위와 같이 어노테이션 기반도 사용 가능

06-4. 부가 기능 정리

Model 파라미터 사용

@RequestMapping("/save")
    public String save(
            @RequestParam("username") String username,
            @RequestParam("age") int age,
            Model model) { 
		...중략
}
  • Model을 파라미터로 받는 것을 확인 가능한데, 스프링 MVC도 이런 편의 기능 제공

ViewName 직접 반환 가능

@GetMapping(value = "/new-form")
public String newForm() {
    return "new-form";
}
  • ModelAndView 사용 안하고 ViewName 직접 반환 가능

@RequestParam

  • 스프링은 HTTP 요청 파라미터를 @RequestParam 로 받을 수 있음
  • request.getParameter(”username”) 과 거의 같은 코드라 보면 됨
  • GET query parameter, POST Form 방식 둘다 지원

@RequestMapping → GetMapping, PostMapping

  • HTTP Method 변환 가능

블로그의 정보

기록하고, 복기하고

ymkim

활동하기