본문 바로가기
Spring/스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술

MVC 프레임워크 만들기 1 - 프론트 컨트롤러

by hk27 2022. 1. 22.
직접 MVC 프레임워크를 만들어 봅시다! 

 

안녕하세요!

오늘은 MVC 패턴을 따르는 프레임워크를 만들어보겠습니다.

 

지난 시간에 MVC 패턴을 적용해서 회원 관리 웹을 만들어보았습니다. 관심 있으신 분은 참고해주세요! 

https://passionate.tistory.com/39

 

MVC 패턴을 적용해서 회원 관리 웹 만들기

안녕하세요. 이번 게시글에서는 MVC 패턴을 적용해서 회원 관리 웹을 만들어보겠습니다. 서블릿과 JSP의 한계 앞서 서블릿(https://passionate.tistory.com/37)과 JSP(https://passionate.tistory.com/38)로 회원..

passionate.tistory.com

 

컨트롤러와 뷰를 분리하니 코드가 깔끔해졌습니다.

그런데 여전히 문제가 있었습니다.

코드에 중복이 많고, 불필요한 코드가 많다는 점이었습니다.

 

오늘은 프론트 컨트롤러로 문제를 해결해보겠습니다. 

 

프론트 컨트롤러(Front Controller)

프론트 컨트롤러 도입 전의 흐름은 아래 사진과 같습니다.

클라이언트가 URL을 입력해서 서버를 호출하면 Controller 서블릿이 불리는 방식입니다. 

 

프론트 컨트롤러 도입 후의 동작은 아래와 같습니다.

프론트 컨트롤러 서블릿이 클라이언트의 요청을 받고, 요청에 맞는 컨트롤러를 찾아서 호출해줍니다.

프론트 컨트롤러 하나로 입구가 정해지고, 서버의 수문장 역할을 담당합니다. 

이렇게 프론트 컨트롤러를 두면, 공통 처리를 할 수 있다는 큰 장점이 있습니다.

클라이언트의 모든 요청에 대해 공통으로 처리하는 부분은 프론트 컨트롤러에서 담당하면 각 컨트롤러는 필요한 역할만 수행하면 되고, 코드가 간결해집니다.

프론트 컨트롤러를 단계적으로 도입해봅시다. 

 

프론트 컨트롤러 도입 - V1

첫 번째로 만들 구조는 아래와 같습니다.

클라이언트의 요청이 들어오면 프론트 컨트롤러가 URL 매핑 정보를 바탕으로 컨트롤러를 찾고, 호출합니다.

컨트롤러가 작업 종료 후 JSP로 포워딩(화면 이동)하여 html 응답을 보냅니다.

코드를 작성해봅시다. 

 

컨트롤러 인터페이스

public interface ControllerV1 {
    void process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException;
}

컨트롤러 인터페이스에 process 메소드를 만듭니다. request, response 객체를 받아서 작업을 수행합니다.

 

회원 등록 컨트롤러

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

회원을 등록 폼을 만드는 컨트롤러입니다.

viewPath에 JSP 파일의 경로를 지정하고 RequestDispatcher 객체를 활용해 포워딩합니다.

 

회원 저장 컨트롤러

public class MemberSaveControllerV1 implements ControllerV1 {
    private MemberRepository memberRepository = MemberRepository.getInstance();
    @Override
    public void process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        String name = request.getParameter("username");
        int 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);
    }
}

회원을 저장하는 컨트롤러입니다. 회원 등록 컨트롤러보다 역할이 많습니다.

먼저 파라미터로 넘어온 사용자의 이름과 나이를 바탕으로 멤버 객체를 만들고 저장소에 저장하며, request 객체에 데이터를 담습니다. 이후 viewPath를 지정해서 포워딩하는 부분은 회원 등록 컨트롤러와 유사합니다.

 

회원 목록 컨트롤러

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

회원 목록 컨트롤러는 저장소에서 모든 객체를 조회하고, request의 attribute로 데이터를 담습니다.

여기서도 화면을 만들기 위해서 viewPath를 지정하고 포워딩합니다.

 

컨트롤러 구현체 3개의 흐름이 유사합니다. 각자의 작업을 수행하고 -> viewPath를 지정해서 forward 합니다. 

 

프론트 컨트롤러

가장 중요한 프론트 컨트롤러를 봅시다.

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

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

