[Spring MVC] [1] 3. 서블릿, JSP, MVC 패턴

윤경·2021년 9월 6일
0

Spring MVC

목록 보기
4/26
post-thumbnail


드디어 간단한 웹 애플리케이션을 만들어보도록 하겠습니다.

실습이 주된 내용이기 때문에 간단한 설명은 주석으로 대신하며 코드와 결과를 위주로 기록하겠습니다.


[1] 회원 관리 웹 애플리케이션 요구사항

굉~장히 간단한 회원 관리 웹 애플리케이션 만들어보기

우리가 할 것은
코어 모듈 만들기(핵심 비즈니스 로직) → 회원 저장, 조회를 서블릿으로 만들기 → 서블릿의 불편한 점들을 개선해 JSP로 만들기 → 또 그 불편한 점을 개선하기 위해 MVC 패턴 적용하기

회원 정보에는 이름(username)과 나이(age)뿐 다른 정보는 없으며 회원 정보, 회원 목록 조회 정도만 수행 가능하도록 할 것이다.

✔️ member

package hello.servlet.domain.member;

import lombok.Getter;
import lombok.Setter;

@Setter @Getter
public class Member {

    private Long id;
    private String username;
    private int age;

    // command+n
    public Member(Long id) {    // 기본 생성자
    }

    public Member(String username, int age) {
        this.username = username;
        this.age = age;
    }
}

✔️ MemberRepository

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 {

    // static이기 때문에 MemberRepository가 아무리 많아도 딱 하나씩 생성되어 있을 거임
    // (사실 싱글톤이기 때문에 static을 붙이지 않아도 됨)
    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으로 생성자를 딱 막아줘야 함
    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() {
        // store에 있는 값들을 모조리 꺼내 리스트를 만들어 돌려줌
        // 즉, store에 있는 값들은 건들이지 않게 됨 (보호)
        return new ArrayList<>(store.values());
    }

    public void clearStore() {  // store를 다 날려버림
        store.clear();
    }
}

회원 저장소를 싱글톤 패턴을 적용.
스프링을 사용하면 스프링 빈으로 등록되지만 최대한 순수 서블릿으로 구현하기 위해 이렇게 코드를 구현함.
싱글톤 패턴은 객체를 단 하나만 생성해 공유해야 하므로 생성자를 private 접근자로 막아 보호함.

✔️ MemberRepositoryTest

package hello.servlet.domain.member;

import org.assertj.core.api.Assertions;
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 {

    // 싱글톤이기 때문에 new --- 이렇게 생성하면 안되고 이렇게 해야됨.
    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);
    }
}

clearStore()로 인해 각 테스트마다 영향을 미치지 않도록 했다. 지금은 없어도 문제가 되진 않지만 추후 많은 테스트가 생길 경우 필요해질 것이다. (테스트의 순서는 보장이 되어있지 않기 때문)


[2] 서블릿으로 회원 관리 웹 애플리케이션 만들기

이제 서블릿으로 앱을 만들어보자.

서블릿으로 회원등록 HTML 폼 만들기

✔️ MemberFormServlet

package hello.servlet.web.servlet;

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 = "memberFormServlet", urlPatterns = "/servlet/members/new-form")
public class MemberFormServlet extends HttpServlet {

    // 싱글톤이어서 new 안됨. (getInstance 써야됨)
    private MemberRepository memberRepository = MemberRepository.getInstance();

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

➡️ 제출하면 save 시킨건데 이것에 대한 form이 아직 없어서 그럼. (정상)

✔️ MemberSaveServlet

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

한 명 더 저장해보면 id=2가 되어있음.

MemberSaveServlet 동작

