3-3. MVC 패턴

shin·2025년 4월 27일

Spring MVC

목록 보기
13/25

1) 개요


(1) 너무 많은 역할

  • 하나의 서블릿이나 JSP 만으로 비즈니스 로직과 뷰 렌더링까지 모두 처리하게 되면, 너무 많은 역할을 하게 됨

  • 결과적으로 유지보수가 어려워짐

    • 비즈니스 로직을 호출하는 부분에 변경이 발생해도 해당 코드를 손대야 하고, UI를 변경할 일이 있어도 비즈니스 로직이 함께 있는 해당 파일을 수정해야 함

    • HTML 코드 하나 수정해야 하는데, 수백줄의 자바 코드가 함께 있는 상황이 생길 수 있음

    • 비즈니스 로직 하나를 수정해야 하는데 수백 수천줄의 HTML 코드가 함께 있는 상황이 생길 수 있음


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

  • 이것이 가장 중요함
  • 진짜 문제는 둘 사이에 변경의 라이프 사이클이 다르다는 점임
  • 에를 들어 UI를 일부 수정하는 일과 비즈니스 로직을 수정하는 일은 각각 다르게 발생할 가능성이 매우 높고, 대부분 서로에게 영향을 주지 않음
  • 이렇게 변경의 라이브 사이클이 다른 부분을 하나의 코드로 관리하는 것은 유지보수하기 좋지 않음
    • 물론 UI가 많이 변하면 함께 변경될 가능성은 있음

(3) 기능 특화

  • 특히 JSP 같은 뷰 템플릿은 화면을 렌더링 하는데 최적화되어 있기 때문에, 이 부분의 업무만 담당하는 것이 가장 효과적임

(4) Model View Controller

  • MVC 패턴은 지금까지 학습한 것처럼 하나의 서블릿이나, JSP로 처리하던 것을 컨트롤러와 뷰라는 영역으로 서로 역할을 나눈 것을 말함
  • 웹 애플리케이션은 보통 이 MVC 패턴을 사용함

컨트롤러

  • HTTP 요청을 받아서 파라미터를 검증하고, 비즈니스 로직을 실행함
  • 그리고 뷰에 전달할 결과 데이터를 조회해서 모델에 담음

서비스

  • 컨트롤러에 비즈니스 로직을 둘 수 있지만, 이렇게 되면 컨트롤러가 너무 많은 역할을 담당함
  • 그래서 일반적으로 비즈니스 로직은 서비스라는 계층을 별도로 만들어서 처리함
  • 그리고 컨트롤러는 비즈니스 로직이 있는 서비스를 호출하는 역할을 담당함
  • 참고로 비즈니스 로직을 변경하면 비즈니스 로직을 호출하는 컨트롤러의 코드도 변경될 수 있음

모델

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

  • 모델에 담겨있는 데이터를 사용해서 화면을 그리는 일에 집중함
  • 여기서는 HTML을 생성하는 부분을 말함

MVC 패턴 이전

MVC 패턴1

MVC 패턴2



2) 적용


  • 서블릿을 컨트롤러로 사용하고, JSP를 뷰로 사용해서 MVC 패턴을 적용
  • Model은 HttpServletRequest 객체를 사용함
  • request는 내부에 데이터 저장소를 가지고 있음
    • request.setAttribute(), request.getAttribute()를 사용하면 데이터를 보관하고 조회할 수 있음


회원 등록

회원 등록 폼 - 컨트롤러

hello.servlet.web.servletmvc.MvcMemberFormServlet

package hello.servlet.web.servletmvc;

import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

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

/WEB-INF

  • 이 경로안에 JSP가 있으면 외부에서 직접 JSP를 호출할 수 없음
  • 우리가 기대하는 것은 항상 컨트롤러를 통해서 JSP를 호출하는 것임

redirect vs forward

  • 리다이렉트는 실제 클라이언트(웹 브라우저)에 응답이 나갔다가, 클라이언트가 redirect 경로로 다시 요청함

    • 따라서 클라이언트가 인지할 수 있고, URL 경로도 실제로 변경됨
  • 반면에 포워드는 서버 내부에서 일어나는 호출이기 때문에 클라이언트가 전혀 인지하지 못함



회원 등록 폼 - 뷰

main/webapp/WEB-INF/views/new-form.jsp

 <%@ 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>
  • 여기서 form의 action을 보면 절대 경로(/로 시작)가 아니라 상대경로로(/로 시작X)인 것을 확인할 수 있음

    • 이렇게 상대경로를 사용하면 폼 전송시 현재 URL이 속한 계층 경로 + save가 호출됨
  • 현재 계층 경로 : /servlet-mvc/members/

  • 결과 : /servlet-mvc/members/save

실행



회원 저장

회원 저장 컨트롤러

MvcMemberSaveServlet

package hello.servlet.web.servletmvc;

import hello.servlet.domain.member.Member;
import hello.servlet.domain.member.MemberRepository;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@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);
        
    }
}
  • HttpServletRequest를 Model로 사용함
  • request가 제공하는 setAttribute()를 사용하면 request 객체에 데이터를 보관해서 뷰에 전달할 수 있음
  • 뷰는 request.getAttribute()를 사용해서 데이터를 꺼내면 됨


회원 저장 - 뷰

main/webapp/WEB-INF/views/save-result.jsp

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

실행



회원 목록 조회

회원 목록 조회 - 컨트롤러

MvcMemberListServlet

