
이제 스프링 MVC의 구조에 대해 알아보자.

보면 알겠지만 이름만 다르지, 전체 구조가 완전히 동일하다. 스프링 MVC도 프론트 컨트롤러 패턴으로 구현되어 있다. 스프링 MVC의 프론트 컨트롤러가 바로 이 DispatcherServlet인 것이다.
기존에 만들었던 MVC 프레임워크와 비교했을 때 변화된 점을 요약하면…
FrontController → DispatcherServlethandlerMappingMap → HandlerMappingMyHandlerAdapter → HandlerAdapterModelView → ModelAndViewviewResolver → ViewResolverMyView → View
DispatcherServlet은 org.springframework.web.servlet.DispatcherServlet 경로에 살고 있다. DispatcherServlet도 결국 서블릿이다.
DispatcherServlet은 FrameworkServlet이라는 걸 상속받고 있는데, 구조를 보면…

올라가다 보면, 익숙한 친구가 보인다. HttpServlet이 딱 버티고 있다. 결국 DispatcherServlet이 HttpServlet을 가지고 있기 때문에 DispatcherServlet도 서블릿으로 동작한다. 스프링 부트는 DispatcherServlet을 서블릿으로 자동 등록하면서 모든 경로에 대해 매핑한다.
애플리케이션 실행 시 DispatcherServlet 서블릿이 호출되면, HttpServlet이 제공하는 service() 메서드가 호출된다. 스프링 MVC는 DispatcherServlet의 부모인 FrameworkServlet에서 service()를 오버라이드 해뒀다.

