본문 바로가기
공부/Spring

[Spring][인프런 스프링 MVC] MVC 구를 직접 개선해가며 스프링 MVC 구조를 이해하기 (controller v1~v4)

by 웅대 2023. 3. 1.
728x90
반응형

본 포스팅은 김영한 강사님의 인프런 강의 "스프링 MVC 1편"을 정리한 포스팅으로 강의 자료에서 사용한 자료를 사용했음을 밝힙니다.

https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-1/dashboard

 

스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술 - 인프런 | 강의

웹 애플리케이션을 개발할 때 필요한 모든 웹 기술을 기초부터 이해하고, 완성할 수 있습니다. 스프링 MVC의 핵심 원리와 구조를 이해하고, 더 깊이있는 백엔드 개발자로 성장할 수 있습니다., -

www.inflearn.com

스프링 MVC의 구조를 이해하기 위해 김영한 강사님께서 JSP와 서블릿을 이용하여 단계적으로 MVC 구조를 발전시켜나가는 과정을 정리해보려한다.

 

먼저 MVC에 대한 간단한 설명은 다음과 같다.

 

C(Controller)

HTTP 요청을 받아서 비즈니스 로직을 실행하고 View에 전달할 데이터를 Model에 전달한다.

 

M(Model)

View가 필요로 하는 모든 데이터를 담고 있다.

V(View)

Model에 담겨있는 데이터를 사용하여 화면을 그린다.

 

이러한 MVC 패턴의 장점은 역할이 철저하게 분리된다는 점이다.

 

View는 화면을 렌더링하는 것에만 집중하고 비즈니스 로직에 대해서는 몰라도 된다.

 

인프런 스프링 MVC 1편

컨트롤러 구조

@WebServlet(name = "이름", urlPatterns = "요청 url")
public class ServletExample extends HttpServlet {

    @Override
    protected void service(
            HttpServletRequest request, HttpServletResponse response
    ) throws ServletException, IOException {
        //비즈니스 로직 실행
        String data = request.getParameter("파라미터 이름");

        //model에 담기
        request.setAttribute("data", data);


        String viewPath = ".../.../.../view.jsp";
        RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
        dispatcher.forward(request, response);
    }
}

위 코드는 서블릿을 이용하여 컨트롤러를 구현한 모습이다.

 

핵심은 dispatcher.forward 함수인데 이는 다른 서블릿이나 jsp 파일로 이동하는 함수이다.

 

위 코드는 보다시피 컨트롤러에서 비즈니스 로직을 실행하고 request에 필요한 데이터를 담아서 view에 보내주고 있다.

 

view는 아무것도 신경쓰지 않아도 단순히 request에 담겨온 데이터를 사용하기만 하면 된다.

 

위 방식의 단점

  1. 중복 코드 : 컨트롤러마다 jsp 파일이 담겨있는 경로가 중복되고 계속 forward 함수를 실행해야한다.
  2. request, response가 사용되지 않아도 존재한다.

이 단점을 해결하기 위해서 front controller를 사용한다.

 

이는 컨트롤러가 실행되기 전에 실행되기 때문에 공통 처리에 유용하다.

 

DispatcerServlet이 front controller이다.

 

인프런 스프링 MVC 1편

프론트 컨트롤러는 다음과 같은 방식으로 구현되어있다.

@WebServlet(name = "이름", urlPatterns = "/공통 요청 url/*")
public class FrontControllerServlet extends HttpServlet {
    private Map<String, Controller> controllerMap = new HashMap<>();
    public FrontControllerServletV1() {
        controllerMap.put("/공통 요청 url/controller1", new
                Controller1());
        controllerMap.put("/공통 요청 url/controller2", new
                Controller2());
        controllerMap.put("/공통 요청 url/controller3", new
                Controller3());
    }
    @Override
    protected void service(HttpServletRequest request, HttpServletResponse
            response)
            throws ServletException, IOException {
        String requestURI = request.getRequestURI();
        Controller controller = controllerMap.get(requestURI);
        if (controller == null) {
            response.setStatus(HttpServletResponse.SC_NOT_FOUND);
            return;
        }
        controller.process(request, response);
    }
}

각각의 역할에 따른 여러 컨트롤러들이 존재한다. 프론트 컨트롤러에서는 우선 이 컨트롤러들을 전부 보관해둔다.

