[스프링 MVC 1편] - 서블릿, JSP, MVC 패턴

Chooooo·2023년 1월 2일
1

스프링 MVC 1편

목록 보기
3/11
post-thumbnail

이 글은 강의 : 김영한님의 - "[스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술]"을 듣고 정리한 내용입니다. 😁😁


이번 시간에는 서블릿, JSP, MVC 패턴에 대해서 공부할 것이다.

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

🎈 간단한 회원관리 웹 애플리케이션을 만들면서 학습을 해보자.
회원은 이름(username), 나이(asge)정도의 정보를 가진 도메인 객체를 만들고 회원 정보를 저장하고 조회하는 기능을 구현해보자.

요구사항

🎃 회원 정보(도메인)

  • 이름 : username
  • 나이 : age

🎃 기능 요구사항

  • 회원 저장
  • 회원 목록 조회

Member - 회원 도메인 모델

package hello.servlet.domain.member;

import lombok.Getter;
import lombok.Setter;

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

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 {
    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;
    }

    //Singleton 에서 생성자를 호출하지 못하도록 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() {
        return new ArrayList<>(store.values());
    }

    public void clearStore() {
        store.clear();
    }
}
  • 싱글톤 패턴을 적용해서 서블릿 컨테이너에서 하나의 저장소만 동작한다.
  • 싱글톤은 기본 생성자의 접근제어자를 private로 하여 추가 생성을 막아준다.
  • sequence도 클래스 변수로 선언해 서블릿 컨테이너 내에서 하나만 관리되도록 한다.
  • 저장소는 HashMap을 사용하여 sequence를 key로 하여 회원 도메인을 value로 사용한다.

테스트 해보기

참고 : 도메인 테스트 케이스를 작성하고자 할 때 CTRL + SHIFT + T를 통해 단축키로 빠르게 테스트 클래스를 생성할 수 있다.

MemberRepositoryTest

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.assertThat;

class MemberRepositoryTest {
    private MemberRepository memberRepository = MemberRepository.getInstance();

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

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

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

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

    @Test
    void findById() {
        //given
        Member savedMember = memberRepository.save(new Member("catsbi", 20));

        //when
        Member foundMember = memberRepository.findById(savedMember.getId());

        //then
        assertThat(foundMember).isEqualTo(savedMember);
    }

    @Test
    void findAll() {
        //given
        Member savedMember1 = memberRepository.save(new Member("catsbi", 20));
        Member savedMember2 = memberRepository.save(new Member("crong", 30));

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

        //then
        assertThat(members).hasSize(2);
        assertThat(members).contains(savedMember1, savedMember2);
    }
}
  • 회원을 저장하고 목록을 조회하는 테스트를 작성했다.
  • 매번 테스트 종료시 저장소를 초기화해서 다른 테스트 케이스에 영향을 주지 않도록 @AfterEach를 만들어줬다.

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

먼저 서블릿으로 만들어보자.

MemberFormServlet - 회원 가입 폼

🎈 회원을 새로 등록하고자 할 때 이름과 나이를 받는 HTML Form 양식을 반환해 주는 서블릿이다.

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 {

    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");
    }
}
  • 회원 가입을 위한 폼을 동적으로 생성하여 반환하는 서블릿이다.
  • 폼 양식을 살펴보면 /servlet/members/save 경로로 POST HTML Form방식으로 전달한다.

MemberSaveServlet - 회원 저장

POST Form 방식으로 전달된 회원 정보를 받아서 회원 객체(Member)를 만들어 저장소에 저장 후 응답 페이지(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;

@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 {
        String username = request.getParameter("username");
        int age = Integer.parseInt(request.getParameter("age"));

        Member member = new Member(username, age);
        Member savedMember = 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>");
    }
}
  • HTML POST Form 방식이니 getParameter로 전송 받은 Form의 파라미터를 꺼낼 수 있다.
  • username, age를 getParameter로 꺼낸다.
  • 저장소에 회원을 저장한 뒤 저장 결과를 HTML 타입으로 응답 메세지로 작성해 반환해준다.
  • 서블릿에서 응답 HTML을 작성하다 보니 동적으로 자바 로직을 추가해서 동적으로 HTML을 생성할 수도 있다.

