개발자로 후회없는 삶 살기

spring PART.서블릿 jsp로 웹 어플리케이션 개발 본문

[백엔드]/[spring | 학습기록]

spring PART.서블릿 jsp로 웹 어플리케이션 개발

몽이장쥰 2023. 4. 10. 00:39

서론

※ 이 포스트는 다음 강의의 학습이 목표임을 밝힙니다.

https://www.inflearn.com/roadmaps/373

 

우아한형제들 최연소 기술이사 김영한의 스프링 완전 정복 - 인프런 | 로드맵

Spring, MVC 스킬을 학습할 수 있는 개발 · 프로그래밍 로드맵을 인프런에서 만나보세요.

www.inflearn.com

 

본론

- 회원 관리 웹 어플

간단한 회원 관리 웹 어플을 만들 것입니다. 만드는 순서가 있습니다. 코어 모듈을 만들고 핵심 비즈니스 로직을 만들고 (저장과 조회) 처음에는 서블릿으로 만들고 이를 개선한 JSP로 만들 것입니다. 또 이를 개선하기 위해서 MVC 패턴을 적용할 것입니다.

 

- 회원 관리 웹 어플 요구사항

회원 정보는 이름과 나이(DB 회원 테이블 정보), 기능은 회원 저장과 목록 조회 2가지입니다.

 

- 구현 시작

회원 엔터티를 domain 밑에 member 패키지를 만들고

 

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

멤버 클래스를 만들고 게터, 세터, 식별자 id, 이름, age 3개의 필드를 가지고 생성자로 이름과 나이를 가집니다. (기본 생성자도 넣어놓습니다.) id는 회원 레포를 만들 것인데 그때 id가 발급이 될 것입니다.

 

회원 레포는 원래는 레포 패키지를 만드는데 지금은 플젝이 작아서 별도로 만듭니다.

 

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

> 해시맵으로 메모리 멤버를 private static으로 만듭니다. sequence가 저장되면 발급되는 id입니다. 레포를 싱글톤으로 만들 것입니다. 스프링을 안쓰니 이렇게 구현 객체 내부에서 싱글톤으로 작성해야합니다.(스프링은 이것을 자동으로 해줍니다.)

> save 메서드에 저장을 할 때 Id 값을 set으로 넣어줍니다. find는 Id로 찾는 것과 All로 전체 다 찾기가 있습니다.

> test에서 afterEach로 쓰기 위해서 clearStore를 만듭니다. 다음시간부터 위에서 만든 메서드로 회원 관리 웹 어플리케이션을 만들어 보겠습니다.

 

 

- 서블릿으로 웹 만들기

이제 서블릿으로 회원 관리 웹 어플을 만들 것입니다. 가장 먼저 서블릿으로 회원 등록 폼을 만듭니다.

 

- 회원 등록 폼

web 이라는 패키지에 servlet이라는 패키지를 만듭니다. 멤버 폼 서블릿 클래스를 만듭니다.

 

@WebServlet(name = "memberFormServlet", urlPatterns = "/servlet/members/new-form")
public class MemberFormServlet extends HttpServlet {
    MemberRepository memberRepository = MemberRepository.getInstance();
    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.setContentType("text/html");
        resp.setCharacterEncoding("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");
    }
}

new-form으로 url을 잡습니다. 

> 저장을 하려면 레포가 필요합니다. 지금 작성하는 서블릿이 서비스라고 생각하면 됩니다. 비즈니스 로직인 서비스는 레포가 필요하니 바로 레포부터 선언하고 간다고 했습니다. 레포가 싱글톤이라서 get으로 불러옵니다. 

> 이제 로직을 짜야합니다. html 데이터를 읽어서 응답 메시지 결과로 html이 나가야합니다. 그래서 헤더에 set으로 메타 정보를 넣습니다. 메시지 바디에는  html일 쭉쭉 적어야합니다. 너무 불편합니다. 자바 코드로 html을 적는게 보통일이 아닙니다.

> 이 html은 form태그로 이름과 나이를 입력 받는 것입니다. 서블릿으로 이걸 하면 자바 코드로 해야해서 작성이 굉장히 불편합니다.

 

> 결과를 보면 html form이 나옵니다. 진짜 그냥 resp에 html만 넣었고 보내고 이런거 안했는데 서블릿 url로 접근하니 html이 나옵니다. 이게 resp의 편리한 점입니다. 입력해보면 에러가 뜹니다. form의 액션이 /save인데 아직 작성을 안해서 그렇습니다.

 

 

-> 회원 저장 코드

새로운 서블릿을 만들고 url을 /save로 합니다.

 

