Lined Notebook

[스프링 MVC - 백엔드 웹 개발 기술] 09. 서블릿, JSP, MVC 패턴

by ymkim

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

이번 시간에는 아주 간단한 회원 관리 웹 애플리케이션을 구축하는 시간을 갖는다

  1. Core 도메인 모듈을 생성 한다
  2. 서블릿을 통해 구현
  3. JSP를 통해 구현
  4. MVC 패턴으로 구현

01-1. 요구사항

  • 회원 정보
    • seq를 위해 id 필드 생성
    • 이름username 필드 생성
    • 나이age 필드 생성

01-2. 도메인 객체 생성 - Member

package hello.servlet.domain.member;

import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class Member {

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

    public Member() {
    }

    public Member(String username, int age) {
        this.username = username;
        this.age = age;
    }
}
  • username과 age를 갖는 필드 생성
  • 여기서의 Member 클래스는 VO의 개념으로 봐도 좋다
  • VO : Value Object의 약자로 값 자체를 표현하기 위해 사용
  • DTO: Data Transfer Object의 약자로 레이어 계층 간 데이터 전송을 위해 사용

01-3. 리포지토리 생성 - 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<>(); // inmemory 처럼 사용
    private static Long seq = 0L; // seq

    private static final MemberRepository instance = new MemberRepository(); // 싱글턴 객체

    public static MemberRepository getInstance() {
        return instance;
    }

    private MemberRepository() {
    }

    public Member save(Member member) {
        member.setId(++seq);
        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();
    }
}
  • 기본적인 CRUD가 가능하도록 MemberRepository를 구성
  • 여기서 봐야 하는 부분은 싱글턴 객체를 사용하는 경우에는 생성자를 private로 막아야 한다
    • private MemberRepository() { }

01-4. 테스트 코드 작성 - 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;

class MemberRepositoryTest {

    MemberRepository memberRepository = MemberRepository.getInstance();

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

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

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

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

    @Test
    public void testFindAll() {
        //given
        Member member1 = new Member("member1", 20);// name, age
        Member member2 = new Member("member2", 30);// name, age

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

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

        //then
        Assertions.assertThat(result.size()).isEqualTo(2);
    }
}
  • 회원 저장 및 전체 조회 테스트 코드를 작성 하였다
  • 여기서 @BeforeEach라는 어노테이션이 존재하는데 JUnit의 테스트 관련 어노테이션을 한번 살펴보자
  • Junit5의 기본 애너테이션은 다음과 같다
    • @Test
    • @BeforeAll
    • @BeforeEach
    • @AfterAll
    • @AfterEach
    • @Disabled

[JUnit5] 기본 테스트 어노테이션(@Test, @BeforeAll, @BeforeEach, @AfterAll, @AfterEach, @Disabled)

@Test

@Test
void test() {
    System.out.println("create1()");
}
  • 본 어노테이션 사용 시 테스트 메서드로 인식하고 테스트
  • JUnit5 기준 접근 제한자 default 상관 없음 ( JUnit4 에서는 public )

@BeforeAll

@BeforeAll
static void beforeAll() {
    System.out.println("@BeforeEach");
}
  • 해당 어노테이션은 테스트 클래스가 초기화 될 때 딱 한번만 수행되는 메서드이다
  • 메서드 시그니처는 static 붙혀야 함

@BeforeEach

@BeforeEach
void beforeEach() {
    System.out.println("@BeforeEach");
}
  • 테스트 메서드 실행 전에 실행
  • 테스트 메서드가 2개라면 각 메서드 호출마다 실행이 된다

@AfterAll

@AfterAll
static void afterAll() {
    System.out.println("@AfterAll");
}
  • 해당 어노테이션은 테스트 클래스 내 테스트 메서드를 모두 실행시킨 후 딱 한번만 수행되는 메서드이다
  • 메서드 시그니처는 static 붙혀야 함

@AfterEach

@AfterEach
void afterEach() {
    System.out.println("@AfterEach");
}
  • 테스트 메서드 실행 후에 실행

