2024.09.09.월.TIL MVC 패턴

김기남·2024년 9월 9일
post-thumbnail

안녕하세요, 오늘은 MVC 패턴에 대해 정리해보았습니다.

MVC 패턴 개요

MVC 패턴은 왜 나오게 된걸까?

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

ex) 비지니스 로직이 존재하는 파일에 HTML이 엄청나게 많다면..?

Java Servlet 만 사용

  • Servlet 예제코드
@WebServlet("/hello-world")
public class HelloWorldServlet extends HttpServlet {
       
	public HelloWorldServlet() {
		super();
	}

	protected void doGet(HttpServletRequest request, HttpServletResponse response) 
    	throws ServletException, IOException {
		
		response.setContentType("text/html;charaset=utf-8");
		PrintWriter out = response.getWriter();
		// 화면을 그리는 코드 START
		out.println("<h1>Hello World!</h1>");
		// 화면을 그리는 코드 END
		out.close();
	}

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

}
  1. 사용자가 Client(브라우저)를 통해 서버에 HTTP Request 즉, API 요청을 합니다.
  2. 요청을 받은 Servlet 컨테이너는 HttpServletRequest, HttpServletResponse객체를 생성합니다.
    1. 약속된 HTTP의 규격을 맞추면서 쉽게 HTTP에 담긴 데이터를 사용하기 위한 객체입니다.
  3. 설정된 정보 ex)URL, GET, POST 를 통해 어떠한 Servlet에 대한 요청인지 찾습니다.
  4. 해당 Servlet에서 service 메서드를 호출한 뒤 브라우저의 요청 Method에 따라 doGet() 혹은 doPost() 등의 메서드를 호출합니다.
  5. 호출한 메서드들의 결과를 그대로 반환하거나 동적 페이지를 생성한 뒤 HttpServletResponse 객체에 응답을 담아 Client(브라우저)에 반환합니다.
  6. 응답이 완료되면 생성한 HttpServletRequest, HttpServletResponse객체를 소멸합니다.

Template Engine

💡 동적인 웹 페이지를 생성하기 위해 사용되는 도구 템플릿을 기반으로 정적인 부분과 동적인 데이터를 결합하여 HTML, XML 등의 문서를 생성한다. 주로 UI를 구성하는데 활용된다.
  • 템플릿 엔진이 나온 이유

    자바 코드로 HTML을 만들어 내는 것이 아닌 HTML 문서에 동적으로 변경해야 하는 부분만
    자바 코드를 넣을 수 있다면 더 편리하다.

  • 대표적인 예

    • Thymeleaf → Spring과 통합이 잘 되어있다. 지원이 잘됨. 다양한 기능
    • JSP → 예전엔 많이 사용했으나, 현재 안쓰는 추세
    • FreeMarker
    • Velocity
    • Mustache
  • JSP(Java Server Pages)

💡 JSP에서 동적인 처리부분은 결국 Servlet으로 변환되어 실행된다.
  • JSP 예제코드
    Servlet 코드에서 HTML을 만드는 부분인 View가 분리되었다.
<%@ 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 코드... -->
    <%
        String url = null;
        // 선택한 사이트를 String으로 받아온다.
        String site = request.getParameter("site");
        // 선택한 사이트가 스파르타라면
        if(site.equals("sprata")) {
            url="teamSparta.jsp";
        // 스파르타가 아니라도 못 빠져나간다.
        } else {
            url="teamSparta.jsp";
        }
    %>
    <!-- 위에서 지정된 페이지로 이동한다. -->
		<!-- JSP 코드... -->
		<!-- HTML 코드... -->
		<!-- JSP 코드... -->
		<!-- HTML 코드... -->
		<!-- JSP 코드... -->
		<!-- HTML 코드... -->
    <jsp:forward page="<%=url %>" />
</body>
</html>

Servlet과 JSP

View가 분리된 핵심은 변경에 있다

비지니로직

비밀번호 검증 → 특수문자 한개포함, 대문자 한개포함