@WebServlet(name = "memberSaveServlet", urlPatterns = "/servlet/members/save")
public class MemberSaveServlet extends HttpServlet {
    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);
    }
}

서비스에서 멤버 레포가 필요하니 레포를 선언합니다. 이제 바디에서 넘어온 값을 읽어야합니다. 쿼리 파라미터 형식은 getParameter로 꺼낼 수 있다 했으니 꺼내서 가져옵니다. getParameter는 무조건 string이라서 타입을 int로 바꿉니다. 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로 내려보자 또 복잡하게 자바 코드로 html을 작성해야합니다. 

 

-> 회원 목록

 

저장된 모든 멤버를 조회해보자 새로운 클래스를 만들고 url은 members로 합니다.

 

@WebServlet(name = "memberListServlet", urlPatterns = "/servlet/members")
public class MemberListServlet extends HttpServlet {
    MemberRepository memberRepository = MemberRepository.getInstance();

    @Override
    protected void service(HttpServletRequest req, HttpServletResponse response) throws ServletException, IOException {
        List<Member> members = memberRepository.findAll();
        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>");
    }
}

레포의 findAll에서 List를 꺼내고 이것을 HTML에 뿌리면 됩니다. 

 

이것을 동적으로 LIST에서 값을 뽑아서 가져오려면 for문을 써야합니다. 데이터를 저자앟고 /members에 가보면 html로 작성한 리스트가 쫙 나옵니다. setCharEnc을 안하면 이렇게 깨집니다.

 

-> 느낌

서블릿이 뭔가 자바 코드를 실행하는 것, 비즈니스 로직을 수행하는 것은 괜찮은데 html 코드를 작성하는게 너무 별로입니다. 그래서 템플릿 엔진을 사용합니다. html에 자바 코드를 넣는 것으로 지금은 자바 코드에 html을 넣은 거였습니다.

 

- jsp로 개선하기

서블릿으로 만드는 것과 똑같은데 편리한 jsp 기능을 해서 차이를 보자 jsp를 하려면 그래들에 의존성을 넣어야합니다. 

 

-> 회원 등록 폼

이건 jsp에 개발을 해야해서 webapp 밑에 만들어야합니다. jsp/members dir을 만들고 new-form.jsp를 만듭니다.

 

이제 폼을 만듭니다. html에 form 태그를 만들듯이 하면 됩니다. action을 /jsp/members/save.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>

 

> 실행을 하려고 하는데 어떻게 정적 페이지를 호출해야할까요? index가 포트 뒤에 '/'만 했던 것처럼 jsp dir가 같은 경로이므로 /jsp/members/new-form.jsp라고 하면 됩니다.

 

-> 회원 저장 로직

save.jsp에 로직을 작성합니다. 서블릿에서는 자바에 html을 넣었는데 jsp에서는 html에 자바를 넣는 것입니다.

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="hello.servlet.domain.member.Member" %>
<%@ page import="hello.servlet.domain.member.MemberRepository" %><%--
  Created by IntelliJ IDEA.
  User: hsb99
  Date: 2023-04-10
  Time: 오전 9:40
  To change this template use File | Settings | File Templates.
--%>
<%
    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);

%>

jsp에서는 <%%>에 로직을 써야합니다. html 밖에 있습니다. import도 page import로 넣어야합니다. 근데 그러면 request는 어떻게 할까요? req, resp는 jsp도 나중에 서블릿으로 자동으로 사용되어서 그냥 쓸 수 있도록 지원이 됩니다.

 

저장을 성공했다는 것을 말해줬었습니다. 그냥 html에 적으면 되고 <%=member.getId()%> 로 위에 jsp에 작성한 자바 코드를 html 코드에 쓸 수 있습니다. 말 그대로 html에 자바 코드를 넣는 것입니다.

 

-> 회원 목록 작성

members.jsp를 url에 치면 목록이 나오게 할 것입니다. 얘도 자바 코드가 필요하니 위에 <%%>로 List를 선언합니다. 밑에 html에서 for문으로 html 안에 자바 코드를 넣습니다.

 

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

 

이렇게 jsp로도 똑같은 결과를 해봤습니다. html을 출력하는 것을 중심으로 하고 자바 코드를 <% %> 부분 부분 넣었습니다. <%=%>는 자바 코드 출력이고 <%%>는 입력입니다.

 

-> 한계

근데 이렇게 해도 두가지 일을 하나의 페이지에 하는 것이 별로입니다. 로직과 뷰 렌더링이 섞여있습니다. 이러면 커밋 충돌도 잘 나고 지저분하고 복잡합니다.

