Spring

[Spring MVC] 스프링 MVC (1)

챛채 2023. 5. 9. 18:01

3.서블릿, JSP, MVC 패턴

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

 

  1. 서블릿만으로 개발 (자바 안에 HTML 작성)
  2. JSP로 개발 (JSP에 자바코드 섞어 작성)
  3. MVC 패턴 사용

요구 사항

  • username, age 입력 가능
  • 저장 누르면 회원관리 DB에 데이터 저장
  • 저장된 회원 정보를 목록 형태로 볼 수 있음

회원 도메인 모델

[Member.java]

회원 username, age 저장하고 가져올 수 있는 도메인 모델

-id는 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.java]

-저장 장소는 static HashMap을 선언하여 메모리 일시적으로 DB로 사용

public class MemberRepository {
		//static으로 MemberRepository 인스턴스 생성이 계속 되어도 1개만 생성
    private static Map<Long, Member> store = new HashMap<>();
    private static long sequence = 0L; //ID 시퀀스
    //Null이 들어갈 가능성이 없기 때문에 long 사용
    //Long, Integer -> Null 사용 vs long, int -> null 사용 불가

    private static final MemberRepository instance = new MemberRepository();
    //싱글톤 사용

    //싱글톤 패턴은 객체를 단 하나만 생성해서 공유해야함
    //싱글톤 쓸 때는 private로 생성자 막아야함 -> 아무나 생성하지 못하게..
   public static MemberRepository getInstance() {
        return  instance;
    }

    private MemberRepository() {
    }

		//회원 저장
    public Member save(Member member) { //아이디 만들기
        member.setId(++sequence); //시퀀스값 하나 증가시킴
        store.put(member.getId(), member);
        return member;
    }

		//Id로 회원 찾기
    public Member findById(Long id) {
        return store.get(id);
    }

		//회원 전체 찾기
    public List<Member> findAll() {
        return new ArrayList<>(store.values());
        //store에 있는 모든 값 다 꺼내서 새로운 arraylist 담아서 넘겨줌
        // -> new ArrayList에 값을 넣거나 밖에서 조작해도 store에 있는 value list 건들고 싶지 않아서 (store 자체를 보호하기 위해)
    }

    //DB stroe 초기화
    public void clearStore() {
        store.clear();
    }
}
  • save : Member의 setId를 통하여 고유값을 증가시켜 셋팅하고 그 id를 store Map에 Key 값으로 Member 객체를 value로 저장한 후 저장한 member 변환
  • findById : id를 파라미터로 넘겨 store에서 꺼내옴
  • findAll : store의 values()를 통하여 ArrayList 형태로 모두 꺼내옴
  • clearStore : map 클리어 처리

 

회원 저장소 테스트 코드

[MemberRepositoryTest.java]

class MemberRepositoryTest { //JUnit5부터 test는 public 없어도 됨
    // MemberRepository memberRepository = new MemberRepository(); 싱글톤이기 때문에 에러뜸(new로 생성x)
    MemberRepository memberRepository = MemberRepository.getInstance();

    @AfterEach
    void afterEach() {
        memberRepository.clearStore(); //test가 끝날 때마다 저장소 초기화 ->다음 테스트에 영향 주지 않기 위해
    }

    @Test
    void save() {
        //given(주어졌을때)
        Member member = new Member("hello", 20);

        //when(실행했을때)
        Member saveMember = memberRepository.save(member);

        //then(결과가 이거어야함)
        Member findMember = memberRepository.findById(saveMember.getId()); //저장소에서 멤버를 찾은 다음에
        assertThat(findMember).isEqualTo(saveMember); //찾은 멤버는 저장된 멤버와 같아야 함
    }

    @Test
    void findAll(){ //모든 걸 조회하는 것 test
        //given
        Member member1 = new Member("member1", 20);
        Member member2 = new Member("member2", 30);

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

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

        //then
        assertThat(result.size()).isEqualTo(2); //result의 size가 2개가 되어야 함
        assertThat(result).contains(member1, member2); //데이터가 맞는지 봐야함(result안에 member1과 member2가 있는지)
    }
}

 

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

 

가장 먼저 회원 등록 HTML폼 서블릿으로 만들기

회원등록폼

[MemberFormServlet.java]

//회원등록용 HTML 서블릿
@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");
    }
}

 

실행

  • http://localhost:8080/servlet/members/new-form

 

회원 저장

[MemberSaveServlet.java]

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;

//HTML Form에서 데이터 입력하고 전송 누르면 실제 회원 데이터 저장
@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");
        //get의 쿼리스트링에서 꺼내든 post방식이든 request.getParameter로 보낼 수 있음
        int age = Integer.parseInt(request.getParameter("age"));
        //request.getParameter의 응답결과는 항상 문자라 숫자타입으로 변환 해야함


        //member 객체 만들음
        Member member = new Member(username, age);
        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>");
    }
}
/*
* 1.파라미터 조회하여 Member 객체 만들음
* 2. Member 객체를 MemberRepository를 통하여 저장
* 3. Member 객체를 사용하여 결과 화면용 HTML을 동적으로 만들어서 응답
* */

 

