인프런/스프링 MVC 1편

4)서블릿,JSP,MVC패턴 적용

backend dev 2023. 1. 16.

서블릿으로 간단한 회원관리 웹어플리케이션을 만들것이다. 그다음은 JSP,  그다음은 MVC패턴으로 만들어볼것이다.

어떤 불편한점이 있어서 MVC패턴이 탄생했는지 알아보고 MVC패턴에 대해 공부해보자.

 

회원이라는 엔티티 == 도메인모델을 만들어준다.

위치는 도메인패키지아래 멤버패키지 밑이다.

 

Member.java

@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.java (스프링없이 짜느라 코드가 저렇다)

/*
    동시성 문제가 고려되어 있지않은 설계이다, 실무에서는 ConcurrentHashMap, AtomicLong 사용을 고려해야한다.
 */
public class MemberRepository {

    private static Map<Long, Member> store = new HashMap<>(); //저장소 , 동시성 문제가 고려되지않았다 실무에서는 수정이 필요한 부분이다.
    //저장을 담당하는 객체이기때문에 static으로 선언하여, 각기 다른 인스턴스라도 같은 저장공간을 공유하기 위함. 많은 객체가 생성되어도 store는 하나만 생성되고 공유된다.
    private static long sequence = 0L; //회원번호를 위한 값도 static으로 해줘야 어디까지 증가했는지 공유가능.
    //하지만 싱글톤패턴으로 할것이기 때문에 위의 2개의 필드에는 static이 필요없긴하지만, 냅둔다  (싱글톤이므로 객체가 단 하나만 생성되서 공유될거니까)
    private static final MemberRepository instance = new MemberRepository();//싱글톤으로 만들것이기 때문에 ,내부에서 static 객체를 만들어서 꺼내쓰도록한다.
    //final로 해서 수정이 불가능하게 한다, static 이므로 객체를 아무리 만들어도 해당 필드는 단 "하나"로 공유된다.

    public static MemberRepository getInstance() { //싱글톤을 하기위해 만들어놓은 객체를 밖에서 꺼내써야하므로 만들어놓은 객체를 리턴하는 메소드를 만든다.(public)
        return instance;
    }
    private MemberRepository() { //싱글톤이 유지되려면 생성자를 private로 만들어서 , 객체 생성을 막아줘야한다
    }

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

}

테스트코드로 테스트

class MemberRepositoryTest {

    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("hello1", 201);
        Member member2 = new Member("hello2", 202);
        memberRepository.save(member1);
        memberRepository.save(member2);
        //when
        List<Member> members = memberRepository.findAll();
        //then
        assertThat(members.size()).isEqualTo(2);
        assertThat(members).contains(member1, member2); // 해당 객체를 포함하고있는지 체크하는 메소드 contains()
    }
}

 

이렇게만든 비즈니스로직을 가지고 회원관리 웹 어플리케이션을 서블릿으로 만들어보자.

 

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

 

@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 writer = response.getWriter();
        //서블릿을 이용해서 해당 url주소를 호출하면 html form형태를 보여줄것이다.
        //서블릿response객체에 html코드를 담아 form형태의 데이터를 전달해줄것이다.
        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코드를 다 자바코드로 입력해야하는 불편함이 있다.
        //코드르 보면 <form action=\"/servlet/members/save\" method=\"post\">\n" 부분이 있는데 form의 전송을 누르면 해당 url를 호출한다.
        //이동하기위해 해당 url패턴을 가지는 서블릿을 만들어준다.
    }
}

form형태가 잘 출력되는것을 확인할 수 있다.

html form을 통해 데이터를 받아서 멤버객체를 만들고 저장하고 결과 화면용 html를 동적으로 만들어서 응답하는

서블릿을 만들자.

 

