관리 메뉴

나구리의 개발공부기록

서블릿,JSP,MVC패턴, MVC패턴(개요/적용/한계) 본문

인프런 - 스프링 완전정복 코스 로드맵/스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술

서블릿,JSP,MVC패턴, MVC패턴(개요/적용/한계)

소소한나구리 2024. 2. 24. 17:07

  출처 : 인프런 - 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술 (유료) / 김영한님  
  유료 강의이므로 정리에 초점을 두고 코드는 일부만 인용  

https://inf.run/Gmptq


1. MVC 패턴

1) 개요

(1) 너무 많은 역할

  • 하나의 서블릿이나 JSP만으로 비즈니스로직과 뷰 렌더링을 모두 처리하면 너무 많은 역할을 하게되고 결과적으로 유지보수가 어려워짐
  • 비즈니스 로직, UI둘중에 하나만 변경할 일이 발생해도 비즈니스로직과 뷰 렌더링을 하는 코드가 함께있는 파일을 수정해야함
  • 수백, 수천줄의 java코드와 html 코드가 섞여있는 파일을 유지보수 한다고 생각하면 수정할 부분이 있는 코드를 찾는 것 조차 매우 어려울것임

(2) 변경의 라이프 사이클

  • 더 중요한 것은 두가지(비즈니스로직과 뷰)의 변경의 라이프 사이클이 다름
  • 매우 큰 변화가 일어날 경우 같이 변경이 일어나기도 하지만 대부분은 UI를 일부 수정하거나, 비즈니스 로직만 일부 수정하는 등 각각 다르게 발생할 가능성이 매우 높음
  • 이러한 수정은 서로에게 영향을 주지 않으므로 변경의 라이프 사이클이 다른 부분을 하나의 코드로 관리하는 것은 유지보수하기 좋지 않음

(3) 기능 특화

  • JSP 같은 뷰 템플릿은 화면을 렌더링 하는데 최적화 되어있고 servlet은 자바 코드를 처리하는데 최적화 되어있음
  • 각자 최적화 되어있는 부분의 업무만 담당하는 것이 효과적

(4) Model View Controller

  • MVC 패턴은 하나의 서블릿이나 JSP로 처리하던 것을 컨트롤러, 뷰라는 영역으로 역할을 분리한 것
  • 웹 애플리케이션은 보통 이런 MVC패턴을 사용

(5) 컨트롤러

  • HTTP 요청을 받아 파라미터를 검증하고 비즈니스 로직을 실행
  • 뷰에 전달할 결과 데이터를 조회 후 모델에 넘김

(6) 모델

  • 뷰에 출력할 데이터를 담아둠
  • 뷰가 필요한 데이터를 모두 모델에 담아서 전달해주는 덕분에 뷰는 비즈니스 로직이나 데이터 접근을 몰라도 되고, 화면을 렌더링 하는 일에만 집중 할 수 있음

(7) 뷰

  • 모델에 담겨있는 데이터를 사용하여 화면을 그리는 일에 집중

** 참고

  • 컨트롤러에 비즈니스 로직을 둘 수 있지만 컨트롤러가 많은 역할을 담당하게 되므로 일반적으로 서비스(service)라는 계층을 별도로 만들어서 비즈니스 로직을 처리함 (물론 작은 프로젝트는 한번에 처리해도 무방함)
  • 컨트롤러는 비즈니스 로직이 있는 서비스를 호출하는 역할을 담당
  • 그래서 비즈니스 로직(service)을 변경하면 비즈니스로직을 호출하는 컨트롤러의 코드도 변경될 수 있음

좌) MVC패턴 이전 / 우) MVC 패턴 후(비즈니스로직과 컨트롤러 로직을 분리)

2) MVC패턴 - 적용

(1) 적용 방식

  • 서블릿: 컨트롤러
  • JSP: 뷰
  • Model: HttpServletRequest 객체 내부에 가지고있는 데이터 저장소를 활용, request.setAttribute(),request.getAttribute()를 사용하여 데이터를 보관하고 조회할 수 있음

(2) MvcMemberFormServlet - 회원 등록 폼(컨트롤러)

  • servlet하위에 mvc.servletmvc 패키지를 생성 후 작성
  • /WEB-INF/.. 경로에 JSP가 있으면 외부에서 직접 JSP를 호출할 수 없고 컨트롤러를 통해서 JSP를 호출해야 함(문법)
  • dispatcher.forward(): 다른 서블릿이나 JSP로 이동할 수 있는 기능으로 서부 내부에서 다시 호출이 발생함

