[Spring] MVC 1편 - 03. 서블릿, JSP, MVC패턴

somyeong·2022년 4월 24일
0

Spring

목록 보기
13/17
post-thumbnail

이 글은 스프링 [스프링 MVC 1편]을 듣고 정리한 내용입니다

  • 이번 강에서는 서블릿, JSP, MVC 패턴 각각으로 회원 관리 웹을 만들면서 각각을 어떻게 사용하는지 살펴볼 것이다.

📌 회원 관리 웹 애플리케이션 요구사항

  • 아주 기본적인 기능인 회원 저장, 회원 목록 조회 기능만 보도록 하겠다.

  • 회원 도메인 모델

package hello.servlet.domain.member;

@Getter
@Setter
public class Member {

    private Long id; //Member를 회원 저장소에 저장하면 회원 저장소가 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;

/**
 * 동시성 문제가 고려되어 있지 않음, 실무에서는 ConcurrentHashMap, AtomicLong 사용 고려
 */
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() { //싱글톤 패턴은 객체를 단 하나만 생성해서 공유해야 하므로 생성자는 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());
    }

    public void clearStore(){
        store.clear();
    }

}
  • 회원 저장소는 싱글톤 패턴을 적용했다.

  • 스프링을 사용하면 스프링 빈으로 등록해서 알아서 싱글톤으로 관리되지만, 지금은 최대한 순수 서블릿 만으로만 구현해보자

  • 회원 저장소 테스트 코드

package hello.servlet.domain.member;

import java.util.List;

import static org.assertj.core.api.Assertions.*;

//Junit5 부터는 public 없어도 됨.
class MemberRepositoryTest {

    //싱글톤이므로 new로하면 안됨 (스프링을 쓰면 스프링 자체가 싱글톤을 보장해 주므로 쓸필요 없음)
    MemberRepository memberRepository = MemberRepository.getInstance();

    @AfterEach //테스트가 끝날때마다 깔끔하게 초기화
    void afterEach(){
        memberRepository.clearStore(); //이 과정이 없으면 test함수 순서가 보장이 안되기 때문에 각 테스트(함수)가 끝날때마다 afterEach함수가 실행되어 clearStore해준다.
    }

    @Test
    void save(){ //새로 생성한 멤버를 저장하고, 저장한 멤버와 id를 통해 찾은 멤버가 같은지 확인하기
        //given
        Member member = new Member("hello", 20);

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

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

    @Test
    void findAll(){ //전체 멤버 수는 맞는지, 각각의 멤버들은 포함한거 맞는지 확인
        //given
        Member member1 = new Member("member1", 20);
        Member member2 = new Member("member2", 30);

        memberRepository.save(member1);
        memberRepository.save(member2);

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

        //then
        assertThat(result.size()).isEqualTo(2);
        assertThat(result).contains(member1,member2); //result가 member1, member2가지고 있는지 확인

    }

}

📌 서블릿으로 웹 애플리케이션 만들기

  • 이번에는 서블릿으로 만들어보자.
  • MemberFormServlet - 회원 등록 폼
package hello.servlet.web.servlet;

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

    //싱글톤이므로 new 안된다(생성자를 private으로 막아놨었음) -> 그래서 getInstacne()로 가져온다!
    private MemberRepository memberRepository = MemberRepository.getInstance();

    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

        response.setContentType("text/html");
        response.setCharacterEncoding("utf-8");

        //서블릿으로 하면, 다음과같이 자바코드로 html코드를 다 더해야 하기때문에 굉장히 불편
        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");
    }
}

-> 위 코드처럼, 자바 코드로 html을 직접 작성해야 하므로 매우 불편한다.

  • MemberSaveServlet - 회원 저장
package hello.servlet.web.servlet;