MemberListServlet - 회원 목록

🎈 저장되어 있는 회원 목록을 모두 보여주는 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 {
        List<Member> members = memberRepository.findAll();

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

        PrintWriter w = response.getWriter();
        w.write("<!DOCTYPE html>");
        w.write("<html lang=\"en\">");
        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("<ul>");
        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("        ");
        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("</ul>");
        w.write("</body>");
        w.write("</html>");
    }
}
  • memberRepository.finAll()를 통해 조회한 모든 회원 정보를 토대로 iterator 순회를 통해 HTML 태그를 작성한다.

🧨 실행

http://localhost:8080/servlet/members

  • 저장된 회원 목록을 확인할 수 있다.

템플릿 엔진으로 - 불편한 HTML 응답 메세지 작성

지금 간단한 예제를 진행하면서도 HTML 코드를 서블릿과 자바 코드만으로 작성을 해줬다.
자바코드 내에서 작성되기 때문에 자바 문법을 통한 동적 생성도 가능하다.
하지만, 이 실습을 실제로 복사 붙혀넣기가 아니라 작성해보면서 따라왔다면 엄청나게 불편하고 오타가나서 에러가 발생할 확률이 높다는 점을 알 수 있다.
그래서 자바코드에서 HTML 코드를 작성하는 것이 아닌 HTML코드에서 자바를 사용하는게 훨씬 편리할 수 있다.
그래서 나온게 템플릿 엔진 이다. 이 템플릿 엔진을 사용하면 HTML 필요한 곳만 코드를 적용해 동적으로 변경이 가능하다.
대표적으로 JSP, Thymeleaf, Freemarker, Velocity 와 같은 템플릿 엔진이 있는데, 우선 JSP으로 지금까지 한 로직을 똑같이 구현해볼 것이다.

참고 : Welcome 페이지 변경

지금까지 진행했고, 앞으로 진행할 예제들을 빠르게 찾아갈 수 있게 모든 경로를 작성한 해당 페이지로 변경하자.

🎈 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로 회원 관리 웹 애플리케이션 만들기

위에서 서블릿과 자바만 가지고 만들었던 회원 관리 웹 애플리케이션에서 HTML 코드를 자바로 작성하기 때문에 생긴 불편함을 해결하기 위해 템플릿 엔진을 사용한다고 했다. 이번에는 템플릿 엔진 중에서 JSP를 사용해서 지금까지 구현한 회원 관리 웹 애플리케이션을 구현해보자.

JSP 라이브러리 추가

JSP를 사용하기 위해선 우선 build.gradle에 라이브러리를 추가해야 한다.

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

  • 라이브러리를 추가하면 다음 버튼을 클릭해서 Gradle을 refresh해주자.

회원등록 폼 JSP

생성 경로 : 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>

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
JSP 사용시 첫 줄에 추가해야하는 코드로 이 문서가 JSP라는 의미다.

🎃 실행

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

회원 저장 JSP

생성 경로 : main/webapp/jsp/members/save.jsp

<%@ page import="hello.servlet.domain.member.Member" %>
<%@ page import="hello.servlet.domain.member.MemberRepository" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
    //request, response 사용 가능
    MemberRepository memberRepository = MemberRepository.getInstance();

    String username = request.getParameter("username");
    int age = Integer.parseInt(request.getParameter("age"));

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

    response.setContentType("text/html");
    response.setCharacterEncoding("utf-8");
%>
<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 ...%> : 자바의 import 문
  • <% ~~ %> : 자바 코드를 입력하는 블럭
  • <%= ~~ %> : 자바 코드 출력 블럭

