Web/Spring

Spring MVC (3) - 서블릿, JSP, MVC 패턴

now0204 2023. 10. 2. 13:37

- 서블릿과 jsp만을 사용해서 웹 어플리케이션을 구성해보고, 간단한 MVC 패턴을 활용해보자

 

1. 회원 관리 웹 어플리케이션 요구사항 

 

//회원 도메인 
@Getter @Setter
public class Member{


	private Long id;
    private String username;
    private int age;
    
    public Member(){}
    public Member(String username, int age){
    this.username = username;
    this.age = age;
    }
}
//회원 저장소

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());
    }
    
    public void clearStore(){
    	store.clear();
    }
}

* 싱글톤 패턴 적용

 

2. 서블릿으로 회원 관리 어플리케이션 만들기 

 

@WebServlet(name = "memberFormServlet", urlPatterns = "/servlet/members/new-form")
public class MemberFormServlet extendst HttpServlet{


	@Override
    protected void service(HttpServletRequest request, HttpServletResponse response)
    throws ServletException, IOException{
    
    	response.setContentType("text/html");
        response.setCharacterEncoding("urf-8");
        PrintWriter w = response.getWriter();
        w.write("..html코드 회원등록 form 제공");
    }
}

- 해당 url로 요청을 보내면 회원등록 폼을 제공하는 서블릿이다.

- form의 action은 /servlet/members/save로 지정해두자

- 이제 넘어온 html form데이터를 저장하는 서블릿을 구성해보자

 

@WebServlet(name = "memberSaveServlet", urlPatterns = "/servlet/members/save")
public class MemberSaveServlet extends HttpServlet {
 private MemberRepository memberRepository = MemberRepository.getInstance();
 @Override
 protected void service(HttpServletRequest request, HttpServletResponse 
response)
 throws ServletException, IOException {
 System.out.println("MemberSaveServlet.service");
 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);
 response.setContentType("text/html");
 response.setCharacterEncoding("utf-8");
 PrintWriter w = response.getWriter();
 w.write("<html>\n" +
 "<head>\n" +
 " <meta charset=\"UTF-8\">\n" +
 "</head>\n" +
 "<body>\n" +
 "성공\n" +
 "<ul>\n" +
 " <li>id="+member.getId()+"</li>\n" +
 " <li>username="+member.getUsername()+"</li>\n" +
 " <li>age="+member.getAge()+"</li>\n" +
 "</ul>\n" +
 "<a href=\"/index.html\">메인</a>\n" +
 "</body>\n" +
 "</html>");
 }
}

- 파라미터를 조회해서 Member를 만들고, memberRepository에 저장한다.

- 결과화면으로 저장된 Member의 id와 이름을 화면에 뿌림

- 마지막으로 회원목록을 보여주는 서블릿이다.

 

@WebServlet(name = "memberListServlet", urlPatterns = "/servlet/members")
public class MemberListServlet extends HttpServlet {
 private MemberRepository memberRepository = MemberRepository.getInstance();
 @Override
 protected void service(HttpServletRequest request, HttpServletResponse 
response)
 throws ServletException, IOException {
 response.setContentType("text/html");
 response.setCharacterEncoding("utf-8");
 List<Member> members = memberRepository.findAll();
 PrintWriter w = response.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("<a href=\"/index.html\">메인</a>");
 w.write("<table>");
 w.write(" <thead>");
 w.write(" <th>id</th>");
 w.write(" <th>username</th>");
 w.write(" <th>age</th>");
 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(" </tbody>");
 w.write("</table>");
 w.write("</body>");
 w.write("</html>");
 }
}

- /servlet/members로 요청이 오면, memberRepository에서 모든 member를 꺼내서 보여준다.

- 서블릿만을 활용하여 동적 HTML문서를 만들어보았다. 다만, 자바코드와 HTML코드가 섞여 있어서 매우 복잡하고 보기 흉하다. HTML문서에 일부 자바 코드만 넣는 것이 훨씬 편할 것이다. 이를 가능하게 해주는 것이

