MVC 패턴

하마·2025년 3월 19일

Spring

목록 보기
11/22

Servelt과 JSP


Servlet이나 JSP만으로 비지니스 로직과 View Rendering 까지 모두 처리하면
너무 많은 역할을 하게 되고, 유지보수가 굉장히 어려워져서(책임이 너무 많음) 고안된 패턴이다.

Web Application은 일반적으로 MVC(Model View Controller) 패턴을 사용한다.

1. Servelt 사용 예시


@WebServlet("/hello-world")
public class HelloWorldServlet extends HttpServlet {
		
	// User 저장소
    private UserRepository repository = new UserRepository();
    
    public HelloWorldServlet() {
        super();
    }

    protected void doGet(HttpServletRequest request, HttpServletResponse response) 
        throws ServletException, IOException {
        
        response.setContentType("text/html;charset=utf-8");
        PrintWriter out = response.getWriter();
        
        try {
            // 비지니스 로직을 처리하는 코드
            // 파라미터 조회 
            String userIdParam = request.getParameter("userId");
            Long userId = null;
            if (userIdParam != null) {
                userId = Long.parseLong(userIdParam);
            }
            
            // 회원 조회 
            String userInfo = repository.findById(userId);
            
            // 화면을 그리는 코드 START
            out.println("<h1>Hello World!</h1>");
            out.println("<div>조회한 회원의 정보: " + userInfo + "</div>");
            // 화면을 그리는 코드 END
            
           
        } catch (NumberFormatException e) {
	        // parsing 에러가 발생한 경우
            out.println("<div>Invalid user ID format</div>");
        } finally {
            out.close();
        }
    }

    protected void doPost(HttpServletRequest request, HttpServletResponse response) 
        throws ServletException, IOException {        
        doGet(request, response);
    }
}

Servelt 동작 순서


  1. 사용자가 클라이언트(브라우저)를 통해 서버에 HTTP Request 즉, API 요청을 함
  2. 요청을 받은 Servlet 컨테이너는 HttpServletRequest , HttpServletResponse 객체를 생성함
  3. 설정된 정보(URL, HTTP Method)를 통해 어떠한 Servlet에 대한 요청인지 찾음
  4. 해당 Servlet에서 service() 메서드를 호출한 뒤 브라우저의 요청 Method에 따라 doGet() 혹은 doPost() 등의 메서드를 호출한다.
  5. 서버에서 응답을 생성한 뒤 HttpServletResponse 객체에 응답을 담아 Client(브라우저)에 반환
  6. 응답이 완료되면 생성한 HttpServletRequest , HttpServletResponse 객체를 소멸한다.

Servelt 사용 시 문제점

  • 화면을 그리는 View 영역과 비즈니스 로직이 Servelt 하나에 모두 섞여있음
  • View를 위한 코드와 비즈니스 로직을 처리하는 코드가 Servlet에 모두 존재하여 유지보수하기 어려워짐
  • 너무 많은 책임을 가지고 있음

2. JSP 사용 예제


<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
		<!-- HTML 코드... -->
		<!-- <%...%> 영역에는 Java 코드를 사용할 수 있다. -->
    <%
		    // 게시글 저장소 싱글톤으로 인스턴스 생성
		    BoardRepository boardRepository = BoardRepository.getInstance();
		    
		    // 게시글 제목, 내용 
        String title = request.getParameter("title");
        String content = request.getParameter("content");
        
        // 게시글 객체 생성
        Board board = new Baord(title, content);
        
        // 게시글 객체 저장
        repository.save(board);
        
    %>
    <div>
        ID : <input type ="text" name = "id" value="<%=id %>">
    </div>
		<!-- JSP 코드... -->
		<!-- HTML 코드... -->
    <jsp:forward page="<%=url %>" />
</body>
</html>
  • Servelt 사용 예제에서 HTML을 만드는 부분인 View 가 분리되었음

Servlet과 JSP 구조