위의 FrameworkServlet.service()를 시작으로 여러 메서드가 호출되면서 DispatcherServlet.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 var20) {
Exception ex = var20;
dispatchException = ex;
} catch (Throwable var21) {
Throwable err = var21;
dispatchException = new ServletException("Handler dispatch failed: " + String.valueOf(err), err);
}
this.processDispatchResult(processedRequest, response, mappedHandler, mv, (Exception)dispatchException);
} catch (Exception var22) {
Exception ex = var22;
triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
} catch (Throwable var23) {
Throwable err = var23;
triggerAfterCompletion(processedRequest, response, mappedHandler, new ServletException("Handler processing failed: " + String.valueOf(err), err));
}
} finally {
if (asyncManager.isConcurrentHandlingStarted()) {
if (mappedHandler != null) {
mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
}
asyncManager.setMultipartRequestParsed(multipartRequestParsed);
} else if (multipartRequestParsed || asyncManager.isMultipartRequestParsed()) {
this.cleanupMultipart(processedRequest);
}
}
}
얘가 request, response를 받으면서 오는데, 코드를 보면 mappedHandler = this.getHandler(processedRequest); 부분의 getHandler()... 여기서 핸들러(컨트롤러)를 꺼낸다. 핸들러가 없어? SC_NOT_FOUND로 404 띄우고 종료되도록 한 부분도 똑같다.
그 다음, 핸들러를 가져와서 등록된 핸들러 어댑터 목록 중에서 해당 핸들러 규약을 지원하는 어댑터를 고르는 부분도 동일하다.
protected HandlerAdapter getHandlerAdapter(Object handler) throws ServletException {
if (this.handlerAdapters != null) {
Iterator var2 = this.handlerAdapters.iterator();
while(var2.hasNext()) {
HandlerAdapter adapter = (HandlerAdapter)var2.next();
if (adapter.supports(handler)) {
return adapter;
}
}
}
throw new ServletException("No adapter for handler [" + String.valueOf(handler) + "]: The DispatcherServlet configuration needs to include a HandlerAdapter that supports this handler");
}
그리고 아래 부분을 보면, 진짜 핸들러(컨트롤러)를 호출해서 ModelAndView를 받고 적용하는 것을 확인할 수 있다.
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
if (asyncManager.isConcurrentHandlingStarted()) {
return;
}
this.applyDefaultViewName(processedRequest, mv);
mappedHandler.applyPostHandle(processedRequest, response, mv);
doDispatch() 메서드의 로직을 정리해보자면, 먼저 핸들러를 조회하고, 그 핸들러를 처리할 수 있는 핸들러 어댑터를 조회한다. 그리고 핸들러 어댑터를 실행해서 핸들러를 실행하고 마지막에는 ModelAndView를 반환한다.
this.processDispatchResult(processedRequest, response, mappedHandler, mv, (Exception)dispatchException);
그리고 그 다음에 processDispatchResult()가 호출되는데, 이게 뭐냐하면…
private void processDispatchResult(HttpServletRequest request, HttpServletResponse response, @Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv, @Nullable Exception exception) throws Exception {
boolean errorView = false;
if (exception != null) {
if (exception instanceof ModelAndViewDefiningException) {
ModelAndViewDefiningException mavDefiningException = (ModelAndViewDefiningException)exception;
this.logger.debug("ModelAndViewDefiningException encountered", exception);
mv = mavDefiningException.getModelAndView();
} else {
Object handler = mappedHandler != null ? mappedHandler.getHandler() : null;
mv = this.processHandlerException(request, response, handler, exception);
errorView = mv != null;
}
}
if (mv != null && !mv.wasCleared()) {
this.render(mv, request, response); // render() 호출
if (errorView) {
WebUtils.clearErrorRequestAttributes(request);
}
} else if (this.logger.isTraceEnabled()) {
this.logger.trace("No view rendering, null ModelAndView returned.");
}
if (!WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
if (mappedHandler != null) {
mappedHandler.triggerAfterCompletion(request, response, (Exception)null);
}
}
}
render()를 호출하고 있다. render() 코드를 자세히 뜯어보자.
protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception {
Locale locale = this.localeResolver != null ? this.localeResolver.resolveLocale(request) : request.getLocale();
response.setLocale(locale);
String viewName = mv.getViewName(); // 1. 논리 이름 가져오기
String var10002;
View view;
if (viewName != null) {
// 2. 물리 이름으로 변환
view = this.resolveViewName(viewName, mv.getModelInternal(), locale, request);
if (view == null) {
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) {
var10002 = String.valueOf(mv);
throw new ServletException("ModelAndView [" + var10002 + "] neither contains a view name nor a View object in servlet with name '" + this.getServletName() + "'");
}
}
if (view instanceof SmartView smartView) {
smartView.resolveNestedViews(this::resolveViewNameInternal, locale);
}
if (this.logger.isTraceEnabled()) {
this.logger.trace("Rendering view [" + String.valueOf(view) + "] ");
}
try {
if (mv.getStatus() != null) {
request.setAttribute(View.RESPONSE_STATUS_ATTRIBUTE, mv.getStatus());
response.setStatus(mv.getStatus().value());
}
// 3. 실제 뷰를 렌더링한다.
view.render(mv.getModelInternal(), request, response);
} catch (Exception var8) {
Exception ex = var8;
if (this.logger.isDebugEnabled()) {
this.logger.debug("Error rendering view [" + String.valueOf(view) + "]", ex);
}
throw ex;
}
}
정리하자면, 컨트롤러가 돌려준 ModelAndView에서 논리 뷰 이름을 꺼낸다. 이 논리 이름은 등록된 뷰 리졸버들을 통해 물리 뷰(예: JSP의 실제 경로)로 해석된다. 만약 뷰 이름 대신 뷰 객체가 직접 들어있다면 그 객체를 그대로 사용한다. 상태 코드가 지정돼 있다면 응답에 반영한 뒤, 최종적으로 view.render(model, request, response)가 실행된다. JSP라면 보통 모델 데이터를 request 속성에 옮겨 담고 RequestDispatcher.forward(...)로 서버 내부 포워드가 일어나며, 그 결과 HTML이 만들어져 클라이언트로 반환된다.
자, 정말 중요한 내용들이다. 다시 한번 스프링 MVC 구조 흐름에 대해 상기해보고 넘어가자.

