[Spring] FrontController 도입하기 V3 - 서블릿 종속성 제거

2025. 6. 23. 12:06·Spring/MVC
 

[Spring] FrontController 도입하기 V2 - 포워딩 중복 제거

https://bdisappointed.tistory.com/154 [Spring] FrontController 도입하기 V1[Spring] 서블릿과 JSP로 MVC 흉내내기 , 그리고 한계점서블릿과 JSP를 통해 MVC를 구현 해볼 예정이다. 구현은 다음과 같다.서블릿 : 컨트롤

bdisappointed.tistory.com

이번에는 사용하지 않는 request나 response를 제거하는 서블릿 종속성을 제거 해본다.

 

리팩토링 절차

쉽게 얘기 해서, 파라미터 정보를 Map으로 저장하여 대신 넘기도록 하면 컨트롤러가 서블릿에 종속 되지 않고 넘겨 받은 파라미터들로만 지지고 볶고 하면 된다는 뜻이다.

하지만 이전에 우리는 save 컨트롤러에서 request가 제공하는 attribute를 통해 데이터를 전달했었는데, 만약 request객체를 사용하지 않아서 데이터를 담을 모델이 없다면 ? -> 이제 우리는 새로운 Model 이라는 객체를 만들어 데이터를 전달하면 된다.

추가적으로, 뷰이름의 중복을 제거하기 위해 컨트롤러가 뷰의 논리적 이름만 반환하고, 절대 위치는 프론트 컨트롤러가 처리하도록 하겠다.

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


HttpServletRequest 종속성 제거 -> ModelView 객체 추가

여태 attribute를 사용하는 등의 request에 종속적인 컨트롤러만 사용했는데, 이러한 종속성을 해결하기 위해 데이터를 담는 Model 객체를 만들고 View 이름까지 전달하는 객체를 만들어보겠다.

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;
    }
}

ModelView 객체는 뷰의 이름과 뷰를 렌더링할 때 필요한 데이터를 담을 Map을 가지고 있다.

어떤 타입의 데이터를 보관할지 모르니 타입을 Object로 설정했다.

 


 Controller V3

package hello.servlet.web.frontcontroller.v3;

import hello.servlet.web.frontcontroller.ModelView;

import java.util.Map;

public interface ControllerV3 {

    // 서블릿 기술 종속 X
    ModelView process(Map<String, String> paramMap);
}

이전 인터페이스들과의 차이점은 더이상 request와 response를 넘기지 않는다는 것이다. -> 서블릿 기술을 사용하지 않기 때문에 종속성이 해결 되었다.

우리는 HttpServletRequest가 제공하는 파라미터를 미리 paramMap에 담아 호출해주기만 하면 된다. -> 프론트 컨트롤러 코드에서 확인

그리고 우리는 뷰 이름과 Model을 포함한 ModelView 객체를 반환하면 된다.

 

회원 등록

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");
    }
}

new-form 이라는 논리적 이름만 지정하여 반환한다.

 

회원 저장

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); // 해당 모델 뷰 안에 Map 모델 키값 입력
        return mv;
    }
}

save-result라는 뷰 이름을 지정한 모델 뷰를 만들고, 해당 모델 뷰에 데이터를 담아 반환한다.

 

회원 조회

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;
    }
}

save와 같다.

모든 회원의 리스트를 받아 모델에 담아 반환하기만 하면 된다.


프론트 컨트롤러

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.MemberListControllerV3;
import hello.servlet.web.frontcontroller.v3.controller.MemberFormControllerV3;
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;

import static jakarta.servlet.http.HttpServletResponse.SC_NOT_FOUND;

@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(SC_NOT_FOUND);
            return;
        }

        // 서블릿 종속성 해소를 위해 paraMap을 넘겨야함 -> 더이상 컨트롤러가 Request와 Response를 몰라도 됨
        Map<String, String> paramMap = createParamMap(request); // request 에 있는 파라미터를 모두 호출
        ModelView mv = controller.process(paramMap); // 모델 뷰 구성 : name=members / value(model)=List(모든 멤버)

        String viewName = mv.getViewName(); // members
        MyView view = viewResolver(viewName); //WEB-INF/views/"members.jsp를 viewPath로 가진 MyView 생성
        view.render(mv.getModel(),request,response);
    }

    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) {
        return new MyView("/WEB-INF/views/" + viewName + ".jsp");
    }
}