> 코드 상위의 절반은 회원을 저장하기 위한 비즈니스 로직이고 나머지 하위 절반은 결과를 html로 보여주기위한 뷰 영역입니다. 자바코드, 데이터 조회하는 레포 등등 다양한 코드가 모두 하나의 jsp에 노출되어 있습니다. jsp가 너무 많은 역할을 합니다. 실무에 들어가면 수백 수천 줄이 들어갑니다. 그러면 정말 jsp 지옥을 보게 됩니다. 영한님도 jsp 만줄을 본 적이 있었습니다.

-> mvc 패턴 등장


보여주는 것과 로직을 분리한다는 것으로 로직은 서블릿처럼 다른 곳에서 처리하고 jsp는 목적에 맞게 뷰를 그리는 일에 집중하자 지금까지 한 프로젝트를 서블릿과 jsp의 MVC 패턴을 적용해서 jsp는 뷰만 있고 서블릿에는 로직만 넣어서 리팩토링 해보겠습니다.

 

- mvc 패턴 개요

서블릿이나 jsp만 가지고 개발하면 비즈니스 로직을 처리하는 부분, 화면을 처리하는 부분을 하나에서 처리해야합니다. 

1. 너무 많은 역할

너무 많은 역할을 해서 유지보수가 어려워집니다.

2. 변경의 라이프 사이클

항상 뭔가 설계에서 좋은 고민을 해야하면 변경 주기가 다르면 분리해야합니다. 근데 UI를 변경하는 일과 비즈니스 로직을 변경하는 일이 거의 대부분 따로따로 일어납니다. 변경의 라이프 사이클이 다른 부분을 하나의 코드로 관리하는 것은 유지보수에 좋지 않습니다.

3. 기능 특화

jsp는 화면 렌더링에, 서블릿은 자바 코드에 최적화 되어있어서 본인들의 역할만 하는게 좋습니다.

 

- Model View Controller

MVC 패턴은 지금까지 학습한 것처럼 하나의 서블릿이나 JSP 로 처리하던 것을 컨트롤러, 뷰 영역으로 탁 쪼개는 것입니다. 웹 어플은 보통 MVC 패턴을 씁니다.

 

-> 컨트롤러


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

> 고객이 요청하면 컨트롤러에서 비즈니스 로직을 다 수행하고 모델에 데이터를 담아서 뷰 로직을 실행하면 모델의 데이터를 참고해서 뷰를 그립니다. 그리고 응답이 나가는 것입니다. 컨트롤러가 서블릿이고 뷰가 jsp입니다. 하나로 되어있던 것을 두개로 분리하는 것입니다. 모델을 통해서 데이터를 전달합니다.

-> 모델

뷰에 전달할 데이터를 담는 것으로 뷰가 필요한 데이터를 모두 모델에 담아서 전달해주는 덕분에 뷰는 비즈니스 로직이나 데이터 접근은 몰라도 화면을 렌더링하는 일에 집중할 수 있습니다. "어? 이 데이터 어디에서 찾아야하지? 그럴 때마다 모델만 보면 됩니다." 모델 덕분에 뷰가 비즈니스 로직을 호출해야하는 의존 관계가 다 끊어지게 되는 것입니다.

-> 뷰

모델에 담긴 데이터를 사용해서 화면을 그리는 일에 집중합니다. 보통 html을 그립니다. 

 

-> 강화

보통 비즈니스 로직은 핵심 로직입니다. 서비스 클래스에 작성됩니다. 서비스가 레포를 사용합니다. 비즈니스가 이 그림처럼 나뉘어지는데 컨트롤러는 파라미터 꺼내기, api 스펙을 튕기는 걸 하고 로직이 잘 맞으면 서비스를 호출해서 주문을 하고 데이터에 접근합니다.

 

> 그리고 컨트롤러가 결과를 받아서 모델에 전달합니다. ex) 회원 조회라면 조회한 회원 목록 결과를 모델에 담고  뷰를 호출하면 뷰가 모델에 값을 꺼내서 쭉합니다.

+ 컨트롤러에 비즈니스 로직을 둘 수도 있는데 이렇게 되면 컨트롤러가 너무 많은 역할을 합니다. 뷰도 호출하고 파라미터도 건들고 로직도 실행하고 등등 그래서 그러지 말고 컨트롤러는 그냥 조종하는 역할만 합니다. 비즈니스 로직을 호출하는 역할까지만 하는게 좋고 로직은 서비스라는 계층을 별도로 둬서 처리하고 컨트롤러가 서비스를 호출하도록 합니다.

 

Comments