[Spring-MVC] 프레임워크 만들기 - 프론트 컨트롤러, view 분리, model 추가 (4)

조대훈·2024년 2월 23일
0

김영한 Spring MVC -1

목록 보기
4/9
post-thumbnail

v1, v2 를 제외한 v3 이해가 어려워 해당 부문만 자세하게 정리했다.

프론트 컨트롤러 패턴 소개

앞서 기존에 했었던 예제에서는 출입문이 다 제각기 있어서 각 페이지 마다 각각의 서블릿이 있어야 했다. 따라서 공통된 작업에 낭비되는 코드들이 많았다. 가령 일일히 path를 지정해준다던가

프론트 컨트롤러 특징

  • 프론트 컨트롤러 서블릿 하나로 클라이언트의 요청을 받음
  • 프론트 컨트롤러가 요청에 맞는 컨트롤러를 찾아서 호출
  • 입구를 하나로
    - 공통처리가 가능
    - 프론트 컨트롤러를 제외한 나머지는 서블릿을 사용하지 않음

URL 매핑을 해서 클라이언트에서 요청이 오면, WAS 서버가 처음 요청이 오는 곳이 Servlet인데 그 역할을 이미 프론트 컨트롤러가 대신 해주기 때문에 나머지 컨트롤러들은 HttpRequest, Httpresponse등을 상속 받고 사용한다 던가 할 필요가 사라졌다.

스프링 웹MVC와 프론트 컨트롤러

스프링 웹 MVC의 핵심도 Front Controller
스프링 웹 MVC 자체가 DispatcherServlet이 FrontController 패턴으로 구현되어 있다.

프론트 컨트롤러 도입 - ver 1

이번 파트에서는 구조적인 부분만 프론트 컨트롤러를 도입한다

Front Controller는 수문장 같은 역할을 담당 시켜 매핑정보를 담아 놓는다
가령 /a 로 호출이 오면 컨트롤러 A가 호출되고, /b 가 호출되면 컨트롤러 B가 호출되게 한다.
![[Pasted image 20240222131928.png]]

이제 컨트롤러는 요청이 오면 이전에 했던 예제 그대로 JSP를 forward(서버 내부에서 일어나는 연결 요청) 하고 JSP에서 HTML 응답을 하게 하면 된다.

예제 도입에 앞서 컨트롤러를 다형성을 적용해 인터페이스로 만들 것이다.
이 인터페이스를 토대로 회원 저장, 목록,회원 폼 컨트롤러를 모두 구현할 것이다.

번외

인터페이스의 메서드를 구현할 때엔 접근제어자를 명시할 필요가 없다. 인터페이스를 구현하는 클래스 에서는 인터페이스에 정의된 메서드를 모두 public으로 취급 한다.

프론트 컨트롤러 view 분리 -v2

모든 컨트롤러에서 뷰로 이동하는 부분에 중복이 있고 깔끔하지 않았다

String  viewPath = "/WEB-INF/views/new-form.jsp";
RequestDispatcher dispatcher = request.getRequestDisPatcher(viewPath);
dispatcher.forward(request,response);

이 부분을 전담으로 처리하는 view라는 객체를 별도로 만들어줄 것이다.

이제는 컨트롤러에서 JSP로 직접 포워드 해줬지만, 이제는 그러지 않고 MyView 라는 객체를 만들어서 반환 해줄 것이다. 이후에 ForntController가 MyView를 호출 한다.

더 복잡해지는 것 같지만? 코드를 살펴보자

프론트 컨트롤러 Servlet 종속성 제거 -v3

  • http -> Map
    - 서블릿 기술을 몰라도 컨트롤러가 작동: 종속성 제거 : 파라메터 주입 필요 X
    - 리퀘스트 객체를

컨트롤러 입장에서 HttpServletRequest, HttpServletResponse는 꼭 필요할까?
요청 파라미터 정보는 Map으로 대신 넘기도록 하면 지금 구조에서는 컨트롤러가 서블릿 기술을 몰라도 동작할 수 있다. 그리고 request 객체를 Model로 사용하는 대신 별도의 Model 객체를 만들어 반환한다. 우리가 구현하는 컨트롤러가 서블릿 기술을 전혀 사용하지 않도록 변경 하면, 코드도 매우 단순해지고 테스트코드 작성이 쉬워진다.

뷰 이름 중복 제거