핸들러 조회: 핸들러 매핑을 통해 요청 URL에 매핑된 핸들러(컨트롤러)를 조회한다.
핸들러 어댑터 조회: 핸들러를 실행할 수 있는 핸들러 어댑터를 조회한다.
핸들러 어댑터 실행: 핸들러 어댑터를 실행한다.
핸들러 실행: 핸들러 어댑터가 실제 핸들러를 실행한다.
ModelAndView 반환: 핸들러 어댑터는 핸들러가 반환하는 정보를 ModelAndView로 변환해서 반환한다.
viewResolver 호출: 뷰 리졸버를 찾고 실행한다.
InternalResourceViewResolver가 자동으로 등록되고 사용된다.View 반환: 뷰 리졸버는 뷰의 논리 이름을 물리 이름으로 바꾸고, 렌더링 역할을 담당하는 뷰 객체를 반환한다.
InternalResourceView(JstlView)를 반환하는데, 내부에 forward() 로직이 있다.뷰 렌더링: 뷰를 통해서 뷰를 렌더링한다.
스프링 MVC의 가장 큰 강점은 DispatcherServlet 코드의 변경 없이 원하는 기능을 변경하거나 확장할 수 있다는 것이다. 이 인터페이스들만 구현해서 DispatcherServlet에 등록하면 입맛에 맞는 나만의 컨트롤러를 만들 수도 있다.
<주요 인터페이스 목록>
핸들러 매핑: org.springframework.web.servlet.HandlerMapping
핸들러 어댑터: org.springframework.web.servlet.HandlerAdapter
뷰 리졸버: org.springframework.web.servlet.ViewResolver
뷰: org.springframework.web.servlet.View
지금은 전혀 사용되지 않지만, 과거에 주로 사용되었던 스프링이 제공하는 핸들러 매핑과 핸들러 어댑터가 어떤 것들이 있는지 자세히 알아보자. 일단 스프링은 Controller라는 인터페이스가 있다. 얘는 @Controller랑은 전혀 다른 애다.
@FunctionalInterface
public interface Controller {
@Nullable
ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception;
}
간단하게 구현해보자면…
package hello.servlet.web.springmvc.old;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.Controller;
@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 null;
}
}
컨트롤러를 “/springmvc/old-controller” 라는 이름의 스프링 빈으로 등록했다. 그럼 빈의 이름으로 URL을 매핑할 것이다. 한번 실행해보자.

이 OldController... 어떻게 호출된거지? 먼저 “localhost:8080/springmvc/old-controller” 를 호출하면 HTTP 요청이 들어간다. 그럼 일단 핸들러 매핑에서 OldController를 찾아와야 한다. 이 컨트롤러가 호출되려면 2가지가 필요한데, 일단 핸들러 매핑에서 이 컨트롤러를 찾을 수 있어야 한다. 무슨 말이냐면 스프링 빈의 이름으로 핸들러를 찾을 수 있는 핸들러 매핑이 필요하다는 것이다. 그리고 OldController를 찾아오면 인터페이스 Controller를 실행할 수 있는 핸들러 어댑터를 찾고 실행해야 한다.
스프링은 이미 필요한 핸들러 매핑과 핸들러 어댑터를 대부분 구현해뒀다. 내가 직접 핸들러 매핑이나 핸들러 어댑터를 만들 일이 거의 없다. 스프링 부트를 사용하면 자동으로 핸들러 매핑과 어댑터를 여러 가지 등록해준다.
<대표적인 핸들러 매핑>
RequestMappingHandlerMapping: 애노테이션 기반의 컨트롤러인 @RequsetMapping에서 사용BeanNameUrlHandlerMapping: 스프링 빈의 이름으로 핸들러를 찾는다.
<대표적인 핸들러 어댑터>
RequestMappingHandlerAdapter: 애노테이션 기반의 컨트롤러인 @RequestMapping에서 사용HttpRequestHandlerAdapter: HttpRequestHandler 처리SimpleControllerHandlerAdapter: Controller 인터페이스 처리
위의 OldController 컨트롤러를 설명해보자면, 먼저 핸들러 매핑으로 핸들러를 조회한다고 했다. 지금 URL이 빈 이름으로 등록되어 있기 때문에 말 그대로 빈 이름으로 핸들러를 찾아주는 BeanNameUrlHandlerMapping가 실행되고, OldController가 반환되는 것이다. 그리고 HandlerAdapter의 supports() 메서드가 호출될 텐데, SimpleControllerHandlerAdapter가 Controller 인터페이스를 지원하므로 대상이 된다. DispatcherServlet이 조회한 SimpleControllerHandlerAdapter를 실행하면서 핸들러 정보도 함께 넘겨준다. 마지막으로 SimpleControllerHandlerAdapter는 핸들러인 OldController를 내부에서 실행하고, 그 결과를 반환하는 것이다.
이번에는 다른 핸들러로 연습해보자.
@FunctionalInterface
public interface HttpRequestHandler {
void handleRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException;
}
HttpRequestHandler를 구현해보면,
package hello.servlet.web.springmvc.old;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.HttpRequestHandler;
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");
}
}

