개발놀이터

MVC 프레임워크 만들기 본문

Spring/Spring

MVC 프레임워크 만들기

마늘냄새폴폴 2022. 1. 6. 20:18

이 포스팅은 인프런 김영한 님의 스프링 핵심 원리 기본 편을 보고 각색한 포스팅입니다. 자세한 내용은 강의를 확인해주세요

 

우리는 앞으로 점진적인 버전업을 통해 스프링 MVC를 구현해 볼 것이다.

-프론트 컨트롤러 도입 (v1)

-View 분리 (v2)

-Model 추가 (v3)

-단순하고 실용적인 컨트롤러 (v4)

-유용한 컨트롤러 (v5)

순으로 구현해 나갈 것이다. 

 

스프링 MVC에서 가장 중요한 부분이 바로 프론트 컨트롤러 패턴이다. 스프링에선 프론트 컨트롤러를 디스패쳐 서블릿으로 구현했다. 

 

프론트 컨트롤러란 서블릿 하나를 모든 컨트롤러 앞에 두고 클라이언트 요청을 이 앞에 있는 서블릿이 먼저 받게 하여 프론트 컨트롤러가 요청에 맞는 컨트롤러를 찾아서 호출해주는 역할을 한다. 

기본이 되는 엔티티와 Repository를 먼저 만들고 V1부터 차례대로 만들어보겠다. 

Member.java

@Getter @Setter
public class Member {

    private Long id;
    private String username;
    private int age;

    public Member() {
    }

    public Member(String username, int age) {
        this.username = username;
        this.age = age;
    }
}

MemberRepository.java

/**
 * 동시성 문제가 고려되어 있지 않음, 실무에서는 ConcurrentHashMap, AtomicLong 사용 고려
 */
public class MemberRepository {

    private static Map<Long, Member> store = new HashMap<>();
    private static long sequence = 0L;

    private static final MemberRepository instance = new MemberRepository();

    public static MemberRepository getInstance() {
        return instance;
    }

    private MemberRepository() {
    }

    public Member save(Member member) {
        member.setId(++sequence);
        store.put(member.getId(), member);
        return member;
    }

    public Member findById(Long id) {
        return store.get(id);
    }

    public List<Member> findAll() {
        return new ArrayList<>(store.values());
    }

    public void clearStore() {
        store.clear();
    }

}

일단 지금은 JPA나 다른 데이터베이스 접근 방식을 사용하지 않고 메모리에 올려서 사용하는 방식을 채택했다. 최대한 예제를 단순하게 하기 위함이니 이 부분을 잘 숙지하고 다음 부분으로 넘어가면 될 것이다. 또한, 뷰 템플릿으로 jsp를 사용하였다. 

V1

V1의 기본 구조는 다음과 같다. 

ControllerV1.java

public interface ControllerV1 {

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

우리는 다형성을 활용하기 위해서 컨트롤러를 인터페이스로 만들었다. 인자값으로 HttpServletRequest, HttpServletResponse를 받아서 구현체를 하나씩 만들 것이다. 

 

MemberFormControllerV1.java

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

MemberFormController는 단순히 뷰로 이동하기만 하는 컨트롤러이다. 

 

MemberListControllerV1.java

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

MemberRepository에서 findAll로 모든 멤버를 찾아와 모델에 담아주는 역할을 하는 컨트롤러이다.

 

MemberSaveControllerV1.java

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

