본문 바로가기
Backend/Spring | SpringBoot

[Servlet] 회원 관리 웹 애플리케이션 만들기2 (프론트 컨트롤러 패턴)

by 2245 2023. 8. 18.

https://wngml56.tistory.com/237

 

회원관리 웹 애플리케이션 만들기 (서블릿, JSP, MVC 패턴)

서론 서블릿, JSP, MVC 패턴으로 회원 관리 웹 애플리케이션을 만들어 봅시다. 본론 회원 관리 웹 애플리케이션 요구사항 회원 정보 이름: username 나이: age 기능 요구사항 회원 저장 회원 목록 조회

wngml56.tistory.com

이어지는 글입니다.

이번에는 프론트 컨트롤러 패턴을 도입해 회원 관리 애플리케이션을 만들어보겠습니다.

 

 

목차

     

    프론트 컨트롤러 패턴 소개

    프론트 컨트롤러 도입 전

    컨트롤러에 공통된 중복 코드들이 포함되어 있습니다. 

     

     

    프론트 컨트롤러 도입 후

    공통 부분을 프론트 컨트롤러에서 처리합니다. 

     

    FrontController 패턴

    • 프론트 컨트롤러 서블릿으로 모든 클라이언트의 요청을 받습니다.
    • 공통적인 처리 후 프론트 컨트롤러가 요청에 맞는 컨트롤러를 찾아 호출합니다.
    • 이로써 프론트 컨트롤러를 제외한 나머지 컨트롤러는 서블릿을 사용하지 않고 순수한 자바 코드로 작성할 수 있습니다.
    • 따라서 테스트 코드를 작성하기에도 용이합니다. 
    • 입구를 하나로!

     

    스프링 MVC와 프론트 컨트롤러

    스프링 웹 MVC의 DispatcherServlet이 FrontController 패턴으로 구현되어 있습니다.

    스프링 웹 MVC의 핵심이 바로 이 FrontController 입니다.

     

    참고 리팩토링
    리팩토링을 할 때는 구조적인 레벨을 손 댈 경우 구조만 건들고 나머지는 그대로 둔 후 테스트를 진행해야 합니다.
    이후에 점점 디테일한 부분을 조금씩 손보고 커밋하고 테스트하는 것이 좋습니다.
    참고
    강의에선 v1, v2, v3, v4, v5 단계적으로 리팩토링을 하지만 중복되는 코드는 제외하고 v3, v4, v5 대해서 작성하겠습니다. 

     

     

    중복을 제거한 프론트 컨트롤러 - v3

    V3 구조

    모든 HTTP 요청은 FrontController에서 받습니다.

    1. 컨트롤러 매핑 map에서 URL 주소에 맞는 컨트롤러를 찾습니다.
    2. 필요한 데이터를 HttpServletRequest에서 꺼내 파라미터 map에 담은 후, 해당 컨트롤러를 호출합니다. (서블릿 종속성 제거) 
    3. 컨트롤러는 비지니스 로직을 처리한 후, ModelView를 생성해 Model에 데이터를 담고, 이동해야할 View의 논리 이름을 담아 반환합니다.  (서블릿 종속성 제거)
    4. 프론트 컨트롤러는 반환된 View를 viewResolver를 통해 논리 이름을 물리 이름으로 바꿉니다. (View 이름 중복 제거)
    5. 프론트 컨트롤러는 MyView를 생성 후 호출하고, MyView는 request에 model을 담아 View의 물리 이름으로 forward 로직을 수행해 JSP를 렌더링합니다. (View 분리)

     

    이제 코드를 작성해보겠습니다.

    ControllerV3 - 컨트롤러 인터페이스 

    프론트 컨트롤러에서 각기 다른 컨트롤러를 호출하기 때문에, 공통된 호출을 위해 인터페이스를 사용합니다. 

    public interface ControllerV3 {
        ModelView process(Map<String, String> paramMap);
    }
    • paramMap : 비지니스 로직에 필요한 파라미터는 프론트 컨트롤러에서 request.getParameter()를 통해 map에 담아 전달합니다.
    • ModelView : 컨트롤러는 비지니스 로직을 수행 후, 렌더링에 필요한 데이터를 담은 Model과 렌더링할 View(JSP)의 이름을 담아 반환합니다. 

     

    ModelView

    뷰의 이름과 랜더링할 때 필요한 model 객체를 가지고 있습니다. 

    public class ModelView {
        private String viewName;
        private Map<String, Object> model = new HashMap<>();
        
        public ModelView(String viewName) {
            this.viewName = viewName;
        }
        
        //Getter, Setter...
    }

     

    MemberFormControllerV3 - 회원 등록 폼

    public class MemberFormControllerV3 implements ControllerV3 {
    
        @Override
        public ModelView process(Map<String, String> paramMap) {
            return new ModelView("new-form");		//view의 논리 이름만 지정합니다. 
        }
    }

     

    MemberSaveControllerV3 - 회원 저장

    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 mv = new ModelView("save-result");	//논리 이름 지정
            mv.getModel().put("member", member);		//model에 필요한 데이터 저장 
            return mv;
        }
    }

     

    MemberListControllerV3 - 회원 목록

    public class MemberListControllerV3 implements ControllerV3 {
        
        private 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);		//model에 필요한 데이터 저장 
            return mv;
        }
    }

     

    FrontControllerServletV3

    ///front-controller/v3 으로 시작하는 모든 URL 요청을 받습니다.  
    @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 {
            //요청 URI에 맞는 컨트롤러 조회 
            String requestURI = request.getRequestURI();
            ControllerV3 controller = controllerMap.get(requestURI);
            if(controller == null) {
                response.setStatus(HttpServletResponse.SC_NOT_FOUND);
                return;
            }
    		
            //request에서 파라미터 map 생성 
            Map<String, String> paramMap = createParamMap(request);
            
            //컨트롤러 호출, 응답으로 Model과 View 반환 
            ModelView mv = controller.process(paramMap);
            
            //view의 논리 이름을 물리 이름으로 변환 
            String viewName = mv.getViewName();
            MyView view = viewResolver(viewName);
            
            //model를 가지고 view로 이동
            view.render(mv.getModel(), request, response);
        }
    
        //request에서 파라미터 map 생성 
        private Map<String, String> createParamMap(HttpServletRequest request) {
            Map<String, String> paramMap = new HashMap<>();
    
            Enumeration<String> paramNames = request.getParameterNames();
            while(paramNames.hasMoreElements()) {
                String name = paramNames.nextElement();
                paramMap.put(name, request.getParameter(name));
            }
    
            return paramMap;
        }
    	
        //view의 논리 이름을 물리 이름으로 변환 
        private MyView viewResolver(String viewName) {
            return new MyView("/WEB-INF/views/" + viewName + ".jsp");
        }
    }
    • urlPatterns = "/front-controller/v3/*"
      • /front-controller/v3 를 포함한 하위 모든 요청은 이 서블릿에서 받습니다.
      • 예) /front-controller/v3, /front-controller/v3/a, /front-controller/v3/a/b

     

    MyView 

    public class MyView {
    
        private String viewPath;		//렌더링할 view 물리 이름 
    
        public MyView(String viewPath) {
            this.viewPath = viewPath;
        }
        
        public void render(Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
            //model의 데이터를 꺼내 request에 담습니다. (jsp는 request.getAttribute()로 데이터를 조회하기 때문에)
            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));
        }
    }

     

     

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

    앞서 만든 v3 컨트롤러는 서블릿 종속을 제거하고, 뷰 경로의 중복도 제거하는 등의 잘 설계된 컨트롤러입니다.

    그런데, 개발자 입장에서 항상 컨트롤러가 ModelView 객체를 생성하고 반환하는 건 조금 번거로운 일입니다. 

    Model도 프론트 컨트롤러에서 만들어 전달해준다면 더 간편하게 코드를 작성할 수 있습니다.

     

    좋은 프레임워크는 아키텍처도 중요하지만, 실제 개발하는 개발자가 단순하고 편리하게 사용할 수 있어야 합니다. 

    이번에는 v3에서 컨트롤러의 ModelView 을 리턴하는 부분을 수정해 v4 버전을 작성해보도록 하겠습니다.

     

    v4 구조

    기본적인 구조는 v3와 같지만, 컨트롤러가 ModelView를 반환하지 않고 ViewName만을 반환합니다.

     

    ControllerV4 - 인터페이스 

    public interface ControllerV4 {
    
        /**
         * @param paramMap
         * @param model
         * @return viewName
         */
        String process(Map<String, String> paramMap, Map<String, Object> model);
    }
    • ModelView를 리턴하는 것이 아니라 뷰의 이름만 String으로 반환합니다.
    • 필요한 model은 프론트 컨트롤러에서 생성해 전달합니다.

     

    MemberFormControllerV4

    public class MemberFormControllerV4 implements ControllerV4 {
    
        @Override
        public String process(Map<String, String> paramMap, Map<String, Object> model) {
            return "new-form";		//단순하게 View의 논리 이름만 반환합니다. 
        }
    }

     

    MemberSaveControllerV4

    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);
            
            //ModelView를 생성하는 것이 아닌 전달받은 model에 데이터를 담습니다. 
            model.put("member", member);
            
            //view의 논리 이름만 반환합니다. 
            return "save-result";
        }
    }

     

    MemberListControllerV4

    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();
            
            //마찬가지로 ModelView를 생성하지 않고 전달받은 model에 데이터를 저장합니다.
            model.put("members", members);
            //View의 논리 이름만 반환합니다. 
            return "members";
        }
    }

     

    FrontControllerServletV4

    • v3과 달라진 것이 거의 없고, 컨트롤러 호출 전에 model을 생성해 넘겨주는 부분만 추가되었습니다.
    • 컨트롤러에서 해당 model에 객체를 저장하면 그대로 담겨있게 됩니다. (주소참조)
    @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);	//컨트롤러가 View의 논리이름만 직접 반환 
            MyView view = viewResolver(viewName);
            view.render(model, request, response);
        }
    
        private Map<String, String> createParamMap(HttpServletRequest request) {
            Map<String, String> paramMap = new HashMap<>();
    
            Enumeration<String> paramNames = request.getParameterNames();
            while(paramNames.hasMoreElements()) {
                String name = paramNames.nextElement();
                paramMap.put(name, request.getParameter(name));
            }
    
            return paramMap;
        }
    
        private MyView viewResolver(String viewName) {
            return new MyView("/WEB-INF/views/" + viewName + ".jsp");
        }
    }

     

    결론

    • 프론트 컨트롤러의 일이 많아질 수록, 각 컨트롤러의 코드가 간편해졌습니다. 
    • 프레임워크나 공통 기능이 수고로워야 사용하는 개발자가 편리해집니다.
    • 점진적으로 프레임워크가 중복을 제거하고 패턴을 도입하면서 편리하게 발전되어 왔습니다.

     

    만약, ControllerV3 와 ControllerV4 를 둘 다 사용하고 싶으면? (어댑터 패턴)

    FrontControllerServletV4 코드를 보겠습니다.

    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());
    }
    • 코드를 보면 FrontControllerV4는 ControllerV4만 사용할 수 있도록 작성되어 있습니다.
    • 만약, ControllerV4 대신 ControllerV3를 controllerMap에 put 한다면 컴파일 오류가 발생합니다.
    • 이때 어댑터 패턴을 도입해 해결할 수 있습니다. 어댑터 패턴을 통해 유연한 프론트 컨트롤러를 작성할 수 있습니다.
    • 다음 글에서 마지막으로 어댑터 패턴(FrontControllerV5) 에 대해 작성하도록 하겠습니다. 

     


    출처

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

     

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

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

    www.inflearn.com