탬플릿 엔진(JSP,Thymeleaf)이다. 

 

3. JSP로만 회원관리 애플리케이션 만들기

 

- 먼저 JSP 라이브러리를 추가하자

//JSP 추가 시작
implementation 'org.apache.tomcat.embed:tomcat-embed-jasper'
implementation 'javax.servlet:jstl'
//JSP 추가 끝

//스프링 부트 3.0 미만, build.gradle에 추가

 

3.1 회원 등록 폼 JSP

 

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
 <title>Title</title>
</head>
<body>
<form action="/jsp/members/save.jsp" method="post">
 username: <input type="text" name="username" />
 age: <input type="text" name="age" />
 <button type="submit">전송</button>
</form>
</body>
</html>

- 첫줄 <%@ page -> JSP문서라는 뜻 

- 해당 JSP에 접근 url은 /jsp/members/new-form.jsp 

- jsp는 서버 내부에서 서블릿으로 변환됨 (MemberFormServlet과 비슷한 형태)

 

3.2 회원 저장 jsp (webapp/jsp/members.jsp)

<%@ page import="hello.servlet.domain.member.MemberRepository" %>
<%@ page import="hello.servlet.domain.member.Member" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
// request, response 사용 가능
 MemberRepository memberRepository = MemberRepository.getInstance();
 System.out.println("save.jsp");
 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);
%>
<html>
<head>
 <meta charset="UTF-8">
</head>
<body>
성공
<ul>
 <li>id=<%=member.getId()%></li>
 <li>username=<%=member.getUsername()%></li>
 <li>age=<%=member.getAge()%></li>
</ul>
<a href="/index.html">메인</a>
</body>
</html>

- JSP내부에서 자바코드를 사용할 수 있다.

- <@ page import -> import문

- <% %> 순수 자바코드

- <%= %> 자바코드 출력 (HTML내부에서 사용)

 

3.3 회원 목록 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>

- 회원 리포지토리를 통해 결과를 조회하고 -> 동적으로 뿌림 

 

*request,response, out(PrintWriter) 등은 JSP내장객체로 굳이 <% %>에서 생성하지 않아도 사용할 수 있는 객체이다.

 

3.4 서블릿과 JSP의 한계

 

- 서블릿 혹은 jsp만을 사용하여 애플리케이션을 개발하는 것에는 한계가 있다.

- 비지니스 로직을 수행하는 자바코드와 view만을 위한 코드가 산재되어 있어서 복잡하고 유지보수가 힘들다.

- 따라서 비지니스 로직은 서블릿 처럼 다른 곳에서 처리하고, JSP는 목적에 맞게 view를 그리는 일에 집중하도록 하자

 

4. MVC 패턴 - 개요 

 

- 너무 많은 역할 : 서블릿이나 JSP만을 사용하면, 하나의 객체가 비지니스 로직+ 뷰 렌더링까지 처리해야함

                            이는 유지보수를 어렵게 만든다. 

- 변경의 라이프 사이클 : 비지니스 로직의 변경과 view의 변경의 사이클은 다르다 

                                       둘 중 하나만 변경되는데 둘다 건들여야한다면, 굉장히 힘들 것이다.

- 기능 특화 : JSP와 같은 뷰 템플릿은 화면을 렌더링 하는데 최적화 되어 있다.

                     따라서 이 부분만 특화하는 것이 효과적이다.

- Model View Controller :  비지니스로직을 처리하는 부분과 View 부분의 역할을 나눈 방법론을 의미한다.

 

* 컨트롤러: 요청 파라미터 검증, 비지니스 로직 실행, 뷰에 전달할 결과를 모델에 담기

    모델 : 뷰에 출력할 데이터 담기 

    뷰 : 모델의 담긴 데이터를 화면에 그리는 일에 집중 

 