@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 {
        //form 데이터의 전송버튼으로 데이터가 들어온다. POST방식인 http form 데이터는 쿼리파라미터형태를 가지므로 request.getParameter() 메소드를 사용할 수 있다.
        String username = request.getParameter("username");
        int age = Integer.parseInt(request.getParameter("age")); //getParameter의 리턴값은 항상 String이므로 int형으로 변환해준다.

        Member member = new Member(username,age); //전달된 데이터를 이용해서 Member객체를 만든다.
        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" + //자바로 html을 짜다보니 중간에 변수값을 넣어 동적으로 html이 생성가능하다.
            "</ul>\n" + //들어오는값에따라 html의 결과가 다름 == 동적
            "<a href=\"/index.html\">메인</a>\n" +
            "</body>\n" +
            "</html>");

    }
}

 

form에 kim,20이라는 데이터를 넣어 전송을 하게되면

다음과 같은 결과 html를 확인할 수 있다.

지금은 리포지토리를 자바코드로 짰기때문에 서버를 내려버리면 저장된값이 다 지워질것이다.

 

저장된 모든회원목록을 조회하는 서블릿을 만들어보자.

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

        //멤버리스트를 html로 동적으로 출력해준다.
        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>");
    }
}

form을 이용해서 다시 멤버를 저장하고 잘 저장됬는지 체크해본다.

잘 저장된게 확인되었다.

서블릿을 이용해서 html를 만들어서 응답하려고하니 html코드를 자바로 짜는것이 고역이다.

자바코드로 짜기때문에 동적으로 html을 만들수는 있기는하지만 코드가 너무 복잡하고 비효율적이다.

그래서 템플릿 엔진이 등장했다. 템플릿 엔진을 사용하면 html 문서에서 필요한 곳만 코드를 적용해서 동적으로 변경할 수 있다. 

템플릿엔진에는 JSP,Thymeleaf 등이 있다. 요즘에는 타임리프를 많이 사용하기때문에 JSP로 이용해서 만드는법을 잠깐 배우고 그 이후로는 타임리프를 사용한다.

 


JSP로 회원관리 웹 어플리케이션 만들기

JSP 라이브러리 추가

JSP를 사용하려면 먼저 다음 라이브러리를 추가해야한다.

//JSP 추가 시작
   implementation 'org.apache.tomcat.embed:tomcat-embed-jasper'
   implementation 'jakarta.servlet:jakarta.servlet-api' //스프링부트 3.0 이상
   implementation 'jakarta.servlet.jsp.jstl:jakarta.servlet.jsp.jstl-api' //스프링부트 3.0 이상
   implementation 'org.glassfish.web:jakarta.servlet.jsp.jstl' //스프링부트 3.0 이상
//JSP 추가 끝

회원등록폼 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>

form의 전송을 누르면 /jsp/members/save.jsp로 데이터가 보내질것이다.

url호출해서 들어가보면 form형태가 잘보인다.

이제 form 데이터가 넘어온걸 이용해서 저장하는 기능이 있는 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" %>
<%//이 기호안에서는 자바 코드를 넣을 수 있다. 서블릿에서 사용했던 코드 일부분을 가져온다.
    MemberRepository memberRepository = MemberRepository.getInstance();

    //jsp도 결국 서블릿으로 바뀌기 때문에 request와 response 객체는 그냥 사용이 가능하다.
    String username = request.getParameter("username");
    int age = Integer.parseInt(request.getParameter("age"));

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

%>
<%--밑에는 이제 html 코드를 작성해주면 된다. 위의 자바코드를 이용해서 동적으로 html코드를 만들어준다.--%>
<html>
<head>
    <meta charset="UTF-8">
</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 코드를 짜는 부분이 나눠져있다. html코드를 짤때 자바코드를 이용할 수 있어서 동적으로 html를 만들기 편하다. + html 코드를 자바로 안짜고 html 그자체로 만드니 편하다.

결과

저장된 멤버리스트를 볼 수 있는 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>
  <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의 한계

서블릿과 JSP의 한계

 

MVC 패턴의 등장

