본문 바로가기

Dot Programming/Java

[Java] 자바 서블릿과 서블릿 컨테이너 2 - MVC 패턴 도입

    MVC 패턴 도입

    계속해서 커지는 JSP

    대략 2000년대 초, 중반까지 대부분의 웹 애플리케이션 개발은 JSP에 대부분의 로직을 포함하고 있었다. 이는 자바 진영뿐만 아니라 PHP, ASP 또한 비슷한 형태로 구현했다. 점점 더 많은 애플리케이션이 웹으로 개발되고, 요구사항의 복잡도는 점점 더 증가했다. 또한 웹 애플리케이션의 수명이 길어지면서 유지보수 업무가 증가했다. JSP에 상당 부분의 로직을 포함하는 것이 초기 개발속도는 더 빨랐지만 유지보수 비용은 증가했다.

     

    점점 커지는 JSP

    MVC 패턴 등장

    이 같은 단점을 보완해 유지보수 비용을 줄이기 위해 MVC(Model, Controller, View) 패턴 기반으로 웹 애플리케이션을 개발하는 방향으로 발전했다. JSP에 집중되던 로직을 Model, View, Controller에 분산시켰다. 2000년 초중반부터 도입된 MVC 패턴은 지금은 웹 애플리케이션 개발 모든 영역에서 적용하고 있는 패턴이다. 자바에서는 MVC 패턴을 도입하여 Servlet을 HTTP 웹 서버에서 사용하던 Controller에 대입시키고 JSP는 View에 대입시킴으로써 JSP의 역할을 분리시켰다. 

     

    MVC Architecture

    동작 과정

    • 1) 클라이언트가 서버에 요청을 보낸다.
    • 2-1) 서블릿에 요청이 전달되어 DB와 매핑된 Java Beans객체를 생성한다.
    • 2-2) Java Bean은 DB에서 적절한 정보를 가져와 저장한다.
    • 2-4) 다시 Servlet으로 넘어가서 추가적인 비즈니스 로직 과정을 수행한다
    • 3) 적절한 View를 선택한다.
    • 4) 선택된 JSP 페이지는 Java Bean과 통신하여 정보를 전달받는다.
    • 5) 동적 처리가 완료되었으면 클라이언트에게 응답한다.

     

    HTTP 웹 서버의 Controller

    HTTP 웹 서버는 서버를 시작하는 시점에 Controller의 인스턴스를 생성하고, 요청 URL과 생성한 Controller 인스턴스를 연결시켜 놓는다. 그리고 클라이언트 요청이 오면 매핑된 요청 URL로 연결시켜준다.

    • 웹 서버 시작 → Controller 인스턴스 생성 → 해당 인스턴스에 요청 URL 연결

    HTTP 웹 서버의 Controller

     

    이해가 쉽게 코드로 나타내면 다음은 HTTP 웹 서버이다. 포트 8080으로 클라이언트 요청이 오면 서버 소켓을 생성하여 쓰레드(RequestHandeler)에 요청을 넘긴다.

    public class WebServer {
        private static final Logger log = LoggerFactory.getLogger(WebServer.class);
        private static final int DEFAULT_PORT = 8080;
    
        public static void main(String args[]) throws Exception {
            int port = 0;
            if (args == null || args.length == 0) {
                port = DEFAULT_PORT;
            } else {
                port = Integer.parseInt(args[0]);
            }
    
            // 서버소켓을 생성한다. 웹서버는 기본적으로 8080번 포트를 사용한다.
            try (ServerSocket listenSocket = new ServerSocket(port)) {
                log.info("Web Application Server started {} port.", port);
    
                // 클라이언트가 연결될때까지 대기한다.
                Socket connection;
                while ((connection = listenSocket.accept()) != null) {
                    RequestHandler requestHandler = new RequestHandler(connection);
                    requestHandler.start();
                }
            }
        }
    }

     

    클라이언트 요청을 넘겨받고 HttpRequest, HttpResponse로 각 요청, 응답 클래스에 in, out 스트림을 할당한다. 

    • HttpRequest에서는 요청 헤더, 바디를 파싱하여 분석한다. 
    • HttpResponse에서는 응답으로 보낼 헤더 내용과 바디를 저장한다.
    • Controller 인스턴스를 생성해서 RequstMapping 클래스를 통해 HttpRequest에서 분석한 요청 URL을 연결한다.
    public class RequestHandler extends Thread {
    	private Socket connection;
    
    	public RequestHandler(Socket connectionSocket) {
    		this.connection = connectionSocket;
    	}
    
    	public void run() {
    		log.debug("New Client Connect! Connected IP : {}, Port : {}", connection.getInetAddress(),
    			connection.getPort());
    
    		try (InputStream in = connection.getInputStream(); OutputStream out = connection.getOutputStream();) {
    			HttpRequest httpRequest = new HttpRequest(in);
    			HttpResponse httpResponse = new HttpResponse(out);
    
    			Controller controller = RequestMapping.getController(httpRequest.getPath());
    			if (controller == null) {
    				String path = getDefaultPath(httpRequest.getPath());
    				httpResponse.forward(path);
    			} else {
    				controller.service(httpRequest, httpResponse);
    			}
    
    		} catch (IOException e) {
    			log.error(e.getMessage());
    		}
    	}
    }

     

    RequstMapping 클래스는 생성과 동시에 static 블록을 통해 Controller에게 반환할 map 인스턴스에 URL을 매핑시켜준다.

    public class RequestMapping {
    	private static Map<String, Controller> controllers = new HashMap<>();
    
    	static {
    		controllers.put("/user/create", new CreateUserController());
    		controllers.put("/user/login", new LoginController());
    		controllers.put("/user/list", new ListUserController());
    	}
    
    	public static Controller getController(String requestUrl){
    		return controllers.get(requestUrl);
    	}
    
    }

     

    서블릿 컨테이너의 서블릿

    서블릿 컨테이너는 서블릿 표준에 대한 구현을 담당하고 있으며 웹 서버가 서블릿 컨테이너 역할과 같다고 생각하면 된다.  HTTP 웹 서버와 마찬가지로 서블릿 컨테이너는 서버가 시작할 때 서블릿 인스턴스를 생성해, 요청 URL과 서블릿 인스턴스를 연결해 놓는다. 클라이언트에서 요청이 오면 요청 URL에 해당하는 서블릿을 찾아 서블릿에 모든 작업을 위임한다.

    • 서블릿 컨테이너 시작 → 서블릿 클래스 찾아서 서블릿 인스턴스 생성 →  해당 인스턴스에 요청 URL 연결 

     

    서블릿은 웹 서버의 Controller, HttpRequest, HttpResponse를 추상화해 인터페이스로 정의해 놓은 표준이다. 즉 HTTP 클라이언트 요청과 응답에 대한 표준을 정해 놓은 것을 서블릿이라 생각하면 된다. 웹 서버에서는 클라이언트에서 요청이 오면 요청 URL에 해당하는 Controller를 찾아 Controller에 실질적인 작업을 위임했다. 서블릿 컨테이너와 서블릿의 동작 방식도 이와 똑같다. 웹 서버에서 사용하던 컨트롤러와 WAS의 서블릿은 같은 역할을 한다고 보면 된다.

    • Controller(HttpRequset, HttpResponse) == Servlet(HttpServletRequest, HttpServletResponse)

     

    다음은 WAS 서버 실행 코드이다. 자바 톰캣 서버로 구현했다. 마찬가지로 8080 포트로 서버가 시작되면 서블릿 컨테이너가 실행된다.

    public class WebServerLauncher {
        public static void main(String[] args) throws Exception {
            String webappDirLocation = "webapp/";
            Tomcat tomcat = new Tomcat();
            tomcat.setPort(8080);
    
            tomcat.addWebapp("/", new File(webappDirLocation).getAbsolutePath());
            logger.info("configuring app with basedir: {}", new File("./" + webappDirLocation).getAbsolutePath());
    
            tomcat.start();
            tomcat.getServer().await();
        }
    }

     

    서블릿 컨테이너는 톰캣으로부터 요청을 넘겨받고 @WebServlet으로 되어있는 클래스를 찾아 서블릿 인스턴스를 생성한다.

    • HttpServlet에 HttpServletRequest, HttpServletResponse를 직접 제공한다. 각 클래스 웹 서버 HttpRequest와 HttpResponse와 역할이 동일하다. 차이점은 있다면 웹 서버에서는 직접 구현했고 WAS는 HttpServlet에서 제공해준다는 점이다.
    • init() 메서드를 실행시켜 해당 서블릿 인스턴스에 요청 URL을 매핑시킨다. 웹 서버 Controller와 역할이 동일하다.
    @WebServlet(name = "dispatcher", urlPatterns = "/", loadOnStartup = 1)
    public class DispatcherServlet extends HttpServlet {
        private static final long serialVersionUID = 1L;
        private static final String DEFAULT_REDIRECT_PREFIX = "redirect:";
    
        private RequestMapping rm;
    
        @Override
        public void init() {
            rm = new RequestMapping();
            rm.initMapping();
        }
    
        @Override
        protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
            String requestUri = req.getRequestURI();
    	Controller controller = rm.findController(requestUri);
            // service 로직 수행...
        }
        
        @Override
        public void destroy() {
            // destory!
        }
    }

     

    Http 웹 서버와 마찬가지로 RequstMapping 클래스는 생성과 동시에 static 블록을 통해 Controller에게 반환할 map 인스턴스에 URL을 매핑시켜준다. 그런데 차이점이 있다. 위에서는 단순 서비스 로직만 매핑시켜줬는데 해당 클래스에서는 jsp와 같은 요청도 보인다. 이유는 응답 바디에 HTML을 담아 보내던 웹 서버와 달라 WAS는 JSP로 HTML을 자바 코드에 담아 보내기 때문이다.

    public class RequestMapping {
        private Map<String, Controller> mappings = new HashMap<>();
    
        void initMapping() {
            mappings.put("/", new HomeController());
            mappings.put("/users/form", new ForwardController("/user/form.jsp"));
            mappings.put("/users/loginForm", new ForwardController("/user/login.jsp"));
            mappings.put("/users", new ListUserController());
            mappings.put("/users/login", new LoginController());
            logger.info("Initialized Request Mapping!");
        }
    
        public Controller findController(String url) {
            return mappings.get(url);
        }
    
        void put(String url, Controller controller) {
            mappings.put(url, controller);
        }
    }

     

    Spring MVC Flow of Control

    이렇게 진화하여 지금 모든 자바 개발자들이 사용하고 있는 Spring MVC 패턴은 어떻게 작동하는지 살펴보자.

    Spring MVC Flow of Control

    동작 과정

    • 1) 클라이언트 요청 GET http://site.com/users
    • DispatcerServlet
      • 모든 요청은 DispatcerServlet이 먼저 받는다.
      • 1-2) RequestMapping에 연결된 url 중 요청 url과 일치하는 Controller객체와 매핑한다.
      • 2) 매핑된 Controller 인스턴스로 사용자 요청 정보를 인자들을 넘긴다.
    • Controller
      • 2-1) 비즈니스 로직을 실행한다.
    • Model
      • 2-2) DAO와 DB를 거쳐 결과값을 가져온다.
      • 2-3) DAO에 가져온 Model 정보를 Controller에게 전달한다.
    • Controller
      • 3) 사용자 요청을 처리한 ModelAndView객체를 DispatcherServlet에 반환한다.
    • DispatcerServlet
      • 4) 해당하는 JSP View를 선택하고 해당 View에 Model 객체를 전송한다.
    • View
      • Model 정보로 동적 웹페이지를 렌더링한다.
    • 5)클라이언트에게 응답한다.

     


    참고

    자바 웹 프로그래밍 Next Step

    https://gmlwjd9405.github.io/2018/11/05/mvc-architecture.html