인프런/스프링 MVC 1편

5)MVC 프레임워크 만들어보기(리팩토링하면서 버전업 v1~v5)

backend dev 2023. 1. 17.

MVC 프레임워크를 단계적으로 업그레이드 시켜보면서 만들어볼것이다.

결국 끝까지 업그레이드를 하게 되면 Spring MVC와 유사한 구조가 될것이다. 직접 MVC프레임워크를 만들어보고, 업그레이드해보면 나중에 Spring MVC를 배울때 좀더 쉽게 이해할수 있을것이다.

프론트 컨트롤러 소개

이전 강의때 서블릿을 이용해서 컨트롤러를 만들었다.

그때는 http 요청마다 컨트롤러를 만들어줘야했고, 그래서 공통적으로 들어가는 코드가 반복되었다.

프론트컨트롤러를 도입하면 프론트컨트롤러에 공동로직을 몰아두고 프론트컨트롤러를 통해 나머지 컨트롤러로 이동하게끔한다. (프론트 컨트롤러만 서블릿이다.(요청을 받아야하니까), 나머지 컨트롤러는 서블릿을 사용하지않아도 된다.( 요청을 받을 필요가없으니까))

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

프론트 컨트롤러를 단계적으로 도입할것이다.

이번 목표는 기존 코드를 최대한 유지하면서 프론트 컨트롤러를 도입해본다.

먼저 구조를 맞춰두고 점진적으로 리팩토링을 해보자.

(버전을 올려가며 리팩토링할것이다)

 

v1 구조

1. http 요청이 들어오면 프론트컨트롤러(서블릿)가 요청을 받아서 url 매핑정보에서 컨트롤러를 조회한다.

(어떤 url이 들어오면 어떤 컨트롤러로 보내줄지 정해놓고)

 

2. url 매핑정보를 보고 조회해서 , 해당 컨트롤러를 호출해준다.

 

3. 컨트롤러는 자기 로직을 수행하고 JSP forward 해준다 (jsp 호출해서 응답을 위한 html를 만들고 html 응답하기위함)

 

컨트롤러는 다형성을 위해 인터페이스로 구현할것이다.

public interface ControllerV1 { //다형성을 위해 인터페이스화

    //서블릿 만들때 service 오버라이드했던것처럼, 그 서비스의 매개변수와 예외처리까지 똑같이 만들어준다. 이름만 다름.
    void process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException;
}

회원 등록 컨트롤러

public class MemberFormControllerV1 implements ControllerV1 {
    @Override
    public void process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        //controller -> view(jsp) 
        String viewPath = "/WEB-INF/views/new-form.jsp";
        RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
        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")); //getParameter의 리턴값은 항상 String이므로 int형으로 변환해준다.

        Member member = new Member(username,age); //전달된 데이터를 이용해서 Member객체를 만든다.
        memberRepository.save(member); //그 멤버객체를 저장해준다.

        //모델에 데이터를 보관해줘야한다.  모델에 데이터를 보관하고 jsp를 호출해서 모델의 데이터를 꺼내서 html를 만들고 응답으로 전달해줄것이다.
        request.setAttribute("member", member);
        String viewPath = "/WEB-INF/views/save-result.jsp";
        RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
        dispatcher.forward(request, response); //서버 내부에서 jsp를 호출

    }
}

회원 목록 컨트롤러

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);
        //서버내부에서 json(뷰)를 호출
        String viewPath = "/WEB-INF/views/members.jsp";
        RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
        dispatcher.forward(request, response);
    }
}

이전에 했던 서블릿 내부 로직을 복붙한다.  이 컨트롤러들은 서블릿의 내부 비즈니스로직만 가지는것이다.

@Webservlet~ 하면서 url 매핑하는 부분이 없으므로 http요청에 맞는 컨트롤러 실행은 프론트컨트롤러에서 진행할것이다.

 

프론트 컨트롤러 (이 버전은 서블릿으로 구현)

//프론트컨트롤러는 서블릿으로 구현하기로했으니, 서블릿 구조를 가진다.
@WebServlet(name = "frontControllerServletV1", urlPatterns = "/front-controller/v1/*") //프론트컨트롤러는 url패턴이 중요하다.
//"/front-controller/v1/*" 이렇게 되어있는데 /front-controller/v1/  url뒤에 어떤값이 붙어 url이 호출되도 이 서블릿이 실행된다는 의미이다, *을 이용
public class FrontControllerServletV1 extends HttpServlet {