@Disabled

@Disabled
@Test
void create3() {
    System.out.println("create3()");
}
  • 테스트 케이스에서 해당 메서드 무시
  • 테스트를 수행하지 않는 메서드에 사용하면 된다

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

지금부터 본격적으로 회원 관리 앱을 만들어본다. 일단 서블릿으로 회원 등록 HTML 폼을 제공해본다

02-1. 회원 등록 FORM - 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 {

    private MemberRepository memberRepository = MemberRepository.getInstance(); // 싱글턴 객체를 가져온다

    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        /**
            서블릿에서 HTML 폼을 내려주려고 하면 일일이 DOM을 HTML 형식으로 만들어서 내려주어야 한다
         */

        response.setContentType("text/html"); // Contents-Type 설정
        response.setCharacterEncoding("UTF-8"); // Encoding 설정

        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");
    }
}
  • URL : /servlet/members/new-form → 기본 폼 양식 반환 서블릿 클래스
  • 특이한 점은 존재하지 않고 HTML 생성해서 화면에 반환 해준다

02-2. 회원 정보 저장 - 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);

				// 회원 정보 저장
        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>");
    }
}
  • URL : /servlet/members/save → 회원 정보 저장 서블릿 클래스
  • request.getParameter(”username”)
    • HTTP request(요청)에 담겨있는 정보는 해당 메서드를 통해 받을 수 있음
  • 파라미터 (username, age)를 저장한 후에 동적으로 HTML 생성하여 반환한다

02-3. 회원 목록 확인 - 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;

/**
 * @since           :       2023-08-23
 * @author          :       youngmin
 * @version         :       1.0.0
 * @description     :       회원 정보 리스트를 반환하는 서블릿 클래스
 * ===========================================================
 * DATE              AUTHOR             NOTE
 * -----------------------------------------------------------
 * 2023-08-23       youngmin           최초 생성
 * 2023-08-23       youngmin           순수 서블릿으로 코드 작성하는거 만만치가 않은 듯
 **/
