개발자로 후회없는 삶 살기

spring PART.MVC 프레임워크 만들기 본문

[백엔드]/[spring | 학습기록]

spring PART.MVC 프레임워크 만들기

몽이장쥰 2023. 4. 11. 00:38

서론

※ 이 포스트는 다음 강의의 학습이 목표임을 밝힙니다.

https://www.inflearn.com/roadmaps/373

 

 

우아한형제들 최연소 기술이사 김영한의 스프링 완전 정복 - 인프런 | 로드맵

Spring, MVC 스킬을 학습할 수 있는 개발 · 프로그래밍 로드맵을 인프런에서 만나보세요.

www.inflearn.com

 

 

본론

 

- mvc 프레임워크 만들기

이전에 한 mvc는 한계점이 많았습니다. 이를 개선하는 mvc 프레임워크를 밑바닥부터 하나씩 만들어 볼 것입니다. 이를 위해서 프론트 컨트롤러 패턴을 소개합니다.

이렇게 업그레이드 해가면 스프링 프레임워크와 유사한 모양이 됩니다. 그러면 스프링을 나중에 사용할 때 스프링의 기능, 사용하는 목적, 이유, 구조를 이해할 수 있습니다.

 

- 프론트 컨트롤러 패턴 소개

도입 전에는 클라가 예를 들어 공통 로직이 필요하면 공통 로직을 깔고 각각 컨트롤러를 호출하고 했어야했습니다. 입구가 없고 아무대나 다 들어올 수 있기에 공통 로직을 모든 입구에 다 넣어야합니다.

 

> 프론트 컨트롤러를 도입하면(얘도 서블릿입니다.) 여기에 공통로직을 모으고 처리합니다. 공통의 관심사를 별도로 모으는 컨트롤러를 앞에 도입하는 것입니다. 항상 프론트 컨트를 통해서 다음 컨트를 호출합니다.

 

-> 특징

프론트 컨트롤러도 서블릿입니다. 그냥 서블릿을 하나 두는 것이고 이 서블릿 하나로 클라이언트 요청을 다 받는습니다.(스프링에서 컨트롤러에 mapping 메서드 두는 느낌?) 그리고 프론트 컨트가 요청에 맞는 컨트롤러를 찾아서 호출합니다. 예전에는 고객의 요청에 맞는 컨트롤러가 직접 호출됐는데 이제는 프론트 컨트롤러가 요청을 다 받고 필요한 공통 처리를 한 후에 "이 요청은 컨트 A한테 가야겠구나" 싶으면 추가로 호출합니다.

 

> 프론트 컨트롤러를 제외한 나머지 컨트롤러는 서블릿을 사용하지 않아도 됩니다. 왜냐하면 요청 맵핑을 할 때 서블릿을 사용해서 요청을 했습니다. 그러니 프론트 컨트롤러가 직접 다른 컨트를 호출해 줄 것이니 서블릿이 필요없습니다. 서블릿으로 만드는 것은 url 맵핑을 해서 컨트롤러를 호출하기 위함이었는데 이것을 프론트 컨트가 대신 해주기 때문에 나머지 컨트는 서블릿을 사용하지 않아도 됩니다. HttpServlet 상속받는 것과 @Webservlet 상속 안 합니다.

 

> 스프링 웹 mvc의 핵심이 바로 프론트 컨트 패턴입니다. 스프링이 디스패쳐 서블릿이 있는데 얘가 제일 핵심입니다. 스프링도 사실 서블릿이 앞에 하나 있는 것입니다. 근데 그 서블릿의 기능이 굉장히 많은 것입니다. 디스패쳐 서블릿이 앞에 있어서 스프링이 되는 것이고 디스패쳐 서블릿이 프론트 컨트 패턴으로 구현이 되어있습니다.

 

-  V1 프론트 컨트롤러 도입

기존 코드를 유지하면서 프론트 컨트롤러만 도입해보자 클라가 http 요청을 하면 프론트 컨트가 요청을 받을 것입니다. 어떤 요청이든 여기서 받습니다. 프론트가 다른 컨트를 호출한다고 했으니 어떻게 호출하면 되는지 하는 맵핑 정보를 여기 넣어둡니다. /A라고 오면 컨트 A가 호출되게 합니다. 요청이 오면 맵핑 정보를 뒤져서 어떤 컨트롤러를 호출하면 되는지 봐서 컨트롤러를 호출하고 호출된 컨트롤러는 앞에서 봤던 대로 JSP forward합니다.

 

 