        //Model에 데이터를 보관한다.
        request.setAttribute("member", member);

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

파라미터로 username, age를 받아와 멤버에 저장하고 모델에 멤버를 담아 뷰로 이동하는 컨트롤러이다. 

 

FrontControllerServletV1.java

@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 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으로 들어오는 모든 url이 이 FrontControllerServletV1을 통과하게 만들었다. 그 후에 url매핑 정보를 Map에 저장하였다. service 부분에서는 request.getRequestURI 메서드를 사용하여 어떤 url로 들어오는지를 캐치해 매핑 정보에 저장되어있는 컨트롤러를 그때그때 바꿔서 호출해준다. 

 

V1에는 모든 컨트롤러에서 뷰로 이동하는 부분에 중복이 있고, 깔끔하지 않다. 

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

이 부분이 항상 모든 컨트롤러에 들어간다. 

 

V2에서는 깔끔하게 분리하기 위해 별도로 뷰를 처리하는 객체를 만들어보자

 

V2

V2의 구조는 다음과 같다. 

 

MyView.java

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를 호출하면 getRequestDispatcher메서드를 이용해 forward하는 로직이 추가되었다. 

 

ControllerV2.java

public interface ControllerV2 {

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

컨트롤러에선 기존에 컨트롤러의 리턴타입이 void였던 것을 MyView를 반환하게 하여 process 로직을 타고 컨트롤러에서 할 일이 끝나면 MyView 값을 통해 render를 호출하도록 설계하였다. 

 

MemberFormControllerV2.java

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

 

MemberSaveControllerV2.java

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

        //Model에 데이터를 보관한다.
        request.setAttribute("member", member);

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

 

MemberListControllerV2.java

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

 

FrontControllerServletV2.java

@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 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 view = controller.process(request, response);
        view.render(request, response);
    }
}

프론트 컨트롤러에서는 매핑정보를 통해 해당 컨트롤러를 호출하고 process를 실행해 해당 컨트롤러의 비즈니스 로직을 실행한 후에 render를 호출해 forward하게 설계하였다. 전 버전에서는 forward가 모든 컨트롤러에서 일어났다면 이번에 개선된 V2에서는 프론트 컨트롤러에서만 forward가 호출되어 컨트롤러의 부담이 한층 덜었다. V2에선 V1과 비교했을 때 컨트롤러가 많이 깔끔해진 것을 볼 수 있다. 

 

하지만 V2에도 문제가 몇가지 있다. 바로 서블릿 종속성과 뷰 이름 중복이다.

 

MemberSaveController와 MemberListController는 모델에 담을 request객체를 제외하면 response는 사용하지도 않는다. 심지어 MemberFormController는 request, response 둘다 사용하지 않는다. 게다가 request객체도 자바의 Map을 이용하면 필요없어진다.

 

또한, MyView를 생성할 때 /WEB-INF/views/논리이름.jsp 가 중복되는 것을 볼 수 있다. 다음 버전에선 이 두개를 해결해보겠다. 

 

V3

V3에선 모델에 담길 객체를 request가 아닌 자바의 Map을 이용하여 서블릿의 종속을 피할 것이다. 또한 viewResolver의 도입으로 논리이름을 넘기면 MyView를 반환하여 뷰 이름의 중복을 피할 것이다.

ModelView.java

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

생성자로 viewName (논리이름) 을 생성해준다. 

 

ControllerV3.java

public interface ControllerV3 {

    ModelView process(Map<String, String> paramMap);
}

컨트롤러는 ModelView를 반환하고 인자값으로 model에 들어갈 값을 넣는다. 

 

MemberFormControllerV3.java

public class MemberFormControllerV3 implements ControllerV3 {

    @Override
    public ModelView process(Map<String, String> paramMap) {
        return new ModelView("new-form");
    }
}

 

MemberSaveControllerV3.java

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

process는 모델 (paramMap)을 인자값으로 받아와 Map에 저장되어있는 값을 꺼내 값을 저장하고 비즈니스 로직을 실행한다. 그 후 ModelView에 논리이름을 넣고 반환한다. 

 

MemberListControllerV3.java

public class MemberListControllerV3 implements ControllerV3 {

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

 

FrontControllerservletV3.java

@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(); // 1번

        ControllerV3 controller = controllerMap.get(requestURI); // 2번
        if (controller == null) {
            response.setStatus(HttpServletResponse.SC_NOT_FOUND);
            return;
        }

        Map<String, String> paramMap = createParamMap(request); // 3번
        ModelView mv = controller.process(paramMap); // 4번

        String viewName = mv.getViewName();
        MyView view = viewResolver(viewName); // 5번

        view.render(mv.getModel(), request, response); // 6번
    }

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

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

이제 점점 복잡해지는데 번호를 붙여가면서 하나씩 살펴보자

