반응형

 

 

"김영한 스프링 입문 - 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술" 내용을 기반으로 작성한 내용입니다.

 

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

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

www.inflearn.com

 

 

 

📝초창기 WEB

초창기 WEB이란 정적 데이터만 전달했습니다 그렇기 때문에 동적인 처리가 불가능합니다.

 

📝CGI

그래서 나온 동적 데이터를 처리하는 CGI라는 프로토콜(인터페이스)가 만들어졌습니다.

HTTP에 대한 요청이 있으면 그 요청에 맞게 서버에서 화면을 만들어서 주는 형식이죠

 

하지만 CGI에는 문제가 있습니다.

  1. Request가 있을 때마다 프로세스를 만들기 때문에 메모리 사용량이 많아 과부하현상이 벌어지곤 했습니다.
  2. 같은 결과값이라도 요청 프로세스가 다른 경우 같은 걸 여러개를 만드는 문제가 있었습니다.

 

이러한 이슈를 보완하기 위해서 만들어진 방법이 있습니다.

  1. 프로세스가 아닌 스레드로 처리를 변경했습니다.
  2. 동일한 요청이 있는 경우 실글톤 패턴을 사용합니다.

 

📝Servlet

이러한 점을 보완해서 등장한 것이 Servlet 입니다.

public class WelcomeServlet extends HttpServlet {
	protected void doPost(HttpServletRequest req, 
    					HttpServletResponse res) throws ServletException, IOException{
   		
        // Request의 파라미터를 받는다.(form field를 읽는다)
        String userid = req.getParameter("userid");
        String password = req.getParameter("password");
        
        // Business Logic 처리.
        String result = doSomething(userid, password);
        
        // 결과값을 담은 View 생성.
        String htmlResponse = "<html>";
        htmlResponse += "<h2>Your userid is: " + username + "<br/>";      
        htmlResponse += "Your password is: " + password + "</h2>";    
        htmlResponse += "</html>";	
        
        // 클라이언트에게 결과값 전송.
        PrintWriter writer = res.getWriter();
        writer.println(htmlResponse); 
   }
}

이렇게 서버에 코딩을 하고 HTML 태그도 붙이면 동적인 페이지를 만들 수 있지만 HTML이 엄청 많아질 수록 처리가 너무 힘듭니다. 또한 서버에 부담이 많이 됩니다.

 

 

📝JSP

이러한 유지보수 및 개발 속도를 개선 시키기 위해서 JSP라는게 나오게 됩니다

<% %> 라는 스크립트 영역이 있고 스크립트 안에 있는 건 서버에서 실행하는 형식입니다.

컴파일시 JSP를 Servlet으로 변환시켜줍니다

하기 코드는 위에 Servlet 코드를 JSP로 변환시킨 것입니다.

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Welcome</title>
</head>
<body>
    <%
        // Request의 파라미터를 받는다.(form field를 읽는다)
        String userid = request.getParameter("userid");
        String password = request.getParameter("password");

        // Business Logic 처리.
        String result = doSomething(userid, password);
    %>

<h2>Your userid is: <%= userid %><br/>
Your password is: <%= password %></h2>
</body>
</html>

이러면 익숙한 구조에 빠르게 개발할 수 있는 구조이지요

 

초창기 서블릿, JSP는 성능이 너무 안 좋았습니다. 속도 수준이 PHP >= ASP >>>> JSP 수준이였고 JDBC의 성능도 최악 JVM 성능도 최악이었습니다 그러다 보니 High Cost인 DB 연결을 DBCP (Connection Pool)을 도입 및 고도화로 인해 PHP = JSP > ASP 까지 끌어 올렸습니다

 

 

📝J2EE & Java EE

J2EE 및 JavaEE의 경우 어플리케이션을 만들기 위해 필요한 스펙의 집합으로 다양한 구성요소가 있습니다 → Servlet, JSP, EJB, JNDI, JMS, JDBC 등

하지만 EJB(Enterprise Java Beans)라는 기술이 핵심인데 사용하다보니 너무 불편했고 그로 인해 사람들을 무료면서 불편사항들을 모두 해결할 수 있는 Spring을 사용하게 됩니다.

 

 

📝Spring (MVC 패턴)

JSP의 가장 안 좋은 점은 비즈니스 코드 + 화면 코드 + 모델(POJO)가 한군데 모여있다는 점으로 유지보수가 매우 힘들다는 점이 있습니다. 그 외에 데이터베이스 연동 세션 관리등 많은 문제점을 개선한 Spring FrameWork가 나오게 됩니다.

Spring MVC의 경우 Model View Controller로 분리 시켜서 프로젝트의 유지보수 및 개발 생산성을 높여주는 역할을 합니다. 또한 의존성 주입(DI), IoC를 이용해 더 효율적인 비즈니스 로직을 이용할 수 있게합니다.

 

 

MVC 변화 1

// @WebServlet은 URL 접근경로와 이름을 설정해서 URL로 접근할 수 있게 해주는 역할입니다.
@WebServlet(name = "mvcMemberSaveServlet", urlPatterns = "/servlet-mvc/members/save") 
public class MvcMemberSaveServlet extends HttpServlet {