지금 MyHttpRequestHandler도 URL이 빈 이름으로 등록되어 있기 때문에 BeanNameUrlHandlerMapping가 실행에 성공하고 MyHttpRequestHandler를 반환하게 된다. 그리고 HttpRequestHandlerAdapter가 HttpRequestHandler 인터페이스를 지원하므로 대상이 된다. 그럼 HttpRequestHandlerAdapter를 실행하면서 핸들러 정보도 함께 넘겨준다. 마지막으로 내부에서 MyHttpRequestHandler를 실행하고 결과를 반환해주는 것이다.
가장 우선순위가 높은 핸들러 매핑과 핸들러 어댑터는 RequestMappingHandlerMapping, RequestMappingHandlerAdapter다. @RequestMapping의 앞글자를 따서 만든 이름인데, 이게 바로 지금 스프링에서 주로 사용하는 애노테이션 기반의 컨트롤러를 지원하는 매핑과 어댑터다. 실무에서는 99.9%의 확률로 이 방식의 컨트롤러를 사용한다.
이번에는 뷰 리졸버에 대해 알아보자.
package hello.servlet.web.springmvc.old;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.Controller;
@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");
}
}
뷰를 사용할 수 있도록 위와 같이 폼을 넣어서 반환하도록 했다. 하지만, 이대로 실행해버리면 웹 브라우저는 오류가 발생한다. 왜냐하면 스프링 부트는 InternalResourceViewResolver라는 뷰 리졸버를 자동으로 등록하는데, 이때 application.properties에 등록한 spring.mvc.view.prefix와 spring.mvc.view.suffix 설정 정보를 사용해서 등록하기 때문이다.
spring.mvc.view.prefix=/WEB-INF/views/
spring.mvc.view.suffix=.jsp
application.properties 파일에 위 내용을 추가해주면 입력 폼이 정상적으로 화면에 출력된다. 이제 뷰 리졸버의 동작 방식에 대해 살펴보자.