1번 : 사용자의 url매핑 정보를 받아서

2번 : 매핑 정보에 맞는 컨트롤러를 찾아온다.

3번 : 파라미터로 넘어온 값의 이름 (username, age) 을 getParameterNames메서드를 통해 다 꺼내와 asIterator 메서드로 iterator화 시킨후 forEachRemaining 메서드와 람다를 활용하여 paramMap에 저장한다. 

4번 : 저장한 paramMap을 process에 넘겨 컨트롤러는 안에있는 값을 꺼내 사용할지 말지를 비즈니스 로직에 따라 결정한다. (MemberSaveControllerV3에서는 paramMap을 사용하지만 나머지 컨트롤러에선 사용하지 않는다.)

5번 : 컨트롤러에서 MyView에 논리이름을 가져와서 viewResolver를 통해 온전한 뷰 이름으로 만든후에 

6번 : render로 forward를 실행한다. 

 

V3컨트롤러는 서블릿 종속성을 제거하고 뷰 경로의 중복을 제거하는 등 잘 설계된 컨트롤러이다. 그런데 실제 컨트롤러 인터페이스를 구현하는 개발자 입장에서 보면 항상 ModelView 객체를 생성하고 반환해야 하는 부분이 조금 번거롭다. 

 

좋은 프레임워크는 아키텍처도 중요하지만, 그와 더불어 실제 개발하는 개발자가 단순하고 편리하게 사용할 수 있어야 한다. 소위 실용성이 있어야 한다. 

 

V4

 V4는 V3와 구조는 똑같지만 ModelView를 반환하지 않고 String을 반환하여 말그대로 진짜 논리 이름만 반환하는 로직으로 설계될 예정이다. 

 

ControllerV4.java

public interface ControllerV4 {

    /**
     * @param paramMap
     * @param model
     * @return viewName
     */
    String process(Map<String, String> paramMap, Map<String, Object> model);
}

이번 컨트롤러에선 모델을 직접 넘기는 방식이다. 저번 컨트롤러에선 ModelView에 있는 model을 getModel()을 사용하여 put 했었다. 하지만 이번엔 모델을 직접 넘겨 개발자가 사용하기에 더 용이하게 설계했다. 또한 반환 타입이 String으로 바뀌어서 진짜 논리이름만 반환하면 되도록 설계하였다. 

 

MemberFormControllerV4.java

public class MemberFormControllerV4 implements ControllerV4 {

    @Override
    public String process(Map<String, String> paramMap, Map<String, Object> model) {
        return "new-form";
    }
}

 

MemberSaveControllerV4.java

public class MemberSaveControllerV4 implements ControllerV4 {

    private MemberRepository memberRepository = MemberRepository.getInstance();

    @Override
    public String process(Map<String, String> paramMap, Map<String, Object> model) {
        String username = paramMap.get("username");
        int age = Integer.parseInt(paramMap.get("age"));

        Member member = new Member(username, age);
        memberRepository.save(member);

        model.put("member", member);
        return "save-result";
    }
}

 

MemberListControllerV4.java

public class MemberListControllerV4 implements ControllerV4 {

    private final MemberRepository memberRepository = MemberRepository.getInstance();

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

        model.put("members", members);
        return "members";
    }
}

 

FrontControllerServletV4.java

@WebServlet(name = "frontControllerServletV4", urlPatterns = "/front-controller/v4/*")
public class FrontControllerServletV4 extends HttpServlet {

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

    public FrontControllerServletV4() {
        controllerMap.put("/front-controller/v4/members/new-form", new MemberFormControllerV4());
        controllerMap.put("/front-controller/v4/members/save", new MemberSaveControllerV4());
        controllerMap.put("/front-controller/v4/members", new MemberListControllerV4());
    }

    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

        String requestURI = request.getRequestURI();

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

        Map<String, String> paramMap = createParamMap(request);
        Map<String, Object> model = new HashMap<>(); //추가

        String viewName = controller.process(paramMap, model);

        MyView view = viewResolver(viewName);
        view.render(model, request, response);
    }

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

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

