Spring Study

[스프링 MVC] #6

yeon.___.jin 2025. 5. 4. 02:26
반응형

김영한님의 스프링 MVC 1편 강의를 듣고 복습 겸 정리하는 포스팅입니다!!

아직 공부하는 중인 학생이라 부족한 부분이 있을 수 있습니다. 

혹시라도 틀린 부분이 있다면 언제든지 댓글로 남겨주세요. 감사합니다 🙇‍♂️


섹션 5. Model 추가 - V3

[이전 버전]

https://lyjduswls.tistory.com/55

 

[스프링 MVC] #5

김영한님의 스프링 MVC 1편 강의를 듣고 복습 겸 정리하는 포스팅입니다!!아직 공부하는 중인 학생이라 부족한 부분이 있을 수 있습니다. 혹시라도 틀린 부분이 있다면 언제든지 댓글로 남겨주세

lyjduswls.tistory.com

MemberSaveControllerV2 - 회원 정보 저장 컨트롤러

public class MemberSaveControllerV2 implements ControllerV2 {

    private MemberRepository memberRepository = MemberRepository.getInstance();

    @Override
    public MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        String username = request.getParameter("username");
        int age = Integer.parseInt(request.getParameter("age"));

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

        request.setAttribute("member", member);

        return new MyView("/WEB-INF/views/save-result.jsp");
    }
}

MemberSaveController의 경우 요청 파라미터 정보가 필요한것이지 사실 request,response가 당장 필요한 것은 아니다. 

 

이번 단계에서 할 작업

1. 컨트롤러가 서블릿 기술을 전혀 사용하지 않도록 변경한다.

  • 구현코드가 단순해지고, 테스트 코드의 작성이 쉬워진다. 
  • request.setAttribute() 대신 별도의 Model 객체로 분리한다. 
  • 요청 파라미터는 자바의 Map을 사용해 전달한다. 

2. 뷰 이름의 중복을 제거한다.

  • 기존의 컨트롤러는 물리 뷰 경로를 직접 반환한다. 
  • 모든 뷰 경로에 /WEB-INF/views/ 와.jsp가 모두 뷰에 중복되어 있다.
  • 컨트롤러는 뷰의 "new-form"과 같은 논리 이름만 반환하고, 물리적 경로 조합은 프론트 컨트롤러에서 처리하도록 변경한다.
    • /WEB-INF/views/new-form.jsp -> new-form
    • /WEB-INF/views/save-result.jsp -> save-result
    • /WEB-INF/views/members.jsp -> members

 

  • 뷰 경로가 변경되더라도 프론트 컨트롤러만 수정하면 됨 → 변경 지점을 하나로 만들 수 있는 설계 = 좋은 설계
  • 컨트롤러가 서블릿 기술과 뷰 경로에 의존하지 않게 되어 재사용성과 테스트성 향상된다.

 

 

V3 구조

  1. 클라이언트가 HTTP 요청을 전송한다.
  2. 프론트 컨트롤러가 매핑정보에 맞는 컨트롤러를 조회한다.
  3. 프론트 컨트롤러가 조회된 컨트롤러를 호출한다.
  4. 컨트롤러는 모델과 뷰가 섞여있는 ModelView 객체를 반환한다. (기존에는 view를 반환했다.)
  5. 프론트 컨트롤러가 뷰 리졸버를 호출한다.
  6. 뷰 리졸버에서 논리 이름을 실제경로로 바꾸고 MyView 객체를 반환한다.
  7. 프론트 컨트롤러가 MyView.render()를 호출한다.
  8. MyView 내부에서 JSP로 forward하여 HTML 응답을 생성하고 클라이언트에 전달한다.

ModelView

package hello.servlet.web.frontcontroller;

import java.util.HashMap;
import java.util.Map;

public class ModelView {
    private String viewName;
    private Map<String, Object> model = new HashMap<>();

    public ModelView(String viewName) {
        this.viewName = viewName;
    }

    public String getViewName() {
        return viewName;
    }

    public void setViewName(String viewName) {
        this.viewName = viewName;
    }

    public Map<String, Object> getModel() {
        return model;
    }

    public void setModel(Map<String, Object> model) {
        this.model = model;
    }
}

 

ControllerV3

package hello.servlet.web.frontcontroller.v3;

import hello.servlet.web.frontcontroller.ModelView;

import java.util.Map;

public interface ControllerV3 {
    ModelView process(Map<String, String> paramMap);
}

ControllerV2와 다르게 ModelView를 반환하는 식으로 바뀌었다.

서블릿 API를 제거해, 서블릿 환경에 종속적이지 않게 개선되었다.

서블릿 API (HttpServletRequest, HttpServletResponse)에 직접 의존 ->서브릿 환경에 종속적이다. 

 

MemberFormControllerV3 - 회원 등록 폼 컨트롤러 

package hello.servlet.web.frontcontroller.v3.controller;

import hello.servlet.web.frontcontroller.ModelView;
import hello.servlet.web.frontcontroller.v3.ControllerV3;

import java.util.Map;