> 컨트롤러를 인터페이스로 만들것입니다. 다형성을 위해서입니다. web에 frontcontroller 패키지를 만듭니다. 이 안에 V1 패키지를 만듭니다. 여기에 인터페이스를 만듭니다. 이게 v1 프론트 컨트롤러입니다. 여기에 서블릿의 서비스와 똑같은 모양의 process 메서드를 가진 컨트롤러를 만듭니다.

 

public interface ContV1 {
    void process (HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException;
}

여기에 인터페이스를 만듭니다. 이게 v1 프론트 컨트롤러입니다. 여기에 서블릿의 서비스와 똑같은 모양의 process 메서드를 가진 컨트롤러 인터페이스를 만듭니다. (이렇게 만든 이유가 이 인터페이스를 구현한 것들이 컨트롤러들이기 때문입니다. 컨트롤러가 예전에 서블릿으로 만들었습니다.)

 

> 서블릿과 비슷한 모양의 인터페이스를 도입합니다. 각 컨트롤러들은 이 인터페이스를 구현합니다. 구현 컨트롤러는 form, list, save로 이게 컨트 A, B, C이다. 그렇게 하면 http 요청이 오면 프론트 컨트롤러는 맵핑 정보를 봐서 일관성이 있게 다형성을 활용해서 인터페이스에 의존하면서 다른 컨트롤러를 편리하게 호출할 수 있습니다.

 

-> 컨트롤러 구현하기

 

v1 밑에 controller 패키지를 만들고 이 안에 컨트롤러를 구현할 것입니다. 일단은 기존 코드를 유지할 것이라고 했으니 기존 코드 그대로 가져오면 됩니다. 회원 등록 컨트롤러를 만듭니다.

 

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

mvc 패턴에서 했던 MvcMemberFormServlet 코드를 그대로 가져옵니다. 정말 말 그대로 회원 등록 컨트롤러를 또 만드는 것인데 그냥 인터페이스를 부모로 두어 프론트 컨트롤러가 다형성으로 호출하기 편리하게 하기 위함입니다. (이게 프론트 패턴을 쓰는 이유입니다. 입구를 두는 것입니다.)

 

> 뷰도 동일하게 사용할 것입니다. 원래 쓰던 뷰 그대로 쓸 것입니다. 이렇게 하기 위해서 다른 프로젝트에서도 경로를 그대로 쓰기 위해서 form 태그에 save를 상대경로로 했습니다.

public class MemberSaveControllerV1 implements ControllerV1 {
    MemberRepository memberRepository = MemberRepository.getInstance();
    @Override
    public void process(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String username = req.getParameter("username");
        int age =Integer.parseInt(req.getParameter("age"));
        Member member = new Member(username, age);
        memberRepository.save(member);

        req.setAttribute("member", member);

        String viewPath = "/WEB-INF/views/save-result.jsp";
        RequestDispatcher requestDispatcher = req.getRequestDispatcher(viewPath);
        requestDispatcher.forward(req, resp);
    }
}

저장 컨트롤러 역시나 구현을 해서 컨트롤하고 이전 서블릿과 같은 코드를 복사합니다.

 

public class MemberListControllerV1 implements ControllerV1 {
    MemberRepository memberRepository = MemberRepository.getInstance();
    @Override
    public void process(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        List<Member> members = memberRepository.findAll();

        req.setAttribute("members", members);

        String viewPath = "/WEB-INF/views/members.jsp";
        RequestDispatcher requestDispatcher = req.getRequestDispatcher(viewPath);
        requestDispatcher.forward(req, resp);
    }
}

회원 목록 컨트롤러 역시나 같게 만듭니다.

 

=> 대망의 프론트 컨트롤러

v1 패키지에 FrontControllerServletV1 클래스를 만듭니다. 얘는 서블릿이어야한다고 했습니다. extends와 @어노를 붙입니다. url 패턴이 중요합니다. *을 붙이는데 이렇게 하면 v1/하위에 어떤 url이 들어와도 일단 이 서블릿이 무조건 호출됩니다. 하위에 들어오는 애들은 일단 무조건 얘가 호출이 됩니다. 이렇게 맵핑이 됩니다.

 

-> 맵핑 정보

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