바뀐 부분중 가장 눈에 띄는 것은 render에 model을 넘긴다는 것이다. jsp는 request.setAttribute로 모델을 전달하는데 개발자가 model에 집어넣은 값을 request.setAttribute로 바꿔주는 역할을 할 것이다. 그리하여 새롭게 추가된 코드는 다음과 같다.

 

MyView.java

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

    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 void modelToRequestAttribute(Map<String, Object> model, HttpServletRequest request) {
        model.forEach((key, value) -> request.setAttribute(key, value));
    }
}

자바의 Map에 forEach메서드와 람다를 사용하여 request.setAttribute로 바꿔준 것을 확인할 수 있다. 

 

여기까지 했다면 거의 다 온것이다. 마지막 V5에서는 유연한 컨트롤러를 만들건데, 어떤 개발자는 V3 방식을 쓰고싶을 수도 있고, 어떤 개발자는 V4방식을 사용하고 싶을수도 있을 것이다. 하지만 우리가 만든 코드는 V4면 V4, V3면 V3이렇게 고정되게 사용할 수 밖에 없다. 왜냐하면 바로 이부분 때문이다. 

 

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

    public FrontControllerServletV4() {
        controllerMap.put("/front-controller/v4/members/new-form", new MemberFormControllerV4());
        controllerMap.put("/front-controller/v4/members/save", new MemberSaveControllerV4());
        controllerMap.put("/front-controller/v4/members", new MemberListControllerV4());
    }

Map에 보면 ControllerV4라고 적혀있는 것을 확인할 수 있다. 이렇게 되면 우린 V4만 사용할 수 밖에 없는것이다. 이제 이것을 Object로 바꿔 어떤 컨트롤러든 수용할 수 있게 설계할 것이다. 

 

 

V5

V5에서는 핸들러 어댑터라는 것이 새로 생기는데 이것이 바로 핸들러를 처리할 수 있는 핸들러 어댑터를 조회하고 그에 맞는 핸들러 (컨트롤러) 를 호출하게 만드는 것이다. 

 

MyHandlerAdapter.java

public interface MyHandlerAdapter {

    boolean supports(Object handler);

    ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws ServletException, IOException;
}

support는 이 핸들러가 우리가 원하는 핸들러와 일치하는지를 확인하는 메서드이다. handle은 실제 컨트롤러를 호출하고 그 결과로 ModelView를 반환하는 것이다. 

 

ControllerV3HandlerAdapter.java

public class ControllerV3HandlerAdapter implements MyHandlerAdapter {

    @Override
    public boolean supports(Object handler) {
        return (handler instanceof ControllerV3);
    }

    @Override
    public ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws ServletException, IOException {
        //MemberFormControllerV3
        ControllerV3 controller = (ControllerV3) handler;

        Map<String, String> paramMap = createParamMap(request);
        ModelView mv = controller.process(paramMap);

        return mv;
    }

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

supports에선 ControllerV3이 맞는지를 확인한다. 그리고 handle에서 ControllerV3으로 다운캐스팅하고 createParamMap이 handle안으로 들어온 것을 확인할 수 있다. 그리고 ModelView를 반환하고 끝이난다.

 

ControllerV4HandlerAdapter.java

public class ControllerV4HandlerAdapter implements MyHandlerAdapter {

    @Override
    public boolean supports(Object handler) {
        return (handler instanceof ControllerV4);
    }

    @Override
    public ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws ServletException, IOException {
        ControllerV4 controller = (ControllerV4) handler;

        Map<String, String> paramMap = createParamMap(request);
        HashMap<String, Object> model = new HashMap<>();

        String viewName = controller.process(paramMap, model);

        ModelView mv = new ModelView(viewName);
        mv.setModel(model);

        return mv;
    }

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

V4는 process가 String을 반환하게 되어있다. 그래서 ModelView 객체를 따로 만들어서 setter를 이용해 값을 설정해준 다음 return하는 모습을 보여준다.

 

FrontControllerServletV5

@WebServlet(name = "frontControllerServletV5", urlPatterns = "/front-controller/v5/*")
public class FrontControllerServletV5 extends HttpServlet {