회원 목록 JSP

생성 경로 : main/webapp/jsp/members.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>
<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 코드를 작성할 때보다는 확실히 오류 발생 확률이나 작성 난이도가 낮아졌다.
하지만 다음과 같은 문제를 직면하게 된다.

  • 화면을 보여주는 HTML 코드와 자바 코드가 한 소스에 섞여있어 가독성이 떨어진다.
  • 데이터 조회(MemberRepository), Java 로직, Request, Response 조작 등 각 계층의 모든 코드가 JSP에 노출되어 있다.

MVC 패턴

드디어 MVC에 대해서 공부해볼 것이다.
도메인 영역, 뷰 영역, 컨트롤러 영역 등 각각의 영역에서 자신의 역할만 담당하도록 분리하는 방법

🎈 비즈니스 로직은 서블릿 처럼 다른곳에서 처리하고, JSP는 목적에 맞게 HTML로 화면(View)을 그리는 일에 집중하도록 하자. 과거 개발자들도 모두 비슷한 고민이 있었고, 그래서 MVC 패턴이 등장했다. 우리도 직접 MVC 패턴을 적용해서 프로젝트를 리팩터링 해보자

MVC 패턴의 등장 배경

이미 얘기했지만 하나의 서블릿이나 JSP만으로 비즈니스 로직과 뷰 렌더링까지 모두 처리하게 되면 하나의 영역에서 너무 많은 역할을 부담지게 된다. 이는 필연적으로 유지보수가 어려워지는 결과를 낳는다.
비즈니스 로직에 문제가 생겨도, UI를 변경해야해도 두 로직이 같이 있는 파일을 수정해야 하는데, 매번 수정할때마다 소스코드가 수백 수천줄이 있는데 Java코드를 수정해야하는데 그 사이에 HTML 코드가 수천줄이 있거나 반대의 경우가 있다고 하면 수정해야 할 코드를 찾는것만해도 비용소모가 커질 것이다.

변경의 라이프 사이클

사실 이게 정말 중요한데, 진짜 문제는 둘 사이에 변경의 라이프 사이클이 다르다는 점이다. 예를 들어서 UI 를 일부 수정하는 일과 비즈니스 로직을 수정하는 일은 각각 다르게 발생할 가능성이 매우 높고 대부분 서로에게 영향을 주지 않는다. 이렇게 변경의 라이프 사이클이 다른 부분을 하나의 코드로 관리하는 것은 유지보수하기 좋지 않다. (물론 UI가 많이 변하면 함께 변경될 가능성도 있다.)

기능 특화

🎈 특히 JSP 같은 뷰 템플릿은 화면을 렌더링 하는데 최적화 되어 있기 때문에 이 부분의 업무만 담당하는 것이 가장 효과적이다.

Model View Controller

MVC 패턴은 지금까지 학습한 서블릿, JSP 영역에서 처리되던 것을 컨트롤러(Controller)와 뷰(View)라는 영역으로 서로 영역을 나눈 것을 말한다. 웹 애플리케이션은 보통 이 MVC 패턴을 사용한다.

컨트롤러

🎃 HTTP 요청을 받아 파라미터를 검증하고 비즈니스 로직을 실행한다. 그리고 뷰에 전달할 결과 데이터를 조회에 모델에 담는다.

모델

🎃 뷰에 출력할 데이터를 담아둔다. 뷰가 필요한 데이터를 모두 모델에 담아 전달해주기에 뷰는 비즈니스 로직이나 데이터 접근에 대해 알 필요가 없고 화면 렌더링에만 집중하면 된다.

🎃 모델에 담겨있는 데이터를 사용해 화면을 그리는 일에 집중한다. 여기서는 HTML을 생성하는 부분을 말한다.