    private MemberRepository memberRepository = MemberRepository.getInstance();

    @Override
    protected void service(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);
        System.out.println("member = " + member);

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

    }
}

Data Model과 로직을 컨트롤러에 두고 View를 JSP로 분리시켜 명확하게 역할이 구분 되지만 중복이 많고 필요하지 않는 코드들도 많이 보입니다.

 

⚠️단점

 

  • forward로 model 정보를 view에 전달하는 코드가 각 controller마다 존재하는 중복 코드
  • jsp가 아닌 thymeleaf같은 다른 템플릿 엔진으로 변경시 코드 전체 수정 필요
  • request, response객체가 필요 없는 곳에서도 반드시 사용해야하며 테스트 코드 작성도 어려움
  • 공통코드가 있을 때 forward처럼 각 controller마다 다 입력해야 함

 

 

MVC 변화 2

MVC 변화 1의 문제점을 해결하기 위해서는 FrontController 패턴을 도입해 공통코드를 앞에서 다 처리하게끔 한다.

 

/** ──── Front Controller ──── **/
@WebServlet(name = "frontControllerServletV1", urlPatterns = "/front-controller/v1/*")
public class FrontControllerServletV1 extends HttpServlet {
    private Map<String, ControllerV1> controllerMap = new HashMap<>();
    
    // controller mapping 관리
    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());
    }
    
    
    // front controller service (공통 처리)
    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        System.out.println("FrontControllerServletV1.service");
        String requestURI = request.getRequestURI();
        ControllerV1 controller = controllerMap.get(requestURI);
        
        if (controller == null) {
            response.setStatus(HttpServletResponse.SC_NOT_FOUND);
            return;
    	}
        
    	controller.process(request, response);
    }
}



/** ──── Controller Interface ──── **/
public interface ControllerV1 {
	void process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException;
}


/** ──── Controller ──── **/
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);
    }
}

FrontController에서 Controller랑 매핑시킬 Map을 만듭니다. 그리고 공통 처리에 필요한 건 service에 작성합니다.

service에서 공통적으로 처리하기 위해 Controller Interface를 상속받아 구체적인 Controller에 구현합니다.

 

요약하면 사용자가 FrontController에 있는 URL Pattern에 해당하는 URL을 요청하면 service 함수가 작동하고 service에 있는 공통 처리 이후에 controllerMap에서 해당 URL에 해당하는 값을 찾아 해당 상세 controller를 실행(process 함수)시킵니다.

 

⚠️단점

  • view로 이동시키는 forward 공통코드 분리 필요

 

MVC 변화 3

/** ──── Front Controller ──── **/
@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);
    }
}


/** ──── 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 dispatcher = request.getRequestDispatcher(viewPath);
        dispatcher.forward(request, response);
    }
}

/** ──── Controller Interface ──── **/
public interface ControllerV2 {
	MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException;
}

/** ──── Controller ──── **/
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");
    }
}

dispatcher.forward로 해당 view로 화면 전환시키는 과정이 중복되게 되는데 이걸 분리시켜 공통작업인 FrontController에게 위임합니다. (render 함수) 상세 Controller에서 처리한 view정보를 MyView라는 객체에 담아서 반환해줍니다.

 

 

⚠️단점

  • Request, Response 객체 필요 없을 때도 사용해야 함 → 서블릿 종속 제거
  • 뷰의 공통 Path 중복 제거 → WEB-INF/views

 

MVC 변화 4

/** ──── Front Controller ──── **/
@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();
        ControllerV3 controller = controllerMap.get(requestURI);
        
        if (controller == null) {
            response.setStatus(HttpServletResponse.SC_NOT_FOUND);
            return;
        }

        Map<String, String> paramMap = createParamMap(request);
        ModelView mv = controller.process(paramMap);
        String viewName = mv.getViewName();
        MyView view = viewResolver(viewName);
        view.render(mv.getModel(), request, response);
    }

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

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


/** ──── ModelView ──── **/
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;
    }
}

/** ──── 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 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));
    }
}


/** ──── Controller Interface ──── **/
public interface ControllerV3 {
    ModelView process(Map<String, String> paramMap);
}


/** ──── Controller ──── **/
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;
    }
}

서블릿종속을 제거하기 위해 데이터를 담는 곳을 Request, Response가 아닌 별도의 Model에 담는다. (ModelView = View에 해당하는 모델정보와 View정보가 들어간다) 또한 별도 Model을 분리시킨 ModelView를 가지고 Render하기 위해서 ModelView를 매개변수로 받는 MyView에서 Render함수를 추가 작성합니다.

공통 Path 중복코드를 제거하기 위해 FrontController의 viewResolver를 통해 뷰 공통 Path를 제거시킵니다.

 

 

 

🔗 출처 및 참고

https://charliecharlie.tistory.com/248

https://jhyonhyon.tistory.com/27

 

 

 

 

반응형