3. 서블릿,JSP,MVC 패턴

ys·2024년 1월 1일

Spring-mvc1

목록 보기
3/7

김영한 강사님의 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술을 듣고 정리한 내용입니다. 자세한 내용은 강의를 참고해주세요

이번강의에는 먼저 서블릿을 이용햇 회원 관리 웹 어플리케이션을 만들어본다. 그 후 서블릿으로 만들었을 때의 불편한점을 JSP를 이용해 해결해 똑같은 회원 관리 어플리케이션을 만들고, 불편한점을 MVC 패턴으로 해결해 본다.

이렇게 실제 기술 발전인 서블릿 -> JSP -> MVC 패턴을 알아보고 코딩을 한 후에, 실제 스프링 MVC 패턴이 얼마나 편리하고 왜 쓰는지를 깨닫는게 이번 강의 목적이다.

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

  • 회원정보 : 이름(username), 나이(age)
  • 기능 요구 사항 : 회원 저장기능, 회원 목록 조회 기능

이렇게 간단한 웹 애플리케이션을 서블릿, JSP, MVC 패턴으로 만들어 보겠다.

  • 이번 예제에서는 먼저 member의 도메인 클래스를 하나 만든다
  • DB를 직접 연동하지 않고, 메모리를 이용해서 저장하기로 한다
  • 서비스는 만들지 않고, repository에 비지니스 로직을 넣기로 함
  • 스프링을 이용하지 않을 거기 때문에, 싱글톤 객체로 repository를 만들기로 함.

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

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

repository(MemberRepository)

/**
 * HashMap은 동시성 문제가 고려되지 않음, 실무에서는 ConcurrentHashMap, AtomicLong 사용 고려
 */

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();
    }
}
  • 이제 서블릿을 이용해 웹 페이지를 만들어보자
  • 먼저 회원 정보를 저장하는 페이지와, 저장한 멤버를 볼 수 있는 페이지, 그리고 전체 멤버를 조회하고 볼 수 있는 페이지 3개를 만든다.

MemberFormServlet - 정보 저장

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

    }
}
  • @Webservlet을 애노테이션을 이용한다
  • Servlet은 응답 메시지를 response로 전달해 줘야 하는데 먼저, content type과 content Encoding을 전달해주고, HTML 코드를 JAVA 코드 안에서 전달해준다...
  • 이때 만든 반환 메시지를 POST방식으로 저장 페이지에 넘겨준다

MemberSaveServlet - 회원 저장

@WebServlet(name = "memverSaveServlet", urlPatterns = "/servlet/members/save")
public class MemverSaveServlet extends HttpServlet {
    MemberRepository memberRepository = MemberRepository.getInstance();
    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        System.out.println("MemverSaveServlet.service");
        String username = request.getParameter("username");
        int age = Integer.parseInt(request.getParameter("age"));

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

    }
}
  • 파라미터를 조회해서 Member 객체를 만든다
  • Member 객체를 MemberRepository를 통해서 저장한다
  • Member 객체를 사용해서 결과 화면용 HTML을 동적으로 만들어서 응답한다.

MemberListServlet - 회원 목록

@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 (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>");
    }
}
  • memberRepository.findAll() 을 통해 모든 회원을 조회한다.
  • 회원 목록 HTML을 for 루프를 통해서 회원 수 만큼 동적으로 생성하고 응답한다.

정리

이렇게, servlet을 이용해 웹 페이지를 만들었다.. 그런데 http응답 메시지를 보낼 때, 자바 코드로 html 문서를 하나하나 작성시 너무 복잡하고 오타날 확률이 크다. 그래서 우리는 html안에다가 자바 코드를 넣는 템플릿 엔진을 사용한다!!!

JSP