** 참고 - redirect vs forward

  • redirect : 실제 클라이언트(웹 브라우저)에 응답이 나갔다가 클라이언트가 redirect 경로로 다시 요청하므로 클라이언트가 인지할 수 있고 실제 url경로도 변경됨
  • forward : 서버 내부에서 일어나는 호출이므로 클라이언트가 전혀 인지하지 못함
package hello.servlet.web.servletmvc;

//컨트롤러 역할
@WebServlet(name = "mvcMemberFormServlet", urlPatterns = "/servlet-mvc/members/new-form")
public class MvcMemberFormServlet extends HttpServlet {
   
    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String viewPath = "/WEB-INF/views/new-form.jsp";
        
        // 컨트롤러에서 뷰로 경로를 이동
        RequestDispatcher dispatcher = req.getRequestDispatcher(viewPath);
        
        // 다른 서블릿이나 JSP로 이동할 수 있는 기능(서버 내부에서 다시 호출이 발생)
        dispatcher.forward(req, resp); 
    }
}

 

(3) new-form.jsp - 회원 등록 폼(뷰)

  • main/webapp/WEB-INF/views 경로에 작성
  • action의 경로를 절대경로(/로 시작하는 경로)가 아닌 상대경로(/로 시작하지 않음)로 지정하면 폼 전송시 현재 URL이 속한 계층 경로 + save가 호출 됨
  • 현재 계층 경로: /servlet-mvc/members/
  • 결과: /servlet-mvc/members/save
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<!-- 상대경로 사용, [현재 URL이 속한 계층 경로 + /save] -->
<form action="save" method="post">
    username: <input type="text" name="username"/> age: <input type="text" name="age"/>
    <button type="submit">전송</button>
</form>
</body>
</html>

 

(4) MvcMemberSaveServlet - 회원 저장(컨트롤러)

  • HttpServletRequest를 Model로 사용
  • request가 제공하는 setAttribute()를 사용하면 request 객체에 데이터를 보관해서 뷰에 전달할 수 있음
  • 뷰는 request.getAttribute()를 사용하여 데이터를 꺼냄
package hello.servlet.web.servletmvc;

@WebServlet(name = "mvcMemberSaveServlet", urlPatterns = "/servlet-mvc/members/save")
public class MvcMemberSaveServlet extends HttpServlet {
    
    private MemberRepository memberRepository = MemberRepository.getInstance();

    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String username = req.getParameter("username");
        int age = Integer.parseInt(req.getParameter("age"));
        
        Member member = new Member(username, age);
        System.out.println("member = " + member);
        memberRepository.save(member);
        
        // Model 데이터 보관
        req.setAttribute("member", member);
        String viewPath = "/WEB-INF/views/save-result.jsp";
        RequestDispatcher dispatcher = req.getRequestDispatcher(viewPath);
        dispatcher.forward(req, resp);
    }
}

 

(5) save-result.jsp - 회원 저장(뷰)

  • <%= request.getAttribute("member")%>로 모델에 저장한 member 객체를 꺼낼 수 있지만 너무 복잡해짐
  • JSP의 ${} 문법을 사용하면 request의 attribute에 담긴 데이터를 편리하게 조회할 수 있음
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <meta charset="UTF-8">
</head>
<body> 성공
<ul>

        <!--자바 코드 사용
        <li>id=<%=((Member)request.getAttribute("member")).getId()%></li>
        <li>username=<%=((Member)request.getAttribute("member")).getUsername()%></li>
        <li>age=<%=((Member)request.getAttribute("member")).getAge()%></li> 
        -->
        
        <!--JSP 표현식(프로퍼티접근법) -->
        <li>id=${member.id}</li>
        <li>username=${member.username}</li>
        <li>age=${member.age}</li>
</ul>
<a href="/index.html">메인</a>
</body>
</html>

 

(6) MvcMemberListServlet - 회원 목록 조회(컨트롤러)

  • request 객체를 사용하여 List<Member> members를 모델에 보관
package hello.servlet.web.servletmvc;

@WebServlet(name = "mvcMemberListServlet", urlPatterns = "/servlet-mvc/members")
public class MvcMemberListServlet extends HttpServlet {
    