* 컨트롤러에 비지니스 로직을 담을 수도 있지만, 그러면 컨트롤러가 하는일이 너무 많음 별도의 서비스 계층을 만들어서 처리하도록 하자.

 

출처: 스프링MVC(인프런) -김영한

 

4.1 MVC 패턴 적용

 

- request를 모델로 사용하자

 

- 회원 등록 폼 - 컨트롤러 

@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);
 }
}

- 포워드는 서버 내부에서 다시 호출하는 기능 수행(기존 request,response 유지)

- 리다이렉트는 실제 클라이언트가 redirect경로로 다시 요청하도록 만듦 (이전 request,response 유지x)

 

- 회원 등록 폼 (뷰)

 

<%@ 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>

- 컨트롤러에 요청하면 아래 등록폼을 보여주도록 구성함 

- save는 상대경로를 사용했는데 상대경로를 사용하면,

  클라이언트 현재요청 url (/servlet-mvc/members/new-form)에서 마지막 부분 (현재 경로)

   만 바뀌어서 요청됨 (/servlet-mvc/members/save)

 

4.2 회원 저장 - 컨트롤러 

 

@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);
 }
}

- 회원등록 폼에서 submit을 누르면, 위 서블릿으로 요청이 들어간다.

- HTML Form데이터를 꺼내서 member를 만든다음 memberRepository에 저장함

- 저장한 값을 request에 담은 뒤 결과 view로 forward한다.

 

- 회원저장 (뷰)

<%@ 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에 저장된 값을 꺼내서 화면에 뿌린다.

 

4.3 회원 목록 조회

 

- 컨트롤러 

@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);
 }
}

- 회원목록을 조회하는 요청이 들어오면, repository에서 memberList를 뽑아서 request에 저장한 다음

- 결과 화면으로 forward한다.

 

- 뷰

<%@ 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>

 

- jstl을 활용하여, request에 저장된 memberList에 member값을 하나씩 꺼내서 화면에 뿌림 

 

5. MVC 패턴의 한계

 

- MVC는 잘 적용했지만, 중복된 코드가 눈에 보인다.

 

포워드 중복: view로 이동하는 코드가 항상 중복 호출 됨 

viewPath에 중복: prefix와 suffix가 중복됨 

사용하지 않는 코드 : 서블릿 중 HttpServletRequest와 Response를 사용하지 않는 서블릿이 있다.

                                  또한 위 매개변수가 있으면, 테스트 어렵게 만든다 (너무 서블릿 종속적)

 

공통처리가 어렵다: 컨트롤러에서 공통 처리해야하는 부분이 증가하면, 중복 코드가 늘어남

 

-> 위와 같은 단순 MVC 패턴의 한계는 프론트 컨트롤러 패턴을 통해 해결할수 있다.

 

*필터랑 프론트 컨트롤러는 다름! 필터는 좀 더 단순한 기능만 수행할 수 있음 

(여러 서블릿이 있을 때 서블릿 간 공통 처리에 가까움, 프론트 컨트롤러 패턴은 서블릿을 하나만 두는 느낌으로 변화)

 

 

* 웹 어플리케이션 root는 webapp부터임! 여기서부터 시작해서 WAS가 뒤적거림

애초에 배포할 때 톰캣에 webapp폴더에 war파일을 둠 (여러 애플리케이션이 was에 webapp에 있음)

하나의 애플리케이션에서 was는 요청을 처리하기 위해 webapp 하위에서 리소스를 찾음 '

 

 

참고자료: 

https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-1

 

스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술 - 인프런 | 강의

웹 애플리케이션을 개발할 때 필요한 모든 웹 기술을 기초부터 이해하고, 완성할 수 있습니다. 스프링 MVC의 핵심 원리와 구조를 이해하고, 더 깊이있는 백엔드 개발자로 성장할 수 있습니다., 원

www.inflearn.com