Spring

Spring MVC 구조 뜯어보기(1) - Servlet

쭈녁 2024. 1. 20. 01:54

서블릿

스프링 부트는 톰캣 서버를 내장하고 있어, 톰캣 서버 설치 없이 서블릿 코드를 사용 가능하다.

 

 

디스패쳐 서블릿은 웹 애플리케이션 서버를 통해 특정 URL에 요청이 오면

요청값인 HttpServletRequest과 응답값인 HttpServletResponse (응답을 세팅하기 위해)

웹 애플리케이션 서버에서 받아온다. 

이렇게 받아온 정보를 URL 정보와 일치하는 컨트롤러를 찾아 로직 수행한다.

 

 

코드 예시)

 

URL에 따른 로직을 찾아 수행하는 구조

- 디스패쳐 서블릿

@WebServlet(name = "frontControllerV1", urlPatterns = "/front-controller/v1/*")
public class FrontControllerV1 extends HttpServlet {
    // HandlerMapper 역할
    private Map<String, ControllerV1> controllerMap = new HashMap<>();

    public FrontControllerV1() {
        controllerMap.put("/front-controller/v1/members/new-form", new MemberFormControllerV1());
        controllerMap.put("/front-controller/v1/members/save", new MemberSaveControllerV1());
        controllerMap.put("/front-controller/v1/members", new MemberListControllerV1());
    }

    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //URL 주소에 맞는 Controller를 찾아옴
	String requestURI = req.getRequestURI();
        ControllerV1 controllerV1 = controllerMap.get(requestURI);
        
        //없으면 status에 애러 코드
        if (controllerV1 == null) {
            resp.setStatus(HttpServletResponse.SC_NOT_FOUND);
            return;
        }
        
        //있으면 해당 컨트롤러의 로직 수행
        controllerV1.process(req, resp);
    }
}

 

위 코드는 "/front-controller/v1/*" 라는 URL로 들어왔을 때 요청 URL에 맞게 Controller의 로직을 수행하는 코드이다. 해당 역할을 디스패쳐서블릿이 해준다.

 

 

 

- 로직을 수행하는 컨트롤러

public class MemberSaveControllerV1 implements ControllerV1 {
    MemberRepository memberRepository = MemberRepository.getInstance();

    @Override
    public void process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        String name = request.getParameter("name");
        Integer age = Integer.parseInt(request.getParameter("age"));
        Member member = new Member(name, age);
        memberRepository.save(member);

        request.setAttribute("member", member);
        String viewPath = "/WEB-INF/views/save-result.jsp";

        RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
        dispatcher.forward(request, response);
    }
}

 

로직(Member를 저장) 수행하고 해당 객체를 request.setAttribute에 넣어 특정 viewPath의 페이지가 랜더링 되도록 한다.

MVC모델의 경우 컨트롤러 마다 반복되는 코드들이 있다. ViewPath에 페이지가 렌더링 하는 과정이 컨트롤러의 로직마다 반복된다. 이 반복되는 부분을 서블릿이 대신 수행 해 주도록 수정하였다.

 

 

 

View 랜더링을 서블릿이 가져온 구조

 

-디스패쳐 서블릿

@WebServlet(name = "FrontControllerV2", urlPatterns = "/front-controller/v2/*")
public class FrontControllerV2 extends HttpServlet {
    private Map<String, ControllerV2> controllerMap = new HashMap<>();

    public FrontControllerV2() {
        controllerMap.put("/front-controller/v2/members/new-form", new MemberFormControllerV2());
        controllerMap.put("/front-controller/v2/members/save", new MemberSaveControllerV2());
        controllerMap.put("/front-controller/v2/members", new MemberListControllerV2());
    }

    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String requestURI = req.getRequestURI();
        ControllerV2 controller = controllerMap.get(requestURI);
        if (controller == null) {
            resp.setStatus(HttpServletResponse.SC_NOT_FOUND);
            return;
        }
        MyView view = controller.process(req, resp);
        view.render(req, resp);
    }
}

 

-View 를 관리하기 위한 객체

public class MyView {
    private String viewPath;

    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 modelToRequestAttribute(Map<String, Object> model, HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException{
        model.forEach((key, value) -> req.setAttribute(key, value));
        RequestDispatcher dispatcher = req.getRequestDispatcher(viewPath);
        dispatcher.forward(req, resp);
    }
}

 

-로직을 수행하는 컨트롤러

public class MemberSaveControllerV2 implements ControllerV2 {
    MemberRepository memberRepository = MemberRepository.getInstance();

    @Override
    public MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        String name = request.getParameter("name");
        Integer age = Integer.parseInt(request.getParameter("age"));
        Member member = new Member(name, age);
        memberRepository.save(member);

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

}

 

컨트롤러 내에서 반복되던 

        RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
        dispatcher.forward(request, response);

코드가 없어지고 MyView라는 객체가 주소값을 받아 이 객체를 통해 페이지가 랜더링 되도록 분리하였다.

 

 

모델을 서블릿이 가지고 있고 그 모델을 컨트롤러가 가져와서 넣어주는 구조

- 디스패쳐 서블릿

@WebServlet(name = "FrontControllerV4", urlPatterns = "/front-controller/v4/*")
public class FrontControllerV4 extends HttpServlet {
    private Map<String, ControllerV4> controllerMap = new HashMap<>();

    public FrontControllerV4() {
        controllerMap.put("/front-controller/v4/members/new-form", new MemberFormControllerV4());
        controllerMap.put("/front-controller/v4/members/save", new MemberSaveControllerV4());
        controllerMap.put("/front-controller/v4/members", new MemberListControllerV4());
    }

    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String requestURI = req.getRequestURI();
        ControllerV4 controller = controllerMap.get(requestURI);
        if (controller == null) {
            resp.setStatus(HttpServletResponse.SC_NOT_FOUND);
            return;
        }
        Map<String, String> paramMap = createParamMap(req);
        HashMap<String, Object> model = new HashMap<>();

        String view = controller.process(paramMap, model);

        MyView myView = viewResolver(view);
        myView.modelToRequestAttribute(model, req, resp);

    }

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

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

 

-로직 수행 컨트롤러

public class MemberSaveControllerV4 implements ControllerV4 {
    MemberRepository memberRepository = MemberRepository.getInstance();

    @Override
    public String process(Map<String, String> paramMap, Map<String, Object> model) {
        String name = paramMap.get("name");
        Integer age = Integer.parseInt(paramMap.get("age"));
        Member member = new Member(name, age);

        memberRepository.save(member);
        model.put("member", member);

        return "save-result";
    }
}

 

request에 들어오던 파라미터 정보를 Map 형식으로 파라미터 이름과 값으로 컨트롤러에 전달하고 컨트롤러는 model이라는 인자를 받아 로직을 수행하여 model이라는 Map형식의 객체 안에 이름과 객체 타입으로 세팅하여 준다.

 

리턴 값으로는 ViewPath에 해당하는 String 값을 리턴하여 서블릿이 페이지에 대한 경로를 ViewResolver를 통해 만들고 MyView 객체를 통해 받은 model을  반환하도록 페이지에 랜더링 하도록 구조를 변경하였다.

 

 

결론

해당 구조는 SpringMvc 모델의 구조와 흡사하다. MVC 구조의 Controller에서 반환 값이 String인 이유,

@RequestMapping을 통해 특정 URL로 요청이 왔을 때 매핑되는 이유,

@PathValiable , @ModelAttribute 등의 메서드가 사용 가능한 이유가 디스패쳐 서블릿이 이러한 공통된 코드를 구조화 하였기 때문에 사용이 가능하다.