실행

  • http://localhost:8080/servlet/members/save

 

회원 목록 조회

[MemberListServlet.java]

@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문 사용
        for (Member member : members) { //회원 목록 html -> for 루프를 통하여 회원 수 만큼 동적으로 생성하고 응답
            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>");
    }
}

MemberListServlet 동작 순서

  1. memberRepository.findAll()을 통해 모든 회원 조회
  2. 회원 목록 HTML을 for 루프 통해 회원 수 만큼 동적으로 생성하고 응답

실행

  • http://localhost:8080/servlet/members

서블릿만으로 개발하는 것은 복잡하고 비효율적이라 자바 코드로 HTML을 만들어 내는 것 보다 HTML문서에 동적으로 변경해야 하는 부분만 자바 코드를 넣을 수 있으면 훨씬 편리해짐 -> 템플릿 엔진 사용 이유 (JSP, Thymeleaf, Freemarker, Velocity 등)

 


3)JSP로 회원 관리 웹 애플리케이션 만들기

-JSP 사용시 라이브러리 추가 build.gradle에 추가 (스프링 부트 3.0 미만)

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-web'

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

	compileOnly 'org.projectlombok:lombok'
	annotationProcessor 'org.projectlombok:lombok'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'

}

 

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

첫 줄은 JSP문서라는 뜻이다.

첫 줄 제외하고는 HTML과 비슷함. JSP는 서버 내부에서 서블릿으로 변환된다.

 

실행

  • http://localhost:8080/jsp/members/new-form.jsp

 

회원 저장 JSP

[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는 사용 가능(문법상 지원 됨)
  //JSP가 Servlet로 자동으로 변환되기 때문에 가능
  MemberRepository memberRepository = MemberRepository.getInstance();
  System.out.println("MemberSaveServlet.service");
  String username = request.getParameter("username");
  //get의 쿼리스트링에서 꺼내든 post방식이든 request.getParameter로 보낼 수 있음
  int age = Integer.parseInt(request.getParameter("age"));
  //request.getParameter의 응답결과는 항상 문자라 숫자타입으로 변환 해야함


  //member 객체 만들음
  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에서는 JAVA 코드 사용을 위해 스크립트 사용

  • 선언부 : <%! %>로 쓴다. 전역변수, 메서드 선언을 한다.
  • 스크립틀릿 : <% %>로 쓴다. 자바 코드를 입력한다.
  • 표현식 : <%= %>로 쓴다. 자바 코드를 출력한다.

 

회원 목록 JSP

[member.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();
    //private 제외 -> 서블릿의 service 메서드 내부로 들어가기 때문
    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에 진입하면 회원 리포지토리를 통해 먼저 회원 리스트를 조회(findAll())하고 결과 List를 for(Member member: members) {...} 로 처리한다.

서블릿과 JSP를 각각 한쪽만 사용하면 장단점이 명확하다.
서블릿만 사용하면 자바 코드에 View 화면을 만드는 HTML코드가 섞여 코드가 지저분해진다.
JSP를 사용하면 HTML 작업을 깔끔하게 처리할 수 있으나 중간중간 동적 변경 코드가 섞여 JSP가 너무 많은 역할을 한 번에 한다. 비지니스 로직과 데이터 접근 리포지토리 등 많은 코드가 섞여있는 것을 확인할 수 있다.

이런 문제를 해결하기 위해 MVC 패턴이 등장한다. 비지니스 로직을 서블릿 같은 곳에서 몰아서 처리하고, JSP는 HTML로 화면을 그리는 일에 집중하도록 만든다. 다음에는 이를 적용해보려고 한다.

 

 

4)MVC 패턴 -개요

하나의 서블릿이나 JSP만으로 비즈니스 로직과 뷰 렌더링 전부 처리하면, 너무 많은 역할을 하게됨 ->유지보수 어려워짐

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

 

Model View Controller

  • 컨트롤러: HTTP 요청을 받아서 파라미터를 검증하고, 비즈니스 로직을 실행한다. 그리고 뷰에 전달할 결과 데이터를 조회해서 모델에 담는다.
  • 모델: 뷰에 출력할 데이터를 담아둔다. 뷰가 필요한 데이터를 모두 모델에 담아서 전달해주는 덕분에 뷰는 비즈니스 로직이나 데이터 접근을 몰라도 되고, 화면을 렌더링 하는 일에 집중할 수 있다.
  • 뷰: 모델에 담겨있는 데이터를 사용해서 화면을 그리는 일에 집중한다. 여기서는 HTML을 생성하는 부분을 말한다.

 

MVC 패턴 이전
MVC 패턴1

 

MVC 패턴2

 

5) MVC 패턴 -적용

서블릿을 컨트롤러로 사용하고 JSP를 뷰로 사용하여 MVC 패턴 적용해보자.

 

Model은 HttpServletRequest 객체를 사용한다. request는 내부에 데이터 저장소를 가지고 있는데, request.setAttribute() , request.getAttribute() 를 사용하면 데이터를 보관하고, 조회할 수 있다.

 