공통 url로 요청이 들어오면 프론트 컨트롤러의 service 메소드가 자동으로 실행된다.

 

세부 url에 따라서 해당 비즈니스 로직을 실행할 컨트롤러를 가져온다.

Controller controller = controllerMap.get(requestURI);

가져오고나면 request와 response를 인자로 넘겨주어 해당 컨트롤러의 process 메소드를 실행한다.

controller.process(request, response);

 

controller의 경우 이제 더이상 HttpServlet을 implement하지 않는다.

public class Controller1 implements Controller {

    @Override
    public void process(HttpServletRequest request, HttpServletResponse
            response) throws ServletException, IOException {

        //비즈니스 로직 실행
        request.setAttribute("key", 보관할 데이터);
        

        String viewPath = "jsp 파일 보관 경로/view.jsp";
        RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
        dispatcher.forward(request, response);
    }
}

위와 같이 process 함수가 존재하고 이 인자로 request와 response를 주기 때문이다.

 

이 process에서 비즈니스 로직을 실행하고 viewPath에 해당하는 곳으로 이동하는 과정을 거친다.

 

그런데 controller마다 viewPath를 생성하고 dispatcher의 forward 함수를 실행하는 과정은 여전히 중복된다.

 

이러한 과정을 공통 처리하기 위해 MyView라는 클래스를 생성한다.

 

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

이제 MyView의 render 메소드를 이용하여 원하는 viewPath로 이동하는 과정을 공통 처리 할 수 있게 되었다.

 

이 과정은 모든 컨트롤러가 가지고 있는 과정이기 때문에 컨트롤러에 이 과정을 진행하는게 아니라 프론트 컨트롤러에게이 역할을 넘겨주는 것이 좋다.

 

그렇게 되면 컨트롤러의 process 메소드는 다음과 같이 변한다.

public class Controller1 implements Controller {

    @Override
    public void process(HttpServletRequest request, HttpServletResponse
            response) throws ServletException, IOException {

        //비즈니스 로직 실행
        request.setAttribute("key", 보관할 데이터);
        
        return new MyView("jsp 파일 보관 경로/view.jsp");


    }
}

컨트롤러의 process 메소드를 실행하는 주체는 프론트 컨트롤러이기 때문에 프론트 컨트롤러는 이 MyView 객체를 받아서 render 메소드를 실행하게 된다.

 

그래서 프론트 컨트롤러는 다음과 같이 변한다.

@WebServlet(name = "이름", urlPatterns = "/공통 요청 url/*")
public class FrontControllerServlet extends HttpServlet {
    private Map<String, Controller> controllerMap = new HashMap<>();
    public FrontControllerServletV1() {
        controllerMap.put("/공통 요청 url/controller1", new
                Controller1());
        controllerMap.put("/공통 요청 url/controller2", new
                Controller2());
        controllerMap.put("/공통 요청 url/controller3", new
                Controller3());
    }
    @Override
    protected void service(HttpServletRequest request, HttpServletResponse
            response)
            throws ServletException, IOException {
        String requestURI = request.getRequestURI();
        Controller controller = controllerMap.get(requestURI);
        if (controller == null) {
            response.setStatus(HttpServletResponse.SC_NOT_FOUND);
            return;
        }
        MyView view = controller.process(request, response);
        view.render();
    }
}

처음과 비교해보면 컨트롤러의 코드가 확실히 줄어들었고 프론트 컨트롤러가 각각의 컨트롤러가 공통적으로 수행하는 작업을 수행한다.

 

지금까지 바뀐 점

  1. 컨트롤러가 직접 forward 함수를 사용하지 않고 viewPath에 대한 정보를 MyView 객체에 담아서 반환한다.
  2. 프론트 컨트롤러에서 MyView 객체를 받아서 그 안의 render 메소드를 실행하여 원하는 경로로 이동한다.

 

아직 개선해야할 부분이 존재한다.

 

각각의 컨트롤러는 HttpServletRequest와 HttpServletResponse를 필요로 하고 이를 프론트 컨트롤러로부터 받아서 사용한다.

 

request를 넘겨주는 이유는 여기에 담긴 파라미터 정보를 사용하기 위함인데 그냥 Model을 하나 만들어서 HttpServletRequest의 모든 파라미터 정보들을 담아서 컨트롤러에게 넘겨주면 각각의 컨트롤러들은 서블릿 기술들을 몰라도 되는 것이다.

 