    public FrontControllerServletV1() {
        controllerMap.put("/front-controller/v1/members/new-form", new MemberFromControllerV1());
        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 {
        super.service(req, resp);
    }
}

어떤 url이 호출되면 그에 맞는 컨트롤러를 호출하라고 할 것입니다. 그래서 Map에 Str과 컨트롤러는 그에 맞는 타입이 일관성있게 하기 위해서 부모인 인터페이스 타입을 넣습니다. 

 

> 생성자에다가 맵핑 정보를 이 서블릿이 생성될 때 미리 담아 놓을 것입니다. key url로 요청이오면 value 컨트롤러 객체의 process를 호출하겠습니다. put해서 url을 넣는데 프론트 컨트가 호출되는 경로와 완전 똑같이 넣어야합니다. *부분만 다르게 해야합니다. new-form이라고 오면 뒤에 new가 실행되게 합니다. 그니깐 일단 프론트에 url을 가지는 호출이 오면 프론트가 호출되게 하는데 그떄 그에 맞는 진짜 컨트롤러를 호출해 줄 것입니다.

> 이것을 save, list 컨트롤러랑 다 맞춰놓습니다. 이 서블릿이 처음에 생성이 될 때 맵핑 정보를 미리 저장해 놓는 것입니다. 지금 보니 save를 form 태그에서 미리 지정해 놓으면 무조건 new-form에서 save로 바꿨습니다.

 

-> 서비스 코드

@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    String requestURI = req.getRequestURI();

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

    controller.process(req, resp );
}

로직을 만들어보자 그림을 보면 처음에 요청이 오면 맵핑 정보를 통해서 어떤 컨트가 호출되야하는지 찾아야하고 그 컨트롤러를 호출해서 jsp를 forward하고 jsp가 html로 응답이 가게 할 것입니다.

> 제일먼저 requestURI라고 있습니다. 이렇게 하면 URL 엔터에서 포트 뒤에 오는 맵핑 메서드 부분을 뽑을 수 있습니다. 이를 뽑아서 어떤 컨트를 호출해야하는지 보고 그에 맞는 컨트를 꺼냅니다. 부모인 인터로 꺼내야 편리하게 호출할 수 있습니다. 이렇게 하면 어떤 구현체 컨트롤러가 호출되어 다 다른 컨트롤러를 new 해야하더라도 인터페이스로 설계하여 일관성있게 코드를 사용할 수 있습니다. 

> null로 없으면 response에 404를 주도록 해야합니다. 404이면 바로 return을 합니다. url이 잘 온 것이면 나머지 컨트롤러의 process를 호출하면 됩니다. 이렇게 하면 끝이납니다. 이렇게 프로세스에 프론트 컨트의 req, resp를 넘겨주기 때문에 나머지 컨트는 서블릿이 아니어도 됩니다. 다형성을 쓴 부분을 잘 이해해야합니다.

 

-> 결과

 

form에 저장하고 전송해보면 save 결과 jsp가 뜹니다. 이것으로 나머지 컨트롤러가 호출되었다는 것을 알 수 있습니다. *로 프론트가 실행이 되고 나머지 컨트롤러가 호출되는 것입니다. 근데 지금 보면 프론트 컨트가 나머지 컨트의 메서드를 실행하는 것입니다. 이게 나머지 컨트는 진짜 그냥 서블릿이 없으니 그냥 클래스의 메서드이기 때문에 프론트가 나머지 컨트를 생성하고 그 레퍼런스로 메서드를 호출하는 개념이 됩니다. > 이것을 점점 바꿔가면서 깔끔하게 프론트 컨트를 도입할 것입니다.

 

-> url 정리

