framework/spring

스프링 MVC 2 - 서블릿, jsp, MVC 패턴

wooweee 2023. 11. 4. 11:57
728x90

1. domain, repository

  • 도메인, 리포지토리 생성
  • repository로 바로 service역할까지 수행
package hello.servlet,domain.member;

// 회원 도메인 모델 = entity model
@Getter @Setter
public class Member {

    private Long id; //repository에서 id 자동으로 줌
    // 생성자 주입으로 받음
    private String username;
    pirvate int age;
    
    public Member(){};
    
    public Member(String username, int age){
    	this.username = username;
        this.age = age;
    }
}
  • public Member findAll(){ return new ArrayList<>(Store.values()); }
    -> new ArrayList를 반환해서 실제 store를 보호
package hello.servlet.domain.member;

// 회원 저장소 repository 생성

public class MemberRepository{
    // 저장소 map {key, value} = {member.id , member 객체}
    private static Map<Long, Member> store = new HashMap<>;
    private static Long sequence = 0L;
    
    // 1. singleton 객체 생성
    private static final MemberREpository instance = new MemberRepository();
    
    public static MemberRepository getInstance() { return instance; }
    
    // 2. singleton - 외부에서 객체 생성 못하도록 함
    private MemberRepository(){} 
    
    public Member save(Member member){
    	member.setId(sequence++);
    	store.put(member.getId(), member);
        return member;
    }
    
    public Member findById(Long id){ store.get(id); }
    // new ArrayList를 반환해서 실제 store를 보호
    public Member findAll(){ return new ArrayList<>(Store.values()); }
    public void clearStore(){ store.clear(); }
}

Test code - 개인 연습용

더보기
public class MemberRepositoryTest {
	
    MemberRepository memberRepository = MemberRepository.getInstance();
    
    @AfterEach
    void afterEach(){ memberRepository.clearStore();}
    
    @Test
    void save(){
    	//given
        Member member = new Member("woowee", "24");
        //when
        Member savedMember = memberRepository.save(member);
        //then
        Member findMember = memberRepository.findById(savedMember.getId());
        Assertions.assertThat(findMember).isEqualTo(savedMember);
    }
    
    @Test
    void findAll(){
    //given
    Member member1 = new Member("1",12);
    Member member2 = new Member("2",12);
    memberRepository.save(member1)
    memberRepository.save(member2)
    //when
    List<Member> result = memberRepository.findAll();
    //then
    Assertions.assertThat(result.size()).equalTo(2);
    
    Assertions.assertThat(result).contains(member1,member2) // contains()
    }
}

 

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

2.1. 회원 등록 form 생성

@WebServlet(name = "memberFormServlet", urlPatterns = "/servlet/members/new-form")
public class MemberFormServlet extends HttpServlet{
    
    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    	
        response.setContentType("text/html");
        response.setCharacterEncoding("utf-8");
        
        PrintWriter w = response.getWriter();
        
        // servlet에서 직접 html code 생성
        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");
    }
}

 

2.2. 회원 저장 code 생성

  • 2.1. 회원 등록 form 생성으로부터 온 요청 data는 queary params의 형태를 가지고 있어서 queary method() 사용가능
  • getParameter(): string으로만 반환, int가 필요시 변환 필요
  • servlet 코드로부터 html 생성하는 것이여서 java code를 이용한 동적 html 생성가능
@WebServlet(name = "memberSaveServlet", urlPatterns = "/servlet/members/save")
pubic class MemberSaveServlet extends HttpServlet {
	
    // singleton으로 공유되는 repository
    private MemberRepository memberRepository = MemberRepository.getInstance();
	
    @Override
	private void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException{

        // 필요 request data 추출
        // form data도 요청으로 올때 queary params로 오기 때문에 queary method 사용 가능
        String username = requset.getParameter("username");
        int age = Integer.parseInt(request.getParameter("age")); // getParameter은 string으로만 반환되므로 int로 변경 필요
        
        // request data 저장
        Member member = new Member(username, age);
        memberRepository.save(member);
        
        // 응답 data header 준비
        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>");
    }
}

 

2.3. 회원 전체 목록 생성

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


}

 

2.4. servlet 정리

  • 동적 html을 만들수 있는 장점이 있지만 html코드 생성이 매우 복잡하고 비효율적
  • 자바코드로 html 만드느 것보다 html 문서 내에 동적으로 변경해야 되는 부분만 자바 코드 넣는 것이 편리
  • 위의 문제 해결을 위해 template engine이 존재
    ex) JSP, Thymeleaf, Freemarker, Velocity 등

 

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

  • 참고
    • jsp: 점점 사향하는 추세(springboot에선 권장하지 않음)
    • spring은 thymeleaf를 밀고 있는 추세이므로 이제 template engine을 접하는 사람들은 thymeleaf를 사용하는 것을 추천
    • templete engine
      • 원래 response 응답을 보낼때 response 헤더정보들과 getWriter()를 이용한 http body data를 추가해줘야한다.
      • templete engine 사용 시 html 내에 header 정보를 넣을 수 있고  html 코드들이 getWriter() 내부에 들어가서 http body data에 들어간다. 그래서 따로 response.xxx 를 사용하지 않는다.