    private final Map<String, Object> handlerMappingMap = new HashMap<>();
    private final List<MyHandlerAdapter> handlerAdapters = new ArrayList<>();

    public FrontControllerServletV5() {
        initHandlerMappingMap(); // 1번
        initHandlerAdapters(); // 2번
    }

    private void initHandlerMappingMap() {
        handlerMappingMap.put("/front-controller/v5/v3/members/new-form", new MemberFormControllerV3());
        handlerMappingMap.put("/front-controller/v5/v3/members/save", new MemberSaveControllerV3());
        handlerMappingMap.put("/front-controller/v5/v3/members", new MemberListControllerV3());

        //V4 추가
        handlerMappingMap.put("/front-controller/v5/v4/members/new-form", new MemberFormControllerV4());
        handlerMappingMap.put("/front-controller/v5/v4/members/save", new MemberSaveControllerV4());
        handlerMappingMap.put("/front-controller/v5/v4/members", new MemberListControllerV4());
    }

    private void initHandlerAdapters() {
        handlerAdapters.add(new ControllerV3HandlerAdapter());
        handlerAdapters.add(new ControllerV4HandlerAdapter());
    }

    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

        Object handler = getHandler(request); // 3번
        if (handler == null) {
            response.setStatus(HttpServletResponse.SC_NOT_FOUND);
            return;
        }

        MyHandlerAdapter adapter = getHandlerAdapter(handler); // 4번

        ModelView mv = adapter.handle(request, response, handler); // 5번

        String viewName = mv.getViewName(); // 6번
        MyView view = viewResolver(viewName); // 7번

        view.render(mv.getModel(), request, response); //8번

    }

    private Object getHandler(HttpServletRequest request) {
        String requestURI = request.getRequestURI();
        return handlerMappingMap.get(requestURI);
    }

    private MyHandlerAdapter getHandlerAdapter(Object handler) {
        //MemberFormControllerV4
        for (MyHandlerAdapter adapter : handlerAdapters) {
            if (adapter.supports(handler)) {
                return adapter;
            }
        }
        throw new IllegalArgumentException("handler adapter를 찾을 수 없습니다. handler=" + handler);
    }

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

이번엔 진짜 복잡해졌다. 이번에도 번호를 매겨 하나하나 확인해보자

 

1번, 2번 : 생성자를 사용하여 FrontController가 만들어질 때 설정 정보를 입력해놓는다. handlerAdapters는 for 문을 돌려가면서 supports를 확인할 때 사용할 리스트이고 handlerMappingMap은 url마다 컨트롤러를 등록해놓는 곳이라고 이해하면 된다.

3번 : requestURI 메서드를 이용하여 매핑정보를 가지고 알맞는 핸들러 (컨트롤러) 를 가져온다. 

4번 : 가져온 핸들러를 가지고 알맞는 어댑터를 가져온다. for문을 돌려서 supports를 확인해 true가 나오면 해당 어댑터를 리턴한다.

5번 : handle을 호출하여 알맞은 핸들러와 그에 맞는 로직을 실행한다. 

6번 : ModelView에서 논리이름을 꺼낸다.

7번 : viewResolver로 논리이름을 완전한 뷰 이름으로 만들어준다.

8번 : V4에서 render에 모델을 같이 넘기는 것과 같은 로직이다. 

 

 

이렇게 우리는 점진적으로 단계를 거쳐 아키텍처도 안정적이고 사용자 (개발자) 가 편리하게 사용할 수 있는 프레임워크를 만들었다. 다음 포스팅에서는 우리가 만든 프레임워크와 실제 스프링 프레임워크를 비교하면서 스프링 MVC 프레임워크에 대해 더 자세히 알아볼 것이다. 

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

동시성문제와 스레드 로컬  (0) 2022.01.10
스프링 MVC 구조 파악하기  (0) 2022.01.07
@Autowired  (0) 2021.12.28
컴포넌트 스캔과 자동 의존 관계 주입  (0) 2021.12.27
싱글톤과 @Configuration  (0) 2021.12.27