viewResolver는 논리 뷰 이름을 절대 뷰 경로로 변경해주는 역할을 한다.

동작과정

먼저 request에서 URI를 가져와 컨트롤러를 찾는다.

이후 createParamap(request) 메서드를 통해 모든 파라미터를 [파라미터 이름, 파라미터 값] 형태로 paramMap에 저장한다. 어떻게 보면 그냥 복사 붙여 넣기 와 같다 ㅋㅋ.

이후 모든 파라미터를 담은 paramMap을 찾은 컨트롤러에 넘겨 process 메서드를 실행한다.

비즈니스 로직을 수행하는 컨트롤러 내부에서는 이제 request나 response에서 파라미터를 가져다 쓰는 것이 아닌 미리 request에서 가져온 데이터를 저장한 map만 따로 전달받아 사용할 수 있게 되었다. 전달받은 map의 데이터로 비즈니스 로직을 수행 한 뒤에, 뷰의 논리적 이름을 포함한 modelView를 생성하고, 데이터도 담아 반환한다.

반환 받은 모델뷰를 프론트 컨트롤러에서 다시 아래와 같이 사용한다

1. 뷰의 논리적 이름 가져오기

2. 해당 논리적 이름을 통해 myView 객체 생성 -> 이때, 절대 경로로 경로를 수정해주는 viewResolver 메서드에 한번 태워서 나온 결과로 생성

3. 생성한 myView에 모델과 request, response를 전달하면 포워딩 로직 수행

 

우리는 MyView 객체가 모델도 전달 받을 수 있도록 수정해줘야한다.

 

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/save-result.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));
    }
}

render 메서드를 오버로딩하였다.

전달 받은 모델의 데이터를 풀어 헤쳐 request의 attribute에 다시 담아준다.

request는 담긴 데이터를 기반으로 dispatcher을 생성하여 저장된 viewPath로 포워딩한다.

 


결론

사실 여기까지만 해도 잘 설계된 컨트롤러이다.

V4 부터는 개발자 친화적으로 리팩토링 할 예정인데, 항상 ModelView를 생성하고 반환하는 부분을 리팩토링 해보겠다 !

그럼 20000~~

'Spring > MVC' 카테고리의 다른 글

[Spring] FrontController V5(完) - 다양한 인터페이스 구현체 처리하기  (2) 2025.06.23
[Spring] FrontController V4 - 개발자 친화적 리팩토링  (0) 2025.06.23
[Spring] FrontController 도입하기 V2 - 포워딩 중복 제거  (0) 2025.06.23
[Spring] FrontController 도입하기 V1  (0) 2025.06.23
[Spring] Front Controller 패턴 - 개요  (0) 2025.06.22
'Spring/MVC' 카테고리의 다른 글
  • [Spring] FrontController V5(完) - 다양한 인터페이스 구현체 처리하기
  • [Spring] FrontController V4 - 개발자 친화적 리팩토링
  • [Spring] FrontController 도입하기 V2 - 포워딩 중복 제거
  • [Spring] FrontController 도입하기 V1
xuv2
xuv2
집에 가고 싶다
  • xuv2
    xuvlog
    xuv2
  • 전체
    오늘
    어제
    • 전체 글 모아보기 (170) N
      • 잡담 (9)
      • 도전 , 자격증 (2)
      • Error (5)
      • Java (23)
      • Spring (39) N
        • Core (10)
        • MVC (20)
        • Thymeleaf (9) N
      • DataBase (6)
        • Database Modeling (4)
        • SQL (2)
      • HTTP (11)
      • Network (17)
      • Software Engineering (3)
      • Operating System (3)
      • Algorithm (16)
      • Project (18)
        • Web (9)
        • iOS (8)
        • Python (1)
      • A.I (13)
      • Linux (5)
  • 블로그 메뉴

    • 홈
  • 링크

    • Github
  • 인기 글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
xuv2
[Spring] FrontController 도입하기 V3 - 서블릿 종속성 제거
상단으로

티스토리툴바