JSP 방식의 문제점

  • JSP를 사용하며 View를 분리했지만, 비즈니스 로직의 일부가 JSP 파일에 존재
  • 여전히 책임이 많아 유지보수가 어려움

MVC 패턴


하나의 Servlet이나 JSP로 처리하던 것들을 Model , View , Controller 영역으로 나눈 것

1. 핵심 내용


  • 기획이 변하지 않는 이상, 비즈니스 로직과 View 의 수정 원인은 별개로 발생
    • 화면 구성에 수정이 발생하면 View만 변경
    • 요구사항에 수정이 발생하는 경우, 비즈니스 로직 변경
      -> 비즈니스 로직View 을 완전히 독립적으로 분리
  • 서로 연관없는 코드끼리 함께 존재할 필요 없다. 완전히 분리하자

2. MVC 패턴 구조


  • 클라이언트가 요청을 보내면 Controller 가 비즈니스 로직을 처리함
  • Controller 가 처리한 비즈니스 로직의 결과를 Model 에 전달
    • Model 은 데이터를 임시로 저장하고 있음
  • 그리고 View 는 필요한 데이터를 Model 에서 참조해서 사용
    • 동적으로 변경된 HTML을 완성시켜서 클라이언트에게 응답

3. Controller


  • Servlet에 해당하는 영역
  1. HTTP Request를 전달받아 파라미터를 검증함
  2. 비즈니스 로직을 실행한다.
    • 비즈니스 로직을 Controller에 포함하게되면 Controller가 너무 많은 역할을 담당하게 되어 일반적으로 Service Layer를 별도로 만들어서 처리한다.
    • Database와 상호작용 하는 Layer를 따로 구분하여 Repository Layer를 추가로 구성한다.
      • Layered Architecture 를 통해 Controller 의 역할을 더욱 세분화
    • Controller도 비지니스 로직을 포함할 수 있지만 일반적으로 Service Layer를 호출하는 역할을 담당한다.
  3. View에 전달할 결과를 조회하여 Model 객체에 임시로 저장한다.

4. Model


  1. View에 출력할 Data를 저장하는 객체이다.
  2. View는 비지니스 로직이나 Data 접근을 몰라도 되고 View Rendering에만 집중하면 된다. (책임 분리)

5. View


  • JSP에 해당하는 영역
  1. Model 객체에 담겨져 있는 Data를 사용하여 화면을 Rendering 한다.

MVC 패턴의 문제점


MVC 패턴을 적용 후 View의 역할은 필요한 데이터를 Model 에서 참조하여 화면을 그리는 역할만 수행하면 된다. 하지만 Controller에 해당하는 부분은 여전히 문제를 가지고 있다.

1. 문제점


  • 각각의 요청을 처리하는 컨트롤러를 만들어줬어야 함
  1. View로 이동하는 forward가 항상 중복 호출된다.
    • dispatcher.forward(request, response)
  2. View의 path를 중복 입력한다.
    • String path= “/WEB-INF/views/new-form.jsp”
    • 파일의 경로/이름/확장자가 바뀌면 수많은 곳에서 리팩토링이 필요
  3. HttpServletResponse 객체를 사용하는 경우가 적다.
    (JSP에서 모두 해결하기 때문)
    • HttpServletRequestHttpServletResponse 는 Test 코드를 작성하기도 매우 힘들다.
  4. 공통 기능이 추가될수록 Controller에서 처리해야 하는 부분들이 많아진다.

2. 공통 기능 처리


  • 모든 컨트롤러에서 공통으로 적용되는 기능을 뜻함
    ex) Log 출력, 인증, 인가 등

  • 각각의 컨트롤러마다 공통적으로 처리해야하는 코드가 포함되어야 함

Q. 공통 기능을 Method로 분리해서 각각의 컨트롤러에서 사용하면 되는 거 아닌가?

A. 공통 기능으로 만들어놓은 Method 또한 항상 중복적으로 호출이 필요합니다.
또한, 사람인 개발자가 작업하다보면 Method를 호출하는 일을 깜빡 할수도 있고 Method가 많아지면 많아질수록 Controller의 책임이 점점 커지겠죠?