한곳에서 모든것을 다하면 문제가 발생하니까 서블릿을 비즈니스로직을 처리하는 부분으로 , JSP를 최대한 html로 화면(뷰)를 그리는 부분으로 사용하게끔하여 서블릿과 jsp로 mvc패턴을 적용해보자. 

MVC 패턴 - 개요

컨트롤러는 비즈니스 로직을 실행하기보다는 비즈니스 로직이 있는 서비스를 호출하는 역할을 담당한다.

(= 비즈니스 로직을 호출)

이게 일반적인 MVC 패턴

위의 그림이 일반적인 MVC 패턴이다. 컨트롤러 안에 비즈니스로직을 두지않고, 서비스라는 별도 계층을 만들어서 

비즈니스로직을 수행한다.

순서대로 

1. 클라이언트가 http요청을 하게된다 (컨트롤러를 호출)

 

2. 컨트롤러가 파라미터를 꺼내고(들어온 데이터를 꺼내고) http요청이 제대로 맞는지 확인하고 잘못되면 400오류와 같은 상태코드를 보내주기도 하고 체크를한다. 잘들어왔다면 서비스나 리포지토리를 호출해서 비즈니스 로직을 수행하거나 데이터를 접근하는등 작업을 수행하게끔한다 그리고 잘됬는지 안됬는지 결과를 받는다 (혹은 바로 데이터를 전달받을수도있다. -> 로직에 따라 다를것이다) 

 

3. 모델에 결과 데이터를 전달해서 모델에 담는다.

 

4. 그다음 뷰 로직으로 넘기면 모델에서 값을 꺼내서 html를 만들어서 응답해준다. (html말고 xml 등을 만들기도 한다는데 일반적으로 html를 만든다고 한다)

 

위의 그림이 컨트롤러에서 비즈니스로직을 수행하는 mvc 패턴중 하나이다. 좋지않은 설계이다.

 

MVC 패턴 - 적용

서블릿을 컨트롤러로 사용하고, JSP를 뷰로 사용할것이다.

서블릿이 비즈니스 로직을 수행하므로 , 즉 컨트롤러가 비즈니스 로직을 수행하므로 좋지않은 설계이긴 하지만

예시로 적용해본다.

모델은 서블릿request 객체를 이용한다. 서블릿request객체에 데이터를 보관하고 조회할수 있기 때문에 모델처럼 사용할것이다. (jsp에서는 서블릿request,response객체를 사용할 수 있으므로 뷰에서 모델의 데이터를 참조할수 있다)

 

컨트롤러 역할 서블릿

@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 {
        //항상 컨트롤러를 통해서 뷰로 넘어가야한다. form형태의 뷰를 보여줘야 데이터를 입력할것이니까, 컨트롤러를 통해 뷰로 이동하게끔하자.
        String viewPath = "/WEB-INF/views/new-form.jsp"; //이동할 경로를 따로 저장해둔다.
        //이동할 jsp의 경로를 전달해서 RequestDispatcher 객체를 만들고, 그 객체의 forward메소드에 서블릿request,response객체를 전달하면서 jsp로 이동한다.
        RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);//getRequestDispatcher()는 컨트롤러에서 뷰로 이동할때 사용하는 메소드이다.
        dispatcher.forward(request, response); //forward()를 이용하여 서블릿에서 jsp를 호출할수있게된다, 서버 내부에서 다시 호출이 발생하는것

    }
}

dispatcher.forward() : 다른 서블릿이나 JSP로 이동할 수 있는 기능이다. 서버 내부에서 다시 호출이발생한다.

뷰 역할 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="save" method="post">를 보면 그냥 "save"라고 되어있는걸 볼 수있다. 이것이 상대경로이다.

현재 URL이 속한 계층 경로 + 해당값으로 이동하는것

 

만약 "/save"라고 했으면 그냥 localhost:8080/save 로 이동하는것과 같은 결과가 됬을것이다.

