본문 바로가기

Programming/스프링

[김영한 스프링 MVC 1] 서블릿, JSP, MVC 패턴

회원 관리 웹 애플리케이션 요구사항
  • 회원 정보

나이 : age

이름 : username

 

 

  • 기능 요구사항

회원 저장, 회원 목록 조회

 

 

  • 클래스
    • 회원 도메인 모델
    package hello.servlet.domain.member;
    
    import lombok.Getter;
    import lombok.Setter;
    
    @Getter @Setter
    public class Member {
    	private Long id; // 회원 저장소에서 id 할당시킴
    	private String username;
    	private int age;
    		
    	public Member() {
      }
     
    	public Member(String username, int age) {
    		this.username = username;
    		this.age = age;
     }
    }
    
    • 회원 저장소
    package hello.servlet.domain.member;
    
    import java.util.ArrayList;
    import java.util.HashMap;
    import java.util.List;
    import java.util.Map;
    
    /**
     * 동시성 문제가 고려되어 있지 않음, 실무에서는 ConcurrentHashMap, AtomicLong 사용 고려
     */
    
    public class MemberRepository {
    
    	private static Map<Long, Member> store = new HashMap<>(); //static 사용
    	private static long sequence = 0L; //static 사용
      private static final MemberRepository instance = new MemberRepository();
    	// 싱글톤
    
      public static MemberRepository getInstance() {
        return instance;
      }
     
      private MemberRepository() { //private 생성자 => 아무나 생성하지 못하도록 방지
      }
     
      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()); //store의 모든 값들 담음
      }
    
    	public void clearStore() {
    		store.clear();
    	}
    }
    
    스프링 ⇒ 스프링 빈이 싱글톤 보장
    but 지금은 순수 서블릿으로 구현하므로 직접 싱글톤 패턴 적용
    • 테스트 코드
package hello.servlet.domain.member;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.assertj.core.api.Assertions.*;

class MemberRepositoryTest {

 MemberRepository memberRepository = MemberRepository.getInstance();

 @AfterEach
 void afterEach() {
	 memberRepository.clearStore();
 }

 @Test
 void save() {
	 //given
	 Member member = new Member("hello", 20);

	 //when
	 Member savedMember = memberRepository.save(member);

	 //then
	 Member findMember = memberRepository.findById(savedMember.getId());
	 assertThat(findMember).isEqualTo(savedMember);
 }

 @Test
 void findAll() {
	 //given
	 Member member1 = new Member("member1", 20);
	 Member member2 = new Member("member2", 30);
	 memberRepository.save(member1);
	 memberRepository.save(member2);

	 //when
	 List<Member> result = memberRepository.findAll();

	 //then
	 assertThat(result.size()).isEqualTo(2);
	 assertThat(result).contains(member1, member2);
 }

}

 

 

 

서블릿으로 회원 관리 웹 애플리케이션 만들기
  • 클래스
    • 회원 등록 폼
    package hello.servlet.web.servlet;
    
    import javax.servlet.ServletException;
    import javax.servlet.annotation.WebServlet;
    import javax.servlet.http.HttpServlet;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    import java.io.PrintWriter;
    
    @WebServlet(name = "memberFormServlet", urlPatterns = "/servlet/members/newform")
    public class MemberFormServlet extends HttpServlet {
    
    	@Override
    	protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    		 response.setContentType("text/html");
    		 response.setCharacterEncoding("utf-8");
    	
    		 PrintWriter w = response.getWriter();
    		 w.write("<!DOCTYPE html>\\n" +
    		 "<html>\\n" +
    		 "<head>\\n" +
    		 " <meta charset=\\"UTF-8\\">\\n" +
    		 " <title>Title</title>\\n" +
    		 "</head>\\n" +
    		 "<body>\\n" +
    		 "<form action=\\"/servlet/members/save\\" method=\\"post\\">\\n" +
    		 " username: <input type=\\"text\\" name=\\"username\\" />\\n" +
    		 " age: <input type=\\"text\\" name=\\"age\\" />\\n" +
    		 " <button type=\\"submit\\">전송</button>\\n" +
    		 "</form>\\n" +
    		 "</body>\\n" +
    		 "</html>\\n");
    	 }
    }
    
    ⇒ 자바로 직접 작성해야 하므로 불편
    • 회원 저장 폼
    package hello.servlet.web.servlet;
    
    import hello.servlet.domain.member.Member;
    import hello.servlet.domain.member.MemberRepository;
    import javax.servlet.ServletException;
    import javax.servlet.annotation.WebServlet;
    import javax.servlet.http.HttpServlet;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    import java.io.PrintWriter;
    
    @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>");
    	 }
    }
    
    • 회원 목록 폼
    package hello.servlet.web.servlet;
    
    import hello.servlet.domain.member.Member;
    import hello.servlet.domain.member.MemberRepository;
    import javax.servlet.ServletException;
    import javax.servlet.annotation.WebServlet;
    import javax.servlet.http.HttpServlet;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    import java.io.PrintWriter;
    import java.util.List;
    
    @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>");
    
    		/*
    		 w.write(" <tr>");
    		 w.write(" <td>1</td>");
    		 w.write(" <td>userA</td>");
    		 w.write(" <td>10</td>");
    		 w.write(" </tr>");
    		*/
    
    		 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>");
    	 }
    
    }
    
  • 템플릿 엔진으로