컨트롤러에서 지정하는 뷰 이름에 중복이 있는 것을 확인할 수 있다. 컨트롤러는 뷰의 논리 이름을 반환하고, 실제 물리 위치의 이른은 프론트 컨트롤러에서 처리하도록 단순화 하자. 이렇게 해두면 향후 뷰의 폴더 위치가 함께 이동해도 프론트 컨트롤러만 고쳐도 된다.

  • /WEB-INF/views/new-form.jsp ->new-form
  • /WEB-INF/views/save-result.jsp-> save-result
  • /WEB-INF/views/members.jsp ->members

prefix suffix를 만들겠다는 뜻

기존 V2에서는 FrontController <- Controller 에서 Model을 반환 했지만 v3에서는 ModelView를 반환한다. 뷰에서 이제 논리 이름만 반환할 것인데 이 논리 이름을 물리 이름으로 바꾸는 게 viewResolver이고 MyView로 전환하고 Render를 호출한다.

ModelView

지금까지 컨트롤러에서 서블릿에 종속적인 HttpServletRequest를 사용했다. 그리고 Model 도 request.setAttribute()를 통해 데이터를 저장하고 뷰에 전달했다.
서블릿의 종속성을 제거하기 위해 Model을 직접 만들고, 추가로 View이름까지 전달하는 객체를 만들어 본다. ( 따라서 이번 v3는 HttpServletRequest를 사용할 수 없다. 따라서request.setAttribute()를 호출할 수도 없다.
따라서 Model이 별도로 필요하다

컨트롤러가 반환 할 모델 뷰, 이름에서도 알수 있듯 뷰에 대한 vieName과 setAttribute를 대신할 ModelView Map 객체가 있다.

@Data
public class ModelView{

	private String viewName;
	private Map<String, Object> model= new HasMap<>();

	public ModelvView(String vieName){
	
	this.viewName= vieName;

	}
	
}

각기 뷰에 해당하는 컨트롤러들을 구현체의 토대가될 인터페이스. 아래에서 볼수 있듯 HttpServlet reuqest, response가 없는 모습을 볼수 있다. v3에서 진행할 자바 맵 객체에 종속적이지 서블릿에 종속적이지 않은 모습을 볼수 있다.


public interface ControllerV3{

Modelview process(Map<String, String> pramMap){

}
}

이제 컨트롤러들을 구현 해보자

폼 컨트롤러


public class MemberFormControllerV3 implements ControllerV3{

	@Override
	public ModelView process(Map<String, String> paramMap){
		return new (ModelView("new-form"))
	}
}

컨트롤러 인터페이스 안에 있는 모델뷰 객체를 구현한 예시. 모델뷰로 전달하는 파라메터는 해당 뷰에대한 ㅍiewName 정보이다.

세이브 컨트롤러


public class MemberSaveControllerV3 implements ControllerV3{

private MemberRepository memberRepository =
								memberRepository.getInstance(); 

	@Override
	protected ModelView process(Map<String, String> paramMap){
		String username= paramMap.get("username");
		int age= Integer.parseInt(paramMap.get("age"));

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

		ModelView mv = new ModelView("save-result");
		mv.getModel().put("member",member);

		return mv;
	}
}

getParameter가 아닌 맵에서 꺼내는 모습. HttpServlet대신 쓰게 된 Map 에서 get 해주고 파라메터를 FrontController에 넘겨준다.

paramMap은 클래스 인터페이스에서 받아와서 쓰는 말 그대로 파라메터 맵이다.
model 은 modelView 에 넘길 String Object 인 모델 대신 쓸 맵이다.

  1. 멤버 이름, 나이 정보 .get()
  2. 이름, 나이 파라메터를 넣은 멤버 객체 만들고
  3. 모델 객체를 .get() 하고 .put() 하기
  4. 모델 뷰 반환하기 ( 프론트 컨트롤러로 )
  5. 이제 프론트 컨트롤러는 JSP로 포워드할 것이다. 뷰 리졸버를 통해

멤버 리스트 컨트롤러


public class MemberListControllerV3 implements ControllerV3{

MemberRepository memberRepository = memberRepository.getInstance();
@Overrride
protected class Service(Map<String,String> paramMap){

	List<Member> members= memberRepository.findAll();

	ModelView mv=new Modelview("members");

	mv.getModel().put(members);

	return mv;
}
	
}
  1. 멤버 리스트에 findAll() 로 모든 멤버 담기
  2. Modelview 생성 후 getModel() 로 모델 불러온 후 .put(members)
  3. 그리고 mv 반환하기

ModelView를 생성할 때 viewName 도 반드시 넣어준다


@WebServlet(name="FrontControllerV3", urlNames="/fornt-controller/v3/*")
public class FrontControllerServletV3 extends HttpServlet{

	private Map<String, ControllerV3> controllerMap = new HasMap<>();

	public FrontControllerServletV3(){
	controllerMap.put("/front-controller/v3/members/new-form",
	new MemberFromControllerV3());
	
	controllerMap.put("/front-controller/v3/members/save",new
	MemberSaveControllerV3());

	controllerMap.put("/front-controller/v3/members",
		new MemberListControllerV3());

	}

	@Override
	protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOEException{

	String requestURI = reuqest.getRequestURI();
	ControllerV3 controller= controller.get(requestURI);

	if(controller ==null){
	
		response.setStatuse(HttpServletResponse.SC_NOT_FOUN);
		
		return; }
	My<String, String> paramMap = createParamMap(request);
	Modelview mv = controller.process(paramMap);

	String viewName= mv.getViewName();
	Myview view = viewResolver(viewName);

	view.render(my.getModel(),request,response);


}

private Map<String, String>createParamMap(HttpServlet request){


	Map<String,String> paramMap = new HashMap<>();
	
	request.getParameterNames().asIterator()
	.forEachRemaining(paraName -> paramMap.put(paramName,
		result.getParameter(paramName)));
		
	return paramMap;
}}}
  1. 클라이언트로부터의 요청이 들어오면
  2. 서블릿 컨테이너에 의해 FrontControllerV3() 클래스으, service() 로 라우팅
  3. service()HttpServletRequest 객체를 통해 클라이언트의 요청정보를 받아온다.
  4. 제일 먼저 getRequestURI()로 현재 해당하는 컨트롤러를 찾는다.
  5. 이때 컨트롤러가 controllerMap 에 없으면 404 오류를 반환한다
  6. 컨트롤러가 있으면 HTTP요청으로 getParameter()로 파라미터를 추출 후 맵으로 변환하기 위하여 getParameterNames, asIterater 로 데이터를 전부 꺼낸 후 paramMapput()해준다.
  7. 요약하면 HTTP 요청 파라메터를 -> 맵 형태로 변환
  8. 요청된 URI에 해당하는 컨트롤러를 실행하고, ModelView 를 반환한다
  9. 이 과정에서 컨트롤러는 실제 비즈니스 로직을 수행하고, 그 결과를 Modelvie 객체에 담아 반환한다
  10. 반환된 ModelView 객체에서 뷰 이름을 가져온다
  11. viewResolver로 실제 JSP 파일 경로로 뷰 이름을 변환한다.
  12. 해당 JSP 파일을 찾아서 뷰를 렌더링 후 클라이언트에게 응답을render 보낸다.