@WebServlet(name="memberServlet", 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")); //request.getParameter한 값은 문자타입이므로 int형으로 변환

        Member member = new Member(username, age);
        memberRepository.save(member);

        //멤버 저장이 잘됐는지 확인하기 위해, 응답을 html코드로 내려보자
        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>");

    }
}

MemberSaveServlet의 동작순서
1. 파라미터를 조회해서 Member객체를 만든다.
2. Member 객체를 MemberRepository를 통해서 저장한다
3. Member 객체를 사용해서 결과 화면용 HTML을 동적으로 만들어서 응답한다.

  • MemberListServlet - 회원 목록
package hello.servlet.web.servlet;

@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 {
        List<Member> members = memberRepository.findAll();

        response.setContentType("text/html");
        response.setCharacterEncoding("utf-8");

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

MemberListServlet의 동작과정
1.memberRepository.findAll()을 통해 모든 회원을 조회한다.
2.회원 목록 HTML을 for루프를 통해서 회원수 만큼 동적으로 생성하고 응답한다.

템플릿 엔진으로

  • 지금까치처럼 자바코드로 HTML을 만드는것은 매우 복잡고 비효율적이다.
  • 차라리 HTML문서에 동적으로 변경해야 하는 부분만 자바 코드를 넣으면 더 편하겠지? -> 그것이 템플릿 엔진이 나온 이유이다.
  • 템플릿엔진을 사용하여 HTML 문서에서 필요한곳만 코드를 적용하여 동적으로 변경할 수 있다.
  • 템플릿엔진 종류: JSP, Thymeleaf, Freemarker, Velocity 등

*JSP 참고

  • JSP는 성능과 기능 면에서 점점 안쓰는 추세이다. 요즘은 스프링과 잘 통합되는 Thymeleaf를 쓴다!!

📌 JSP로 회원 관리 웹 애플리케이션 만들기

  • JSP 사용하려면?
    -> build.gradle에 다음코드를 추가해야한다. 추가하고, 코끼리 눌러서 새로고침 확인해야함.
//JSP 추가 시작
implementation 'org.apache.tomcat.embed:tomcat-embed-jasper' 
implementation 'javax.servlet:jstl'
//JSP 추가 끝
  • 회원 등록 폼 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>

실행 url : http://localhost:8080/jsp/members/new-form.jsp
실행시 .jsp 까지 함께 적어야 한다.

  • 회원 저장 JSP
    • main/webapp/jsp/members/save.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="hello.servlet.domain.member.Member" %> //자바의 import문과 같다.
<%@ page import="hello.servlet.domain.member.MemberRepository" %>
<% //이 부분에는 자바코드를 입력할 수 있다.
    //request, response 그냥 사용 가능 (jsp에서 문법상 지원이 된다)
    MemberRepository memberRepository = MemberRepository.getInstance();

    System.out.println("MemberSaveServlet.service");
    String username = request.getParameter("username");
    int age = Integer.parseInt(request.getParameter("age")); //request.getParameter한 값은 문자타입이므로 int형으로 변환

    Member member = new Member(username, age);
    memberRepository.save(member);

%>
<html>
<head>
    <title>Title</title>
</head>
<body>
성공
<ul>
    <li>id=<%=member.getId()%></li>
    <li>username=<%=member.getUsername()%></li>
    <li>age=<%=member.getAge()%></>
</ul>
<a href="/index.html">메인</a>
</body>
</html>

<% ~~%>:이 부분에서는 자바코드를 입력할 수 있다.
<%= ~~ %>: 이 부분에서는 자바코드를 출력할 수 있다.

  • 회원목록 JSP
    • main/webapp/jsp/member.jsp
<%@ page import="hello.servlet.domain.member.MemberRepository" %>
<%@ page import="hello.servlet.domain.member.Member" %>
<%@ page import="java.util.List" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
    MemberRepository memberRepository = MemberRepository.getInstance();
    List<Member> members = memberRepository.findAll(); //회원 레포지토리 먼저 조회
%> 

<html>
<head>
    <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){ 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작업이 깔끔하고 중간중간 동적 변경이 필요한 부분만 자바코드를 적용할 수 있어서 조금더 편리하였다.
그러나, JSP에서 회원을 저장하는 비즈니스로직, 결과를 보여주는 html등 너무 많은 역할을 한다. -> 요구사항이 복잡해지면 지옥같은 코드가 될것이다.

MVC패턴의 등장

  • 목적: 비즈니스 로직은 서블릿처럼 다른곳에서 처리하고, JSP는 목적에 맞게 HTML로 화면(View)을 그리는 일에 집중하고자 한다.

📌 MVC 패턴 - 개요

지금까지 본 서블릿,JSP의 문제점

  • 너무 많은 역할
    • 하나의 서블릿이나 JSP만으로 비즈니스 로직과 뷰 렌더링까지 모두 처리하게되면, 너무 많은 역할을 하게 되는것이다. 유지보수가 어려워진다
  • 변경의 라이프 사이클이 다름
    • 비즈니스 로직과 뷰 렌더링의 변경 라이프 사이클이 다른데, 이를 하나의 코드로 관리하는것은 유지보수하기 좋지 않다.
    • 예를들어, UI를 일부 수정하는것과 비즈니스 로직을 수정하는 일은 다르게 발생할 확률이 높고, 서로에게 영향을 주지않는다.
  • 기능 특화
    • 특히, JSP같은 뷰 템플릿은 화면을 렌덜이 하는데에 최적화 되어있으므로, 이 업무만 담당하는것이 효과적이다.

🌵 Model View Controller 이란?

  • 서블릿이나 JSP로 처리하던것을 Controller와 View 영역으로 역할을 나눈것이다.
  • 웹 애플리케이션은 보통 MVC패턴을 사용한다

  • 컨트롤러
    • HTTP 요청을 받아서 파라키터를 검증하고, 비즈니스 로직을 실행한다. 그리고 뷰에 전달할 데이터를 조회해서 모델에 담는다
  • 모델
    • 뷰에 출력할 데이터를 담아둔다
    • 뷰가 필요한 데이터를 모두 모델에 담아서 전달해주는 덕분에 뷰는 비즈니스 로직이나 데이터 접근을 몰라도 되고, 화면을 렌더링 하는 일에 집중할 수 있다.
    • 모델에 담겨있는 데이터를 사용해서 화면을 그리는 일에 집중한다.
    • HTML을 생성하는 부분을 말한다.

*참고

  • 컨트롤러에 비즈니스 로직을 둘 수도 있지만, 이것은 컨트롤러가 너무 많은 역할을 담당하게 된다.
  • 일반적으로 비즈니스 로직은 서비스(Service) 계층을 변도로 만들어서 처리한다
  • 그리고 컨트롤러는 비즈니스 로직이 있는 서비스를 호출하는 역할을 한다.
  • 비즈니스 로직을 변경하면, 비즈니스 로직을 호출하는 컨트롤러의 코드 도 변경될 수 있음을 알고있자!!

📌 MVC패턴 - 적용

  • 서블릿을 컨트롤러로, JSP를 뷰로 사용해서 MVC 패턴을 적용해보자
  • Model은 HttpServletRequest 객체를 사용한다
    • request는 내부에 데이터 저장소를 가지고 있는데 request.setAttribute(), request.getAttribute()를사용하면 데이터를 보관하고, 조회할 수 있다.

회원 등록 폼 - 컨트롤러

package hello.servlet.web.servletmvc;

@WebServlet(name="mvcMemverFormServlet", 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 호출
    }
}
  • dispatcher.forward(): 다른 서블릿이나 JSP로 이동할 수 있는 기능. 서버 내부에서 다시 호출이 발생한다.
  • WEB-INF
    • 이 경로안에 JSP가 있으면 외부에서 직접 JSP를 호출할 수 없다. 컨트롤러를 통해서 JSP를 호출하기 위해 사용한 폴더.(관례적이다)
  • Redirect vs Forward
    • 리다이렉트: 실제 클라이언트(웹 브라우저)에 응답이 나갔다가, 클라이언트가 redirect경로로 다시 요청하므로, 클라이언트가 인지할 수 있고 URL 경로도 실제로 변경된다
    • 포워드: 서버 내부에서 일어나는 호출이므로 클라이언트가 전혀 알지 못한다.