Method를 분리하여도 여전히 해결하지 못하는 문제점으로 남습니다.


프론트 컨트롤러 패턴


Servlet(Controller)이 호출되기 전, 공통 기능을 하나의 Servlet에서 처리해주는 패턴
프론트 컨트롤러(Servlet) 하나에 모든 클라이언트측 요청이 들어온다.

  • 요청이 들어오는 입구가 하나가 되도록 구현하고, 거기에서 공통 기능을 처리한다.

1. 프론트 컨트롤러 패턴 구조


2. 프론트 컨트롤러의 역할


  • 모든 요청을 하나의 컨트롤러(프론트 컨트롤러)가 받음
  • 공통 기능 처리
  • 요청을 처리할 수 있는 Controller를 찾아서 호출함(Controller Mapping)
  • 프론트 컨트롤러를 제외한 나머지 컨트롤러는 Servlet을 사용하지 않음
    • 일반 Controller들은 HttpServlet 을 상속받거나, @WebServlet 을 사용하지 않아도 됨

3. 프론트 컨트롤러의 문제점


  • 공통 처리 로직에 모든 컨트롤러가 연결되기 위해서 모든 컨트롤러가 return 하는 결과의 형태가 동일해야 함
  • 그러나 응답을 동일하게 맞추게 되면 확장성, 유지보수성을 잃게 됨
  • 공통 로직에서 응답별로 하나하나 처리할 수 있으나 공통 부분의 책임이 너무 커지게 됨
  • 컨트롤러에서 반환되는 결과가 달라지면 공통 로직의 변경이 필연적임
    -> 어댑터 패턴

어댑터 패턴


다양한 컨트롤러(이하 Handler)를 유연하게 만들기위해 어댑터 패턴을 도입하게 되었다.

  • 컨트롤러들은 동일한 인터페이스를 구현하도록 한다.
  • 해당 인터페이스와 공통 로직 사이에 어댑터를 두어 유연하게 만든다.
  • 서로 다른 인터페이스를 갖는 두 클래스를 연결해주는 패턴이다.

1. 어댑터 패턴 구조


  • Handler는 비지니스 로직을 처리하고 알맞은 결과를 반환한다.
  • 어댑터는 공통 로직과 Handler가 자연스럽게 연결되도록 한다.
  • 프론트 컨트롤러는 공통으로 처리되는 로직을 수행한다.

2. 어댑터 패턴 장점


  • 프론트 컨트롤러, 어댑터, 핸들러 모두 각자의 역할만 수행한다. (책임 분리)
  • 새로운 Handler가 추가되어도 컨트롤러와 어댑터만 추가한다면 공통 로직의 변경이 발생하지 않는다.

요약


  • Servlet 사용
    • 비지니스 로직을 처리하는 코드와 화면을 그리는 View 코드가 함께 존재하는 문제
  • JSP 사용
    • View를 분리했지만, 여전히 비지니스 로직을 JSP에 포함하는 문제
  • MVC 패턴 사용
    • 공통 로직을 처리하는것에 코드가 중복되는 문제
  • 프론트 컨트롤러 패턴 사용
    • 공통 로직을 하나의 입구에서 처리하기 위해 프론트 컨트롤러 패턴 적용
    • 각각의 핸들러 호출 후 응답을 프론트 컨트롤러에 맞게 변형시켜야 하는 문제
  • 어댑터 패턴 사용
    • 프론트 컨트롤러에 적절하게 전달하기 위해 어댑터 패턴 적용
    • 각 객체의 책임이 명확히 분리됨
  • Spring MVC 사용
    • 프론트 컨트롤러 패턴, 어댑터 패턴이 모두 적용된 현재
    • 우리가 사용하는 Spring을 이용한 Web Application 개발 방식에 사용됨

참고자료


Spring 입문 - 3주차

  • MVC 패턴 1강
  • MVC 패턴 2강
  • 실습

0개의 댓글