어떻게 render 되는지?

마이뷰에서 오버로드된 render 함수 중 v3에 쓰이는 render 함수는 이러하다

  1. modelToRequestAttribute 주어진 모델의 데이터를 request객체의 속성(attribute)로 변환한다
  2. RequestDisPatcher JSP파일을 포워딩 하게 위해 생성한다. 이 객체는 Servlet 컨테이너에게 요청을 전달하는 역할을 한다
  3. RequestDisPatcher를 사용하여 지정된 JSP(viewPath) 로 요청을 포워딩한다. 이때 현재의 HttpServletRequestHttpServletResponse가 함께 전달된다. 이를 통해 JSP는 서블릿과 동일한 컨텍스트 안에서 실행되며, JSP 파일에서는 request와 response 객체를 사용하여 클라이언트에게 응답을 생성할 수 있다.
  4. 포워딩 처리 포워딩 후 해당 JSP 파일에서 지정된 작업이 수행된다. JSP파일은 주어진 모델 데이터를 사용하여 동적으로 HTML을 생성하고, 이를 통해 HttpServletResponse를 통해 클라이언트에게 응답으로 보낸다.
public void render(Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {  
  
    modelToRequestAttribute(model, request);  
    RequestDispatcher requestDispatcher = request.getRequestDispatcher(viewPath);  
    requestDispatcher.forward(request, response);  
}
profile
백엔드 개발자를 꿈꾸고 있습니다.

0개의 댓글