또한 jsp 파일들은 같은 폴더 안에 존재하고 확장자 또한 .jsp로 같기 때문에 이를 공통 처리 할 수 있다.

 

이를 개선한 과정을 살펴보면 먼저 프론트 컨트롤러에서 Map을 만든다.

 

Map<String, String> paramMap = createParamMap(request);

 

위 createParamMap 함수는 HttpServletRequest를 인자로 받으면 여기에 담겨있는 모든 파라미터 정보들을 Map에 담아서 반환해주는 함수이다.

 

이제 이 Map을 컨트롤러에게 넘겨주면 컨트롤러는 서블릿 기술들과는 별개로 동작하게 되는 것이다.

 

이는 컨트롤러의 process 함수의 인자로 넘겨줄 예정이다.

 

컨트롤러의 process 함수에서는 map의 정보를 활용하여 비즈니스 로직을 실행하고 viewPath와 view에 전달할 데이터를 반환해야 한다.

 

이를 위한 새로운 ModelView라는 클래스를 만든다.

@Getter
@Setter
public class ModelView {
    private String viewName;
    private Map<String, Object> model = new HashMap<>();
    public ModelView(String viewName) {
        this.viewName = viewName;
    }
}

ModelView에 존재하는 viewName은 jsp 파일의 이름을 뜻한다.

 

jsp 파일들을 존재하는 경로가 동일하고 확장자 또한 동일하기 때문에 jsp 파일의 이름만 가지고 있으면 이를 공통 처리 할 수 있다.

 

model은 view에 전달할 데이터들이 담긴 공간이다. 이를 사용하여 view 파일은 화면을 렌더링 할 수 있다.

 

이제 controller의 process 메소드는 MyView 객체를 반환하는 것이 아닌 이 ModelView 객체를 반환하게 된다.

 

그러면 프론트 컨트롤러에서는 다음과 같이 ModelView 객체를 사용하여 렌더링하게 된다.

ModelView mv = controller.process(paramMap);
String viewName = mv.getViewName();
MyView view = viewResolver(viewName);
view.render(mv.getModel(), request, response);

MyView의 render 함수의 경우 view에 전달할 데이터들을 가지고 있는 model 또한 인자로 필요하게 된다.

 

위 코드에서 viewResolver는 jsp 파일의 이름만 가지고 앞에 jsp 파일들이 위치한 경로와 뒤에 확장자를 조합하여 이동할 url을 생성해내는 메소드이다.

 

지금까지 바뀐 점

  1. 컨트롤러에게 Map을 넘격주기 때문에 컨트롤러는 서블릿 기술들을 몰라도 된다.
  2. viewResolver를 사용하여 경로와 url을 공통 처리 했기 때문에 jsp 파일의 이름만 필요로 한다.

컨트롤러에서 파라미터 정보가 담긴 Map을 얻으면 이를 사용하여 비즈니스 로직을 실행한 다음 viewName과 view 전달할 정보들이 담긴 model을 생성하여 ModelView에 담아서 반환한다.

 

그런데 이렇게 컨트롤러마다 ModelView를 생성해서 반환하기 보다는 다음과 같은 방법을 사용할 수 있다.

 

  1. 컨트롤러는 viewName만 반환한다.
  2. model은 컨트롤러에서 생성하는 것이 아니라 애초에 프론트 컨트롤러에서 빈 model을 생성해서 컨트롤러에 넣어준다. (model은 view에 전달할 데이터가 담긴 공간)
  3. 컨트롤러에서는 받은 model에 view에 전달할 데이터들을 담아준다.

위와 같이 구현하면 컨트롤러에서는 매번 ModelView 객체를 생성하지 않는다.

 

그 대신 model을 프론트 컨트롤러에게서 받고 여기에 값들을 넣어주고 viewName을 반환하면

 

프론트 컨트롤러에서는 viewResolver에서 viewName을 사용하여 url을 만들고 이를 담고 있는 MyView 객체를 생성한다.

 

그리고 프론트 컨트롤러에서 이미 만든 model에는 컨트롤러가 전달할 정보들을 넣어두었기 때문에 MyView 객체의 render 함수에 이 model을 담아주면 된다.

 

이로써 일일이 ModelView 객체를 생성하지 않아도 된다.

 

 

728x90
반응형

댓글