    //url별 컨트롤러 매핑정보를 저장하기 위한 Map이다.   어떤 url이 들어오면 해당 컨트롤러를 호출하라는 정보가 저장
    private Map<String, ControllerV1> controllerMap = new HashMap<>();

    public FrontControllerServletV1() {
        //이 서블릿이 생성될때(스프링부트가 올라가면서 서블릿이 생성되며 서블릿컨테이너에 담길때)
        //Map에 url,컨트롤러 매핑정보가 담기게끔 설정
        controllerMap.put("/front-controller/v1/members/new-form", new MemberFormControllerV1()); //url,컨트롤러객체를 Map에 넣어준다.
        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();// 만약 http://localhost:8080/front-controller/v1/hello 다음 url로 호출한다고했을때
        //getRequestURI() 메소드를 사용하면 서버주소뒷부분 정보를 가져올수있다. /front-controller/v1/hello

        ControllerV1 controller = controllerMap.get(requestURI); //uri를 가지고 map에서 해당 컨트롤러를 찾아온다. 인터페이스를 구현한 컨트롤러라 인터페이스변수로 받을 수있다(다형성)
        if (controller == null) { //이상한 uri라 못찾으면 null
            response.setStatus(HttpServletResponse.SC_NOT_FOUND); //404 상태코드를 응답메시지 상태코드로 설정한다.
            return; // 404인경우에는 바로 return
        }
        //잘 조회됬다면 해당 컨트롤러의 process를 호출한다 (해당 컨트롤러의 비즈니스 로직 호출)
        controller.process(request, response);

    }
}

프론트 컨트롤러가 http요청을 받고, 해당url에 매핑된 컨트롤러를 찾아와 컨트롤러의 비즈니스 로직이 잘 실행되는것을 확인가능. 


View 분리(버전 2) -v2

모든 컨트롤러에서 뷰로 이동하는 코드가 중복으로 있었고 깔끔하지 못했다.

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

이 부분을 깔끔하게 분리하기 위해 별도로 뷰를 처리하는 객체를 만들자.

프론트 컨트롤러가 요청에 맞는 컨트롤러를 호출하고,

(원래는 컨트롤러가 뷰를 직접 호출했지만 이제 그렇게 하지않는다.)

컨트롤러가 MyView라는 객체를 만들어 반환하면, 프론트컨트롤러가 MyView의 render()를 호출해서

MyView가 JSP를 호출하도록 바꿀것이다.

 

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객체를 이용해 forward()를 사용해서 뷰(jsp)를 호출했다.
        //그 과정을 렌더링이라고 부를것이다(뷰를 만들거나, 뷰를 만들기위해 forward 하는 행동들, 결국 뷰를 만드는것이기때문에 렌더링)
        RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
        dispatcher.forward(request,response);

    }
}

ControllerV2 (인터페이스)

public interface ControllerV2 {
    //기존의 ControllerV1 인터페이스와 다른점은 MyView를 반환해준다는점이다.
    MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException;
}

회원등록 컨트롤러

public class MemberFormControllerV2 implements ControllerV2 {
    @Override
    public MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        //MyView객체를 생성할때 경로(uri)만 전달해주면 된다.
        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")); //getParameter의 리턴값은 항상 String이므로 int형으로 변환해준다.

        Member member = new Member(username,age); //전달된 데이터를 이용해서 Member객체를 만든다.
        memberRepository.save(member); //그 멤버객체를 저장해준다.