public class MemberFormControllerV3 implements ControllerV3 {

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

ModelView의 viewName에 논리적인 이름을 넣는다.

 

MemberSaveControllerV3 - 회원 정보 저장 컨트롤러

package hello.servlet.web.frontcontroller.v3.controller;

import hello.servlet.domain.member.Member;
import hello.servlet.domain.member.MemberRepository;
import hello.servlet.web.frontcontroller.ModelView;
import hello.servlet.web.frontcontroller.v3.ControllerV3;

import java.util.Map;

public class MemberSaveControllerV3 implements ControllerV3 {

    private MemberRepository memberRepository = MemberRepository.getInstance();

    @Override
    public 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;

    }
}

이전(위의 사진)에는 HttpServletRequest.getParameter()를 통해 요청 파라미터를 직접 꺼냈지만, V3구조에서는 프론트 컨트롤러가 요청 파라미터를 미리 추출해 Map 형태로 전달하기 때문에 컨트롤러는 단순히 paramMap.get("key")로 값을 꺼내쓰면 된다. 

 

mv.getMoel().put("member", member);

모델은 단순한 Map 구조이므로, 뷰에서 필요한 member 객체를 모델에 담아 반환한다. 

 

MemberListControllerV3- 회원 목록 조회 컨트롤러

package hello.servlet.web.frontcontroller.v3.controller;

import hello.servlet.domain.member.Member;
import hello.servlet.domain.member.MemberRepository;
import hello.servlet.web.frontcontroller.ModelView;
import hello.servlet.web.frontcontroller.v3.ControllerV3;

import java.util.List;
import java.util.Map;

public class MemberListControllerV3 implements ControllerV3 {

    private MemberRepository memberRepository = MemberRepository.getInstance();

    @Override
    public ModelView process(Map<String, String> paramMap) {
        List<Member> members = memberRepository.findAll();
        ModelView mv = new ModelView("members");
        mv.getModel().put("members", members);

        return mv;
    }
}

 

FrontControllerServletV3

package hello.servlet.web.frontcontroller.v3;

import hello.servlet.web.frontcontroller.ModelView;
import hello.servlet.web.frontcontroller.MyView;
import hello.servlet.web.frontcontroller.v3.controller.MemberFormControllerV3;
import hello.servlet.web.frontcontroller.v3.controller.MemberListControllerV3;
import hello.servlet.web.frontcontroller.v3.controller.MemberSaveControllerV3;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

@WebServlet(name = "frontControllerServletV3", urlPatterns = "/front-controller/v3/*")  
public class FrontControllerServletV3 extends HttpServlet {

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

    public FrontControllerServletV3() {
        controllerMap.put("/front-controller/v3/members/new-form", new MemberFormControllerV3());
        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, IOException {

        String requestURI = request.getRequestURI();

        ControllerV3 controller = controllerMap.get(requestURI);
        if(controller == null){
            response.setStatus(HttpServletResponse.SC_NOT_FOUND);
            return;
        }

        Map<String, String> paramMap = createParamMap(request);
        ModelView mv = controller.process(paramMap);

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

        view.render(mv.getModel(),request, response);
    }

    private static MyView viewResolver(String viewName) {
        MyView view = new MyView("/WEB-INF/views/" + viewName + ".jsp");
        return view;
    }

    private static Map<String, String> createParamMap(HttpServletRequest request) {
        Map<String, String> paramMap = new HashMap<>();
        request.getParameterNames().asIterator()
                .forEachRemaining(paramName -> paramMap.put(paramName, request.getParameter(paramName)));
        return paramMap;
    }
}

 

뷰 리졸버

private static MyView viewResolver(String viewName) {
    MyView view = new MyView("/WEB-INF/views/" + viewName + ".jsp");
    return view;
}
  • 컨트롤러가 반환한 논리 이름을 실제 물리 뷰 경로로 변경한다. 
    • 논리 이름: "members"
    • 물리 뷰 경로: "/WEB-INF/views/members.jsp"
  • viewResolver()에서 실제 JSP 경로를 가진 MyView 객체를 반환한다.
view.render(mv.getModel(),request, response);

 

  • 뷰 객체를 통해 HTML 화면을 렌더링한다.
  • MyView.render()는 모델 정보도 함께 받는다.
  • JSP는 request.getAttribute()로 데이터를 조회하기 때문에, 모델의 데이터를 꺼내서 request.setAttribute()로 담아둔다.
  • JSP로 포워드해서 JSP를 렌더링한다. 

 

MyView

package hello.servlet.web.frontcontroller;

import jakarta.servlet.RequestDispatcher;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;
import java.util.Map;

public class MyView {
    private String viewPath;  //"/WEB-INF/views/new-form.jsp" 값은 이미 들어가있다.

    public MyView(String viewPath) {
        this.viewPath = viewPath;
    }

    public void render(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
        dispatcher.forward(request, response);
    }

    public void render(Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        modelToRequestAttribute(model, request);
        RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
        dispatcher.forward(request, response);
    }

    private static void modelToRequestAttribute(Map<String, Object> model, HttpServletRequest request) {
        model.forEach((key, value) -> request.setAttribute(key,value));
    }
}
반응형

'Spring Study' 카테고리의 다른 글

[스프링 MVC] #8  (0) 2025.05.10
[스프링 MVC] #7  (0) 2025.05.04
[스프링 MVC] #5  (1) 2025.05.03
[스프링 MVC] #4  (0) 2025.05.03
[스프링 MVC] #3  (2) 2025.04.27