    private MemberRepository memberRepository = MemberRepository.getInstance();

    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        System.out.println("MvcMemberListServlet.service");
        List<Member> members = memberRepository.findAll();
        
        req.setAttribute("members", members);
        
        String viewPath = "/WEB-INF/views/members.jsp";
        RequestDispatcher dispatcher = req.getRequestDispatcher(viewPath);
        dispatcher.forward(req, resp);
    }
}

 

(7) 회원 목록 조회 - 뷰

  • 모델에 담아둔 members를 JSP가 제공하는 taglib기능을 사용하여 반복하면서 출력(기존의 for문을 리펙토링)
  • members리스트에서 member를 순서대로 꺼내서 item변수에 담고 출력하는 과정을 반복
  • <c:forEach> 이 기능을 사용하려면 <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%> 이렇게 선언을 해야함
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<html>
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<a href="/index.html">메인</a>
<table>
    <thead>
    <tr>
        <th>id</th>
        <th>username</th>
        <th>age</th>
    </tr>
    </thead>
    <tbody>
    <c:forEach var="item" items="${members}">
        <tr>
            <td>${item.id}</td>
            <td>${item.username}</td>
            <td>${item.age}</td>
        </tr>
    </c:forEach>
    </tbody>
</table>
</body>
</html>

 

** 추가적인 JSP의 기능(강의)는 수많은 자료들이 있으므로 검색하거나 관련된 강의,책등을 참고 -> 단, 실무에서는 거의 안쓰는 추세임

3) MVC패턴 - 한계

(1) 한계

  • MVC패턴을 적용한 덕분에 컨트롤러의 역할과 뷰를 렌더링 하는 역할을 구분할 수 있었음
  • 특히 뷰는 화면을 그리는 역할에 충실한 덕분에 코드가 깔끔해지고 직관적이게 됨
  • 그러나 컨트롤러는 코드의 중복이 많고 필요하지 않은 코드들도 보임

(2) MVC 컨트롤러의 단점

  • 포워드 중복
    - view로 이동하는 코드가 항상 중복 호출 되어야 함
    - 메서드로 공통화 해도 되지만 해당 메서드도 항상 직접 호출해야 함
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);

 

  • viewPath에 중복
    - prefix : /WEB-INF/views/
    - suffix : .jsp
    - 만약 jsp가 아닌 thymeleaf같은 다른 뷰로 변경한다면 전체 코드를 다 변경해야 함
String viewPath = "/WEB-INF/views/members.jsp";

 

  • 사용하지 않는 코드
    - response는 현재 코드에서 사용되지 않고 request도 사용할 때가 있고 사용하지 않을 때가 있음
    - HttpServletRequest, HttpServletResponse를 사용하는 코드는 테스트 케이스를 작성하기도 어려움
service(HttpServletRequest request, HttpServletResponse response)

 

  • 공통 처리가 어려움
    - 기능이 복잡해질수록 컨트롤러에서 공통으로 처리해야 하는 부분이 계속 증가 될 것임
    - 공통기능을 메서드로 뽑으면 될 것 같지만 메서드를 항상 호출해야하고 실수로 호출하지 않으면 문제가 되며 호출하는 것 자체도 중복임

(3) 정리하면 공통 처리가 어렵다는 것

  • 공통 처리가 어려운 문제를 해결하려면 컨트롤러 호출 전에 공통 기능을 처리해야함
  • 수문장의 역할을 하는 무언가가 필요한데 프론트컨트롤러(Front Controller)패턴을 도입하면 문제를 해결할 수 있음
  • 스프링 MVC의 핵심도 프론트컨트롤러임

4) 정리

  • servlet과 자바코드만으로 개발했을 때 불편했던 점(html을 렌더링 하는 코드들..)을 JSP로 해결
  • JSP로 html폼에 자바코드를 삽입하여 문제를 해결했으나 JSP코드 안에 자바코드와 html코드가 섞여있어 유지보수가 어려워짐
  • MVC패턴을 적용하여 servlet은 컨트롤러, JSP는 뷰로 각각의 역할에 충실하게 하도록하며 문제를 해결
  • 그러나, 아직까지도 컨트롤러의 영역에서 로직의 중복이 많고 공통처리가 어려운 문제가 발생
  • 프론트컨트롤러(스프링MVC의 핵심)로 해결