        //모델에 데이터를 보관해줘야한다.  모델에 데이터를 보관하고 jsp를 호출해서 모델의 데이터를 꺼내서 html를 만들고 응답으로 전달해줄것이다.
        request.setAttribute("member", member);

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

request,response는 해당 컨트롤러의 process메소드를 사용할 프론트컨트롤러가 보내준다. (프론트컨트롤러는 서블릿이므로)  받은 response 객체에 값을 담으면 , 나중에 프론트컨트롤러가 MyView의 렌더를 실행시킬때 다시 request,response객체를 전달할것고  MyView안에서 그 객체들을 jsp로 넘기므로 , response에 담긴값을 사용 할 수 있다.

회원목록 컨트롤러

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

프론트컨트롤러V2 (서블릿)

//프론트컨트롤러는 서블릿으로 구현하기로했으니, 서블릿 구조를 가진다.
@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()); //url,컨트롤러객체를 Map에 넣어준다.
        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); //uri를 가지고 map에서 해당 컨트롤러를 찾아온다. 인터페이스를 구현한 컨트롤러라 인터페이스변수로 받을 수있다(다형성)
        if (controller == null) { //이상한 uri라 못찾으면 null
            response.setStatus(HttpServletResponse.SC_NOT_FOUND); //404 상태코드를 응답메시지 상태코드로 설정한다.
            return; // 404인경우에는 바로 return
        }

        //컨트롤러의 process메소드를 실행해서 비즈니스로직을 실행하고,MyView 객체를 받아온다.
        MyView myView = controller.process(request, response);
        myView.render(request, response); // 받아온 MyView의 render()를 실행해서 jsp(뷰)를 호출한다(forward()). 
    }
}

잘동작하는것을 확인가능


Model 추가 - (버전3)

버전2의 컨트롤러들을 보자.

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")); //getParameter의 리턴값은 항상 String이므로 int형으로 변환해준다.

        Member member = new Member(username,age); //전달된 데이터를 이용해서 Member객체를 만든다.
        memberRepository.save(member); //그 멤버객체를 저장해준다.

        //모델에 데이터를 보관해줘야한다.  모델에 데이터를 보관하고 jsp를 호출해서 모델의 데이터를 꺼내서 html를 만들고 응답으로 전달해줄것이다.
        request.setAttribute("member", member);

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

HttpServletRequest request, HttpServletResponse response가 원래는 RequestDispatcher 객체를 생성해서 forward()할때 쓰이거나, getparameter로 파라미터값을 받아올때 쓰였다.

forward()는 MyView를 도입해서 컨트롤러안에서는 더 이상사용되지않고, 요청 파라미터 정보는 자바의 Map으로 대신 넘기도록하면 컨트롤러가 서블릿기술을 몰라도 동작할 수 있다.

각 컨트롤러에서 MyView를 반환할때 

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

이런식이다.

이때 /WEB-INF/views/파일이름.jsp 에서 파일이름빼고는 myview만들때 항상같다. (중복이 있는것을 확인)

그래서 컨트롤러는 뷰의 논리이름(save-result같은 "파일이름")을 반환하고 

실제 물리이름(논리이름을 포함한 전체 주소)는 프론트컨트롤러에서 처리하도록 단순화한다.

뷰의 위치가 바뀌어도 프론트컨트롤러에서 처리하므로 , 프론트컨트롤러만 고치면 되게끔

 버전3(v3)의 구조

http요청이 들어오고, http 요청에 따라 매핑된 컨트롤러를 호출하는것까지는 같다.

 

컨트롤러가 ModelView라는 객체를 프론트 컨트롤러에 반환해줄것이다 ( 모델이랑 뷰가 섞여있는 객체)

(뷰의 논리이름이 들어있다.)

 

프론트 컨트롤러는 뷰의 논리이름을 가지고 물리이름(물리위치)를 만들기 위해 viewResolver를 호출한다

viewResolver는 물리위치를 가진 MyView를 반환해주고 

 

프론트컨트롤러는 받은 MyView의 render()를 호출해준다.

 

MyView는 jsp를 호출하여 html를 만들고 응답한다.

서블릿객체를 안받을거니까 서블릿객체에 데이터를 담았던것을 할수없다 -> Model이 필요하다.

 

 

ModelView

public class ModelView { //스프링에는 modelAndview라는게 있다.
    private String viewName; //뷰의 논리적이름(논리이름)을 저장할 변수
    private Map<String, Object> model = new HashMap<>();//모델역할(데이터를 저장)을 할 Map객체

    public ModelView(String viewName) {
        this.viewName = viewName;
    }
    //@Getter,@Setter써도된다.
    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;
    }
}

ControllerV3(인터페이스)

public interface ControllerV3 {
    ModelView process(Map<String, String> paramMap); //파라미터값이 담긴 Map을 전달하고, ModelView객체를 반환해준다.
}

회원등록 컨트롤러

public class MemberFormControllerV3 implements ControllerV3 {

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

회원저장 컨트롤러

public class MemberSaveControllerV3 implements ControllerV3 {