package hello.servlet.web.servletmvc;
 
import hello.servlet.domain.member.Member;
import hello.servlet.domain.member.MemberRepository;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;

@WebServlet(name = "mvcMemberListServlet", urlPatterns = "/servlet-mvc/members")
public class MvcMemberListServlet extends HttpServlet {
 
 	private MemberRepository memberRepository = MemberRepository.getInstance();
    
    @Override
 	protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
 		
        System.out.println("MvcMemberListServlet.service");
 		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);
        
    }
 }
  • request 객체를 사용해서 List<Member> members를 모델에 보관함


회원 목록 조회 - 뷰

main/webapp/WEB-INF/views/members.jsp

<%@ 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>
    <th>id</th>
    <th>username</th>
    <th>age</th>
    </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>
  • 모델에 담아둔 members를 JSP가 제공하는 taglib기능을 사용해서 반복하면서 출력함

  • members 리스트에서 member를 순서대로 꺼내서 item 변수에 담고, 출력하는 과정을 반복함

  • <c:forEach> 이 기능을 사용하려면 다음과 같이 선언해야 함

    • <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>

  • 해당 기능을 사용하지 않고, 아래와 같이 출력해도 되지만 매우 복잡하고 지저분함
 <%
for (Member member : members) {
        out.write("    <tr>");
        out.write("        <td>" + member.getId() + "</td>");
        out.write("        <td>" + member.getUsername() + "</td>");
        out.write("        <td>" + member.getAge() + "</td>");
        out.write("    </tr>");
    }
 %>
  • JSP와 같은 뷰 템플릿은 이렇게 화면을 렌더링 하는데 특화된 다양한 기능을 제공함

실행



3) 한계 - MVC 컨트롤러의 단점


  • MVC 패턴을 적용한 덕분에 컨트롤러의 역할과 뷰를 렌더링하는 역할을 명확하게 구분할 수 있음
  • 특히 뷰는 화면을 그리는 역할에 충실한 덕분에 코드가 깔끔하고 직관적임
  • 단순하게 모델에서 필요한 데이터를 꺼내고 화면을 만들면 됨
  • 그런데 컨트롤러는 딱 봐도 중복이 많고, 필요하지 않는 코드들도 많이 보임

(1) 포워드 중복

  • View로 이동하느 코드가 항상 중복 호출되어야 함
  • 물론 이 부분을 메서드로 공통화해도 되지만, 해당 메서드도 항상 직접 호출해야 함
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);

(2) ViewPath에 중복

String viewPath = "/WEB-INF/views/new-form.jsp";
  • prefix : /WEB-INF/views/

  • suffix: .jsp

  • 그리고 만약 jsp가 아닌 thymeleaf 같은 다른 뷰로 변경한다면 전체 코드를 다 변경해야 함


(3) 사용하지 않는 코드

  • 다음 코드를 사용할 때도 있고, 사용하지 않을 때도 있음
    • 특히 response는 현재 코드에서 사용되지 않음
HttpServletRequest request, HttpServletResponse response
  • 그리고 이런 HttpServletRequest, HttpServletResponse를 사용하는 코드는 테스트케이스를 작성하기도 어려움

(4) 공통 처리가 어려움

  • 기능이 복잡해질수록 컨트롤러에서 공통으로 처리해야 하는 부분이 점점 더 많이 증가할 것임

  • 단순히 공통 기능을 메서드로 뽑으면 될 것 같지만, 결과적으로 해당 메서드를 항상 호출해야 하고, 실수로 호출하지 않으면 문제가 될 것임

    • 그리고 호출하는 것 자체도 중복임
  • 공통 처리가 어려운 문제를 해결하려면 컨트롤러 호출전에 먼저 공통 기능을 처리해야 함

    • 소위 수문장 역할을 하는 기능이 필요함
  • 프론트 컨트롤러(Front Controller) 패턴을 도입하면 이런 문제를 깔끔하게 해결할 수 있음(입구를 하나로)

    • 스프링 MVC의 핵심도 바로 이 프론트 컨트롤러에 있음

Front Controller

모든 HTTP 요청을 한 곳에서 받아서, 공통 처리를 한 다음에 각 컨트롤러로 분기(dispatch)해주는 것을 말함

  • 일반적으로는 사용자가 다양한 URL로 요청을 보냄

    • 각각의 요청을 각각의 컨트롤러가 직접 다 받으면 공통 작업(인증, 로깅 등)을 매번 해야 하고 코드가 중복됨
  • Front Controller가 모든 요청을 한 입구에서 먼저 받음

    • 요청이 무엇인지 분석하고 필요한 공통 작업을 먼저 처리
    • 그 다음 실제 일을 처리할 컨트롤러에게 넘겨줌
  • DispatcherServlet가 스프링 MVC에서의 Front Controller

    • 사용자의 모든 요청은 DispatcherServlet이 먼저 받음
    • 공통 작업을 하고, 해당 요청을 처리할 핸들러(컨트롤러)를 찾아서 호출해줌
  • 기존 : /servlet-mvc/members/save → MvcMemberSaveServlet 직접 호출

    • 서블릿이 직접 Model에 데이터 저장하고 뷰포워드
  • Front Controller 방식 : /servlet-mvc/* → FrontControllerServlet가 먼저 호출됨

    • FrontController가 Controller를 호출하고, 결과(ModelView)를 받아서 뷰를 포워드


스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술

profile
Backend development

0개의 댓글