![MVC패턴과 변천사 - 1](https://img1.daumcdn.net/thumb/R750x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbTPpMP%2FbtsGtTGQbo7%2FVmFjxApP9hXRC3UItpVNuK%2Fimg.png)
이전 글에서 MVC패턴 등장계기는 JSP를 사용할 때, 비즈니스 로직코드인 Java코드와 HTML코드가 한 파일안에 같이 있기때문에 유지보수에 어려움을 겪었고 이 문제를 해결하기위해 MVC패턴을 사용한다고 설명했다
JSP와 JSP가 사장되는 이유 + MVC패턴을 사용하는 이유
JSP등장 이전 JSP의 등장 이전에는 클라이언트에게 동적인 코드를 제공하기위해서 Servlet내부에서 서비스 코드를 작성하여 HTML코드와 함께 응답을 해주었다. 클라이언트에게 동적인 화면을 제공
lee-dev-log.tistory.com
MVC패턴이란
MVC패턴은 Model, View, Controller의 약자를 의미한다.
View: View는 화면을 렌더링하는 역할을 하며 간단히 말해서 HTML코드를 브라우저(사용자에게)를 통해 보여주는 역할을 한다. 이 때 JSP에서 <% %>영역 안에 있는 Java코드를 통해 HTML코드 안에서 동적으로 표시될 데이터를 받았었는데 View에서 동적인 데이터를 사용하기 위해 Model에 담겨있는 데이터를 사용한다.
Model: View에 출력할 데이터를 담아두며 View에서 필요한 데이터를 모두 모델에 담아 전달해주므로 View는 비즈니스 로직이나 데이터 접근을 몰라도 되며 사용자(클라이언트)에게 화면을 렌더링하는 단일책임의 역할을 만족하게 된다.
Contoller: 클라이언트로부터 HTTP요청을 받아 데이터 처리하기도하며 이 데이터를 비즈니스 로직을 통해 데이터를 가공하여 Model에 담아 View로 전달한다.
사용자의 입력 데이터를 검증하기도 하며 위에 코드에서 본 등록된 회원 DB에 저장, DB에 있는 회원을 Model에 담는 것을 의미한다.
이 Controller는 비즈니스 로직도 처리하고 Model을 View로 넘겨주고 사용자로부터 입력받은 데이터를 검증하는 등 너무 많은 역할을 수행하기 때문에 다시 한 번 Service계층으로 나누어 사용되는 것이 일반적이다.
Service계층: Service계층에서는 비즈니스 로직 코드를 가지고 있으며 Controller에 의해 호출되어 사용된다.
MVC패턴을 자세히 알기 전
MVC패턴을 익히기 전에 알아야할 몇 가지 개념들을 정리해보겠다.
WEB-INF디렉터리
WEB-INF디렉터리안에 jsp파일을 넣으면 외부에서 해당 jsp파일을 호출할 수 없게된다. 쉽게 말하자면 WEB-INF가 아닌 다른 디렉터리 jsp파일이 존재할 경우 URL주소에 localhosh:8080/test/member-form.jsp 와 같이 jsp파일을 요청하여 HTML코드를 받을 수 있었지만, WEB-INF디렉터리 안에 JSP파일을 넣을 경우 URL주소로 접근할 수 없게되고 반드시 Controller를 통해서 접근해야만 jsp파일을 서버로부터 받을 수 있다
위의 결과를 보면 WEB-INF가 아닌 임의의 디렉터리에 있다면 JSP를 URL경로를 통해 직접 요청할 수 있다.
JSP의 경로를 WEB-INF로 설정해줬을 때를 살펴보면
경로를 변경하면 위와 같이 URL경로를 통해 직접 접근할 수 없음을 확인할 수 있으며 서버는 상태코드로 404를 반환한다.
MVC패턴 적용
회원을 등록하는 MemberForm
@WebServlet(name = "memberForm", urlPatterns = "/mvc/new-form")
public class MemberForm 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);
dispatcher.forward(req, resp);
}
}
@WebServlet어노테이션의 속성값으로 name은 해당 서블릿을 식별해주는 역할을 하며, urlPatterns속성은 속성에 해당하는 URL요청이 들어왔을 때 service()메서드를 호출하여 아래 요청을 처리한다.
1. JSP경로를 viewPath변수로 생성
2. dispathcher를 통해 경로를 설정
3. forward로 req, resp값을 넘긴다
Form에서는 별다른 로직없이 new-form view로 넘겨주는 역할을 수행하며, req,resp에도 별다른 값을 넘겨주지는 않는다
입력받은 Member 저장하는 MemberSave
HTML form태그로부터 입력받은 데이터를 저장해야한다
클라이언트로부터 이름과 나이의 값을 form태그로 입력받아 "name", "age"의 키를잡고 사용자의 입력값을 value로 저장하며 JSP내부적으로는 이 키값을 Map컬렉션 형태로 처리한다
form의 속성을 보면 "action"은 save값인 것을 확인할 수 있는데 이 값은 상대경로로 처리되며 현재 위치하는 디렉터리를 기준으로 save경로로 이동시킨다. 따라서 전송버튼을 눌렀을 때
http://localhost:8080/mvc/save 와 같이 URL주소를 이동시킨다.
그리고 HTTP method는 post타입이기때문에 HTTP message body에 내부적으로 쿼리파라미터형식으로 데이터를 서버로 날려보낸다. 결과적으로 클라이언트가
이름: hong, 나이: 20으로 입력하면
name=hong&age=20 의 문자열로 전송되어 서버에서 파싱하여 처리하게 된다.
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>$Title$</title>
</head>
<body>
<form action="save" method="post">
이름<input type="text" name="name"><br>
나이<input type="text" name="age"><br>
<button type="submit">전송</button>
</form>
</body>
</html>
이제 컨트롤러에서는 어떻게 처리하는지 살펴보자
MemberRepository는 임의의 저장소로 Map컬렉션으로 값을 저장하며 싱글톤 패턴이 적용되어있다.
HTML코드를 설명할 때 form태그에서 action속성을 "save"로 설정하여 상대경로를 통해 http://localhost:8080/mvc/save로 URL요청이 들어온다고 설명했고 컨트롤러에서도 urlPatterns속성의 값이 "/mvc/save"인 것을 확인할 수 있다.
그렇기 때문에 위에 form태그에서 요청된 submit은 아래에 컨트롤러로 넘어오는 것을 확인할수 있으며 request파라미터로 "name"과 "age"의 키값을 가지고 있다.
컨트롤러에서는 넘어온 파라미터 값들을 매개변수로 갖는 Member객체를 생성해주어 싱글톤이 적용된 memberRepository변수에 save()메서드를 이용하여 저장해준다
@WebServlet(urlPatterns = "/mvc/save")
public class MemberSave extends HttpServlet {
MemberRepository memberRepository = MemberRepository.getInstance();
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String name = req.getParameter("name");
int age = Integer.parseInt(req.getParameter("age"));
Member member = new Member(name, age);
memberRepository.save(member);
req.setAttribute("member", member);
String viewPath = "/WEB-INF/views/save.jsp";
RequestDispatcher dispatcher = req.getRequestDispatcher(viewPath);
dispatcher.forward(req, resp);
}
}
setAttribute
setAttribute가 중요한데 이 메서드가 지금은 MVC패턴에서 M인 Model역할을 하고 있다. (이 후 버전업 된MVC에서는 더 명확하게 사용)
setAttribute메서드를 사용하여 위에서 생성한 member변수를 "member"라는 키 값에 담아 Map컬렉션 형식으로 저장하며 member는 저장소에 저장되어 dispathcher를 통해 forward될 때 member데이터도 viewPath에 지정된 주소로 함께 넘어가게 된다. (여기서는 /WEB-INF/views/save.jsp이다.)
dispathcher.forward(req,resp); 코드를 통해서도 req와 resp를 파라미터로 넘긴다는 것을 다시 확인할 수 있다.
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>$Title$</title>
</head>
<body>
OK<br>
<span>ID: ${member.id}</span><br>
<span>NAME: ${member.name}</span><br>
<span>AGE: ${member.age}</span><br>
<a href="members">목록으로</a>
</body>
</html>
${member.xxx}는 setAttribute를 통해서 넘겨받은 member변수를 통해서 해당하는 값을 출력하는 것이다.
html태그를 통해서 결과를 보여주고 링크를걸어 다시 "members"상대경로를 통해 목록으로 이동할 수 있다.
이동하면 localhost:8080/mvc/members 경로로 이동하게 된다.
MemberList 회원 목록출력
@WebServlet(urlPatterns = "/mvc/members")
public class MemberList extends HttpServlet {
MemberRepository memberRepository = MemberRepository.getInstance();
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
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);
}
}
지금까지 설명한 내용과 같으며 List컬렉션에 MemberRepository에 저장되어있는 모든 회원목록을 저장하여 setAttribute메서드를 사용하여 "members"라는 키값으로 /WEB-INF/views/members.jsp 페이지로 forward시킨다.
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<html>
<head>
<title>$Title$</title>
</head>
<body>
<table>
<thead>
<th>ID</th>
<th>Name</th>
<th>Age</th>
</thead>
<tbody>
<c:forEach var="item" items="${members}">
<tr>
<td>${item.id}</td>
<td>${item.name}</td>
<td>${item.age}</td>
</tr>
</c:forEach>
</tbody>
</table>
<a href="new-form">등록하기</a>
</body>
</html>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
jstl태그를 사용하여 JSP에서 for문을 보다 쉽고 깔끔하게 출력할 수 있으며 JSTL의 문법은 자바의 기본문법과 매우 유사하다.
지금까지 MVC패턴을 적용함으로써 기존에 JSP하나의 파일에 비즈니스 로직과 HTML UI가 존재하는 것을 분리하여 유지보수를 더욱 편리하게 할 수 있었다.
하지만 form, save, list 세 개의 컨트롤러에서 공통으로 사용되는 코드가 있는데 바로 dispathcher를 생성하고 viewPath를 설정하고 foward하는 코드였다.
String viewPath = "/WEB-INF/views/new-form.jsp";
RequestDispatcher dispatcher = req.getRequestDispatcher(viewPath);
dispatcher.forward(req, resp);
String viewPath = "/WEB-INF/views/save.jsp";
RequestDispatcher dispatcher = req.getRequestDispatcher(viewPath);
dispatcher.forward(req, resp);
String viewPath = "/WEB-INF/views/members.jsp";
RequestDispatcher dispatcher = req.getRequestDispatcher(viewPath);
dispatcher.forward(req, resp);
위와 같이 반복되는 코드도 있으며 HttpServlet resp객체와같이 사용되지 않는 코드도 있었다.
MVC패턴에 FrontController추가
FrontController
FrontController의 역할
1. 프론트 컨트롤러 하나의 서블릿으로 클라이언트의 요청에 맞는 컨트롤러를 호출함으로써 요청을 처리할 수 있다
2. 이전의 MVC패턴에서 중복된 코드들을 FrontController에서 미리 처리할 수 있다
3. 후에 사용할 SpringMVC의 DispatcherServlet의 역할을 수행하는 것이 FrontController이다
FrontController는 클라이언트와 기능을 수행하는 컨트롤러(구현체)사이에서 중계를 해주는 역할을 한다.
코드로 직접 살펴보자
@WebServlet(urlPatterns = "/front/mvc/*")
public class FrontControllerV1 extends HttpServlet {
private Map<String, ControllerV1> controllerMap = new HashMap<>();
public FrontControllerV1() {
controllerMap.put("/front/mvc/new-form", new MemberFormControllerV1());
controllerMap.put("/front/mvc/save", new MemberSaveControllerV1());
controllerMap.put("/front/mvc/members", new MemberListControllerV1());
}
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String requestURI = request.getRequestURI();
ControllerV1 controller = controllerMap.get(requestURI);
controller.process(request, response);
}
}
위 클래스는 프론트 컨틀로러의 역할을 수행하는 컨트롤러로 클라이언트의 요청에 필요한 컨트롤러를 호출하여 process()메서드로 실행시킨다.
다형성으로 설계되어있으며 controllerMap의 Map컬렉션은 요청 URL주소인 String타입을 키로 받고, Controller인터페이스를 값으로 갖는다.
controllerMap의 String타입 즉, 키로 들어오는 문자열은 request.getRequestURI() 메서드를 통하여 요청받은 URI의 주소를 가져오며 그에 맞는 컨트롤러를 호출하는 것이다.
이렇게 설계하고 만약 "front/mvc/new-form"요청이 들어오면 MemberFormController를 호출하게되고 해당 객체의 process메서드를 실행하여 비즈니스 로직을 수행한다.
public interface ControllerV1 {
void process(HttpServletRequest request, HttpServletResponse response)throws IOException, ServletException;
}
이 개념이 이해가 잘 안된다면 다형성을 조금 더 공부하는 것을 추천한다.
아래 코드는 프론트 컨트롤러를 추가한 후에 Controller를 구현한 구현체이며 이전에 보았던 MemberSave클래스와 유사하지만 @WebServlet어노테이션이 빠진 것을 알 수 있으며, 이는 프론트 컨트롤러에서 서블릿 하나만을 사용하여 요청을 처리한다는 것을 의미한다.
public class MemberSaveControllerV1 implements ControllerV1 {
MemberRepository memberRepository = MemberRepository.getInstance();
@Override
public void process(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
String name = request.getParameter("name");
int age = Integer.parseInt(request.getParameter("age"));
Member member = new Member(name, age);
memberRepository.save(member);
request.setAttribute("member", member);
String viewPath = "/WEB-INF/views/save.jsp";
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);
}
}
지금까지는 다형성을 사용하여 FrontController를 설계하였고 서블릿을 프론트 컨트롤러 하나에만 적용된 것을 확인할 수 있었다.
중복코드제거
이제 각 기능을 구현한 구현체에서 아래와 같이 중복되는 코드들을 제거해보자.
String viewPath = "/WEB-INF/views/new-form.jsp";
RequestDispatcher dispatcher = req.getRequestDispatcher(viewPath);
dispatcher.forward(req, resp);
String viewPath = "/WEB-INF/views/save.jsp";
RequestDispatcher dispatcher = req.getRequestDispatcher(viewPath);
dispatcher.forward(req, resp);
String viewPath = "/WEB-INF/views/members.jsp";
RequestDispatcher dispatcher = req.getRequestDispatcher(viewPath);
dispatcher.forward(req, resp);
MyView클래스를 만들어 이 객체에서 viewPath를 매핑하고 포워딩처리를 시킨다.
public class MyView {
private String viewPath;
public MyView(String viewPath) {
this.viewPath = viewPath;
}
public void rend(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);
}
}
MyView객체를 생성할 때 viewPath 문자열을 받아 디스패쳐를 통해 포워딩하는 것 또한 모두 똑같은 원리이다.
단지 역할을 분리해줌으로써 코드의 중복도 제거되는 셈이다
이렇게 MyView객체를 생성하고 rend메서드를 호출하면 MyView객체는 화면을 랜더링하는 역할까지 수행하게 되며 View에 필요한 역할을 모두 수행하게 되어 단일책임원칙 또한 만족한다.
때문에 void였던 컨트롤러 인터페이스의 process반환타입을 MyView로 수정해주어야하며 프론트 컨트롤러에서 이 MyView객체를 반환 받아서 다형성을 통해 생성된 객체에 맞는 View를 렌더링한다
public interface Controller {
MyView process(HttpServletRequest request, HttpServletResponse response)throws IOException, ServletException;
}
코드의 큰 변화는 없지만 viewPath를 설정하고 포워딩하는 코드를 제거한 것을 확인할 수 있다.
MemberSave
public class MemberSave implements Controller{
MemberRepository memberRepository = MemberRepository.getInstance();
@Override
public MyView process(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
String name = request.getParameter("name");
int age = Integer.parseInt(request.getParameter("age"));
Member member = new Member(name, age);
memberRepository.save(member);
request.setAttribute("member", member);
String viewPath = "/WEB-INF/views/save.jsp";
MyView myView = new MyView(viewPath);
return myView;
}
}
또한 이해를 조금이라도 수월하게 하기위해서 코드를 풀어썼지만 아래의 코드를 한 줄로 정리할 수 있다.
String viewPath = "/WEB-INF/views/save.jsp";
MyView myView = new MyView(viewPath);
return myView;
return new MyView("/WEB-INF/views/save.jsp");
MemberList
public class MemberList implements Controller{
MemberRepository memberRepository = MemberRepository.getInstance();
@Override
public MyView process(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
List<Member> members = memberRepository.findAll();
request.setAttribute("members", members);
return new MyView("/WEB-INF/views/members.jsp");
}
}
MemberForm
public class MemberForm implements Controller{
@Override
public MyView process(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
return new MyView("/WEB-INF/views/new-form.jsp");
}
}
프론트 컨트롤러에서 MyView를 반환받은 뒤 rend()메서드를 통해 렌더링 처리를하여 클라이언트에게 JSP를 응답한다.
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String viewPath = req.getRequestURI();
ControllerV2 controller = controllerMap.get(viewPath);
MyView view = controller.process(req, resp);
view.rend(req, resp);
}
위 정리한 내용들은 모두 인프런에서 김영한님의 강의를 듣고 학습한 내용을 스스로 정리한 것임을 밝힙니다.
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-1#
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!