    private MemberRepository memberRepository = MemberRepository.getInstance();
    @Override
    public ModelView process(Map<String, String> paramMap) {
        //파라미터값들은 프론트컨트롤러에서 Map에 담아 전달인자로 보내주니까 그걸 쓰기만하면된다.
        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); //ModelView의 모델을 가져와서 데이터를 넣어준다. (뷰에 넘기기위해)
        return modelView;
    }
}

회원목록 컨트롤러

public class MemberListControllerV3 implements ControllerV3 {

    private 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); //ModelView객체의 Model역할인 Map은 <String,Object>이므로 List도 받을수있다.
        return modelView;
    }
}

프론트컨트롤러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 MemberFormControllerV3()); //url,컨트롤러객체를 Map에 넣어준다.
        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();

        ControllerV3 controller = controllerMap.get(requestURI); //uri를 가지고 map에서 해당 컨트롤러를 찾아온다. 인터페이스를 구현한 컨트롤러라 인터페이스변수로 받을 수있다(다형성)
        if (controller == null) { //이상한 uri라 못찾으면 null
            response.setStatus(HttpServletResponse.SC_NOT_FOUND); //404 상태코드를 응답메시지 상태코드로 설정한다.
            return; // 404인경우에는 바로 return
        }

        //만든 paramMap을 전달해주고 , 반환된 ModelView객체 받기   컨트롤러에서 비즈니스로직을 진행하고, 뷰에 필요한 데이터는 ModelView의 모델안에 넣었다.
        ModelView modelView = controller.process(createParamMap(request));
        String viewName = modelView.getViewName();

        //ModelView의 논리이름을 가지고 , 물리주소로 바꿔주고 MyView객체도 만들어서 리턴하는 viewResolver를 이용한다.
        MyView view = viewResolver(viewName);

        view.render(modelView.getModel(),request,response); // viewResolver를 이용해 받은 물리주소를 가지고있는 MyView의 렌더를 실행한다.
        //ModelView의 모델에 담았던 데이터도 넘겨준다.

    }

    //ModelView의 논리이름을 가지고 , 물리주소로 바꿔주고 MyView객체도 만들어줄  ViewResolver기능을 만들어준다.
    private static MyView viewResolver(String viewName) {
        return new MyView("/WEB-INF/views/" + viewName + ".jsp");
    }

    private static Map<String, String> createParamMap(HttpServletRequest request) { //extract method로 메소드 추출 (단축키 컨트롤+알트+쉬프트+t)
        // paramMap 만들기 , 파라미터값들을 다 넣어준다.
        Map<String, String> paramMap = new HashMap<>();
        request.getParameterNames().asIterator().forEachRemaining(
            paramName -> paramMap.put(paramName, request.getParameter(paramName)));
        return paramMap;
    }
}

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객체를 이용해 forward()를 사용해서 뷰(jsp)를 호출했다.
        //그 과정을 렌더링이라고 부를것이다(뷰를 만들거나, 뷰를 만들기위해 forward 하는 행동들, 결국 뷰를 만드는것이기때문에 렌더링)
        RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
        dispatcher.forward(request,response);

    }

    //모델도 받기위해 render()를 오버로딩한다.
    public void render(Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        //jsp는 request.getAttribute로 데이터를 꺼내기때문에 , 모델에 있는값을 다 request에 넣어줘야한다.
        model.forEach((key, value) -> request.setAttribute(key, value));
        RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
        dispatcher.forward(request,response);
    }
}

render() 오버로딩 추가.

뷰 객체(여기서는 MyView)를 통해서 JSP로 포워드 하게되고 JSP를 렌더링한다.

v3 잘동작!


단순하고 실용적인 컨트롤러 (버전4)

v4 구조

ControllerV4(인터페이스)

public interface ControllerV4 {
    String process(Map<String, String> paramMap, Map<String, Object> model);

}

반환값은 ViewName(논리 뷰 이름)이기때문에 String

ModelView를 사용하지않으므로,  직접 model역할을 할 Map객체를 컨트롤러에서 만들어서 넘겨준다

넘겨준 Map에 데이터를 담기만 하면된다.

회원 등록 컨트롤러

public class MemberFormControllerV4 implements ControllerV4 {

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

회원 저장 컨트롤러

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

    }
}

