Spring/MVC

스프링 MVC 밑바닥부터 만들어보기4 - 프론트 컨트롤러와 view의 분리

ssung 2022. 4. 14. 14:53

프론트 컨트롤러는 개별적인 컨트롤러에 들어가기전에 가장 먼저 들어오게되는 컨트롤러이다. 프론트 컨트롤러를 사용한 방식을 '프론트 컨트롤러 패턴' 이라고 부르는데 스프링 MVC도 프론트 컨트롤러 패턴으로 구성되어있다.

 

 

가정먼저 프론트 컨토를러의 구조를 만들어보고 그 이후에 세세한 부분들을 바꿔가면서 진행해보도록 하겠다.

전반적인 구조는 프론트 컨트롤러, 컨트롤러 인터페이스, 컨트롤러로 짜여질 것이다.

 

 

요청의 흐름을 먼저 살펴보자면

이런 흐름으로 처리 될 것이다.

 

패키지는 package hello.spring_mvc_study.web.frontcontroller.v1 에서 진행될 것이고 버전이 바뀔때마다 v1이라는 패키지명만 변경될 것이다. JSP는 앞에 만들었던 파일을 그대로 사용할 예정이다.

 

 

 

 

Controller V1

가장 먼저 컨트롤러 인터페이스를 만들어주자. 이제 부터는 프론트 컨트롤러에서 연결되는 작업이기 때문에 다형성을 위해서 인터페이스를 만들고 구현체를 만들어 갈 것이다.

public interface ControllerV1 {

    void process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException;
}

 

다음으로 Form컨트롤러, Save컨트롤러, List컨트롤러를 만들건데 컨트롤러에 들어가는 로직은 앞전에 만든 ServletMVC와 동일하다. 다른점은 인터페이스를 상속받았다는 것과 더이상 서블릿을 사용하지 않는다는 점이다.

public class MemberFormControllerV1 implements ControllerV1 {

    @Override
    public void process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        String path = "/WEB-INF/views/new-form.jsp";
        RequestDispatcher dispatcher = request.getRequestDispatcher(path);
        dispatcher.forward(request, response);
    }
}
public class MemberSaveControllerV1 implements ControllerV1 {

    private MemberRepository memberRepository = MemberRepository.getInstance();

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

        String path = "/WEB-INF/views/save.jsp";
        RequestDispatcher dispatcher = request.getRequestDispatcher(path);
        dispatcher.forward(request, response);
    }
}
public class MemberListControllerV1 implements ControllerV1 {

    private MemberRepository memberRepository = MemberRepository.getInstance();

    @Override
    public void process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        List<Member> members = memberRepository.findAll();
        request.setAttribute("members", members);

        String viewPath = "/WEB-INF/views/members.jsp";
        RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
        dispatcher.forward(request, response);
    }
}

잘 보면 인터페이스를 상속받으면서 더이상 서블릿으로 만드는 것이 아닌 그냥 클래스로 만들어진 것을 알 수 있다. 이제 가장처음 요청을 받는 프론트 컨트롤러만 서블릿으로 처리하고 나머지는 일반 클래스로 처리 될 것이다.

 

 

그렇다면 URL을 어떻게 매핑해서 해당 컨트롤러로 전달할 수 있을까? 이것도 프론트 컨트롤러에서 처리 될 예정이다.

로직은 앞의 내용과 동일하기 때문에 특별히 설명하지 않겠다.

 

 

이제 대망의 프론트 컨트롤러를 만들어보자.

@WebServlet(name = "frontControllerServiceV1", urlPatterns = "/front-controller/v1/*")
public class FrontControllerServiceV1 extends HttpServlet {

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

    public FrontControllerServiceV1() {
        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 request, HttpServletResponse response) throws ServletException, IOException {
        String requestURI = request.getRequestURI();

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

        controller.process(request, response);
    }
}