/hsb/*로 하면 /hsb를 포함한 하위 모든 요청은 이 서블릿에서 받아들입니다. /hsb/fsf, /hsb/fsdffsff 이 다 여기서 받습니다. 이는 url 엔터를 해도 프론트가 호출됩니다.

이제부터 불편했던 점들을 하나씩 바꿔보겠습니다. 그러다 보면 프론트 컨트롤러가 들어간 MVC인 스프링 프레임워크가 생길 것입니다.

 

- V2 뷰 분리

모든 컨트롤러에서 뷰로 이동하는 부분에 중복이 있고 깔끔하지 않았습니다. 모든 컨트에서 뷰로 이동해야하기 때문에 forward하는 이 부분이 모든 컨트롤러에서 중복이 있었습니다. 이를 깔끔하게 하기 위에 이를 처리하는 전담 view라는 객체를 만들 것입니다.

 

-> 그림

요청이오면 프론트가 맵핑 정보에서 컨트롤러를 호출합니다. 원래는 이 컨트롤러에서 jsp로 직접 forward 했는데 이제는 그러지 않고 myview라는 뷰 역할을 하는 객체를 만들어서 반환을 하면 프론트에서 대신 my의 render를 호출하면 my가 jsp로 foward하도록 할 것입니다. 컨트롤러가 더이상 jsp 포워드에 대해 고민하지 않아도 됩니다. 이제 단순하게 myview만 생성해서 반환하면 됩니다. 정말 컨트롤러는 조종하는 역할만 하게 되는 것이고 정말 깔끔해집니다. 

 

-> myview

 

my를 만들 것입니다. 다른데서도 쓸 것이라서 프론트 컨트 패키지 밑에 myview 클래스를 만듭니다.

 

public class MyView {
    private String viewPath;

    public MyView(String viewPath) {
        this.viewPath = viewPath;
    }
    
    public void render(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        RequestDispatcher requestDispatcher = req.getRequestDispatcher(viewPath);
        requestDispatcher.forward(req, resp);
    }
}

얘는 viewpath를 가지고 생성자에서 초기화합니다. 그리고 기존에 jsp로 이동하는 것을 렌더링이라고 표현하는데 render 메서드로 req, resp를 받은 후에 서비스와 같은 모양을 가지고 여기서 디스패쳐 선언하고 forward하는 것을 작성합니다.

> 기존에 각 컨트에서 하는 로직을 myview에서 하도록 넣습니다. 그리고 jsp 뷰를 만드는 것을 렌더링한다고 합니다.

 

-> 컨트 v2

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

이제 컨트롤러 v2를 만들고 인터페이스를 만듭니다. 기존의 v1과 똑같은데 반환을 my를 반환하도록합니다. 기존에 process는 void였는데 myview를 반환하도록 인터를 설계합니다. v2입니다.

 

-> 구현하기

public class MemberFormControllerV2 implements ControllerV2 {
    @Override
    public MyView process(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        return new MyView("/WEB-INF/views/new-form.jsp");
    }
}

회원 등록 form 서블릿부터 만듭니다. 역시나 얘는 구현체로 서블릿으로 하지 않습니다. process를 오버라이딩하면 되는데 이전 코드를 보면 viewpath를 만들고 디스패쳐 선언하고 forword 했습니다. > 이제는 My를 생성하고 생성자로 path를 넣어주면 됩니다. 그리고 반환합니다. 이러면 끝입니다. 예전에 컨트롤러에 있던 디스패쳐의 forward가 사라집니다. 처음에 말했던 디스패쳐 중복이 사라집니다.

 

public class MemberListControllerV2 implements ControllerV2 {
    MemberRepository memberRepository = MemberRepository.getInstance();
    @Override
    public MyView process(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        List<Member> members = memberRepository.findAll();

        req.setAttribute("members", members);

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

> save 컨트,  List도 만듭니다. 이것과 같게 고칩니다. 

 

=> 프론트 컨트롤러

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

    public FrontControllerServletV2() {
        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 myView = controller.process(req, resp);
        myView.render(req, resp);
    }
}

프론트 컨트롤러를 V2로 만듭니다. 맵핑 정보를 넣는 것은 똑같습니다. 이제 프로세스가 MyView를 반환을 하게 설계를 했니다. 그리고 원래는 부모 컨트롤러의 레퍼런스로 자식 구현체의 process를 프론트에서 직접 실행했는데 이제는 view.render를 하면 됩니다. 이렇게 하니 점점 공통작업을 프론트가 하게 됩니다. 실행해보면 잘 됩니다.

 

-> 코드 설명

 

회원 가입을 하려고 하면 /front-controller/v2/members/new-form이 호출되어 /front-controller/v2/*이 있는 프론트 서블릿이 호출됩니다. 그러면 프론트에서 원하는 컨트를 찾고 process를 호출합니다. 원하는 컨트에 들어가서 process를 호출하면 new MyView를 리턴합니다. my는 이미 viewpath가 들어가 있고 디스패쳐가 render에 있으니 jsp로 forward가 되는 것입니다.

 

> 더 이상 컨트롤러가 jsp를 렌더링하지 않고 view를 반환해서 프론트 컨트에서 이런 공통 로직을 다 처리합니다. 이렇게 해서 화면을 렌더링하기 위한 view가 생성이 됐습니다.

 

- V3 모델 추가

이제 모델을 추가해보자 먼저 서블릿에 대한 종속성을 제거할 것입니다.

 

1) 서블릿 종속성 제거

무슨 말이냐면 컨트입장에서 process의 인자인 req, resp가 전혀 필요가 없습니다. 원래는 컨트에서 jsp를 호출할 때 req.dispacher가 필요했는데 이제는 프론트가 호출하니 필요가 없습니다.

 

req, resp가 필요한 게 아니고 save 컨트롤러에서 넣을 username과 age 쿼리 파라미터 정보가 필요한 것입니다. 그래서 요청 파라미터 정보는 자바의 map으로 대신 넘기도록 할 것입니다. 프론트에서  map으로 바꿔서 이 각 컨트롤러들에 대신 넘기도록 할 것입니다. 그럼 이 컨트롤러들은 ★ req, resp 등 서블릿 기술을 몰라도 동작할 수 있습니다. 이렇게 process가 사용하지 않은 매개변수를 없애도 됩니다.

 

2) 뷰 이름 증복 제거

서블릿 종속 말고 뷰 이름 중복 제거도 할 것입니다. 지금 프리픽스, 서브픽스가 중복입니다. 각 컨트롤러는 뷰의 논리 이름(/hsb/aaa.jpg에서 aaa)을 반환하고 실제 물리 위치(/hsb/aaa.jpg)의 이름은 프론트 컨트에서 처리하도록 단순화하겠습니다. ★ 프론트에서 최대한 지저분한 일을 다 해주고 핵심 로직인 나머지 컨트에서 작성하는 것을 단순화하는 것입니다. 프론트 컨트는 하나인데 실제 개발에서 나머지 컨트는 수십, 수백개가 될 것입니다.

 

> 이렇게 하면 뷰 폴더가 /web-inf에서 다른 것으로 달라져도 "/WEB-INF/views/new-form.jsp"를 원래 컨트롤러에서 viewpath라고 있었으니 그걸 다 고쳐야하는데 이제는 프론트에서만 바꾸면 됩니다.

 

3) Model 객체

request 객체를 Model로 사용하는 대신 별도의 모델 객체를 만들어서 반환합니다. req 대신에 모델이라는 객체를 만들고 거기에 값을 넣을 것입니다.

 

> 궁극적으로 우리가 구현하는 컨트가 서블릿 기술을 전혀 사용하지 않도록 변경해보겠습니다. 이렇게 하면 구현 코드도 매우 단순해지고, 테스트 코드 작성이 쉽습니다.

 

 

-> v3의 구조

요청이 오면 맵핑정보를 봐서 컨트롤러를 호출합니다. 근데 기존에는 view를 반환했습니다. 이제는 model, view가 섞인 모델뷰를 반환합니다. 모델은 req를 대신하는 Map 컬랙션으로 할 것이고 view는 viewPath만 가지고 있습니다. 프론트에서 viewPath를 받아서 뷰리졸버에게 주어 논리 이름을 물리 이름으로 바꾸고 myview의 생성자에게 주면서 myview를 반환합니다. myview가 뷰 전담 객체로 물리 이름을 필드로 가지고 생성자에서 초기화했었습니다. 그리고 프론트가 my의 렌더 메서드를 모델 뷰에서 가져온 모델을 주면서 호출할 것입니다. 랜더 메서드는 뷰를 전담하는 myview의 디스패쳐 호출 메서드였습니다.

 

 

-> modelview

지금까지 각 컨트에서 모델에 값을 넣기 위해 서블릿에 종속적인 req.setattribute를 사용해왔습니다. 실제 Model을 만들고 Map으로 추가로 모델, 뷰이니 뷰를 가지고 있어야합니다. 뷰 대신 뷰 논리 경로를 가질 것입니다. 이 논리 경로는 모델 뷰를 컨트롤러의 process가 반환하는 것이니 컨트롤러가 원래 물리 경로를 myview에 초기화하면서 반환하던 것을 컨트롤러는 논리 경로만 반환하게 하는 것이고 프론트에서 뷰 리졸버에게 논리 경로를 줘서 물리 경로를 만들고 myview를 생성할 것입니다.

 

+ 이게 모델 뷰이고 이번 버전에서는 httpservletreq가 없습니다. 그래서 set도 못 하고 별도의 model이 필요합니다.

 

-> 구현 시작

 

모델 뷰를 만들 것입니다. my처럼 어디서든 사용할 것이라서 frontcontroller 패키지 바로 밑에 만듭니다.

 

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

    public ModelVeiw(String viewName) {
        this.viewName = viewName;
    }
}

얘는 필드로 viewname과 model을 가집니다. 아까 뷰의 논리적 이름을 가진다고 했습니다. 모델 뷰는 뷰에 대한 것과 모델에 대한 것이 있습니다.

 

-> 컨트롤러 구현

public interface ControllerV3 {
    ModelView process(Map<String, String> paramMap);
}

v3 패키지를 만들고 나머지 컨트를 위한 인터페이스를 만듭니다. 얘는 my가 아닌 모델 뷰를 반환한다고 했습니다. 파라미터로 Map을 넣고 그냥 Str, str을 합니다. 이게 req, resp를 대신할 것입니다. v2와 비교하면 서블릿 기술이 다 사라졌습니다. 이제 각 컨트롤러는 디스패쳐를 안하고 myview에게 전담했기 때문에 req, resp을 사용하지 않습니다. 근데 save를 보면 req.setAttribute는 해야하는 것입니다. 따라서 그 기능만 하는 것을 Map으로 만들어서 process의 인자로 줍니다.

 

public class MembFormV3 implements ContV3 {
    @Override
    public ModelV process(Map<String, String> paramMap) {
        return new ModelV("new-form");
    }
}

구현을 합니다. form 컨트롤러 먼저 만드는데 모델 뷰를 반환한다고 했고 걔가 생성자로 논리 이름을 넣는다고 했습니다. 모델 뷰의 생성자에 논리 이름을 넣는것입니다. 원래 form 컨트는 "/WEB-INF/views/new-form.jsp"를 사용했었는데 new-form만 씁니다. 아니 정말 점점 쉬워집니다!

 

public class MemberSaveControllerV3 implements ControllerV3 {
    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 modelView = new ModelView("save-result");
        modelView.getModel().put("member", member);

        return modelView;
    }
}

모델뷰는 논리 경로도 가진다고 했습니다. 생성자로 무조건 가지도록 되어있습니다. 또한 컨트를 만들면 현재 v3 프로세스는 모델 뷰를 리턴하게 되어있습니다. 따라서 무조건 모델 뷰를 만들어야하고 그 모델 뷰는 생성자에 따라 무조건 논리 경로가 필요합니다. 근데 모델이 뭔가 생각해보면 이전 예제해서는 members 등 view에 전달할 데이터를 저장했었습니다. 그래서 이전 save 서블릿에서 member를 레포에 save하고 저장 결과를 jsp에 뿌리기 위해 그떄 당시 모델인 req.setAttri를 했엇던 것입니다. 

> 그러니 이번에는 새로 만든 모델인 모델 뷰에 member를 넣습니다. 즉 모델 뷰는 jsp(뷰)에 뿌릴 데이터가 지금 이 컨트에 있으면 넣고 아니면 논리 경로만 해서 생성합니다.(논리 경로는 나중에 프론트에서 받아서 맵퍼 정보 만들 때 사용할 것입니다.)

 

> save 컨트이니 뷰에 뿌리기 위해 모델 뷰에 member를 저장합니다. 그리고 리턴합니다. 저장된 멤버를 뒤에 프론트에서 처리해서 jsp로 뿌려줄 것입니다.

 

public class MemberListControllerV3 implements ControllerV3 {
    MemberRepository memberRepository = MemberRepository.getInstance();

    @Override
    public ModelView process(Map<String, String> paramMap) {
        List<Member> members = memberRepository.findAll();

        ModelView modelView = new ModelView("members");
        modelView.getModel().put("members", members);

        return modelView;
    }
}

> 목록 컨트는 멤버s를 모델에 넣어야합니다. 역시나 논리 경로를 넣어 생성하고 모델을 넣고 반환합니다.

 

 

-> 프론트 컨트롤러 v3

@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 MemberFromControllerV3());
        controllerMap.put("/front-controller/v3/members/save", new MemberSaveControllerV3());
        controllerMap.put("/front-controller/v3/members", new MemberListControllerV3());
    }

    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String requestURI = req.getRequestURI();

        ControllerV3 controller = controllerMap.get(requestURI);
        if(controller == null) {
            resp.setStatus(HttpServletResponse.SC_NOT_FOUND);
            return;
        }
        // 여기가 req 대신 Map 사용한 것
         Map<String, String> paramMap = createParamMap(req);
		
        ModelView modelView = controller.process(paramMap);
        String viewName = modelView.getViewName();

        MyView myView = viewResolver(viewName);
        myView.render(modelView.getModel(), req, resp);
    }

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

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

프로세스 인자를 넣는 것부터 오류가 납니다. req, resp를 없애소 paramMap을 넘겨야합니다. 이 안에 현재 시점에 요청온 질의 문자열 정보를 다 꺼내서 넣고 넘겨야합니다. 왜냐하면 paramMap이 req 대신 하는 것인데 req.getParameter로 질의 문자열을 꺼냈었기 때문입니다. 이를 프론트에서 공통 처리를 하고 나머지 컨트에 보내는 것입니다.

> 다 꺼내기 위해서 res.getParamNames를 하고 asInter를 써서 forEach를 해서 구합니다. 람다식으로 paramMap에 put으로 질의 문자열 키 = 벨류 값을 다 넣어줍니다. 이를 프로세스의 인자로 넣습니다. 메서드 추출로 마무리합니다.

 

-> 이제 모델 뷰를 건듭니다. 

그림에서 컨트롤러 호출하고 모델 뷰까지 받는 것까지 성공했습니다. 이제 논리 이름을 물리 이름으로 바꿔야합니다. 바꾸고 my에게 보내야합니다. (v2에서 my가 대신 jsp 호출해주는 애로 디스패쳐가 있는 전담하는 애였고 호출하기 위해서는 절대 경로가 필요했었습니다.) 뷰 리졸버는 실제 뷰를 찾아주는 해결자입니다.

> 지금 모델뷰의 viewname은 논리이름입니다. 이제 우리가 원하는 /WEB-INF/views/save-result.jsp 이 경로를 만들어주는 것을 뷰 리졸버가 해줄 것입니다. 얘는 그냥 모델 뷰의 getViewName을 사이에 두고 앞과 뒤에 /WEB-INF와 .jsp를 붙이면 되긴 합니다. 이것을 my의 생성자로 넣고 my의 render를 호출하면 jsp를 호출합니다. (뷰 리졸버를 만들면 views 폴더가 바뀔 때 컨트롤러 코드는 전혀 변경하지 않고 뷰 리졸버 코드만 고치면 된다는 장접이 있습니다.)

 

public void render(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    RequestDispatcher requestDispatcher = req.getRequestDispatcher(viewPath);
    requestDispatcher.forward(req, resp);
}


public void render(Map<String, Object> model, HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    modelToRequestAttribute(model, req);
    RequestDispatcher requestDispatcher = req.getRequestDispatcher(viewPath);
    requestDispatcher.forward(req, resp);
}

private static void modelToRequestAttribute(Map<String, Object> model, HttpServletRequest req) {
    model.forEach((key, value) -> req.setAttribute(key, value));
}

여기서 한개가 빠졌습니다. 랜더링을 할 때 모델 내용을 읽어서 jsp에 담을 것입니다. 그래서 render에 모델을 넘겨줘야해서 render 메서드를 오버로딩합니다.

> 모델에 있는 데이터를 꺼내서 디스패텨가 jsp를 호출할 것입니다. forEach로 다 꺼내서 map에 저장되어있던 키 벨류를 다 가져옵니다. 이 모델 MAP에 저장되어있던 것들이 "members", member라서 이 모양 그대로 다시 req.setAtrribute에 넣어줘야합니다. 왜냐하면 원래 req를 모델로 사용했을 때 req.setAttribute("member", member); 이렇게 했기 때문입니다.

> 그 다음에 디스패쳐를 호출하는 것입니다. 그러면 그냥 req를 모델로 사용하고 디스패쳐를 forward하는 원래 하던 방식과 같아지는 것입니다.

 

-> 정리

프론트 컨트롤러는 할 일이 많아집니다. 하지만 실제 구현한 컨트는 되게 편리합니다. 공통으로 할 일이 이렇게 많았다는 얘기입니다. 생각해보면 무료 강의에서 한 스프링이 제일 쉽습니다. 이게 다 지금 어려워서 그걸 개선한게 스프링입니다.

Comments