지금까지 서블릿, 자바 코드만으로 HTML을 만들어봄⇒ 서블릿으로 동적 HTML 문서 생성 가능

⇒ BUT 매우 복잡, 비효율적 ⇒ 차라리 HTML 문서에 동적으로 변경해야 하는 부분만 자바 코드를 넣을 수 있다면 더 편리

 

⇒ 템플릿 엔진 등장 (HTML 문서에서 필요한 곳만 코드 적용, 동적으로 변경)

ex) JSP, Thymeleaf, Freemarker, Velocity

 

※ JSP ⇒ 사장되는 추세 / 스프링에서는 Thymeleaf 권장

 

 

 

 

  • html
    • main/webapp/index.html
    <!DOCTYPE html>
    <html>
    <head>
     <meta charset="UTF-8">
     <title>Title</title>
    </head>
    <body>
    <ul>
     <li><a href="basic.html">서블릿 basic</a></li>
     <li>서블릿
     <ul>
     <li><a href="/servlet/members/new-form">회원가입</a></li>
     <li><a href="/servlet/members">회원목록</a></li>
     </ul>
     </li>
     <li>JSP
     <ul>
     <li><a href="/jsp/members/new-form.jsp">회원가입</a></li>
     <li><a href="/jsp/members.jsp">회원목록</a></li>
     </ul>
     </li>
     <li>서블릿 MVC
     <ul>
     <li><a href="/servlet-mvc/members/new-form">회원가입</a></li>
     <li><a href="/servlet-mvc/members">회원목록</a></li>
     </ul>
     </li>
     <li>FrontController - v1
     <ul>
     <li><a href="/front-controller/v1/members/new-form">회원가입</a></li>
     <li><a href="/front-controller/v1/members">회원목록</a></li>
     </ul>
     </li>
     <li>FrontController - v2
     <ul>
     <li><a href="/front-controller/v2/members/new-form">회원가입</a></li>
     <li><a href="/front-controller/v2/members">회원목록</a></li>
     </ul>
     </li>
     <li>FrontController - v3
     <ul>
     <li><a href="/front-controller/v3/members/new-form">회원가입</a></li>
     <li><a href="/front-controller/v3/members">회원목록</a></li>
     </ul>
     </li>
     <li>FrontController - v4
     <ul>
     <li><a href="/front-controller/v4/members/new-form">회원가입</a></li>
     <li><a href="/front-controller/v4/members">회원목록</a></li>
     </ul>
     </li>
     <li>FrontController - v5 - v3
     <ul>
     <li><a href="/front-controller/v5/v3/members/new-form">회원가입</a></
    li>
     <li><a href="/front-controller/v5/v3/members">회원목록</a></li>
     </ul>
     </li>
     <li>FrontController - v5 - v4
     <ul>
     <li><a href="/front-controller/v5/v4/members/new-form">회원가입</a></
    li>
     <li><a href="/front-controller/v5/v4/members">회원목록</a></li>
     </ul>
     </li>
     <li>SpringMVC - v1
     <ul>
     <li><a href="/springmvc/v1/members/new-form">회원가입</a></li>
     <li><a href="/springmvc/v1/members">회원목록</a></li>
     </ul>
     </li>
     <li>SpringMVC - v2
     <ul>
     <li><a href="/springmvc/v2/members/new-form">회원가입</a></li>
     <li><a href="/springmvc/v2/members">회원목록</a></li>
     </ul>
     </li>
     <li>SpringMVC - v3
     <ul>
     <li><a href="/springmvc/v3/members/new-form">회원가입</a></li>
     <li><a href="/springmvc/v3/members">회원목록</a></li>
     </ul>
     </li>
    </ul>
    </body>
    </html>
    

 

 

 

JSP로 회원 관리 웹 애플리케이션 만들기
  • JSP 라이브러리 추가
// 스프링부트 3.0 미만
implementation 'org.apache.tomcat.embed:tomcat-embed-jasper'
implementation 'javax.servlet:jstl' //

// 스프링부트 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' //

추가 후 gradles refresh

 

 

  • JSP