먼저 urlPatterns를 살펴보면 /front-controller/v1/* 이라고 되어있는데 fron-controller/v1 하위로 들어오는 모든 url을 이 컨트롤러에서 받겠다는 의미이다.

그리고 Map을 이용해서 컨트롤러를 매핑할 수 있도록 준비한다. 프론트 컨트롤러가 호출되면 Map에 각 컨트롤러를 매핑할 URL이 저장된다. 이것은 해당 URL이 key값으로 들어오면 매핑시킬 컨트롤러를 반환하겠다는 의미이다.

 

이제 본격 로직을 살펴보면 먼저 request.getRequestURI()를 이용해서 URI를 가져오고 있다. 이 메소드를 사용하면 사용자가 입력한 URI를 가져올 수 있다.

예를 들어 http://localhost:8080/front-controller/v1/members/new-form라는 주소로 클라이언트가 요청한다면 /front-controller/v1/members/new-form 이라는 값을 가져올 수 있게된다.

 

가져온 URI를 Map에 조회해서 존재한다면 매핑키실 컨트롤러를 반환받는다.

if문을 이용해서 해당하는 URI가 없다면 404코드를 반환하게 하고 해당 URI가 존재한다면 process를 호출해서 해당 컨트롤러를 동작시킨다. 그렇게 되면 컨트롤러에서 로직을 처리하고 JSP로 forward를 보내며 view까지 전달 될 것이다.

 

 

 

이렇게해서 프론트 컨트롤러의 구조를 완성했다. 생각보다 어렵지 않게 느껴질 수 있는데 아직은 구조만 만든 상태이고 이제부터 세부적인 문제들을 고쳐 갈 예정이다. 아직은 SpringMVC와는 거리가 있는것 같지만 그래도 조금씩 가까워져 가는게 보인다.

 

 

 

 

 

 

Controller V2

컨트롤러를 보면 view를 호출하는 로직이 계속해서 중복되고있다. 이 부분을 따로 분리해보도록 하자.

뷰를 호출 하는 클래스인 MyView를 만들어주고 모든 컨트롤러에서 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);
    }
}

jsp를 호출하는 경로인 viewPath를 받는다. 이 경로는 컨트롤러가 넘겨주는 경로를 받는 것이다.

프론트 컨트롤러에게 request와 response를 전달받아 forward하여 jsp를 호출할 수 있다.

 

 

 

컨트롤러에서 MyView를 반환해야 되기 때문에 인터페이스의 return타입도 변경해주자.

public interface ControllerV2 {

    MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException;
}

 

그리고 컨트롤러 3개를 만들자. 이것도 위의 코드와 거의 동일하지만 return타입과 view를 랜더링하는 부분만 변경되었다.

public class MemberFormControllerV2 implements ControllerV2 {

    @Override
    public MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        return new MyView("/WEB-INF/views/new-form.jsp");
    }
}
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.jsp");
    }
}
public class MemberListControllerV2 implements ControllerV2 {

    private MemberRepository memberRepository = MemberRepository.getInstance();

    @Override
    public MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        List<Member> members = memberRepository.findAll();
        request.setAttribute("members", members);

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

원래 코드와 비교해보면 훨씬 깔끔해졌다. forward부분은 jsp의 경로를 MyView에 담아서 return하도록 바뀐걸 알 수 있다. 이렇게 반환된 MyView를 프론트 컨트롤러가 받는다.

 

 

 

@WebServlet(name = "frontControllerServiceV2", urlPatterns = "/front-controller/v2/*")
public class FrontControllerServiceV2 extends HttpServlet {

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

    public FrontControllerServiceV2() {
        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 request, HttpServletResponse response) throws ServletException, IOException {
        String requestURI = request.getRequestURI();

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

        MyView myView = controller.process(request, response);
        myView.render(request, response);
    }
}

프론트 컨트롤러도 앞의 코드와 거의 동일하지만 컨트롤러가 반환한 MyView를 받아서 render를 호출하여 forward하는 부분만 변경되었다.

 

 

이렇게 MyView를 호출함으로서 전체 흐름은 조금 더 복잡해진 것 같지만 코드는 훨씬 간결해지고 역할도 적절하게 나누어지고 있다. 하지만 아직도 개선할 부분이 많이 남았다. 지금은 Model부분을 request에 담아서 전송하고 있지만 이것을 대체할 것도 필요하고 그렇게 되면서 request와 response도 필요없게되면 없어져도 될 것 같다.

 

 

 

 

 

 

 

모든 내용은 인프런의 김영한님의 강의를 참고하여 작성되었습니다.

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

 

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

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

www.inflearn.com

 

 

사용된 전체 코드는 깃허브에서 보실 수 있습니다.

https://github.com/ssung0810/spring_study/tree/main/spring_mvc_study

 

GitHub - ssung0810/spring_study

Contribute to ssung0810/spring_study development by creating an account on GitHub.

github.com