  1. 파라미터 조회해 Member 객체 생성
  2. Member 객체를 MemberRepository를 통해 저장
  3. Member 객체를 사용해 결과 화면용 HTML을 동적으로 만들어 응답

✔️ MemberListServlet

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 {

        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 루프를 통해 동적 생성해 응답

✔️ 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>

즉,

이렇게 서브릿과 자바 코드만으로 HTML을 만들었다.
서블릿 덕분에 동적으로 원하는 HTML을 맘껏 만들 수 있었다. 정적 HTML 문서라면 화면이 계속 달라지는 회원 저장 결과, 회원 목록.. 이런 동적인 HTML을 만들 수 없었을 것이다.

그런데, 코드에서 보듯 매우 복잡하고 비효율 적이며 오타가 쉽게 발생할 수 있다.
자바 코드로 HTML을 만들어 내는 것보다 HTML 문서에 동적으로 변경할 부분만 자바 코드를 넣는게 어떨까 ??

이런 이유로 나오게 된 것이 템플릿 엔진이다.
템플릿 엔진은 HTML 문서에서 필요한 곳만 코드를 적용해 동적으로 변경할 수 있다.

(템플릿 엔진: JSP, Thymeleaf, Freemarker, Velocity ..등)

📌

JSP는 성능과 기능면에서 다른 템플릿 엔진과 경장에서 밀림. 템플릿 엔진들은 각 장단점이 있는데 JSP는 잠깐 다루고 스프링과 잘 통합되는 Thymeleaf를 사용할 것.


[3] JSP로 회원 관리 웹 애플리케이션 만들기

서블릿으로 만드는 거랑 사실상 똑같다.

✔️ build.gradle

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

‼️ 코드 작성 후 코끼리 필수 ‼️

✔️ 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>

action="/jsp/members/save.jsp" 주의하기

<%@ page contentType="text/html;charset=UTF-8" language="java" %\> ➡️ JSP 문서라는 뜻. 즉, 이렇게 문서가 시작되어야 한다.

회원 등록 폼 JSP는 첫 줄을 제외하면 HTML과 다를 것이 없다. JSP는 서버 내부에서 서블릿으로 변환되는데 MemberFormServlet과 거의 비슷한 모습으로 변환된다.

✔️ save.jsp

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="hello.servlet.domain.member.MemberRepository" %>
<%@ page import="hello.servlet.domain.member.Member" %>
<%
    // 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>
    <title>Title</title>
</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="hello.servlet.domain.member.MemberRepository" %\> ➡️ 자바의 for문

<% ~~ %> ➡️ 이 안에 자바 코드 입력 가능

<%= ~~ %> ➡️ 이 안에 있는 자바 코드는 출력 됨

회원 저장 JSP는 회원 저장 서블릿 코드와 같다.
단, HTML을 중심으로 하며 자바 코드를 부분부분 입력해준 것만 다르다. (<% ~~ %>로 HTML 중간에 자바 코드를 출력한다.)

✔️ member.jsp

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="java.util.List" %>
<%@ page import="hello.servlet.domain.member.MemberRepository" %>
<%@ page import="hello.servlet.domain.member.Member" %>
<%
    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>

회원 레퍼지토리를 먼저 조회, 결과 List를 사용해 중간 <tr><td> (HTML태그)를 반복해 출력

⬆️ 두 명의 멤버를 회원가입 시킨 후 결과

📌 서블릿, JSP의 한계

서블릿 으로 개발 시 View 화면을 위한 HTML을 만드는 작업이 자바 코드에 섞여 지저분하고 복잡했다.
그런데 JSP 덕분에 뷰를 생성하는 HTML 작업을 깔끔해지고, 중간중간 동적으로 변경이 필요한 부분만 자바 코드를 적용할 수 있었다.
하지만 아직 완벽하다고 할 수 없다.

회원 저장 JSP를 보면 코드 상위 절반은 회원을 저장하기 위한 비즈니스 로직, 나머지 하위 절반만 결과를 HTML로 보여주기 위한 뷰 영역이었다. (회원 목록도 마찬가지)

JAVA코드, 데이터를 조회하는 레퍼지토리 등 다양한 코드가 모두 너무 JSP에 노출되어 있다. 즉, JSP가 너무 많은 역할을 떠맡고 있다.

그래서 등장한 MVC 패턴

비즈니스 로직은 서블릿처럼 다른 곳에서 처리, JSP는 목적에 맡게 HTML로 view를 그리는 일에 집중하도록 하자.


[4] MVC 패턴 - 개요

너무 많은 역할

하나의 서블릿이나 JSP 만으로 비즈니스 로직을 처리하는 부분, 화면을 처리하는 뷰 렌더링을 처리하게 되면, 너무 많~은 역할을 맡게 되고 유지보수가 어려워 진다.

비즈니스 로직을 호출하는 부분에 수정 사항이 발생해도 해당 코드 를 고치고, UI를 수정할 일이 생겨도 해당 코드 를 고친다.
이는 코드가 길어질수록 아주 불편한 일이다.

⭐️ 변경의 라이프 사이클

정말 중요한 문제는 변경 라이프 사이클이 다르다는 것이다.

예를 들어, UI 일부를 수정하는 일, 비즈니스 로직을 수정하는 일. 이는 거의 각각 다르게 발생하며 서로에게 영향을 주지 않는다.

변경 라이프 사이클이 다른 부분을 하나의 코드로 관리하는 것은 유지보수 관점에서 나쁘다.

기능 특화

특히 JSP 같은 뷰 템플릿은 화면을 렌더링하는데 최적화 되어 본인의 역할만 담당하는 것이 가장 효과적이다.

MVC

: Model View Controller

하나의 서블릿이나 JSP로 처리하던 것을 컨트롤러와 뷰 영역으로 서로 역할을 나눈 것이다.
웹 애플리케이션은 보통 MVC 패턴을 사용한다.

Controller

: HTTP 요청을 받아 파라미터로 검증, 비즈니스 로직(ex. member.save())을 수행한다. 그리고 뷰에 전달할 결과 데이터를 조회해 모델에 담는다.

Model

: "이 데이터 어디서 찾아야 되지? 이런 고민을 더 이상 할 필요가 없다. 왜냐? 모델에 가면 다 있다 !!

뷰이 출력할 데이터를 담는다.
뷰가 필요한 데이터를 모두 모델에 담에 전달해준 덕분에 뷰는 비즈니스 로직이나 데이터 접근을 몰라도 되며, 화면을 렌더링하는 일에만 집중할 수 있다.

View

: 모델에 담긴 데이터를 사용해 화면을 그리는 일에 집중한다.
여기서는 거의 대부분 HTML을 생성하는 부분을 의미한다.

📌

컨트롤러에 비즈니스 로직을 둘 수도 있지만(사실 좋은 방법은 X, 호출까지만 하는 것이 좋음), 이렇게되면 컨트롤러가 너무 많은 역할을 담당한다.
그래서 일반적으로 비즈니스 로직은 Service라는 계층을 별도로 만들어 처리한다. 그리고 컨트롤러는 비즈니스 로직이 있는 서비스를 호출한다.

참고로 비즈니스 로직을 변경하면 비즈니스 로직을 호출하는 컨트롤러의 코드도 변경될 수 있다.


[5] MVC 패턴 - 적용

서블릿을 컨트롤러로 사용, JSP를 뷰로 사용

아, 모델은 HttpServletRequest객체를 그냥 모델로 사용한다.

request는 내부에 데이터 저장소를 가지는데 setAttribute(), getAttribute()를 사용해 데이터를 보관, 조회 가능하다.

✔️ MvcMemberFormServlet

package hello.servlet.web.servletmvc;

import javax.servlet.RequestDispatcher;
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;

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

📌 /WEB-INF

이 경로 안에 있는 JSP는 외부에서 직접 JSP를 호출할 수 없다.
항상 !! 컨트롤러를 통해 JSP를 호출해야 한다.

redirect 🆚 forward

리다이렉트는 실제 클라이언트(웹 브라우저)에 응답이 나갔다가, 클라이언트가 리다이렉트 경로로 다시 요청해 클라이언트가 인지하며 URL 경로도 변경된다.

반면, 포워드는 서버 내부에서 일어나는 호출이기 때문에 클라이언트가 인지하지 못한다.

✔️ 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>

form의 action은 절대 경로가 아니라 (/로 시작하지 않는) 상대 경로인 것을 볼 수 있다.

이렇게 상대 경로를 사용하면 폼 전송 시 현재 URL이 속한 계층 경로 + save가 호출된다.

(사실, 여기선 별로 얻는게 없지만 앞으로 다른 컨트롤러에서도 계속 사용하기 위해 상대 경로로 설정)

✔️ MvcMemberSaveServlet

package hello.servlet.web.servletmvc;

import hello.servlet.domain.member.Member;
import hello.servlet.domain.member.MemberRepository;

import javax.servlet.RequestDispatcher;
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;

@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); // request 객체 내 저장소에 저장

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

✔️ 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>

JSP는 ${ 문법을 제공하는데, 이 문법을 사용하면 request의 attribute에 담긴 데이터를 편리하게 조회할 수 있다.

MVC 덕분에 컨트롤러 로직과 뷰 로직을 확실하게 분리할 수 있었다.
향후 화면 수정이 필요하면 뷰 로직만 변경하면 된다.

✔️ MvcMemberListServlet

package hello.servlet.web.servletmvc;

import hello.servlet.domain.member.Member;
import hello.servlet.domain.member.MemberRepository;

import javax.servlet.RequestDispatcher;
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.util.List;

@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);   // "members": key, members: value

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

request 객체를 사용해서 List<Member> members 를 모델에 보관.

여기서 느껴야할 것: 아니, 왜 자꾸 반복하고 있지??

member.jsp

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<html>
<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}">   // collectio
        <tr>
            <td>${item.id}</td>
            <td>${item.username}</td>
            <td>${item.age}</td>
        </tr>
    </c:forEach>
    </tbody>