참고

  • MVC만 보면 비즈니스 로직을 컨트롤러에 두는 것 같지만 이렇게 되면 이번엔 컨트롤러에서 너무 많은 책임이 부가된다. 그렇기에 일반적으로 비즈니스 로직은 서비스(service)라는 계층을 별도로 만들어서 처리한다. 그리고 컨트롤러는 비즈니스 로직이 있는 서비스를 호출하는 역할을 담당한다. 참고로 비즈니스 로직을 변경하면 비즈니스 로직을 호출하는 컨트롤러의 코드도 변경될 수 있다. 앞에서는 이해를 돕기 위해 비즈니스 로직을 호출한다는 표현 보다는, 비즈니스 로직이라 설명했다

지금까지 이러던게

  • MVC 패턴1

  • MVC 패턴2

MVC패턴 - 적용

MVC 패턴에 대해서 개념을 알아봤다. 하나의 영역(이전 시간에는 JSP)에 여러 영역의 코드들을 모아놨었는데 이를 MVC 패턴을 사용해서 분리를 해 볼 것이다.
우선, 서블릿은 컨트롤러로 사용하고, JSP는 뷰만 담당하도록 MVC패턴을 적용해보자.
그리고 Model은 HttpServletRequest에서 attribute를 이용해 데이터를 보관 및 조회할 것이다.

회원 등록 폼 - 컨트롤러 : MvcMemverFormServlet

회원 등록 페이지 서블릿 구현

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);
    }
}
  • dispatcher.forward()
    다른 서블릿이나 JSP로 이동할 수 있는 메서드, 서버 내부에서 다시 호출이 발생한다.

참고 : /WEB-INF

이 경로 안에 JSP가 있으면 외부(웹 브라우저...)에서 직접 JSP를 호출할 수 없다. 즉 위에서 처럼 경로를 직접 지정하며 JSP를 호출해도 호출할 수 없다는 의미. 이 경로 내에 있는 VIEW 파일은 컨트롤러를 통해서만 호출 가능하다.

참고 : redirect vs forward

리다이렉트는 실제 클라이언트(웹)에 응답이 나갔다가, 클라이언트가 redirect경로로 다시 요청한다. 그렇기에 클라이언트가 인지할 수 있고, URL 경로도 실제로 변경된다.
반면, forward의 경우 서버 내부에서 일어나는 호출이기 때문에 클라이언트는 인지할 수 없다.

회원 등록 폼 - 뷰 : new-form.jsp

회원 등록 JSP 페이지 구현
생성 경로 : main/webapp/WEB-INF/views/new-form.jsp

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

🎊 action = "save"
폼에서 이렇게 써놨는데, 지금까지 학습했을 때 경로 앞에 / 슬래쉬를 붙인 것과 다르게 슬래쉬를 쓰지 않았는데, 이것은 절대경로가 아닌 상대경로로 시작한다는 것을 의미한다.
이렇게 상대 경로로 폼을 전송하면 현재 URL이 속한 계층 경로 + 작성한 상대경로로 호출된다.

현재 계층 경로 : /servlet-mvc/members/
결과 : /servlet-mvc/members/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);
        Member savedMember = 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);
    }
}

🎈 request.setAttribute("member", member)
⇒ HttpServletRequest를 Model로 사용한다.
⇒ request가 제공하는 setAttribute() 메서드를 사용해 request객체에 데이터를 보관해서 뷰에 전달할 수 있다.
⇒ 뷰에서는 request.getAttribute()를 사용해 저장한 데이터를 꺼내 조회할 수 있다.

회원 저장 - 뷰 : save-result.jsp

생성 경로 : main/webapp/WEB-INF/views/save-result.jsp

<%@ page import="hello.servlet.domain.member.Member" %>
<%@ 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>