비밀번호 검증 → 특수문자 두개포함, 대문자 세개포함.

비지니스 로직과 View는 대부분 수정이 함께 일어나지 않고 각자 발생한다.
(서로에게 연관이 거의 없다, 동시에 변하는 경우는 기획이 변하는 경우)

연관 없는 코드끼리 함께 존재하는 이유가 무엇인가?
JSP같은 View Template 들은 화면을 Rendering하는 역할만 수행하는것이 가장 효과적이다.
MVC는 하나의 Servlet이나 JSP로 처리하던 것들을 Controller, View 영역으로 나눈것이다.

Controller, Service, Repository → MVC 이후의 일.

Controller

💡 1. HTTP Request를 받아서 Parameter를 검증하고 2. 비지니스 로직을 실행한다. 3. View에 전달할 결과 Data를 조회하여 Model에 담아준다.
  • Controller에도 비지니스 로직을 사용하기도 한다.
    하지만 이렇게되면 너무 많은 역할을 담당하기 때문에 일반적으로 비지니스 로직은
    Service Layer를 별도로 만들어 처리한다.(계층구조)
    이렇게 계층이 따로 존재하면 Controller는 Service를 호출하는 역할을 담당한다.

Model

💡 View에 출력할 Data를 담아둔다. View가 필요한 Data를 모두 Model이 가지고 있기 때문에 View는 비지니스 로직이나 Data 접근을 몰라도 되고 Rendering만 집중하면 된다.

View

💡 Model에 담겨있는 Data를 사용하여 화면을 Rendering 한다.

MVC 패턴

  • Spring MVC를 의미하는것이 아닌 MVC 패턴 그자체
  • MVC 패턴이 적용된 JSP 예시코드

디렉토리 구조

  • src/main/java
    • 해당 디렉토리 내부 .java 파일만 Compile
  • webApp
    • JSP, HTML 존재
  • WEB-INF
    • 확장 라이브러리 Library(lib) 폴더, web.xml(설정파일)

build.gradle 의존성 추가

// Spring Boot 3.0 미만
implementation 'org.apache.tomcat.embed:tomcat-embed-jasper'
implementation 'javax.servlet:jstl'

// Spring Boot 3.0 이상
implementation 'org.apache.tomcat.embed:tomcat-embed-jasper'
implementation 'jakarta.servlet:jakarta.servlet-api'
implementation 'jakarta.servlet.jsp.jstl:jakarta.servlet.jsp.jstl-api'
implementation 'org.glassfish.web:jakarta.servlet.jsp.jstl'

Post(블로그 글) Object

@Getter
@Setter
public class Post{
			// PK
      private Long id;
			// 제목
      private String title; 
			// 내용
      private String content; 
	
			public Post() {}
      
			public Post(String title, String content) {
          this.title = title;
          this.content = content;
			}
}

블로그 작성 버튼 클릭(생략) → 블로그 작성 폼 Servlet

@WebServlet(
	name = "mvcPostFormServlet", 
	urlPatterns = "/mvc/posts/form"
)
  public class MvcPostFormServlet extends HttpServlet {

		@Override
    protected void service(
														HttpServletRequest request,
														HttpServletResponse response
		) throws ServletException, IOException {
      
			// webapp/WEB-INF/views -> WEB-INF 내부의 파일은 외부에서 호출할 수 없다.
			String path = "/WEB-INF/views/postForm.jsp";
      // 이때 Controller에서 View로 이동
			RequestDispatcher dispatcher = request.getRequestDispatcher(path);
      // forward : Servlet에서 다른 Servlet이나 jsp 호출
			dispatcher.forward(request, response);

		}
}


// 참고
@Controller
public class PostController {

		// Service 생성자 주입
	
		@PostMapping("/posts")
		public void create(@RequestBody RequestDTO dto) {
			
			service.create(dto);
			
			return "post";
		}
}
  • Redirect 와 Forward Redirect
    클라이언트에 Response(redirect URL)가 전달 되었다가
    클라이언트 측에서 redirect 경로로 다시 요청한다.
    URL이 실제로 변한다. Forward
    서버 내부에서 발생하는 호출로 클라이언트가 인지하지 못한다.