3.1. 설정

  • library 추가(springboot 3.0 이상)
// build.gradle
// dependencies{} 내부에 작성

dependencies{
//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 추가 끝
}

 

  • webapp 하위에 .jsp 생성
    • webapp - jsp - members - new-form.jsp 생성
    • servlet과 달리 따로 요청 url을 작성하지 않음
    • http://localhost:8080/jsp/members/new-form.jsp(webapp 파일경로와 동일)으로 요청 오면 해당 file의 내용을 응답으로 보내줌
  • webapp에 jsp 넣는 이유
    • template engine이기 때문에 webapp에 넣어줘야함
    • webapp이 root 시작 위치이다.
      from action을 보면 /members/~~ 이렇게 작성을 하면 프로젝트 파일로 봤을때 webapp/members/~~ 와 같은 의미

 

3.2. 회원 등록 form 생성

  • <%@ page contentType="text/html;charset=UTF-8" language="java" %> : jsp file이란 의미. 필수값
<%@ 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>

 

3.3. 회원 저장 code 생성

  • <%@ page import= %>: 자바의 import 문
  • <%  ~~ %>:  java code 입력가능
  • <%=  ~~ %>:  java code 출력가능
  • HttpServlet 의 request, response: jsp에서 지원해줘서 그냥 사용 가능 - 예약어
<%@ page import="hello.servlet.domain.member.MemberRepository" %>
<%@ page import="hello.servlet.domain.member.Member" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>

<%-- request, response 사용 가능: jsp에서 지원해줌 --%>
<%
    MemberRepository memberRepository = MemberRepository.getInstance();

    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);
%>
<%-- html 시작 --%>
<html>
<head>
    <title>Title</title>
    <meta charset="UTF-8">
</head>
<body>
    success
    <ul>
        <li>id=<%=member.getId()%></li>
        <li>id=<%=member.getUsername()%></li>
        <li>id=<%=member.getAge()%></li>
    </ul>
<a href="/index.html">메인</a>
</body>
</html>

 

3.4. 회원 전체 목록 생성

  • out: getWriter() 같은 예약어
<%@ 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 시작 --%>
<html>
<head>
    <title>Title</title>
    <meta charset="UTF-8">
</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>

 

3.5. jsp 정리

  • servlet보다는 확실히 정리된 느낌이긴 하지만 JSP가 너무 많은 역할을 한다.
    java code, 데이터 조회, html 출력

  • 비즈니스 로직은 서블릿이, html은 template engine이 가각 분업을 통한 집중을 하는 것이 좋다.
    이를 실현한 것이 MVC 패턴이다.

 

4. MVC 패턴

4.1. 개요

  • 서블릿, jsp만으로 모든 작업을 수행시 유지보수가 어려워진다.
  • 중요
    • 분리는 변경 주기(변경의 라이프 사이클)가 다를 경우 분리한다를 기준으로 둔다.
    • ex) UI 변경하는 일과 비즈니스 로직 변경하는 일은 서로 따로 따로 일어난다.

 

  • Model View Controller (참고. spring mvc 패턴으로 설명되어 있다.)
  • 실제 mvc 패턴 링크
    1. 컨트롤러
      • HTTP 요청을 받아서 파라미터를 검증하고, 비즈니스 로직을 실행
        • 비즈니스 로직은 원래 service나 domain에서 처리 → repository에 저장 (요 덩어리들을 model이라고 한다.)
        • 현재는 실습이여서 controller에서 service logic 처리를 하고 domain에 저장
      • 뷰에 전달할 결과 데이터를 조회해서 모델에 담는다.

    2. 모델 
      • mvc 패턴의 추상적인 개념인 model이 아닌 spring web에서 말하는 구체적인 model을 의미
      • 데이터를 이동시켜주는 박스 정도로 생각하면 된다.
      • 뷰에 출력할 데이터를 담는다.
      • 뷰가 필요한 데이터를 모두 모델에 담아서 전달해주는 덕분에 뷰는 비즈니스 로직이나 데이터 접근을 몰라도 되고, 화면을 렌더링 하는 일에 집중
        = 말이 모델이 하는거지 크게 보면 controller 가 view와 model(추상개념인 domain 의미)을 분리시켜준다.

      • 모델에 담겨있는 데이터를 사용해서 화면을 그리는 일에 집중
      • HTML을 생성(xml, excel 도 가능)하는 부분을 의미

 

mvc 패턴

 