회원 목록 컨트롤러

public class MemberListControllerV4 implements ControllerV4 {
    private 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";
    }
}

프론트컨트롤러V4

//프론트컨트롤러는 서블릿으로 구현하기로했으니, 서블릿 구조를 가진다.
@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()); //url,컨트롤러객체를 Map에 넣어준다.
        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); //uri를 가지고 map에서 해당 컨트롤러를 찾아온다. 인터페이스를 구현한 컨트롤러라 인터페이스변수로 받을 수있다(다형성)
        if (controller == null) { //이상한 uri라 못찾으면 null
            response.setStatus(HttpServletResponse.SC_NOT_FOUND); //404 상태코드를 응답메시지 상태코드로 설정한다.
            return; // 404인경우에는 바로 return
        }

        Map<String, String> paramMap = createParamMap(request);
        Map<String, Object> model = new HashMap<>(); //프론트컨트롤러에서 모델을 만들어준다.

        String viewName = controller.process(paramMap, model);//프론트컨트롤러에서 만든 모델을 넘겨서, 데이터를 담게한다.
        MyView myView = viewResolver(viewName);
        myView.render(model, request, response);

    }

    //ModelView의 논리이름을 가지고 , 물리주소로 바꿔주고 MyView객체도 만들어줄  ViewResolver기능을 만들어준다.
    private static MyView viewResolver(String viewName) {
        return new MyView("/WEB-INF/views/" + viewName + ".jsp");
    }

    private static Map<String, String> createParamMap(HttpServletRequest request) { //extract method로 메소드 추출 (단축키 컨트롤+알트+쉬프트+t)
        // paramMap 만들기 , 파라미터값들을 다 넣어준다.
        Map<String, String> paramMap = new HashMap<>();
        request.getParameterNames().asIterator().forEachRemaining(
            paramName -> paramMap.put(paramName, request.getParameter(paramName)));
        return paramMap;
    }
}

 

잘동작!


유연한 컨트롤러1 (버전5)

public interface ControllerV3 {
    ModelView process(Map<String, String> paramMap); //파라미터값이 담긴 Map을 전달하고, ModelView객체를 반환해준다.
}
public interface ControllerV4 {
    String process(Map<String, String> paramMap, Map<String, Object> model);

}

프론트컨트롤러에서

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

public FrontControllerServletV4() {
    controllerMap.put("/front-controller/v4/members/new-form", new MemberFormControllerV4()); //url,컨트롤러객체를 Map에 넣어준다.
    controllerMap.put("/front-controller/v4/members/save", new MemberSaveControllerV4());
    controllerMap.put("/front-controller/v4/members", new MemberListControllerV4());
}

이런식으로 url과 컨트롤러를 매핑할때 사용하는 Map 객체에서 Controller4 인터페이스를 넣고 사용하기때문에

ControllerV3도 같이 사용하는것을 불가능해보인다.

 

V5 구조

핸들러 : (사전적의미) 취급[처리]하는 사람

컨트롤러의 이름을 더 넓은 범위인 핸들러(handler)로 변경. 컨트롤러의 개념뿐만아니라 해당 어댑터만 있으면 다 처리가능하기 때문 (핸들러가 컨트롤러라고 생각하면됨)

 

1. HTTP요청이 들어오면 프론트컨트롤러가 url호출에 맞는 핸들러(컨트롤러)를 찾는다.

2. 찾은 핸들러를 처리할 수 있는 핸들러 어댑터를 찾는다 

3. 찾은 핸들러 애댑터를 이용하여 핸들러를 호출하고 ModelView를 반환받는다.

4. ModelView의 논리이름값을 이용하여 viewResolver를 호출해서 MyView를 반환받는다.

5. ModelView에 있는 모델과 서블릿request,response 객체를 넘기며 MyView.render()를 실행한다.

6. JSP를 호출하여 JSP를 렌더링 하고 HTML를 응답한다.

 

 

핸들러 어댑터 (인터페이스)

public interface MyHandlerAdapter {

    boolean supports(Objects handler); //핸들러(컨트롤러)가 넘어왔을때, 이 핸들러를 지원할수있는지 체크하는 메소드

