Spring MVC

Spring MVC없이 Servlet, JSP로 MVC 구현해보기 - 2

cwchoiit 2024. 4. 26. 15:38
728x90
반응형
SMALL

 참고자료:

 

스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술 | 김영한 - 인프런

김영한 | 웹 애플리케이션을 개발할 때 필요한 모든 웹 기술을 기초부터 이해하고, 완성할 수 있습니다. 스프링 MVC의 핵심 원리와 구조를 이해하고, 더 깊이있는 백엔드 개발자로 성장할 수 있습

www.inflearn.com

 

회원 관리 애플리케이션을 서블릿을 사용해서 만들어보자. 간단하게만 일단 만들어보자.

 

Member

package org.example.servlet.domain.member;

import lombok.Data;

@Data
public class Member {
    private Long id;
    private String username;
    private int age;

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

 

MemberRepository

package org.example.servlet.domain.member;

import lombok.Getter;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

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() {}

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

 

데이터베이스는 없지만 메모리 상에서 회원 정보를 저장하게 해보자. 우선 그럴려면 멤버를 저장할 자료구조가 필요한데 그 HashMap을 이용해보자. 동시성 문제에 대해선 고려하지 않은채로 진행하자.

 

그리고, 이 MemberRepository는 딱 한 개의 인스턴스만 존재하는 싱글톤이다. 그래서 getInstance()로만 이 클래스의 인스턴스에 접근이 가능하도록 만들었다. 

 

save(Member member), findById(Long id), findAll(), clearStore() 이렇게 4개의 public 메서드가 있다.

 

 

테스트 코드도 작성하자.

MemberRepositoryTest

package org.example.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.*;
import static org.junit.jupiter.api.Assertions.*;

class MemberRepositoryTest {

    MemberRepository memberRepository = MemberRepository.getInstance();

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

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

        Member savedMember = memberRepository.save(member);

        Member findMember = memberRepository.findById(savedMember.getId());

        assertEquals(savedMember, findMember);
    }

    @Test
    void findAll() {
        Member member1 = new Member("member1", 20);
        Member member2 = new Member("member2", 20);

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

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

        assertEquals(members.size(), 2);
        assertThat(members).contains(member1, member2);
    }
}

 

Member, MemberRepository를 만들고 이를 이용한 CRUD에 대한 간단한 테스트 코드를 작성했다. 이제 서블릿을 사용해서 클라이언트와 서버간 통신을 해서 간단한 애플리케이션을 만들어보자.

 

package 경로 path/your/package/web/servlet안에 MemberFormServlet을 만들자.

이 클래스는 유저를 생성할 때 필요한 폼을 HTML로 보여주는 클래스가 될 것이다.

 

MemberFormServlet

package org.example.servlet.web.servlet;

import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.example.servlet.domain.member.MemberRepository;

import java.io.IOException;
import java.io.PrintWriter;

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

    private final MemberRepository memberRepository = MemberRepository.getInstance();

    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

        resp.setContentType("text/html;charset=utf-8");

        PrintWriter writer = resp.getWriter();
        writer.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 작성이 너무너무너무 불편하다는 사실이다. 이런 불편함덕에 JSP가 나타나고 Spring MVC가 나타나고 하는거지만 결국 어떤 발전 과정이 있는지 아는게 중요하기 때문에 서블릿으로 MVC패턴을 만드는 것을 해보는 것이다.

 

여튼 저렇게 폼 하나를 만들면 우리의 서버에서 잘 뿌려주는지 확인할 수 있다.

username, age를 입력받는 폼이 잘 나온다. 지금은 전송 버튼을 누르면 에러가 발생한다. 폼을 보면 알겠지만, action="/servlet/members/save" 인데 이 경로에 대한 서블릿을 만들지 않았기 때문이다. 만들어보자.

 

MemberSaveServlet

package org.example.servlet.web.servlet;

import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.example.servlet.domain.member.Member;
import org.example.servlet.domain.member.MemberRepository;

import java.io.IOException;
import java.io.PrintWriter;

@WebServlet(name = "memberSaveServlet", urlPatterns = "/servlet/members/save")
public class MemberSaveServlet extends HttpServlet {

    private final MemberRepository memberRepository = MemberRepository.getInstance();

    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

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

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