서블릿으로 웹 페이지를 만들면, html 코드를 모두 자바 안에 넣어줘야 하므로 매우 불편했다. 그렇다면 템플릿 엔진을 사용해서 코딩을 하면 어떻까?

  • 템플릿 엔진에는 JSP, Thymeleaf, Freemarker, Velocity가 있고 이번엔 JSP를 이용해 코드를 만들어 볼 것이다
  • 먼저 build.gradle에
    implementation 'org.apache.tomcat.embed:tomcat-embed-jasper' implementation 'javax.servlet:jstl'코드를 넣어서 업데이트(코끼리 모양)버튼을 눌러준다 -> 근데 난 오류났어...
    implementation javax.servlet:jstl 대신 아래 코드 3개를 build.gradle에 넣어준다
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는 .java가 아니므로 /main/java안에 넣어주는게 아니라 java와 같은 경로인 webapp에 만들어 준다
  • JSP는 html 코드 안에 java코드도 같이 넣어주는 것이다
    먼저 JSP 코드임을 알려주기 위에 다음과 같은 코드를 맨 첫줄에 적어준다
  • <%@ page contentType="text/html;charset=UTF-8" language="java" %>
  • 그리고 자바 코드임을 HTML코드와 구분해줘야 하기 때문에, <% %> 안에 넣어주고 비지니스 로직을 넣어준다.
    이제 가볍게 코드를 봐보자! 너무 JSP에 집중하지 말자 -> 요즘에는 Thymeleaf를 많이 사용하는 추세다!
<%@ 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>
<%@ 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 사용 가능
 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);
%>
<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>
<%@ 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 코드안에 비지니스 로직 코드인 JAVA코드를 넣은 것이다. 그렇게 되면 JSP가 너무 많은 역활을 하게된다... JAVA코드, 데이터 조회코드,HTML, 리포지토리등등 다양한 코드가 JSP에 노출되어 있다..
유지보수가 엄청 힘들어진다...---> 역할의 분리???
이렇게 등장한 것이 MVC 패턴!!! JSP의 화면을 그리는 부분과, Servlet의 비지니스 로직 부분을 나누어 보자!

MVC 패턴의 등장

  • 비지니스 로직은 서블릿 처럼 다른 곳에서 처리하고, JSP는 목적에 맞게 HTML 화면(view)을 그리는 일에 집중해보자
  • 과거 개발자들도 모두 비슷한 경험!!! -> 그래서 등장한게 MVC 패턴

MVC 패턴

  • 하나의 서블릿, JSP만으로 비지니스 로직, 뷰 렌더링 모두 처리하면 너무 많은 역할 -> 유지보수에 어려움...
  • 변경 라이프 사이클이 다르면 코드를 분리하자!
    • 비지니스 로직 수정과, UI 수정하는 일은 각각 다르게 발생할 가능성이 높고, 대부분이 서로 영향이 없다
    • 이렇게 변경의 라이프 사이클이 다른 부분을 하나의 코드로 관라한다면 -> 유지보수에 어려움을 겪는다
  • 기능 특화
    • JSP는 화면 렌더링에 특화되있다
    • Servlet : java코드 실행 최적화(객체,싱글톤,멀티쓰레드)
  • MVC 패턴은 서블릿이나 JSP 로 처리하던 것을 컨트롤러, 뷰 라는 영역으로 서로 역할을 나누는 것을 말한다
    • 컨트롤러 : HTTP 요청을 받아서 파라미터 검증, 비지니스 로직 실행, 뷰에 전달할 결과 데이터를 모델에 담는다
    • 모델: 뷰에 출력할 데이터를 담는다. 뷰가 필요한 데이터를 모두 모델에 담아주기 때문에, 뷰는 비지니스 로직, 데이터 접근에 대해 몰라도 되고 오직 화면 렌더링에 집중할 수 있다
    • : 모델에 담겨있는 데이터를 사용해서 화면을 그리는 일에 집중. HTML을 생성하는 부분을 말한다