회원 등록 폼-컨트롤러

[MvcMemberFormServlet.java]

 

@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 dispather = request.getRequestDispatcher(viewPath);//viewPath로 경로 이동
        dispather.forward(request, response); //다른 서블릿이나 JSP로 이동할 수 있는 기능(서버내부에서 다시 호출)


    }
}

 

dispatcher.forward()를 통해 다른 서블릿, JSP로 이동한다. 서버 내부에서 호출이 다시 발생한다.

또한 viewPath를 보면 경로가 "/WEB-INF/..."로 되어 있다. /WEB-INF라는 경로 안에 JSP가 있으면 외부에서 직접 JSP를 호출할 수 없다. 컨트롤러를 통해 우회하여 접근할 수만 있다.(보안 목적 등)

forward vs redirect

redirect의 경우 실제 클라이언트(브라우저)로 갔다가(302) 다시 redirect경로(location)로 요청을 보낸다. 즉 2번 요청, 응답이 오고간다. 반면, 포워드는 서버 내부에서 일어나는 호출로 클라이언트가 인지하지 못한다.

회원을 등록하는 폼-뷰

[new-form.jsp]

main/webapp/WEB-INF/views/new-form.jsp

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

<!-- 상대경로 사용, [현재 URL이 속한 계층 경로 + /save] ->http://localhost:8080/servlet-mvc/members/save-->
<!--절대 경로 action ="/save" ->http://localhost:8080/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을 보면 상대경로(/로 시작X) 로 되어 있다. 이 경우 현재 URL 계층경로 + save가 호출된다.

  • 현재 계층 경로 : /servlet-mvc/members/
  • 결과 : /servlet-mvc/members/save

 

실행

  • http://localhost:8080/servlet-mvc/members/new-form

 

'전송'버튼 클릭시 회원 저장을 하는 컨트롤러

회원 저장-컨트롤러

[MvcMemberSaveServlet.java]

@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);
        System.out.println("member = " + member);
        memberRepository.save(member);

        //Model에 데이터 보관
        request.setAttribute("member", member);
        //request 객체에 데이터를 보관해서 뷰에 전달할 수 있음
        //뷰는 request.getAttribute()를 사용해서 데이터를 꺼내면 됨.

        String viewPath = "/WEB-INF/views/save-result.jsp";
        RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
        dispatcher.forward(request, response);

    }
}

 

회원 저장-뷰

[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)request.getAttribute("member")).getId()%></li> //반환타입이 object라 member로 캐스팅 해야함--%>
<%--    <li>id=<%=((Member)request.getAttribute("member")).getUsername()%></li>--%>
<%--    <li>id=<%=((Member)request.getAttribute("member")).getAge()%></li>--%>

    <li>id=${member.id}</li>
    <li>username=${member.username}</li> <%--getUsername 호출됨 (jsp 프로퍼티 접근법)--%>
    <li>age=${member.age}</li>

</ul>
<a href="/index.html">메인</a>
</body>
</html>

실행

  • http://localhost:8080/servlet-mvc/members/new-form

MVC 덕분에 컨트롤러 로직과 뷰 로직 확실하게 분리 가능하다. 화면에 수정이 필요할 땐 뷰 로직만 변경하면 된다!

 

 

 

회원 목록 조회 -컨트롤러

[MvcMemberListServlet.java]

 

@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 {

        System.out.println("MvcMemberListServlet.service");
        List<Member> members = memberRepository.findAll();

        request.setAttribute("members",members); //model에 담아야함 (키,값)

        String viewPath = "/WEB-INF/views/members.jsp";
        RequestDispatcher dispather = request.getRequestDispatcher(viewPath);
        dispather.forward(request, response);


    }
}

request 객체를 사용하여 List<Member> members를 모델에 보관

 

 

회원 목록 조회-뷰

[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>
    <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>

<%--    <%--%>
<%--        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>

모델에 담아둔 members를 JSP가 제공하는 taglib기능을 사용해서 반복하면서 출력했다.

members 리스트에서 member 를 순서대로 꺼내서 item 변수에 담고, 출력하는 과정을 반복한다.

이 기능을 사용하려면 다음과 같이 선언해야 한다.

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>

 

실행

  • http://localhost:8080/servlet-mvc/members

 

6)MVC 패턴-한계

MVC패턴을 사용해도 문제점 몇 가지가 보인다.

  • 포워드 중복 : VIew 이동 코드의 중복
  • ViewPath에 경로 중복 : /WEB-INF/views/가 중복되며 .jsp도 중복된다.
  • 사용하지 않는 코드 : response를 안 쓰는데도 forward에 보낸다.

이런 문제 해결을 위해 컨트롤러 호출 전에 공통 기능을 처리해야한다. 선처리를 가능하게 해주는 프론트 컨트롤러 패턴을 도입하면 문제 해결이 가능하다.

다음에는 MVC프레임워크를 만들어 프론트 컨트롤러 패턴을 적용해보려고 한다.