회원 등록 폼 - 뷰

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Title</title>
</head>
<body>

<%--코드 재활용하기 위해 상대 경로 사용, [현재 URL이 속한 계층 경로 + /save] --%>
<%--이렇게 /(슬래쉬) 없이 쓰면 알아서 [현재 경로 + /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>

회원 저장 - 컨트롤러

package hello.servlet.web.servletmvc;

@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);
        memberRepository.save(member);
        //여기까진 이전 서블릿코드와 똑같음

        //Model에 데이터를 보관한다
        request.setAttribute("member",member); //requset객체의 내부 저장소에 저장함. (map같은것들이 있음)

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


    }
}
  • HttpServletRequest를 Model로 사용한다
    • request가 제공하는 setAttribute()를 사용하면 request객체에 데이터를 보관해서 뷰에 전달할 수 있다.
    • 뷰는 request.getAttribute()를 사용해서 데이터를 꺼내면 된다.

회원저장 - 뷰

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Title</title>
</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에 담긴 데이터를 편하게 조회할 수 있다.
  • MVC 덕분에 컨트롤러 로직과 뷰 로직을 확실하게 분리한것을 확인할 수 있다. 향후, 화면 수정이 발생하면 뷰 로직만 변경하면 된다.

회원 목록 조회 - 컨트롤러