MVC 로직 설명

  • MVC 패턴은 컨트롤러를 거처셔 view로 진입한다
  • JSP의 view 템플릿으로 이동하는 역할(메서드)이 필요하다
  • request.getRequestDispatcher(): 컨트롤러에서 view로 이동할 때 사용하는 메서드, 파라미터로 view의 경로를 받는다
  • dispatcher.forward(request, response)를 이용해 Surblet에서 JSP를 호출할 수 있다
    • 다른 서블릿이나 JSP로 이동할 수 잇는 기능
    • 다시 클라이언트에 가서 웹 브라우저를 응답하고 다시 요청(Redirect) 가 아니라
    • 바로 서버 내부에서 JSP를 호출하는 것 -> 클라이언트가 인지할 수 없다 :forward
  • 컨트롤러, 모델, 뷰 -> 웹 MVC 모델
  • 도메인 : 비지니스 도메인 객체(DTO,VO ->여기서는 model)
  • 서비스 : 핵심 비지니스 로직을 구현
  • 리포지토리 : 데이터 베이스 접근, 도메인 객체를 DB에 저장 및 관리
    • 여기서는 간단한 예제라 서비스와 리포지토리가 합쳐져 있다
    • 또한 DB는 접근하지 않고 메모리에 저장하는 구조이다.

MVC 패턴 - 적용

  • 컨트롤러 : 서블릿
  • 뷰 : JSP
  • 모델 : HttpServletRequest의 객체 -> request의 내부 저장소,request.setAttribute(key,value), request.getAttribute(key)
String viewPath = "/WEB-INF/views/new-form.jsp";
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request,response);
  • 위 코드처럼 Controller에서 View로 이동하는 코드가 필요하다.

회원등록 - MVC 패턴 이용

@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 dispatcher = request.getRequestDispatcher(viewPath);
        dispatcher.forward(request,response);
    }
}
<%@ 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>
  • 위와 같이 Controller와 View를 서블릿과 JSP로 구분해준다.
  • 여기서 보면 경로가 WEB-INF로 조금 신기한데 이렇게 경로를 지정하고 파일을 생성하는 이유가 있다
  • WEB-INF
    • 여기 안에 있는 파일들(html)은 컨트롤러를 거쳐서 불러져야 한다.
    • 이 경로 안에 있는 JSP는 외부에서 직접 JSP를 호출할 수 없다.
    • 컨트롤러에서 호출되어야 함
  • JSP는 ${} 문법인 프로퍼티 문법을 제공
    • request(Model)에 담긴 데이터를 편리하게 조회할 수 있다.

회원저장 - MVC 패턴 이용

@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);
        String viewPath = "/WEB-INF/views/members.jsp";
        RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
        dispatcher.forward(request, response);
    }
}
<%@ 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>

회원조회 - MVC 패턴 이용

@WebServlet(name = "mvcMemberSaveServlet", urlPatterns = "/servlet-mvc/members/save")
public class MvcMemberSaveServlet extends HttpServlet {
    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

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

        //Model에 데이터를 보관한다
        request.setAttribute("member", member);

        String viewPath = "/WEB-INF/views/save-result.jsp";
        RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
        dispatcher.forward(request,response);
<%@ 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>
  • 모두 다 컨트롤러에서 요청 http 메시지에서 파라미터를 받고, 비지니스 로직을 호출하고, Model에 데이터를 담고 , View 부분으로 제어권을 넘기는 부분을 볼 수 있다
  • JSP는 화면을 렌더링 하는 부분에 집중하고 있다

MVC 패턴의 한계

  • MVC 패턴으로 인해 컨트롤러의 역할, 뷰를 렌더링하는 역할 명확하게 구분가능
  • but,,, 코드의 중복이(forward부분,viewPath부분) 너무 많고 필요 없는 코드들도 있다(response부분...)
  • 그리고 공통처리부분또한 어렵다...
    -----> 공통 처리 기능이 필요함!!! 소위 수문장 역할 -> Front Controller 패턴을 도입

스프링 MVC의 핵심 또한 Front Controller부분에 있다. 다음 시간부터 이부분에 대해 배워보겠다!

profile
개발 공부,정리

0개의 댓글