프론트 컨트롤러는 서블릿입니다. v1/으로 들어온 모든 요청을 받습니다. 

중요한 것은, 생성자에서 맵에 URI와 컨트롤러를 저장해둡니다. 

요청이 들어오면 URI 정보를 받아서 적합한 컨트롤러를 찾고, 컨트롤러의 process 메소드를 수행합니다.

 

프론트 컨트롤러를 도입해서 코드를 완성하였습니다. 

모든 요청이 프론트 컨트롤러를 거치게 되었습니다.

 

이제 중복을 제거해보겠습니다.

컨트롤러에서 viewPath를 정하고 RequestDispatcher를 불러서 forward 하는 코드가 중복됩니다.

이 부분을 프론트 컨트롤러에서 한 번에 처리하면 좋을 것입니다. 

이러한 아이디어로 V2를 만들어봅시다. 

 

View 분리 - V2

V2의 구조는 아래 사진과 같습니다. 

먼저 별도로 뷰를 처리하는 MyView를 만들어봅시다.

 

객체가 왜 필요하지? 싶을 수 있습니다. 프론트 컨트롤러에서 작업을 수행해도 되니까요.

그러나 단일 책임 원칙으로, 뷰 처리를 전담하는 객체를 만들고 프론트 컨트롤러에서는 호출하는 역할만 수행하면 코드를 유지 보수하기 더 좋습니다.

 

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

 

필드로 viewPath를 갖고, render 메소드가 있습니다.

render는 viewPath로 forward를 수행합니다. 

각 controller에 있던 코드가 render 메소드에 들어갔음을 볼 수 있습니다.

 

컨트롤러 인터페이스

public interface ControllerV2 {
    MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException;
}

위에서 본 V1 컨트롤러 인터페이스와의 가장 큰 차이점은 MyView를 반환한다는 것입니다. 

원래 컨트롤러는 process 메소드를 수행하고 아무 값도 반환하지 않았는데, 이제는 MyView 객체를 반환합니다.

 

컨트롤러 

컨트롤러 코드는 수정된 부분을 위주로 살펴봅시다. 

/* 1. 회원 등록 폼
	String viewPath = "/WEB-INF/views/new-form.jsp";
	RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
	dispatcher.forward(request, response);
*/
return new MyView("/WEB-INF/views/new-form.jsp");

/* 2. 회원 저장
	String viewPath = "/WEB-INF/views/save-result.jsp";
	RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
	dispatcher.forward(request, response);
 */
return new MyView("/WEB-INF/views/save-result.jsp");

/* 3. 회원 목록
	String viewPath = "/WEB-INF/views/members.jsp";
	RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
	dispatcher.forward(request, response);
 */
return new MyView("/WEB-INF/views/members.jsp");

중복이 있던 코드는 사라지고, return 줄이 추가된 것을 볼 수 있습니다.

URI를 사용해서 MyView 객체를 만들어서 반환합니다.

 

프론트 컨트롤러

프론트 컨트롤러도 수정된 부분을 위주로 살펴보겠습니다.

// controller.process(request,response);

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

프론트컨트롤러에서 MyView 객체를 받고, render 메소드를 호출합니다.

render 메소드에서 JSP파일로 포워드됩니다.

 

프론트 컨트롤러의 도입으로 뷰 처리 로직의 중복을 없앨 수 있었습니다.

 

다음 게시글에서는 서블릿 종속성을 제거하고, 뷰 이름의 중복을 제거해보겠습니다.

https://passionate.tistory.com/41

 

MVC 프레임워크 만들기 2 - 모델 추가, 실용적인 컨트롤러

안녕하세요! 오늘은 지난 게시글에 이어서 MVC 프레임워크를 발전 시켜 보겠습니다. 지난 게시글에는 프론트 컨트롤러 패턴을 도입해서 코드 중복을 제거하였습니다. 관심 있는 분들은 아래 게

passionate.tistory.com

 

인프런  '스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술' 강의를 듣고 공부하며 정리한 자료입니다. 

잘못된 부분은 피드백 주시면 감사하겠습니다. 

글 읽어주셔서 감사합니다 :-)

 

참고 자료

스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술, 섹션 4. MVC 프레임워크 만들기 https://www.inflearn.com/course/스프링-mvc-1

 

 

 

댓글