@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>");
/*
 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>");
    }
}
  • URL : /servlet/members → 회원 정보 출력 서블릿 클래스
  • 회원 정보를 출력하여 화면에 반환 해주는 클래스 작성
  • 자바 코드로 HTML을 만들어내는 것보다 차라리 HTML 문서에 동적으로 변경해야 하는 부분만 자바 코드를 넣을 수 있다면 훨씬 편할 것이다

지금까지 순수 자바 & 서블릿을 통해 비즈니스 로직을 작성하였는데 서블릿이 기본적인 HTTP Message 파싱 Socket Connection 등에 대한 모든 부분을 처리해주는 것은 좋으나.. HTML 문서를 자바 로직에서 일일이 작성 하는 것은 정말 비효율적인 것 같다. 이러한 부분을 보완 하기 위해 나온 부분이 JSP, Thymleaf 등의 템플릿 엔진이다. 다음에는 JSP로 이러한 회원 관리 앱을 만들어보는 시간을 갖는다.

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

위에서 말했다시피 서블릿(Servlet)은 사용자가 일일이 수행해야 하는 HTTP 메시지 파싱, Socket Connection 등에 대한 사사로운 부분들을 대신 수행 해주는 역할을 하지만 HTML 문서를 내려주어야 하는 경우 비즈니스 로직 상에서 HTML 태그를 일일이 만들어서 내려주어야 하였다. 지금부터 JSP 템플릿 엔진을 사용하여 이러한 부분을 보완해 보는 시간을 가져보자.

03-1. JSP 사용을 위해 library 추가

implementation 'org.apache.tomcat.embed:tomcat-embed-jasper'
implementation 'javax.servlet:jstl'
  • JSP 사용을 위해 위 라이브러리를 추가 한다
  • JSP는 webapp 밑에 JSP 파일을 만들어야 한다
  • webapp
    • jsp
      • members
        • new-form.jsp
        • save.jsp
      • members.jsp

03-2. 기본 회원 폼 - 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>
  • 이름, 나이를 등록하는 HTML FORM은 위와 같다
  • 여기서는 action 에 ‘/jsp/members/save.jsp’ 파일 경로를 지정 해주었다
  • 해당 경로(파일)로 이동하면서 해당 파일을 모두 랜더링하여 JSP 파일을 실행하는 흐름
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
  • 첫 줄은 서블릿이라는 의미

03-3. 회원 저장 - save.jsp

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

  System.out.println("MemberSaveServlet.service");
  String username = request.getParameter("username");
  int age = Integer.parseInt(request.getParameter("age"));

  Member member = new Member(username, age);
  Member savedMember = 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 파일은 위와 같다
  • JSP 안에서는 Import만 해주면 자바 코드를 다 이용할 수 있다
  • <% ~~ %>
    • 자바 코드 작성
  • <%= ~~ %>
    • 자바 코드 출력

03-4. 회원 조회 - members.jsp

<%@ page import="hello.servlet.domain.member.Member" %>
<%@ page import="java.util.List" %>
<%@ page import="hello.servlet.domain.member.MemberRepository" %>
<%@ 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) {
            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>
  • FOR - LOOP 반복 하는 부분만 보면 될 것 같음

서블릿과 JSP의 한계

JSP를 써도 뭔가 효율적으로 사용하는 느낌이 들지 않음… 2개의 역할이 섞여 있는 기분이다

서블릿으로 개발할 때는 뷰(View) 화면을 위한 HTML을 만드는 작업이 자바 로직에 섞여 지저분하고 복잡했음. JSP를 사용한 덕에 뷰를 생성하는 HTML 작업을 깔끔하게 가져가고, 중간중간 동적으로 변경이 필요한 부분에만 자바 코드를 적용. 그런데 이렇게 해도 해결되지 않는 고민이 있음.

 

서두가 길었다, 결론은 JSP가 현재 너무 많은 역할을 가지고 있는 상태이다, 만약 프로젝트 규모가 커진다고 한다면 JSP 파일 안에 비즈니스 로직과 VIEW를 담당하는 로직이 섞여 지옥의 유지보수를 맛보게 될 것 이다.

 

그래서 MVC(Model - View - Controller) Pattern 이 탄생하게 되었다. 역할을 분리하자.

04. MVC 패턴 - 개요

04-1. JSP + Servlet, 너무 많은 역할을 담당한다

  • 하나의 “서블릿이나 JSP” 만으로 비즈니스 로직과 뷰 랜더링을 하면 너무 많은 역할을 담담하게 되고
    유지보수가 어려워 짐
  • 비즈니스 로직을 호출하는 부분에 변경이 발생해도 해당 코드를 손대야 하고 UI를 변경할 일이 있어도
    해당 파일을 수정해야 함
  • HTML 하나 수정하는데 수백줄을 자바 코드와 함께 있다고 상상해보자
    • 유지보수가 정말 어려워진다

04-2. 변경의 라이플 사이클

기획자의 사소한 화면 변경 요청이 있는 경우

  • 예를 들어 UI를 일부 수정하는 일과 비즈니스 로직을 수정하는 일은 각각 다르게 발생할 가능성이 높고 서로에게 영향을 주지 않는다
    • ex) 글자 하나 고쳐주세요
    • ex) 버튼 하나 고쳐주세요
  • 이렇듯이 변경의 라이프 사이클이 다른 부분을 하나의 코드로 관리하는 것은 유지보수 관점에서 정말 좋지 않음

04-3. JSP 기능 특화

SRP(단일 책임 원칙)에 맞게 역할을 분리한다

Servlet을 자바 로직을 처리하는데 특화 되어있다

  • JSP와 같은 뷰 템플릿은 화면을 랜더링하는데 특화 되어있다
  • 이렇듯 특화되어 있는 부분이 다른데 하나의 파일로 구성을 하는 것은 좋지 않다

04-4. Model View Controller

  • MVC 패턴은 기존에 하나의 서블릿이나, JSP로 처리하는 것을 MVC(Model, View, Controller)로 분리시킨 개념
    • 컨트롤러 : HTTP 요청을 받아서 파라미터를 검증하고, 비즈니스 로직을 실행하는 역할을 담당합니다. 그리고 뷰에 전달할 결과 데이터를 조회해서 모델에 담습니다.
    • 모델 : 뷰에 출력할 데이터를 담아둔다. 뷰가 필요한 데이터를 모두 모델에 담아서 전달해주는 덕분에 뷰는 비즈니스 로직이나 데이터 접근을 몰라도 되고, 화면을 랜더링 하는 일에 집중할 수 있다.
    • 뷰 : 모델에 담겨있는 데이터를 사용해서 화면을 그리는 일에 집중한다. 여기서는 HTML을 생성하는 부분을 말한다.
  • 일반적으로 model에 데이터를 담는데 내부적으로는 어떻게 되는거지?
    • request.setAttribute(”내용”)
    • 해당 부분은 다음 장에서 알아본다

참고

컨트롤러는 전달, 서비스는 비즈 로직 처리

컨트롤러에 비즈니스 로직을 둘 수 있지만, 이렇게 되면 컨트롤러가 너무 많은 역할을 담당하게 된다. 일반적으로 비즈니스 로직은 서비스(Service)라는 계층을 별도로 만들어 처리하며 컨트롤러는 서비스를 호출하는 역할을 담당.

05. MVC 패턴 - 적용

이번에는 위 내용을 기반으로 실제 MVC 패턴을 적용하는 시간을 갖는다.

서블릿을 Controller로 JSP를 View로 사용한다. 또한 Model은 HttpServletRequest

객체를 사용한다.

// HttpServletRequest request
request.setAttribute();
request.getAttribute();

회원 폼 출력

05-1. 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 { // 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란?
  • redirect vs forward의 차이
    • redirect(리다이렉트)는 실제 클라이언트(웹 브라우저)에 응답이 나갔다가, 클라이언트가 redirect 경로로 다시 요청한다. 따라서 클라이언트가 인지할 수 있고, URL 경로도 실제로 변경 됨
    • forward(포워드)는 서버 내부에서 일어나는 호출이기 때문에 클라이언트가 인식하지 못한다

05-2. 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> 
  • form action=”save”
    • 절대 경로(/) 시작이 아니라 상대 경로(/로 시작 X)하는 것을 확인 가능
    • 상대 경로를 사용하면 폼 전송 시 해당 URL이 속한 계층 경로 + save 호출
    • 현재 계층 경로 “/servlet-mvc/members/
    • 결과 “/servlet-mvc/members/save”

회원 저장

05-3. 회원 저장 - 컨트롤러

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);
        memberRepository.save(member);

        // model에 데이터를 보관해야 한다
        // ex) model.setAttribute("member", member);
        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); 이 부분이다. model을 사용하는 경우 기본적으로 model.setAttribute(”key”, “value”)와 같이 쓰는데 이러한 부분은 HttpServletRequest 객체 안에 지정이 되어있는 부분 이였다.

05-4. 회원 저장 - 화면

<%@ page import="hello.servlet.domain.member.Member" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Title</title>
</head>
<body>
<ul>
  <-- AS-IS -->
  <--<li>id=<%=((Member)request.getAttribute("member")).getId()%></li>
  <li>username=<%=((Member)request.getAttribute("member")).getUsername()%></li>
  <li>age=<%=((Member)request.getAttribute("member")).getAge()%></li>-->

  <-- TO-BE -->
  <-- 기존 JSP에서 사용하던 방식을 JSP가 제공해주는 표현식인 Property 접근법을 사용하여 출력 -->
  <li>id=${member.id}</li>
  <li>username=${member.username}</li>
  <li>age=${member.age}</li>
</ul>
<a href="/index.html">메인</a>
</body>
</html>
  • AS-IS를 보면 JSP 내에서 너무 힘들게 값을 가져와야 한다
  • TO-BE를 보면 ${} 지정자를 사용하여 서버측에서 넘겨준 값을 쉽게 추출할 수 있다

회원 목록 조회

05-5. 회원 출력 - 컨트롤러

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);
    }
}
  • 달라진 부분은 존재하지 않는다

05-6. 회원 출력 - 화면

<%@ 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="member" items="${members}">
            <tr>
                <td>${member.id}</td>
                <td>${member.username}</td>
                <td>${member.age}</td>
            </tr>
        </c:forEach>
    </tbody>
</table>
</body>
</html>
  • 출력 시에는 <c:forEach 구문을 사용하여 값을 반복적으로 출력 해준다

06. MVC 패턴 - 한계

MVC 패턴을 적용한 덕분에 화면, 비즈니스 로직 역할은 확실히 구분 하였다. 뷰는 현재 직관적이지만,

컨트롤러는 계속해서 반복적인 코드가 발생한다.

06-1. MVC 컨트롤러의 단점

포워드 중복

RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response); // 다른 서블릿이나 JSP로 이동할 수 있는 기능, 서버 내부에서 다시 호출 발생
  • View로 이동하는 코드가 항상 중복 호출되는 상황이다
  • 메서드로 빼서 Util 성으로 뺄수도 있지만, 그 메서드도 호출해야함

ViewPath에 중복

String viewPath = "/WEB-INF/views/new-form.jsp";
  • prefix: /WEB-INF/views/
  • suffix: .jsp
  • 만약 JSP가 아닌 thymleaf 같은 다른 템플릿 사용시 전체 코드 변경 필요함

사용하지 않는 코드

HttpServletRequest request, HttpServletResponse response
  • 위 코드를 사용할 때도 있고, 사용하지 않을 때도 존재
  • 위 코드는 테스트 케이스 작성도 어려움

공통 처리 어려움

기능이 복잡해질 수록 컨트롤러에서 처리해야 하는 부분이 점점 더 많이 증가할 것이다. 단순히 공통 기능을 메서드로 뽑으면 될 것 같지만 결과적으로 해당 메서드를 항상 호출해야 하고, 실수로 호출하지 않으면 문제가 발생한다. 그리고 호출하는 것 자체도 중복이다.

개인 생각 Spring MVC의 동작원리에 대해 설명할 때 프론트 컨트롤러(Dispatcher Servlet)이 존재한다. 이렇듯 Spring에서는 추상화된 Dispatcher Servlet을 통해 공통적인 기능을 처리한 후에 컨트롤러를 호출하는 구조로 되어있다.

Spring MVC 요청 흐름 과정 사용자가 WEB에 요청을 하게 되면 Dispatcher Servlet이 요청을 받게 된다. 후에 Dispatcher Servlet은 HandlerMapping을 호출하여 해당 요청 URL에 맞는 서블릿을 메모리에서 찾게 된다. 다시 한번 Dispatcher Servlet은 Handler Adapter를 호출하여 이전 요청 정보를 통해 실제 컨트롤러를 호출한다. 여기서 컨트롤러가 요청을 처리하여 데이터를 저장하여 Model(모델) 저장, view 이름을 저장하여 ViewResolver에게 요청을 전달한다. 마지막으로 ViewResolver가 해당 요청을 처리하여 사용자에게 반환하는 메커니즘을 가지고 있다.

정리하자면 공통 처리가 어렵다는 문제가 있다.

위 문제를 해결하려면 컨트롤러 호출 전에 먼저 공통 기능을 처리해야 한다. 소위 수문장 역할을 하는 기능이 필요하다. 프론트 컨트롤러(Front Controller)패턴 을 도입하면 이러한 문제를 깔끔하게 해결할 수 있다. (입구를 하나로) 스프링의 핵심도 바로 이 프론트 컨트롤러에 있음.

블로그의 정보

기록하고, 복기하고

ymkim

활동하기