강의 링크는 아래와 같습니다.
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-1/
이제 회원 관리 앱 애플리케이션을 만들어보며 MVC 패턴 등장 배경에 대해 알아보겠다.
실습에서는 먼저 멤버 클래스를 만들고 멤버를 저장할 저장소 MemberRepository를 만든다.
멤버 코드는 생략하며, MemberRepository 코드는 다음과 같다.
public class MemberRepository {
private static Map<Long, Member> store = new HashMap<>();
private static long sequence = 0L;
private static final MemberRepository instance=new MemberRepository(); //싱글톤
public static MemberRepository getInstance(){
return instance;
}
private MemberRepository(){ //싱글톤을 위해 생성자를 막는다
}
public Member save(Member member){
member.setId(++sequence);
store.put(member.getId(), member);
return member;
}
public Member findById(Long id){
return store.get(id);
}
public List<Member> findAll(){
return new ArrayList<>(store.values());
}
}
실습에서 서블릿을 활용해 회원 관리 웹 애플리케이션을 만들었다.
MemberFormServlet, MemberSaveServlet, MemberListServlet 등 다양한 서블릿을 만들었다.
그중에서도 저장한 멤버를 보여주는 MemberListServlet 코드만 확인해보겠다.
//멤버 리스트를 보여주는 서블릿
@WebServlet(name="memberListServlet",urlPatterns = "/servlet/members")
public class MemberListServlet extends HttpServlet {
private MemberRepository memberRepository=MemberRepository.getInstance();
@Override
protected void service(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException {
res.setContentType("text/html");
res.setCharacterEncoding("utf-8");
List<Member> members = memberRepository.findAll();
PrintWriter w = res.getWriter();
w.write("<html>");
w.write("<head>");
w.write(" <meta charset=\"UTF-8\">");
w.write(" <title>Title</title>");
w.write("</head>");
w.write("<body>");
//너무 많아 축약
w.write(" </thead>");
w.write(" <tbody>");
for (Member member : members) {
w.write(" <tr>");
w.write(" <td>" + member.getId() + "</td>");
w.write(" <td>" + member.getUsername() + "</td>");
w.write(" <td>" + member.getAge() + "</td>");
w.write(" </tr>");
}
//많아서 축약
w.write("</html>");
}
}
이렇게 서블릿과 자바 코드로 HTML을 만들면 오타가 발생해서 에러가 날 확률도 높고 알아보기도 힘들다.
물론 동적인 데이터를 제공하기는 하지만 서블릿으로 Form을 만들고,
회원 목록과 같은 동적인 데이터를 보여주는 동적 HTML은 사실상 불가능할 것이다.
그래서 템플릿 엔진이 등장했다.
템플릿 엔진을 이용하면 HTML 문서에서 필요한 곳만 코드를 적용해서 동적으로 변경할 수 있다.
이제 JSP를 간단하게 알아보겠다.
우선 JSP 라이브러리를 추가해야 한다. 이를 사용하기 위해 우리는 스프링 부트를 만들 때 Jar가 아닌 War로 생성했다.
아래 코드만 build.gradle에 추가하면 JSP를 사용할 수 있다.
implementation 'org.apache.tomcat.embed:tomcat-embed-jasper'
implementation 'javax.servlet:jstl'
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
이 줄은 JSP 문서라는 뜻이다. JSP 문서는 이렇게 시작한다.
회원 저장 서블릿과 회원 조회 서블릿을 JSP를 이용해 리팩터링 실습을 진행했다.
저장된 멤버 리스트를 보여주는 서블릿 코드가 다음과 같이 바뀐 것이다.
<--!모든 회원 조회-->
<%@ page import="java.util.List" %>
<%@ page import="hello.servlet.domain.member.MemberRepository" %>
<%@ page import="hello.servlet.domain.member.Member" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
MemberRepository memberRepository = MemberRepository.getInstance();
List<Member> members = memberRepository.findAll();
%>
<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>
<%
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>");
}
%>
</tbody>
</table>
</body>
</html>
JSP는 자바 코드를 그대로 다 사용할 수 있다.
<%@ page import="hello.servlet.domain.member.MemberRepository" %> 자바의 import 문과 같다.
<% ~~ %> 이 부분에는 자바 코드를 입력할 수 있다.
<%= ~~ %> 이 부분에는 자바 코드를 출력할 수 있다.
회원 저장 JSP를 보면, 회원 저장 서블릿 코드와 같다. 다른 점이 있다면, HTML을 중심으로 하고, 자바 코드를 부분 부분 입력해주었다. <% ~ %>를 사용해서 HTML 중간에 자바 코드를 출력하고 있다.
분명 서블릿으로만 개발할 때보다는 분명히 깔끔해지고 나아졌다.
그러나 우리의 JSP 파일을 보면 비즈니스 로직과 결과물 HTML을 보여주려는 뷰 영역이 겹친다.
우리의 코드는 짧지만 이런 구조를 가지고 실무에서 사용하면 정말 알아볼 수도 없고 유지 보수도 힘들 것이다.
그래서 등장한 것이 MVC 패턴이다.
하나의 서블릿, JSP로 비즈니스 로직과 뷰 렌더링까지 처리하면, 너무 많은 역할을 담당하게 되다. 유지보수가 정말 힘들어질 것이다.
뷰를 고쳐도 해당 파일을 고치고, 비즈니스 로직을 고쳐도 해당 파일을 고쳐야 한다.
변경의 라이프 사이클
진짜 문제는 둘 사이에 변경의 라이프 사이클이 다르다는 점이다.
예를 들어서 UI를 일부 수정하는 일과 비즈니스 로직을 수정하는 일은 각각 다르게 발생할 가능성이 매우 높고 대부분 서로에게 영향을 주지 않는다. 이렇게 변경의 라이프 사이클이 다른 부분을 하나의 코드로 관리하는 것은 유지 보수하기 좋지 않다.
MVC 패턴
이제 MVC 패턴에 대해 본격적으로 알아보겠다. MVC 패턴은 Model, View, Controller의 약어다.
- Controller - HTTP 요청을 받아서 파라미터를 검증하고, 비즈니스 로직을 실행한다. 그리고 뷰에 전달할 결과 데이터를 조회해서 모델에 담는다.
- Model - 뷰에 출력할 데이터를 담아둔다. 뷰가 필요한 데이터를 모두 모델에 담아서 전달해주는 덕분에 뷰는 비즈니스 로직이나 데이터 접근을 몰라도 되고, 화면을 렌더링 하는 일에 집중할 수 있다.
- View - 모델에 담겨있는 데이터를 사용해서 화면을 그리는 일에 집중한다. 여기서는 HTML을 생성하는 부분을 말한다.
참고
컨트롤러에 비즈니스 로직을 둘 수도 있지만, 이렇게 되면 컨트롤러가 너무 많은 역할을 담당한다.
그래서 일반적으로 비즈니스 로직은 서비스(Service)라는 계층을 별도로 만들어서 처리한다.
그리고 컨트롤러는 비즈니스 로직이 있는 서비스를 호출하는 담당 한다.
참고로 비즈니스 로직을 변경하면 비즈니스 로직을 호출하는 컨트롤러의 코드도 변경될 수 있다.
MVC 패턴 적용 이전에 로직은 아래와 같았다.
MVC 패턴을 적용한 후 이후 로직은 아래와 같다.
이제 서블릿을 컨트롤러로 사용하고, JSP를 뷰로 사용해서 MVC 패턴을 적용해보자.
Model은 HttpServletRequest 객체를 사용한다. request는 내부에 데이터 저장소를 가지고 있는데,
request.setAttribute() , request.getAttribute()를 사용하면 데이터를 보관하고, 조회할 수 있다.
회원 등록 컨트롤러만 살펴보자.
// WEB-INF 이 경로안에 JSP가 있으면 외부에서 직접 JSP를 호출할 수 없다. 우리가 기대하는 것은 항상 컨트롤러를 통해서 JSP를 호출하는 것이다.
//회원 저장 컨트롤러
@WebServlet(name="mvcMemberSaveServlet",urlPatterns = "/servlet-mvc/members/save")
public class MvcMemberSavaServlet extends HttpServlet {
private MemberRepository memberRepository = MemberRepository.getInstance();
@Override
protected void service(HttpServletRequest req, HttpServletResponse res) 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, res); // 다른 서블릿이나 JSP로 이동할 수 있는 기능이다. 서버 내부에서 다시 호출이 발생한다.
}
}
redirect vs forward
리다이렉트는 실제 클라이언트(웹 브라우저)에 응답이 나갔다가, 클라이언트가 redirect 경로로 다시 요청한다.
따라서 클라이언트가 인지할 수 있고, URL 경로도 실제로 변경된다.
반면에 포워드는 서버 내부에서 일어나는 호출이기 때문에 클라이언트가 전혀 인지하지 못한다.
다음은 회원 저장 결과물을 보여주는 JSP 파일 코드다.
<--! 회원 저장 결과물 출력 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>
JSP는 ${} 문법을 제공하는데, 이 문법을 사용하면 request의 attribute에 담긴 데이터를 편리하게 조회할 수 있다.
모델에 담아둔 members를 JSP가 제공하는 taglib기능을 사용해서 반복하면서 출력했다.
members 리스트에서 member를 순서대로 꺼내서 item 변수에 담고, 출력하는 과정을 반복한다.
MVC 덕분에 컨트롤러 로직과 뷰 로직을 확실하게 분리한 것을 확인할 수 있다. 향후 화면에 수정이 발생하면 뷰 로직만 변경하면 된다.
현재 MVC의 한계
MVC 패턴을 적용한 덕분에 컨트롤러의 역할과 뷰를 렌더링 하는 역할을 명확하게 구분할 수 있다.
특히 뷰는 화면을 그리는 역할에 충실한 덕분에, 코드가 깔끔하고 직관적이다.
단순하게 모델에서 필요한 데이터를 꺼내고, 화면을 만들면 된다.
그런데 컨트롤러는 딱 봐도 중복이 많고, 필요하지 않은 코드들도 많이 보인다.
뷰로 이동하는 포워드 코드가 중복되고, ViewPath에 중복되고 HttpServletRequest request, HttpServletResponse response 같은 사용되지 않는 코드들도 있다.
그리고 이런 HttpServletRequest , HttpServletResponse를 사용하는 코드는 테스트 케이스를 작성하기도 어렵다.
또한 공통 처리가 어렵다. 기능이 많을수록 컨트롤러가 공통으로 처리하는 부분이 많아진다.
메서드로 뽑는다 해도 항상 호출하면 이 또한 중복이다.
정리하면 공통 처리가 어렵다는 문제가 있다.
이 문제를 해결하려면 컨트롤러 호출 전에 먼저 공통 기능을 처리해야 한다.
소위 수문장 역할을 하는 기능이 필요하다.
프런트 컨트롤러(Front Controller) 패턴을 도입하면 이런 문제를 깔끔하게 해결할 수 있다. (입구를 하나로!)
스프링 MVC의 핵심도 바로 이 프런트 컨트롤러에 있다. 다음 시간에 본격적으로 정리해보자!
이상으로 포스팅을 마치겠습니다.
댓글