    ModelView handle(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException;
    //핸들러 어뎁터에서 핸들러를 호출하고 반환으로 ModelView를 반환한다.
}

ControllerV3HandlerAdapter

public class ControllerV3HandlerAdapter implements MyHandlerAdapter { //ControllerV3를 처리할수있게해주는 핸들러어댑터

    @Override
    public boolean supports(Object handler) {
        return (handler instanceof ControllerV3); //입력으로 들어온 handler(컨트롤러)가 ControllerV3의 인스턴스이면 true를 반환
    }


    @Override
    public ModelView handle(HttpServletRequest request, HttpServletResponse response,Object handler) throws ServletException, IOException {
        //object로 들어온 handler러를 ControllerV3로 캐스팅한다.  (supports 메소드로 ControllerV3의 인스턴스인지 체크할것이기때문에,캐스팅해도 괜찮다)
        ControllerV3 controllerV3 = (ControllerV3) handler;
        //ControllerV3의 컨트롤러에 필요한 값들을 만들어서 컨트롤러의 비즈니스 로직을 실행하고
        Map<String, String> paramMap = createParamMap(request);
        ModelView modelView = controllerV3.process(paramMap); // ModelView를 받아서
        return modelView; //반환한다.
        //해당 핸들러에 맞게 어댑터의 로직도 바뀐다.
    }
    private static Map<String, String> createParamMap(HttpServletRequest request) { //extract method로 메소드 추출 (단축키 컨트롤+알트+쉬프트+t)
        // paramMap 만들기 , 파라미터값들을 다 넣어준다.
        Map<String, String> paramMap = new HashMap<>();
        request.getParameterNames().asIterator().forEachRemaining(
            paramName -> paramMap.put(paramName, request.getParameter(paramName)));
        return paramMap;
    }
}

 

ControllerV3 버전인 컨트롤러(핸들러)를 처리할 수 있는 어댑터이다.

supports() 메소드를 통해 해당 핸들러(컨트롤러)가 ControllerV3 객체타입인지 체크한다. (여기서 ControllerV3는 인터페이스이므로, 구현객체인지 체크하는것이다)

 

handle()메소드에서는 핸들러를 객체타입에 맞게 캐스팅해주고, 내부 로직을 실행시키기 위해 작업을 해준후 내부로직을 실행시킨다. ModelView를 반환해야하므로, ModelView를 반환해주면 받아서 return하고,

아니라면 직접생성해서라도 반환한다.

 

프론트컨트롤러V5 (서블릿)

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

//    private Map<String, ControllerV4> controllerMap = new HashMap<>(); 기존의 컨트롤러 매핑용 Map은 ControllerV4로 정해져있지만
    private final Map<String, Object> handlerMappingMap = new HashMap<>(); //핸들러 매핑용 Map은 어떤 컨트롤러든 들어갈 수 있어야하므로 Object를 넣었다.
    private final List<MyHandlerAdapter> handlerAdapters = new ArrayList<>();//핸들러 어댑터를 저장할 리스트

    public FrontControllerServletV5() {
        initHandlerMappingMap();

        initHandlerAdapters();
    }
    private void initHandlerMappingMap() {
        // url과 그 url에 매핑할 컨트롤러객체를 넣어준다.  handlerMappingMap는 Object를 받기때문에 어떤것이든 받을 수 있다(Object는 최상위 클래스이기때문)
        handlerMappingMap.put("/front-controller/v3/members/new-form", new MemberFormControllerV3());
        handlerMappingMap.put("/front-controller/v3/members/save", new MemberSaveControllerV3());
        handlerMappingMap.put("/front-controller/v3/members", new MemberListControllerV3());
    }
    private void initHandlerAdapters() {
        handlerAdapters.add(new ControllerV3HandlerAdapter());
    }

    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        Object handler = getHandler(request);
        if (handler == null) {
            response.setStatus(HttpServletResponse.SC_NOT_FOUND);
            return;
        }
        MyHandlerAdapter adapter = getHandlerAdapter(handler); // 찾을 핸들러어댑터를 저장할 인터페이스 변수
        ModelView modelView = adapter.handle(request, response, handler); //어댑터를 이용해서 핸들러를 호출한다.

        String viewName = modelView.getViewName();
        MyView myView = viewResolver(viewName);