</table>

</body>
</html>

모델에 담아둔 members를 JSP가 제공하는 taglib기능을 사용해서 반복하면서 출력했다. members 리스트에서 member를 순서대로 꺼내서 item 변수에 담고, 출력하는 과정을 반복한다.

<c:forEach> 이 기능을 사용하려면 다음과 같이 선언해야 한다.
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%\>


[6] MVC 패턴 - 한계

위에서도 구현하며 로직이 자꾸 반복된다는 것을 느꼈다.

MVC 패턴을 적용해 컨트롤러의 역할과 뷰를 렌더링하는 역할을 명확히 구분했다.

특히 뷰가 화면을 그리는 역할에 충실하게 돼 코드를 직관적으로 만들 수 있었다.

그런데 컨트롤러는 중복이 많고 필요하지 않은 코드도 많아보인다.

📌 MVC 컨트롤러 단점

  • 포워드 중복
    : View로 이동하는 코드가 항상 중복 호출된다.

메소드로 공통화한다고 해도 공통화 한 메소드를 또 호출해야 하기 때문에 중복된다. ("호출이라도 중복 되는 것이 싫다!!")

RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);
  • ViewPath 중복
String viewPath = "/WEB-INF/views/new-form.jsp";

앞에는 항상 /WEB-INF/views/, 뒤에는 항상 .jsp가 붙는다.

  • 사용하지 않는 코드
HttpServletRequest request, HttpServletResponse response

이 코드는 사용하지 않을 때도 있다. 특히 response는 사용되지 않는다.

그리고 이런 HttpServletRequest, HttpServletResponse 를 사용하는 코드는 테스트 케이스를 작성하기도 어렵다.

  • 공통 처리가 어려움
    기능이 복잡해질수록 컨트롤러에서 공통으로 처리할 부분이 점점 많아진다.
    단순히 공통 기능 메소드로 뽑는다고 해도 공통처리한 메소드를 또 호출해야하고, 혹여 실수로 호출하지 않는다면 문제가 발생한다.

즉, 가장 큰 문제는 공통 처리가 어려움

우리는 수문장 역할을 하는 프론트 컨트롤러(Front Controller) 패턴이 필요하다.

지금은 아무데서나 다 들어오지만 수문장을 만들면 그 객체를 통해 컨트롤러를 호출하도록 바꾸면 된다. (대표 컨트롤러 딱 하나 만들기)

이것이 바로 스프링 MVC의 핵심 !!


profile
개발 바보 이사 중

0개의 댓글