상대경로는 /를 붙이지않는것을 기억하자.

/WEB-INF 아래에있는 자원들은 외부에서 호출해도 불러지지않는다(WAS 규칙)

리다이렉트는 클라이언트가 응답이 나갔다가,클라이언트가 리다이렉트경로로 다시 요청한다. 그래서 클라이언트가 인지할 수 있고, URL 경로도 바뀐걸 확인할 수 있었다.

하지만 포워드는 서버 내부에서 일어나는 호출이기 때문에 클라이언트가 인지하지못한다. 그리고 URL경로도 바뀌지않는다.  

위의 경우는 서블릿으로 url호출이 들어왔고 서버내부에서 jsp를 호출해서 form를 만들어서 응답을 보낸것이다.

리다이렉트는 url호출이 들어왔는데 응답으로 다른 url를 보내주고(리다이렉트 헤더로), 다시 웹브라우저가 그 url를 호출해서 이동한다 (2번 호출이됨)

하지만 이동할곳을 응답하는게 아니고 서버 내부에서 jsp를 호출하고 뷰를 만들어 응답하는것이기 때문에forward는 url의 변경이 없고, 클라이언트가 인식하지 못한다.

jsp를 이용해서 form을 띄운것인데, url이 변경되어있지않은것을 확인할 수 있다.

회원저장 컨트롤러 (서블릿)

@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")); //getParameter의 리턴값은 항상 String이므로 int형으로 변환해준다.

        Member member = new Member(username,age); //전달된 데이터를 이용해서 Member객체를 만든다.
        memberRepository.save(member); //그 멤버객체를 저장해준다.

        //모델에 데이터를 보관해줘야한다.  모델에 데이터를 보관하고 jsp를 호출해서 모델의 데이터를 꺼내서 html를 만들고 응답으로 전달해줄것이다.
        request.setAttribute("member", member);
        String viewPath = "/WEB-INF/views/save-result.jsp";
        RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
        dispatcher.forward(request, response); //서버 내부에서 jsp를 호출
    }
}

회원저장 - 뷰 (JSP)

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>

 

 

<ul>
  <li>id=<%=((Member)request.getAttribute("member")).getId()%></li>
  <li>username=<%=((Member)request.getAttribute("member")).getUsername()%></li>
  <li>age=<%=((Member)request.getAttribute("member")).getAge()%></li>
</ul>

이렇게 request에서 값을꺼내서 캐스팅해서 가져와서 사용하는 방법도 있지만 코드가 너무 복잡해져서 

JSP에서 지원하는 ${}문법을 사용한다.

form을 통해서 데이터가 들어오면 잘 저장되는것을 확인했다.

회원목록조회 컨트롤러 (서블릿)

@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);
        //서버내부에서 json(뷰)를 호출
        String viewPath = "/WEB-INF/views/members.jsp";
        RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
        dispatcher.forward(request, response);
    }
}

회원목록 - 뷰 (JSP)

members.jsp

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%> <%--jsp에서 제공하는 기능을 사용하기위해 임포트--%>
<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}">
    <tr> 
      <td>${item.id}</td>
      <td>${item.username}</td>
      <td>${item.age}</td>
    </tr>
  </c:forEach>
  </tbody>
</table>
</body>
</html>

회원목록이 잘보이는것을 확인할 수 있다.

 


서블릿+jsp로 만든 MVC패턴 한계

컨트롤러(서블릿),뷰(JSP)로 만들어본 MVC패턴의 한계가 있다.

위의 컨트롤러(서블릿)코드를 보면

어떤 특정 기능을 위해 컨트롤러를 계속만든다. 그리고 컨트롤러안에 코드들이 반복이 된다.

 

이 문제를 해결하기 위해 프론트 컨트롤러 패턴을 도입해서 해결한다.

하나의 컨트롤러에서 http요청들을 다 받아서 처리하는! 

 

 

댓글