🎃 ${member.id}
⇒ 원래 <%=request.getAttribute("member")%>를 사용해서 모델에 저장한 member 객체를 꺼낼 수있지만 이를 실제 사용하려면 형변환을 한 뒤 그 객체에서 필요한정보를 getter를 통해 꺼내는 작업을 해야해서 많이 번거로워진다.
⇒ 그래서 JSP는 ${} 문법을 제공하는데, 이를 사용하면 request의 attribute에 담긴 데이터를 프로퍼티조회할 수 있다. (member.getId() - X)(member.id - O)

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 {
        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);
    }
}
  • Model(request)에 attribute에 조회한 멤버 리스트 컬렉션을 저장한다.

회원 목록 조회 - 뷰 : members.jsp

생성 경로 : 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>
    <title>Title</title>
</head>
<body>
<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 패턴을 사용하기 전의 JSP에서는 스크립트릿을 이용해서 자바 코드를 불러서 향상된 for문을 사용해서 동적으로 테이블 태그를 생성했다. 하지만 그 코드가 지저분해지고, HTML 영역에 자바 코드가 뒤섞이는 문제가 있었다.
  • 이를 해결하기 위해 JSP가 제공하는 기능을 사용해 반복하면서 출력하도록 구현.

🧨 참고
앞서 설명했듯이 JSP를 학습하는 것이 이 강의의 주 목적이 아니다.

MVC 패턴 - 한계

MVC패턴을 적용한 덕분에 컨트롤러의 역할과 뷰를 렌더링하는 역할을 명확하게 구분할 수 있었다.
이처럼 MVC패턴을 사용함으로써 JSP에 부과되는 수많은 영역들의 코드들을 분리할 수 있었기에, 컨트롤러, 모델, 뷰가 명확하게 구분되었다. 그럼으로써 JSP에는 화면을 그리는 코드를 제외하고는 다 걷어낼 수 있었다. 하지만 이런 MVC 패턴을 적용함에도 문제는 남아있다.

MVC 컨트롤러의 단점

forward 중복

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

🎈 View로 이동하는 코드가 항상 중복 호출되고 있다. 그럼 이 코드를 모듈화해서 공통화하면 해결될까 생각하지만, 그 모듈화된 메서드도 항상 직접 호출을 해야하면서 중복이 발생한다.

ViewPath 중복

String viewPath = "/WEB-INF/views/new-form.jsp";
회원 관리 웹 애플리케이션을 MVC 패턴을 적용해서 구현하면서 작성한 ViewPath만 최소 3번인데, 중복되는 부분을 봤을 것이다. 내가 호출하고자 하는 jsp파일이 위치한 상위 디렉토리 경로와 .jsp라는 접미사다.

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

그리고 만약 jsp가 아닌 thymeleaf 같은 다른 뷰로 변경한다면 전체 코드를 다 변경해야 한다.

사용하지 않는 코드

모든 서블릿에서 공통적으로 받는 매개변수와 forward를 통해 전달하는 인자값으로 request, response가 있다. 근데 이 중에서 response는 이번 예제에서는 사용하지 않는 경우가 많았다.
HttpServletRequest request, HttpServletResponse response

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

공통 처리의 어려움

기능이 복잡할수록 컨트롤러에서 공통으로 처리해야하는 부분이 증가할 것인데, 이를 모듈화 하는 비용도 있고, 이 모듈을 항상 호출하는데 신경써야하며 호출을 하지 않았을때 생기는 문제도 있다.

종합하자면..

여러 문제들이 있지만, 결국 이 문제들 또한 종합하자면 공통 처리에 대해서 어려움이 있다는 부분이다. ViewPath부터 request, response객체 forward코드까지 중복되는게 많다.
그래서 이러한 문제를 해결하기 위해서는 컨트롤러가 호출되기 전에 공통적으로 처리될 수 있는 부분은 처리되어야 한다. 그리고 이 문제의 해결책은 프론트 컨트롤러(Front Controller) 패턴을 사용해서 해결할 수 있는데, 이 스프링 MVC의 핵심이 프론트 컨트롤러에 있다.

profile
back-end, 지속 성장 가능한 개발자를 향하여

0개의 댓글