블로그 작성 Form JSP

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
	<meta charset="UTF-8">
	<title>블로그 포스트 작성 페이지/title>
</head>
<body>
<h1>블로그 글쓰기</h1>
<form action="save" method="post">
	title: <input type="text" name="title" placeholder="제목" /> 
	content: <input type="text" name="content" placeholder="내용" /> 
<button type="submit">저장</button>
</form>

</body>
</html>

블로그 작성 폼 Submit → 블로그 글 저장 Servlet

@WebServlet(
	name = "mvcPostSaveServlet", 
	urlPatterns = "/mvc/posts/save"
)
  public class MvcPostSaveServlet extends HttpServlet {
		
		// Singleton
		private PostRepository postRepository = PostRepository.getInstance();
	
		@Override
    protected void service(
														HttpServletRequest request,
														HttpServletResponse response
		) throws ServletException, IOException {
      
			String title= request.getParameter("title");
			String content = request.getParameter("content");

			Post post = new Post(title, content);
			postRepository.save(post);

			// Model에 Data를 보관한다.
			// request 내부 임시 저장소에 post저장
			request.setAttribute("post", post); // MODEL

			String path = "/WEB-INF/views/save-result.jsp"
			RequestDispatcher dispatcher = request.getRequestDispatcher(path);
			disPatcher.forward(request, response);

		}
}

PostRepository

public class PostRepository {
	private static Map<Long, Post> database = new HashMap<>();
	
	private static long incrementId = 0L;
	private static final PostRepository instance = new PostRepository();
  
	public static PostRepository getInstance() {
	  return instance;
	}
  
	private PostRepository() {}

	// Post 저장			
	public Post save(Post post) {
	  post.setId(++incrementId);
    database.put(post.getId(), post);
    return post;
	}
	
	// Post 전체 조회
	public List<Post> findAll() {
	  return new ArrayList<>(database.values());
	}

}

저장결과(save-result.jsp) JSP → View

<!-- request 객체 안에 setAttribute로 post를 임시 저장했음 -->
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
	<meta charset="UTF-8">
</head>
<body> 
	<h1>글작성 성공!!!</h1>
  <ul>
    <li>id=<%=post.getId()%></li>
    <li>title=<%=post.getTitle()%></li>
    <li>content=<%=post.getContent()%></li>
	</ul>
	<a href="/index.html">홈 화면으로 돌아가기</a>
</body>
</html>

블로그 글 목록조회 Servlet

@WebServlet(
	name = "mvcPostListServlet", 
	urlPatterns = "/mvc/posts"
)
public class MvcPostListServlet extends HttpServlet {
      
	private PostRepository postRepository = PostRepository.getInstance();

  @Override
	protected void service(
													HttpServletRequest request,
													HttpServletResponse response
					) throws ServletException, IOException {
	  // 회원 목록 조회 
		List<Post> posts = postRepository.findAll();
		
		// request 객체의 임시저장소 Model에 posts를 저장
    request.setAttribute("posts", posts);
		
    String path = "/WEB-INF/views/posts.jsp";
    RequestDispatcher dispatcher = request.getRequestDispatcher(path);
    dispatcher.forward(request, response);
	} 
}