        resp.setContentType("text/html;charset=utf-8");
        PrintWriter writer = resp.getWriter();

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

 

POST Method로 들어온 요청에 담긴 username, age를 받아와서 MemberRepository를 통해 멤버를 저장하는 로직이 있다.

그리고 저장된 멤버를 보여주는 HTML을 응답으로 돌려준다.

 

여기서 문제는, 비즈니스 로직과 응답에 대한 뷰를 처리하는게 동시에 있다는 사실이다. 즉, 이 서블릿이 하고 있는 업무가 너무 많다. 여하튼 서블릿을 이용해서 멤버를 만들어내는 폼을 보여주는 화면과 폼 데이터를 전송해서 멤버를 저장하고 저장된 멤버를 보여주는 작업을 완료했다. 아래 화면은 폼 화면에서 username에 "choi", age에 "30"을 넣고 전송버튼을 눌렀을 때 결과 화면이다.

 

 

이제 멤버리스트 화면을 보여주는 서블릿이 필요하다.

MemberListServlet

package org.example.servlet.web.servlet;

import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.example.servlet.domain.member.Member;
import org.example.servlet.domain.member.MemberRepository;

import java.io.IOException;
import java.io.PrintWriter;
import java.util.List;

@WebServlet(name = "memberListServlet", urlPatterns = "/servlet/members")
public class MemberListServlet extends HttpServlet {

    private final MemberRepository memberRepository = MemberRepository.getInstance();

    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        List<Member> members = memberRepository.findAll();

        resp.setContentType("text/html;charset=utf-8");
        PrintWriter w = resp.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>");
    }
}

 

딱히 설명할 내용은 없는것같다. 멤버 전체를 보여주는 화면이다. 바로 서블릿이 보여주는 화면을 봐보자.

 

이렇게 멤버를 생성하고 저장된 멤버들을 보여주는 화면을 만들어봤다. 사용을 해보니 서블릿으로 동적인 HTML 파일도 만들수 있고 화면을 사용자에게 뿌려주는것도 잘되고 좋은것 같지만 서블릿의 가장 큰 단점은 HTML을 작성해내기가 너무 힘들다는 것이다. 이것을 해결하기 위해 템플릿 엔진이 나왔다. 대표적인 것이 JSP, Thymeleaf이다. JSP를 먼저 해보고 JSP가 서블릿의 문제를 어떻게 해결했고 어떤 불편함이 있길래 Thymeleaf가 나왔는지 또 알아보자.

 

서블릿 대신 JSP를 사용하기

implementation 'org.apache.tomcat.embed:tomcat-embed-jasper'
implementation 'javax.servlet:jstl:1.2'

JSP를 사용하려면 이렇게 의존성을 추가해줘야 한다.

 

이제 JSP 파일을 생성할건데, src/main/webapp/jsp 경로안에 만들어야 한다.

그래서 멤버를 생성하기 위해 입력하는 폼을 보여줄 jsp 파일을 src/main/webapp/jsp/members/new-form.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>

 

이렇게 만들면 브라우저에 webapp 아래부터 경로 그대로를 입력해주면 된다.

 

이제 new-form.jsp에서 form이 보내는 경로인 save.jsp 파일을 만들어야 한다.

save.jsp

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

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

    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()%></li>
</ul>
<a href="/index.html">메인</a>
</body>
</html>

JSP는 로직과 HTML을 딱 구분지어서 이렇게 작성할 수 있다. 

이제 멤버 리스트를 보여주는 화면인 members.jsp 파일도 만들어보자.

 

members.jsp

<%@ page import="org.example.servlet.domain.member.MemberRepository" %>
<%@ page import="org.example.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를 이용해서, 같은 내용을 서블릿에서 JSP로 변경해봤다. 확실히 HTML을 쉽게 작성할 수 있다. 근데 여전히 맘에 들지 않는다. 두가지 일을 한 곳에서 다 해버리고 있다는 게 불편하다. 비즈니스 로직과 뷰가 동일한 곳에서 작성되다보니 지저분하다. 

 

결론

서블릿과 JSP를 사용해서 아주아주 작은 웹 애플리케이션을 구현해봤다. 

 

서블릿의 단점

  • HTML 코드를 작성하는게 너무 힘들다.

서블릿의 단점을 극복하고자 JSP가 등장했지만? 

JSP의 단점

  • 비즈니스 로직을 담당하는 부분과 화면을 담당하는 부분을 같이 다루고 있기 때문에 복잡하고 지저분하다.
  • 소스가 커지면 커질수록 감당하기 어려워진다.

저 위 코드는 비즈니스 로직이 너무너무 간단하니까 눈에 잘 들어오기라도 하지만 소스가 커지면 커질수록 점점 아찔해질거다. 이런 문제를 해결하고자 MVC 패턴이 등장한것이다. 비즈니스 로직은 비즈니스 로직만 다루고, 화면은 화면에만 집중할 수 있도록 말이다.

 

이제 스프링 MVC를 배울건데 배우기전에 MVC 패턴을 이해하는 과정이 필요하다. 그 과정을 다음에 다뤄보겠다.

728x90
반응형
LIST