        myView.render(modelView.getModel(), request, response);

    }

    private MyHandlerAdapter getHandlerAdapter(Object handler) {//핸들러의 어댑터를 찾는 로직
        for (MyHandlerAdapter handlerAdapter : handlerAdapters) { // 모든 어댑터에 대해 돌면서
            if (handlerAdapter.supports(handler)) { // 해당 핸들러를 지원하는 어댑터라면
                return handlerAdapter;
            }
        }
        throw new IllegalArgumentException("handler adapter를 찾을 수 없습니다."); // 어댑터를 찾지못했을때 실행되면서 예외를 발생시킨다.
    }

    private Object getHandler(HttpServletRequest request) { //http요청의 uri를 이용하여 매핑된 핸들러를 찾는 메소드
        String requestURI = request.getRequestURI();
        return handlerMappingMap.get(requestURI);
    }
    private static MyView viewResolver(String viewName) {
        return new MyView("/WEB-INF/views/" + viewName + ".jsp");
    }
}
private final Map<String, Object> handlerMappingMap = new HashMap<>(); //핸들러 매핑용 Map은 어떤 컨트롤러든 들어갈 수 있어야하므로 Object를 넣었다.
private final List<MyHandlerAdapter> handlerAdapters = new ArrayList<>();//핸들러 어댑터를 저장할 리스트

핸들러를 url호출에 맞게 매핑해서 저장해놓고 나중에 http요청이 왔을때 해당 핸들러를 찾기위한 Map 객체

핸들러어댑터를 저장해놓고, 핸들러에 맞는 어댑터를 빼서 쓰기 위한 List 객체

결과 확인


유연한 컨트롤러2 - V5(버전5)

이번에는 V3 뿐만 아니라 V4의 컨트롤러(핸들러)까지 처리할 수있게끔 설정해보자.

private void initHandlerMappingMap() {
    // url과 그 url에 매핑할 컨트롤러객체를 넣어준다.  handlerMappingMap는 Object를 받기때문에 어떤것이든 받을 수 있다(Object는 최상위 클래스이기때문)
    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());//v4 처리하는 핸들러도 추가해준다.
}

컨트롤러V4핸들러 어댑터

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);
        Map<String, Object> model = new HashMap<>();

        String viewPath = controller.process(paramMap, model); //ControllerV4는 로직 실행후 논리뷰이름을 반환한다.
        ModelView modelView = new ModelView(viewPath); // handle의 반환타입은 ModelView이므로 반환타입을 맞춰주기 위해 새로 객체를 만들고 설정한다.
        modelView.setModel(model);
        return modelView;
    }

    private static Map<String, String> createParamMap(HttpServletRequest request) { //extract method로 메소드 추출 (단축키 컨트롤+알트+쉬프트+t)
        // paramMap 만들기 , 파라미터값들을 다 넣어준다.
        Map<String, String> paramMap = new HashMap<>();
        request.getParameterNames().asIterator().forEachRemaining(
            paramName -> paramMap.put(paramName, request.getParameter(paramName)));
        return paramMap;
    }
}

새롭게 V4도 동작하도록 수정해주었다.  해야할일은 핸들러매핑에 사용하는 Map에 v4컨트롤러 객체들을 추가해주고, 컨트롤러v4 어댑터도 만들어서 핸들러어댑터조회할떄쓰이는 리스트에 추가해둔다.

핸들러 어댑터를 만들때 다른 어댑터핸들러처럼 ModelView를 반환하게끔해주어서

프론트컨트롤러에서 코드수정이 필요없게끔한다 (프론트컨트롤러는 수정이 필요없으므로 다른 컨트롤러를 사용하고 싶을때는 매핑에 값추가하고 해당 컨트롤러의 어댑터를 추가해주기만 하면되서 간단해진다)


정리

스프링을 쓰다보면

@RequestMapping("/hello") 이런 어노테이션을 봤을것이다 ( 컨트롤러(핸들러)에서)

이게 위에서 url에 맞춰서 매핑하기 위해 만들었던 Map<String,Object> handlerMappingMap에다가 매핑하듯이 하는 어노테이션인거고

 

해당 어노테이션이 있는 컨트롤러(핸들러)를 처리하는것이 RequestMappingHandlerAdapter라고 스프링에 존재한다.

(RequestMapping -> 해당 url요청을 매핑한다 ,  RequestMappingHandlerAdapter -> url요청 매핑 핸들러들을 처리하는 어댑터)

 

 

 

댓글