블로그 글 목록조회 JSP(posts.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>
</head>
<body>
<a href="/index.html">홈 화면으로 돌아가기</a>
<table>
	<thead>
	  <th>id</th>
	  <th>title</th>
	  <th>content</th>
  </thead>
  <tbody>
	<%
	  for (Post post : posts) {
			out.write("<tr>");
			out.write("  <td>" + post.getId() + "</td>");
			out.write("  <td>" + post.getTitle() + "</td>");
			out.write("  <td>" + post.getContent() + "</td>");
			out.write("</tr>");
		}
	%>
  </tbody>
</table>
</body>
</html>

정리

우리는 MVC가 적용되며 Controller와 View가 분리된것을 보았다.
이제 화면에 수정이 발생하면 화면(View)만 수정하고
로직에 수정이 발생하면 로직만 수정하여 배포하면 된다.

MVC 패턴의 문제점과 한계

💡 MVC 패턴을 적용하니 **View**는 Model에서 필요한 데이터를 참조하여 화면을 그리면된다. 하지만 Controller는??

  • Controller 문제점
  1. dispatcher.forward(request, response) View로 이동하는 forward가 항상 중복 호출된다.
  2. String path= “/WEB-INF/views/new-form.jsp” View의 path를 입력(중복 작업)한다.
    1. 경로가 바뀌거나 JSP 이외의 확장자를 사용하려면 전체가 변경되어야 한다.
  3. HttpServletResponse 객체를 사용하는 경우가 적다. (JSP에서 모두 해결하기 때문)
    1. HttpServletRequest와 Response는 Test 코드를 작성하기도 매우 힘들다.
      (Request Response를 내가 계속 만들어야 하나..ㅎ)
  4. 공통 처리
    기능이 추가될수록 Controller에서 공통으로 처리해야 하는 부분들이 많아진다.
    ex) log 출력, 인증, 인가 등등..

단순히 공통 기능을 Method로 분리하면 될것같지만
이것 또한 항상 중복적으로 호출이 필요하다.
이때까지만 해도 요청이 들어오는 입구가 너무 많다!
Servlet이 호출되기 전에 공통 기능을 처리 해주는것이 바로
프론트 컨트롤러 패턴이다.(입구가 하나)

프론트 컨트롤러 패턴


💡 프론트 컨트롤러(Servlet) 하나에 모든 클라이언트측 요청이 들어온다.(공통 처리 가능)

프론트 컨트롤러의 역할

  • 모든 Request를 하나의 프론트 컨트롤러가 받는다.
  • 공통 기능을 처리한다.
  • 프론트 컨트롤러가 Request에 맞는 Controller를 찾아서(컨트롤러 매핑) 호출한다.
    • 프론트 컨트롤러를 제외한 나머지 컨트롤러는 Servlet을 사용하지 않아도 된다!
      → 일반 Controller들은 HttpServlet을 상속받거나 @WebServlet 등을 사용하지 않아도 된다.

프론트 컨트롤러 의문점

💡 FrontController 를 사용한다면 모든 컨트롤러에서 같은 형태의 응답을 해야하는가?

퍼즐 예시

  • 위 그림처럼 공통 처리 로직에 모든 컨트롤러가 연결되기 위해서는
    모든 컨트롤러가 return 하는 결과의 형태가 동일해야 한다.
  • 하지만, Controller 마다 로직이나 응답해야하는 결과는 당연히 다를테고
    응답을 동일하게 맞추려고 한다면 해당 어플리케이션은 확장성, 유지보수성을 잃는다.
  • 공통 로직에서 응답별로 퍼즐을 다시 하나하나 처리할 수 있으나
    공통 부분의 책임이 너무 커지게된다.
    또한, 컨트롤러에서 반환되는 결과가 달라지면 공통처리 부분의 변경또한 불가피하다.
    (변경이 힘들다)

Dispatcher Servlet

💡 Spring MVC의 핵심은 프론트 컨트롤러(DispatcherServlet)이다.
  • Spring도 결국 Servlet을 사용하고 있고 이것이 바로 DispatcherServlet 이다.
  • DispatcherServlet은 사실 우리가 모르게 엄청나게 많은 역할을 해주고 있었다.

정리


💡 MVC 패턴 → 개선→ 프론트 컨트롤러 패턴 → 개선→ Spring MVC

오늘의 코드카타

class Solution {

    public int[] solution(int start_num, int end_num) {
        int[] answer = new int[end_num - start_num + 1];
        for (int i = 0; i < answer.length; i++) {
            answer[i] = start_num + i;
        }
        return answer;
    }
}

시작 넘버부터 종료 넘버까지 배열에 담기

profile
새로운 시작~!

0개의 댓글