<%@ page import=”” %> ⇒ 자바 import와 동일
<%  %> ⇒ 자바 코드 입력
<%= %> ⇒ 자바 코드 출력

 

  • 회원 등록 폼 (main/webapp/jsp/members/new-form.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>

 

  • 회원 저장 폼 (main/webapp/jsp/members/save.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>

 

  • 회원 목록 폼 (main/webapp/jsp/members.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의 한계

서블릿 ⇒ 뷰 화면을 위한 HTML 생성하는 작업이 자바 코드에 섞여 지저분, 복잡

JSP ⇒ 뷰 생성하는 HTML 작업 깔끔 BUT 비즈니스 로직, 리포지토리 코드 등 너무 다양한 코드가 JSP에 노출됨

JSP가 너무 많은 역할 담당 ⇒ 유지 보수 어려움

 

MVC 패턴 등장

비즈니스 로직은 서블릿처럼 다른 곳에서 처리

JSP는 목적에 맞게 HTML로 화면(View)을 그리는 일에 집중

 

 

 

MVC 패턴 - 개요

 

  • 변경의 라이프 사이클

자바, HTML 코드 간 변경의 라이프 사이클이 다름

UI - 비즈니스 로직을 수정하는 일은 각각 다르게 발생할 가능성이 매우 높고 대부분 서로에게 영향을 주지 않음

이러한 변경 라이프 사이클이 다른 부분을 하나의 코드로 관리 => 유지보수에 ▲

(물론 UI가 많이 변하면 함께 변경될 가능성 有)

 

 

  • 기능 특화

JSP 등 뷰 템플릿 => 화면 렌더링에 최적화

=> 해당 업무만 담당하는 것이 가장 효과적

 

 

  • Model View Controller

하나의 서블릿/JSP로 처리하던 것을 컨트롤러(Controller)와 뷰(View)라는 영역으로 서로 역할을 나눈 것

 

컨트롤러: HTTP 요청을 받아 파라미터 검증, 비즈니스 로직 실행 → 뷰에 전달할 결과 데이터를 조회해 모델에 담음
모델: 뷰에 출력할 데이터를 담아둠 => 뷰는 비즈니스 로직이나 데이터 접근을 몰라도 되고, 화면 렌더링에 집중
뷰: 모델에 담겨있는 데이터를 사용해 화면 그리는 일에 집중 (여기서는 HTML을 생성하는 부분)

 

일반적으로 비즈니스 로직은 서비스(Service)라는 계층을 별도 생성, 처리

컨트롤러는 비즈니스 로직이 있는 서비스를 호출

 

 

  • MVC 패턴 이전

 

 

  • MVC 패턴 1

 

 

  • MVC 패턴 2

 

 

 

MVC 패턴 - 적용

Model => HttpServletRequest 객체 사용

request => 내부에 데이터 저장소 가짐
=> request.setAttribute() , request.getAttribute() 로 데이터 보관/조회

 

 

  • 회원 등록 폼 (MvcMemberFormServlet)
  • 컨트롤러
@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); //다른 서블릿이나 JSP로 이동 => 서버 내부에서 다시 호출 발생
  }
}

 

  • 뷰 (main/webapp/WEB-INF/views/new-form.jsp)
<%@ 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>

 

※ /WEB-INF

이 경로 안에 JSP가 있으면 외부에서 직접 JSP 호출 불가

=> 항상 컨트롤러를 통해 JSP를 호출

 

※ redirect vs forward

redirect :

실제 클라이언트(웹 브라우저)에 응답이 나갔다가, 클라이언트가 redirect 경로로 다시 요청

=> 클라이언트가 인지할 수 있고, URL 경로도 실제로 변경됨

 

forward :

서버 내부에서 일어나는 호출이기 때문에 클라이언트가 전혀 인지 X

 

 

  • 회원 저장
  • 컨트롤러 (MvcMemberSaveServlet)
@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); 
    
    request.setAttribute("member", member); //Model에 데이터 보관
    
    String viewPath = "/WEB-INF/views/save-result.jsp";
    
    RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath); // 이동 경로 지정
    dispatcher.forward(request, response); // 페이지 이동
 }
}

 

  • 뷰 (main/webapp/WEB-INF/views/save-result.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>

 

<%= request.getAttribute("member")%> 대신 ${} 사용해 request의 attribute에 담긴 데이터를 편리하게 조회

 

 

 

  • 회원 목록 조회
  • 컨트롤러
@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);
  }
}

 

  • 뷰 (main/webapp/WEB-INF/views/members.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</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>

 

 

 

MVC 패턴 - 한계

MVC 패턴 => 컨트롤러의 역할 - 뷰 렌더링 역할을 명확하게 구분 가능 
특히 뷰는 화면을 그리는 역할에 충실
but 컨트롤러는 중복이 많고, 필요하지 않은 코드들도 많음

 

 

  • 단점
  • 포워드 중복

View로 이동하는 코드가 항상 중복 호출됨

이 부분을 메서드로 공통화해도 되지만 해당 메서드도 항상 직접 호출해야 함

RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);

 

  • ViewPath에 중복
String viewPath = "/WEB-INF/views/new-form.jsp";

prefix: /WEB-INF/views/
suffix: .jsp

=> jsp가 아닌 thymeleaf 같은 다른 뷰로 변경한다면 전체 코드를 다 변경해야 함

 

  • 사용하지 않는 코드

request, response 사용할 때도, 사용하지 않을 때도 있음

테스트 코드 적용도 어려움

 

  • 공통 처리 어려움

기능이 복잡해질 수록 컨트롤러에서 공통 처리해야 하는 부분 점점 증가

단순히 공통 기능을 메서드로 뽑으면 될 것 같아도, 결과적으로 해당 메서드를 항상 호출해야 함(=중복)

 

 

=> 프론트 컨트롤러(Front Controller) 패턴

컨트롤러 호출 전에 먼저 공통 기능을 처리