'스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술' 수업을 듣고 정리한 내용입니다.
이전 챕터에서 직접 만든
MVC 프레임워크
와스프링 MVC
을 비교해보자❗️
직접 만든 MVC 프레임워크 구조
SpringMVC 구조
FrontController
라 부르던 서블릿은 DispatcherServlet
으로 쓰이고 있고, handlerMappingMap
은 HandlerMapping
으로 이름만 달라졌고 사용처는 거의 똑같다.
비교를 하면 이와 같다.
직접 만든 MVC 프레임워크 → 스프링 MVC 비교
FrontController
→DispatcherServlet
handlerMappingMap
→HandlerMapping
MyHandlerAdapter
→HandlerAdapter
ModelView
→ModelAndView
viewResolver
→ViewResolver
MyView
→View
스프링 MVC를 공부할 때 해당 직접 만들었던 키워드를 보며 역할들을 이해하면 된다.
org.springframework.web.servlet.DispatcherServlet
- 스프링 MVC도 프론트 컨트롤러 패턴으로 구현되어 있다.
- 스프링 MVC의 프론트 컨트롤러가 바로 디스패처 서블릿(DispatcherServlet)이다.
그리고 이 디스패처(DispatcherServlet) 서블릿이 바로 스프링 MVC의 핵심이다.
(1) DispatcherServlet 서블릿 등록
DispatcherServlet
도 부모 클래스에서HttpServlet
을 상속 받아서 사용하고, 서블릿으로 동작한다.
DispatcherServlet
➡️FrameworkServlet
➡️
HttpServletBean
➡️HttpServlet
- 스프링 부트는
DispatcherServlet
을 서블릿으로 자동으로 등록하면서 모든 경로urlPatterns="/"
에 대해서 매핑한다.- 즉, SpringMVC 역시 프론트 컨트롤러 패턴으로 구현되어 있고, DispatcherServlet이 프론트 컨트롤러의 역할을 한다고 생각하면 된다.
참고 : 경로의 우선순위도 자세할수록 높다. 그러므로 기존에 등록한 서블릿도 함께 동작한다.
DispatcherServlet - diagram
DispatcherServlet
을 보면 결국 HttpServlet
을 상속받아 사용한다는 것을 알 수 있다.
(2) 요청 흐름
- (a) 서블릿이 호출되면
HttpServlet
이 제공하는service()
가 호출된다.- (b) 스프링 MVC는
DispatcherServlet
의 부모인FrameworkServlet
에서service()
를 오버라이드 해두었다.- (c)
FrameworkServlet.service()
를 시작으로 여러 메서드가 호출되면서DispatcherServlet.doDispatch()
가 호출된다.
지금부터
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 (mappedHandler == null) {
noHandlerFound(processedRequest, response);
return;
}
//2.핸들러 어댑터 조회 - 핸들러를 처리할 수 있는 어댑터
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
/**
* 3. 핸들러 어댑터 실행
* -> 4. 핸들러 어댑터를 통해 핸들러 실행
* -> 5. ModelAndView 반환
*/
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
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);
}
mappedHandler = getHandler(processedRequest);
noHandlerFound(processedRequest, response);
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
SpringMVC 구조
🔔 동작 순서
1. 핸들러 조회 : 핸들러 매핑을 통해 요청 URL에 매핑된 핸들러(컨트롤러)를 조회한다.
2. 핸들러 어댑터 조회 : 핸들러를 실행할 수 있는 핸들러 어댑터를 조회한다.
3. 핸들러 어댑터 실행 : 핸들러 어댑터를 실행한다.
4. 핸들러 실행 : 핸들러 어댑터가 실제 핸들러를 실행한다.
5. ModelAndView 반환 : 핸들러 어댑터는 핸들러가 반환하는 정보를 ModelAndView로 변환해서 반환한다.
6. viewResolver 호출 : 뷰 리졸버를 찾고 실행한다.
➡️ JSP의 경우 :InternalResourceViewResolver
가 자동 등록되고, 사용된다.7. View반환 : 뷰 리졸버는 뷰의 논리 이름을 물리 이름으로 바꾸고, 렌더링 역할을 담당하는 뷰 객체를 반환한다.
➡️ 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는 코드 분량도 매우 많고, 복잡해서 내부 구조를 다 파악하는 것은 쉽지 않다.
사실 해당 기능을 직접 확장하거나 나만의 컨트롤러를 만드는 일은 없으므로 걱정하지 않아도 된다.
왜냐하면 스프링 MVC는 전세계 수 많은 개발자들의 요구사항에 맞추어 기능을 계속 확장왔고, 그래서 여러분이 웹 애플리케이션을 만들 때 필요로 하는 대부분의 기능이 이미 다 구현되어 있다.
그래도 이렇게 핵심 동작방식을 알아두어야 향후 문제가 발생했을 때 어떤 부분에서 문제가 발생했는지 쉽게 파악하고, 문제를 해결할 수 있다.
그리고 확장 포인트가 필요할 때, 어떤 부분을 확장해야 할지 감을 잡을 수 있다. 실제 다른 컴포넌트를 제공하거나 기능을 확장하는 부분들은 강의를 진행하면서 조금씩 설명하겠다.
지금은 전체적인 구조가 이렇게 되어 있구나 하고 이해하면 된다.
우리가 지금까지 함께 개발한 MVC 프레임워크와 유사한 구조여서 이해하기 어렵지 않았을 것이다.
- 이전까지 핸들러 매핑과 핸들러 어댑터를 단순하게
Map
,List
콜렉션을 이용해서 등록한 뒤 검색해서 사용을 하였다.- springMVC에서는 어떻게 핸들러 매핑과 핸들러 어댑터를 사용하는지 알아보자.
- 지금은 전혀 사용되지 않지만, 과거에 주로 사용했던 스프링이 제공하는 간단한 컨트롤러로 핸들러 매핑과 어댑터를 이해해보자!
과거 버전 스프링 컨트롤러 인터페이스
org.springframework.web.servlet.mvc.Controller
public interface Controller {
ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse
response) throws Exception;
}
스프링도 처음에는 이런 딱딱한 형식의 컨트롤러를 제공했다.
📝 참고
Controller
인터페이스는@Controller
애노테이션과는 전혀 다르다.
Controller 인터페이스를 구현하는 OldController
main.java.hello.web.springmvc.old.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")
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"
/springmvc/old-controller
라는 이름의 스프링 빈으로 등록되었다.
실행
http://localhost:8080/springmvc/old-controller
OldController.handleRequest
이 출력되면 성공이다.
스프링 MVC 구조
💡 이 컨트롤러가 호출되기 위해서는 2가지가 필요하다!
(1) HandlerMapping(핸들러 매핑)
- 핸들러 매핑에서 이 컨트롤러를 찾을 수 있어야 한다.
- 스프링 빈의 이름으로 핸들러를 찾을 수 있는 핸들러 매핑이 필요하다.
(2) HandlerAdapter(핸들러 어댑터)
- 핸들러 매핑을 통해서 찾은 핸들러를 실행할 수 있는 핸들러 어댑터가 필요하다.
Controller
인터페이스를 실행할 수 있는 핸들러 어댑터를 찾고 실행해야 한다.
스프링 부트에서는 자동으로 핸들러 매핑과 핸들러 어댑터를 등록해준다.
(직접 핸들러 매핑과 핸들러 어댑터를 만드는 일은 거의 없다.)
0 = RequestMappingHandlerMapping : 애노테이션 기반의 컨트롤러인 @RequestMapping에서 사용한다.
1 = BeanNameUrlHandlerMapping : 스프링 빈의 이름으로 핸들러를 찾는다.
0 = RequestMappingHandlerAdapter : 애노테이션 기반의 컨트롤러인 @RequestMapping에서 사용한다.
1 = HttpRequestHandlerAdapter : HttpRequestHandler 처리
2 = SimpleControllerHandlerAdapter : Controller 인터페이스(애노테이션X, 과거에 사용) 처리
1. 핸들러 매핑으로 핸들러 조회
(1)HandlerMapping
을 순서대로 실행해서, 핸들러를 찾는다.
(2) 이 경우 빈 이름으로 핸들러를 찾아야 하기 때문에 이름 그대로 빈 이름으로 핸들러를 찾아주는BeanNameUrlHandlerMapping
가 실행에 성공하고 핸들러인OldController
를 반환한다.
2. 핸들러 어댑터 조회
(1)HandlerAdapter
의supports()
를 순서대로 호출한다.
(2) 이전에 찾은 핸들러(OldController
)가 실행가능한 어댑터를 찾는다.
(3) 이 중에서SimpleControllerHandlerAdapter
가Controller
인터페이스를 지원하므로 대상이 된다.
3. 핸들러 어댑터 실행
(1) 디스패처 서블릿이 조회한SimpleControllerHandlerAdapter
를 실행하면서 핸들러 정보도 함께 넘겨준다.
(2)SimpleControllerHandlerAdapter
는 핸들러인OldController
를 내부에서 실행하고, 그 결과를 반환한다.
📌 정리 - OldController 핸들러매핑, 어댑터
OldController
를 실행하면서 사용된 객체는 다음과 같다.HandlerMapping = BeanNameUrlHandlerMapping
HandlerAdapter = SimpleControllerHandlerAdapter
핸들러 매핑과, 어댑터를 더 잘 이해하기 위해
Controller
인터페이스가 아닌 다른 핸들러를 알아보자.
HttpRequestHandler
핸들러(컨트롤러)는 서블릿과 가장 유사한 형태의 핸들러이다.
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
- 가장 우선순위가 높은 핸들러 매핑과 핸들러 어댑터이다.
- 지금 스프링에서 주로 사용하는 애노테이션 기반의 컨트롤러를 지원하는 매핑과 어댑터이다.
- 실무에서는 99.9% 이 방식의 컨트롤러를 사용한다.
View
의 논리 이름을 물리 이름으로 완성시켜주는 뷰 리졸버를 springMVC에서는 어떻게 만들까?
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
이 출력될 것이다.(컨트롤러는 정상 호출, Whitelabel Error Page 오류가 발생한다.)이유
➡️ new-form
이라는 viewPath
는 물리이름으로 완성하기 위해선 어떤 경로인지 상위 경로 prefix
가 없으며 이게 html인지 jsp인지 확장자를 저장한 suffix
가 없기 때문이다.
application.properties
에 다음 코드를 추가하자!
spring.mvc.view.prefix=/WEB-INF/views/
spring.mvc.view.suffix=.jsp
다시 실행시 정상적으로 페이지가 실행될 것이다.
스프링 부트는
InternalResourceViewResolver
라는 뷰 리졸버를 자동으로 등록하는데, 이때application.properties
에 등록한spring.mvc.view.prefix
,spring.mvc.view.suffix
설정 정보를 사용해서 등록한다.
- 예로 설정 없이 전체 경로를 주어도 동작한다.
return new ModelAndView("/WEB-INF/views/new-form.jsp");
실행
http://localhost:8080/springmvc/old-controller
스프링 MVC 구조
위 springMVC 구조에서 6, 7번 과정에서 뷰 리졸버가 동작한다.
스프링 부트가 자동 등록하는 뷰 리졸버
1 = BeanNameViewResolver : 빈 이름으로 뷰를 찾아서 반환한다. (예: 엑셀 파일 생성 기능에 사용)
2 = InternalResourceViewResolver : JSP를 처리할 수 있는 뷰를 반환한다.
OldController
를 예를 들면, 이때 동작하는 뷰 리졸버는?
➡️ new-form
으로 등록된 Bean은 없다. 그렇기에 다음으로 InternalResourceViewResolver
가 호출된다.
1. 핸들러 어댑터 호출
- 핸들러 어댑터를 통해
new-form
이라는 논리 뷰 이름을 획득한다.2. ViewResolver 호출
new-form
이라는 뷰 이름으로viewResolver
를 순서대로 호출한다.BeanNameViewResolver
는new-form
이라는 이름의 스프링 빈으로 등록된 뷰를 찾아야 하는데 없다.InternalResourceViewResolver
가 호출된다.3. InternalResourceViewResolver
- 이 뷰 리졸버는
InternalResourceView
를 반환한다.4. 뷰 - InternalResourceView
InternalResourceView
는 JSP처럼 포워드forward()
를 호출해서 처리할 수 있는 경우에 사용한다.5. view.render()
view.render()
가 호출되고InternalResourceView
는forward()
를 사용해서 JSP를 실행한다.
💡 참고
InternalResourceViewResolver
는 만약 JSTL 라이브러리가 있으면InternalResourceView
를 상속받은JstlView
를 반환한다.JstlView
는 JSTL 태그 사용시 약간의 부가 기능이 추가된다.
💡 참고
다른 뷰는 실제 뷰를 렌더링하지만, JSP의 경우forward()
통해서 해당 JSP로 이동(실행)해야 렌더링이 된다. JSP를 제외한 나머지 뷰 템플릿들은forward()
과정 없이 바로 렌더링 된다.
💡 참고
Thymeleaf
뷰 템플릿을 사용하면ThymeleafViewResolver
를 등록해야 한다. 최근에는 라이브러리만 추가하면 스프링 부트가 이런 작업도 모두 자동화해준다.
- 스프링에서 제공하는 애노테이션을 기반으로 컨트롤러를 구현해 동작을 확인해보자.
- 스프링이 제공하는 컨트롤러는 애노테이션 기반으로 동작해서, 매우 유연하고 실용적이다.
- 과거에는 자바 언어에 애노테이션이 없기도 했고, 스프링도 처음부터 이런 유연한 컨트롤러를 제공한 것은 아니다.
@RequestMapping
기존에@WebServlet
에서 urlPattern을 사용해주고,Component
에 빈 이름으로 URL을 작성해서 사용했지만, 이제는 이@RequestMapping
애노테이션을 사용해서 편리하게 컨트롤러 구현이 가능하다.
RequestMappingHandlerMapping
RequestMappingHandlerAdapter
➡️ 이전까지 공부한 내용에서 여러 핸들러 매핑과 핸들러 어댑터가 등록된다고 했고 우선순위가 있다고 했는데, 가장 우선순위가 높은 핸들러 매핑과 핸들러 어댑터는RequestMappingHandlerMapping
과RequestMappingHandlerAdapter
이다.
➡️@RequestMapping
애노테이션은 핸들러 매핑과 핸들러 어댑터의 앞 글자를 따서 만든 애노테이션이다. 이것이 바로 지금 스프링에서 주로 사용하는 애노테이션 기반의 컨트롤러를 지원하는 핸들러 매핑과 어댑터이다. 그리고 실무에서는 99.9% 이 방식의 컨트롤러를 사용한다.
그럼 이제 본격적으로 애노테이션 기반의 컨트롤러를 사용해보자❗️
지금까지 만들었던 프레임워크에서 사용했던 컨트롤러를 @RequestMapping
기반의 스프링 MVC 컨트롤러로 변경해보자.
package hello.servlet.web.springmvc.v1;
import hello.servlet.web.frontcontroller.ModelView;
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
애노테이션이 있어서 컴포넌트 스캔의 대상이 된다.)@RequestMapping
ModelAndView
RequestMappingHandlerMapping
은 스프링 빈 중에서 @RequestMapping
또는 @Controller
가 클래스 레벨에 붙어 있는 경우에 매핑 정보로 인식한다.
따라서 다음 코드도 동일하게 동작한다.
@Component //컴포넌트 스캔을 통해 스프링 빈으로 등록 @RequestMapping
public class SpringMemberFormControllerV1 {
@RequestMapping("/springmvc/v1/members/new-form")
public ModelAndView process() {
return new ModelAndView("new-form");
}
}
물론 컴포넌스 스캔 없이 다음과 같이 스프링 빈으로 직접 등록해도 동작한다.
@RequestMapping
public class SpringMemberFormControllerV1 {
@RequestMapping("/springmvc/v1/members/new-form")
public ModelAndView process() {
return new ModelAndView("new-form");
}
}
ServletApplication
//스프링 빈 직접 등록
@Bean
TestController testController() {
return new TestController();
}
실행
http://localhost:8080/springmvc/v1/members/new-form
package hello.servlet.web.springmvc.v1;
import hello.servlet.domain.member.Member;
import hello.servlet.domain.member.MemberRepository;
import hello.servlet.web.frontcontroller.ModelView;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Map;
@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
를 통해 Model
데이터를 추가할 때는 addObject()
를 사용하면 된다. 이 데이터는 이후 뷰를 렌더링 할 때 사용된다.
package hello.servlet.web.springmvc.v1;
import hello.servlet.domain.member.Member;
import hello.servlet.domain.member.MemberRepository;
import hello.servlet.web.frontcontroller.ModelView;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;
import java.util.List;
import java.util.Map;
@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;
}
}
실행
http://localhost:8080/springmvc/v1/members/save
http://localhost:8080/springmvc/v1/members
💡 참고
@Controller
애노테이션이 없어도 직접 설정영역에서 빈으로 등록해줘도 되고,- 클래스영역에
@RequestMapping
과@Component
애노테이션을 사용하면 정상적으로 등록되어 사용할 수 있다.
RequestMapping
을 잘 보면 클래스 단위가 아니라 메서드 단위에 적용된 것을 확인할 수 있다.
따라서, 컨트롤러 클래스를 유연하게 하나로 통합할 수 있다.
package hello.servlet.web.springmvc.v2;
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 javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.List;
/**
*클래스 단위->메서드 단위
*RequestMapping 클래스 레벨과 메서드 레벨 조합
* */
@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;
}
// /springmvc/v2/members 호출 됨
// 더할 것이 없으면 비우기
@RequestMapping
public ModelAndView members() {
List<Member> members = memberRepository.findAll();
ModelAndView mv = new ModelAndView("members");
mv.addObject("members", members);
return mv;
}
}
SpringMemberXXXControllerV1
에 있던 회원 등록 폼, 회원 가입, 회원 목록 조회 메서드들을 모두 SpringMemberControllerV2
로 모았다./spring/v1/members
까지는 중복된다. 이런 중복되는 경로는 클래스단위의 @RequestMapping
에 작성해서 경로 조합을 시도할 수 있다.
📣 조합
- 컨트롤러 클래스를 통합하는 것을 넘어서 조합도 가능하다.
- 다음 코드는
/springmvc/v2/members
라는 부분에 중복이 있다.
➡️@RequestMapping("/springmvc/v2/members/new-form")
➡️@RequestMapping("/springmvc/v2/members")
➡️@RequestMapping("/springmvc/v2/members/save")
이렇게 사용해도 되지만, 중복을 제거할 수 있다.
클래스 레벨에 다음과 같이 @RequestMapping
을 두면 메스드 레벨과 조합이 된다.
@Controller
@RequestMapping("/springmvc/v2/members")
public class SpringMemberControllerV2 {}
📣 조합 결과
클래스 레벨 @RequestMapping("/springmvc/v2/members")
- 메서드 레벨
@RequestMapping("/new-form")
→/springmvc/v2/members/new-form
- 메서드 레벨
@RequestMapping("/save")
→/springmvc/v2/members/save
- 메서드 레벨
@RequestMapping
→/springmvc/v2/members
실행
http://localhost:8080/springmvc/v2/members/new-form
http://localhost:8080/springmvc/v2/members
- V3까지 진행되며 컨트롤러를 하나로 통합까지 했다.
- 다만 v3은 ModelView를 개발자가 직접 생성해서 반환했기 때문에, 불편하다.
- 이전 공부했었던 것과 같이 v4를 만들면서 실용적으로 개선할 수 있다.
스프링 MVC는 개발자가 편리하게 개발할 수 있도록 수 많은 편의 기능을 제공한다.
실무에서는 지금부터 설명하는 방식을 주로 사용한다.
package hello.servlet.web.springmvc.v3;
import hello.servlet.domain.member.Member;
import hello.servlet.domain.member.MemberRepository;
import lombok.Getter;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* v3
* Model 도입
* ViewName 직접 반환
* `@RequestParam 사용
* `@RequestMapping -> @GetMapping, @PostMapping
*/
@Controller
@RequestMapping("/springmvc/v3/members")
public class SpringMemberControllerV3 {
private MemberRepository memberRepository = MemberRepository.getInstance();
// GET인 경우에만 호출 (좀 더 편리하게 작성할 수 있다.)
// @RequestMapping(value = "/new-form", method = RequestMethod.GET)
@GetMapping("/new-form")
public String newForm(){
return "new-form";
}
// 데이터 변경시 POST
// @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";
}
// /springmvc/v3/members 호출 됨
// 더할 것이 없으면 비우기
// 조회이므로 GET
// @RequestMapping(method =RequestMethod.GET)
@GetMapping
public String members(Model model) {
List<Member> members = memberRepository.findAll();
model.addAttribute("members", members);
return "members";
}
}
Model 파라미터
➡️save()
,members()
를 보면Model
을 파라미터로 받는 것을 확인할 수 있다.
➡️ 추가할 데이터는 이Model
에 추가해주면 되고, 기존처럼ModelAndView
객체를 만든 뒤 여기에 데이터를 넣어줄 필요가 없다.
ViewName 직접 반환
➡️ 뷰의 논리 이름을 반환할 수 있다.
➡️ 애노테이션 기반의 컨트롤러는ModelAndView
뿐 아니라ViewName
을 직접 반환해도 동작한다.
@RequestParam 사용
➡️ 스프링은 HTTP 요청 파라미터를@RequestParam
으로 받을 수 있다.
➡️@RequestParam("username")
와request.getParameter("username")
은 거의 같은 코드다.
➡️ 그렇기에GET
,POST Form
방식을 모두 지원한다.
@RequestMapping → @GetMapping, @PostMapping
➡️ 메서드들을 보면@RequestMapping
이 아니라@GetMapping
,@PostMapping
애노테이션이 있다.
➡️@RequestMapping
애노테이션은 URL만 매칭하는게 아니라 HTTP Method도 구분할 수 있다.예를 들어서, URL이
/new-form
회원 등록 폼 요청은GET
으로 올 때만 매핑시키려면 method 속성에GET
을 작성하면 된다.
@RequestMapping(value = "/new-form", method = RequestMethod.GET)
그리고
@GetMapping
,@PostMapping
는 이런 method를 미리 지정해둔 애노테이션으로 가독성과 편의성을 더 높혀서 쓰기 편하다❗️
또한PUT
,DELETE
,PATCH
모두 애노테이션이 준비되어 있다.
💡 참고
@GetMapping
,@PostMapping
내부 코드를 보면@RequestMapping
애노테이션의 메소드를 미리 지정해둔 애노테이션이다.- 이런 애노테이션의 유연함이 스프링 사용을 편리하게 해준다.
실행
http://localhost:8080/springmvc/v3/members/new-form
http://localhost:8080/springmvc/v3/members
참고 자료