일단 핸들러 어댑터를 호출해서 new-form이라는 논리 이름을 뽑아낸다. 그럼 이 논리 이름을 가지고 뷰 이름으로 viewResolver를 순서대로 호출한다. BeanNameViewResolver는 new-form이라는 이름을 가진 스프링 빈으로 등록된 뷰를 찾아야 하는데 없다. 그래서 그 다음 우선순위인 InternalResourceViewResolver가 호출되는 것이다. 이 뷰 리졸버는 InternalResourceView를 반환한다. 그럼 InternalResourceView는 JSP처럼 forward()를 호출해서 처리할 수 있는 경우에 사용한다. 결국 view.render()가 호출되고 InternalResourceView는 forward()를 사용해서 JSP를 실행하는 것이다.
기존에 만들었던 회원 관리 애플리케이션을 실제 스프링 MVC를 활용해서 만들어보자.
스프링이 제공하는 컨트롤러는 애노테이션 기반으로 동작한다. 스프링은 애노테이션을 활용해서 매우 유연하고, 실용적인 컨트롤러를 만들었는데, 이게 바로 @RequestMapping 애노테이션을 사용하는 컨트롤러다.
// 회원 등록 폼
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 애노테이션이 붙으면 스프링이 자동으로 스프링 빈으로 등록한다. 그리고 @RequestMapping을 통해 요청 정보를 매핑한다. “/springmvc/v1/members/new-form” 주소가 호출되면 process() 메서드가 호출되는 것이다. 메서드가 호출되면 ModelAndView를 반환하는 형태다.
RequestMappingHandlerMapping은 스프링 빈 중에서 @RequestMapping 또는 @Controller가 클래스 레벨에 붙어 있는 경우에 매핑 정보로 인식한다. 따라서 아래 코드도 동일하게 동작한다.
// 회원 등록 컨트롤러
package hello.servlet.web.springmvc.v1;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;
@Component
@RequestMapping
public class SpringMemberFormControllerV1 {
@RequestMapping("/springmvc/v1/members/new-form")
public ModelAndView process() {
return new ModelAndView("new-form");
}
}
스프링 부트 3.0 이상부터는 클래스 레벨에 @RequestMapping이 붙어 있어도 스프링 컨트롤러로 인식하지 않는다. 오직 @Controller가 있어야만 스프링 컨트롤러로 인식한다. 그리고 @RestController는 해당 애노테이션 내부에 @Controller를 포함하고 있어서 인식이 가능하다. 따라서 위와 같이 @Controller가 붙어 있지 않는 코드는 스프링 컨트롤러로 인식되지 않는다.
이제 마저 나머지 컨트롤러도 추가해보자.
// 회원 저장 컨트롤러
package hello.servlet.web.springmvc.v1;
import hello.servlet.domain.member.Member;
import hello.servlet.domain.member.MemberRepository;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;
@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;
}
}
mv.addObject("member", member)를 보면, 스프링이 제공하는 ModelAndView를 통해 모델 데이터를 추가할 때는 addObject()를 사용하면 된다. 이 데이터는 이후에 뷰를 렌더링 할 때 사용된다.
// 회원 목록 컨트롤러
package hello.servlet.web.springmvc.v1;
import hello.servlet.domain.member.Member;
import hello.servlet.domain.member.MemberRepository;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;
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;
}
}
위에서는 @RequestMapping을 보면 메서드 레벨에 붙어 있는 것을 볼 수 있다. 이제 아래 코드와 같이 리팩토링 해보자.
package hello.servlet.web.springmvc.v2;
import hello.servlet.domain.member.Member;
import hello.servlet.domain.member.MemberRepository;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;
import java.util.List;
@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;
}
}
이번엔 클래스 레벨에 @RequestMapping("/springmvc/v2/members")를 작성하고, 나머지 메서드 레벨에서 @RequestMapping을 두고 조합하는 것도 가능하다. 이게 중복도 제거되고 훨씬 깔끔해보인다.
실무에서는 이제부터 작성할 코드처럼 작성하면 된다.
package hello.servlet.web.springmvc.v3;
import hello.servlet.domain.member.Member;
import hello.servlet.domain.member.MemberRepository;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@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(value = "/save", method = RequestMethod.POST)
@PostMapping("/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(method = RequestMethod.GET)
@GetMapping()
public String members(Model model) {
List<Member> members = memberRepository.findAll();
model.addAttribute("members", members);
return "members";
}
}
지금 save()나 members() 메서드를 보면, Model을 파라미터로 받고 있다. 이러면 스프링이 제공하는 모델을 그대로 사용할 수 있다. 그리고 String 타입으로 ViewName을 직접 반환해서 로직을 더 깔끔하게 가져갈 수 있다. @RequestParam 애노테이션을 활용해서 요청 파라미터를 받을 수 있다. 그냥 @RequestParam("username")이라고 하면, request.getParameter("username")과 같은 코드라고 생각해도 무방하다. 마지막으로 @RequestMapping 애노테이션은 보다시피 URL만 매칭하는 것이 아니라 HTTP 메서드도 제한할 수 있다. 물론 이 애노테이션을 사용하기 보다는 각 메서드에 맞는 애노테이션, @GetMapping이나 @PostMapping 등이 있으니 그걸 사용하도록 하자.