package hello.servlet.web.servletmvc;

@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 {
        List<Member> members = memberRepository.findAll();

        request.setAttribute("members",members); //request 객체를 사용하여 List<Members> members를 모델에 보관함.

        String viewPath = "/WEB-INF/views/members.jsp";
        RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
        dispatcher.forward(request,response);

    }
}

회원 목록 조회 - 뷰

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

모델에 담아둔 members를 JSP가 제공하는 taglib기능을 사용하여 반복하면서 출력하였다.
<c:forEach> 이 기능을 사용하려면 다음과 같이 선언해야 한다.
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>

JSP는 요즘 거의 사용하지 않으므로, 이런게 있구나 하고 넘어가자!!


📌 MVC패턴 - 한계

  • MVC패턴을 적용해서 컨트롤러의 역할과 뷰를 랜더링하는 역할을 명확하게 구분 할 수 있다.
  • 뷰는 화면을 그리는 역할에 충실하여 코드가 깔끔하고 직관적이다. 단순하게 모델에서 필요한 데이터를 꺼내고, 화면을 만들면 된다
  • 그러나, 컨트롤러는 중복이 많고 필요하지 않는 코드도 포함한다.

MVC 컨트롤러 단점

  • 포워드 중복
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
  dispatcher.forward(request, response);
  • viewPath 중복
String viewPath = "/WEB-INF/views/new-form.jsp";
  • 사용하지 않는 코드
    다음 코드를 사용할 때도 있고, 사용하지 않는 코드도 있다.
    HttpServlet request, HttpServletResponse respone

  • 공통 처리가 어렵다

    • 기능이 복잡해질 수록 컨트롤러에서 공통으로 처리해야 하는 부분이 더 많아질 것이다.

해결하려면?

  • 컨트롤러 호출 전에 먼저 공통기능을 처리하자
  • 수문장 역할을 하는 기능이 필요하다 -> 프론트 컨트롤러 패턴을 도입하자 (스프링 MVC의 핵심이기도 하다)
profile
공부한 내용 잊어버리지 않게 기록하는 공간!

0개의 댓글