4.2. MVC 패턴 적용

  • servlet: controller로 사용
  • jsp: view로 사용
  • request 임사공간 : model로 사용

 

  • 구체 내용
    1. Model : HttpServletRequest 객체 사용 - request 내부에 데이터 저장소가 존재
      • request.setAttribute(): 데이터 보관
      • request.getAttribute(): 데이터 조회

    2. /WEB-INF: was의 규칙
      • 해당 경로 내에 jsp가 존재시 외부에서 직접 jsp 호출이 불가
      • 항상 controller를 통해서 jsp 호출

    3. RequestDispatcher
      • controller servlet의 request, response를 다른 source code에서도 (=jsp 파일) 동일하게 사용할 수 있도록 해주는 class

      • getRequsetDispatcher(path) : RequestDispatcher class로 포장된 dispatcher 객체를 얻을 수 있는 기능.
        • request.getRequestDispatcher(viewPath);
          request 내부에 path를 가진 dispatcher를 담는다.

        • 객체바인딩으로 만들어진 객체.
        • 객체바인딩: 개발자의 코드가 아닌 데이터로 객체를 만들어내는 과정
        • 해당 viewPath에서 현 page의 req, res를 사용하겠다는 의미

      • dispatcher.forward(req, res)
        • request에 dispatcher의 path정보가 들어 있다.
        • 더 나아가서 request에 model을 담으면 해당 model도 넘어가진다.

        • 사용할 request, response를 getRequsetDispatcher(path)의 path로 보내주는 method
        • 포워딩은 다른 서블릿이나 JSP로 이동할 수 있는 기능. 서버 내부에서 재호출이 발생

 

  • forward, redirect 차이점 
    • redirect: 2번의 요청과 응답으로 url이 변경
    • forward: 서버 내부적으로 servlet -> jsp로 변경되는 것으로 url이 변경되지 않는다.

출처:&nbsp;https://dololak.tistory.com/502

 

4.3. 회원 등록 form 생성

  • 현재 보낼 logic이 없어도 controller -> view로 가는 mvc 규칙을 지킴
package hello.servlet.web.servletmvc;

// controller
@WebServlet(name= "mcvMemberFormServlet", urlPatterns= "/servlet-mvc/members/new-form")
public class McvMemberFormServlet extends HttpServlet{
    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

    String viewPath = "/WEB-INF/views/new-form.jsp";
    // 해당 경로로 가는 RequestDispatcher class 의 객체 만들기 - 그래야 포워딩 기능을 수행할 수 있기 때문 
    RequestDispatcher dispatcher = requeset.getRequestDispatcher(viewPath);
    dispatcher.forward(requset, response);
    
}

 

  • 상대 경로 사용
    • /servlet-mvc/members/new-form       ->        /servlet-mvc/members/save
  • 절대 경로 사용
    • /servlet-mvc/members/new-form       ->        /servlet-mvc/members/new-form/save
// view

<form action="save" method="post">
    username: <input type="text" name="username" />
    age: <input type="text" name="age" />
    <button type="submit">전송</button>

 

4.4. 회원 저장 code 생성

  • 원래 받은 request에 Attribute 추가(member) -> dispatcher 추가 -> forward
package hello.servlet.web.servletmvc;

@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 {
    	//Controller
        String username = request.getParameter("username");
        int age = Integer.parseInt(reqeust.getParameter("age"));
        
        Member member = new Member(username, age);
        memberRepository.save(member);
        
        //Model
        request.setAttribute("member", member); // {key, value}
        
        //View
        String viewPath = "/WEB-INF/views/save-result.jsp";
        RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
        dispatcher.forward(request.response);
    }

 

  • setAttribute로 저장한 member을 view에서 사용
  • ${~~} : view에 특화된 logic, Model로 부터 받은 member을 바로 쓸 수 있다. 
//view
<ul>
  <li>id=${member.id}</li>
  <li>username=${member.username}</li>
  <li>age=${member.age}</li>
</ul>

 

4.5. 회원 전체 목록 생성

package hello.servlet.web.servletmvc;

@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 {
    	List<Member> members = memberRepository.findAll();
        
        // model 담기
        request.setAttribute("members", members);

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

 

  • ${} : property 접근법, view에 특화된 logic, model로부터 받은 member를 바로 꺼내서 사용 가능
//view
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<html>
    </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>
</html>

 

4.6. mvc 패턴 정리

  • 단점
    • forward 중복, ViewPath 중복, 사용하지 않는 코드  == 공통처리가 어렵다. 
  • 해결방안
    • 프론트 컨트롤러 패턴 사용: controller 호출전에 먼저 공통 기능을 처리하는 역할
  • jsp 정리
    • <% %> : java code 작성란, request, response 사용 가능
    • <%= %> : <% %>에서 생성한 변수 사용
    • <%-- --%> : 주석
    • ${} : servlet에서 전달한 변수 사용
    • <c:forEach> </c:forEach> : 반복문

 

 

이전 발행글 : 스프링 MVC 1 - 서블릿

다음 발행글 : 스프링 MVC 3 - MVC 프레임워크 만들기

 


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