※ 이 포스트는 스프링 실습 과정에서 작성하기 때문에 정보가 부정확할 수 있는 부분이 있습니다.
따라서 참고만 해주시고 틀린 부분이 있을 경우 알려주시면 감사하겠습니다.
이번 포스트는 김영한님의 '스프링 MVC 1편 - 백엔드 웹개발 핵심 기술' 강의를 수강하고 배운 내용을 정리하여 작성하였습니다.
따라서 스프링에 대해 더 자세히 공부하고 싶으신 분은 인프런에서 해당 강의를 수강하시길 추천합니다.
또한 서블릿, mvc가 무엇인지 아예 모르시는 분들은 이전 포스트인 https://fadet-coding.tistory.com/34를
보시거나 다른 기초 설명들을 보고 오시면 좋을 것 같습니다.
Index
1 서블릿과 JSP의 한계
2 mvc와 서블릿 basic - View와 Controller 분리 + Model
3 mvc와 서블릿 - 프론트 컨트롤러의 도입 배경
서블릿과 JSP의 한계
지난 번엔 서블릿에 대해 조금 살펴봤습니다. 요즘은 세줄 요약을 하는 것이 유행이니 이전 포스트의 서블릿 관련 내용을 가지고 해보겠습니다.
1. HTTP 웹 통신 과정에서 중요한 서블릿의 관리를 컨테이너에게 위임한다,
2. HttpServletResquest, HttpServletResponse 등 JAVA코드를 파싱하는 역할도 서블릿에게 위임한다.
3. 개발자들은 다른 과정들을 위임함으로 비즈니스 로직 설계에 몰두할 수 있다.
뭐 세줄 요약이 포스트 맨 처음에 있다는건 중요한 문제가 아니니 넘어갑시다. 하여튼 이제 서블릿을 조금 알았으니 개발자들에게 서블릿의 등장이 어떤 의미로 다가오는지 약간은 이해할 수 있지 않나싶습니다만
"<!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");
아직도 서블릿 안에서 이런 페이지 생성 코드를 JAVA로 하나하나 작성하는 일은 더 알고싶지 않습니다. 그래서 첫 포스트에서 우리는 JSP를 알아봤죠. JSP는 서블릿의 메소드 내 JAVA 코드로 html 메시지를 입력하는 것에서 나아가 하나의 html 문서 안에서 JAVA 코드가 적용되도록 하는 스크립트 언어였습니다. 여기서 이전 포스트를 보지 않으신 분들 중 JSP를 쓰는 것이 서블릿을 사용하는 것과 분리해서 생각하시는 분들이 계실 것 같습니다. 하지만 JSP는 결과적으로 서블릿으로 변환되어 작동합니다, 한마디로 계속 서블릿을 이용하고 있지만 앞선 페이지 생성 방식을 보완하기 위해 중간 과정에 JSP 사용 단계를 추가했다고 생각하면 도비니다.
JSP를 써서 하나의 .jsp파일 내에서 view와 로직을 동시에 관리한다는 게 좋게 다가올 수도 있을 가능성은 존재합니다. 또한 현존하는 레거시 코드 중 아직도 JSP로 짜여 잘 돌아가는 것도 꽤나 많습니다.
// members.jsp
<%@ 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();
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>
...
하지만 이전 포스트에서 알아봤듯이 현재 개발 환경에선 JSP의 단점이 더 부각되는 것이 사실입니다. 지난 번에 짧게 알아본 단점을 조금 더 살펴보겠습니다. 일단 현재 스프링을 개발하고 있다면 '템플릿 엔진'이라는 들어봤으리라 생각합니다. 앞선 글을 보셨으면 알겠지만 템플릿 엔진이란 말 그대로 템플릿 양식에 프로그래밍 언어를 결합한 엔진입니다. 이 말은 얼핏보면 강력한 기능일 수 있겠습니다만 JAVA 사용자 입장에서 봤을 때 모듈화란 특성에 반합니다. 또한 유명한 다른 템플릿 엔진을 꼽아보자면 Thymeleaf, Freemarker 등이 존재하지만 JSP는 이들과는 조금 다른 템플릿 엔진입니다.
저 둘과 다르게 JSP는 서버 사이드 종속 기술이며 사용을 위해 별도의 JSP용 서블릿이 존재해야하며 별도의 forward 요청이 많아질 수 밖에 없습니다. 잘 이해가 안되시죠? 그래서 포스트를 진행해나가면서 이해하실 수 있도록 JSP를 사용할 겁니다.
mvc와 서블릿 basic - View와 Controller 분리 + Model
위에서 서블릿과 JSP에 대해 주절주절 나열했습니다만 결국 하고 싶은 말은 'View와 비즈니스 로직을 분리시키고 싶다'입니다. 이 말은 개발자들에게 가장 Critical한 문제인 View와 로직의 결합으로 한 Component가 너무 많은 역할을 한다는 것으로 이어집니다.
이게 뭐 그렇게 중요하냐고 생각할 수 있겠지만 예시를 들자면 서비스가 진행 중인데 갑자기 UI 변경 요청 사항이 생겨 그것을 반영하려고 jsp를 수정해야 한다고 가정하겠습니다. UI 변경사항은 대부분 html 부분만 수정하면 됩니다만 jsp 파일 안에는 html코드와 JAVA코드가 섞여 있습니다. 위에서 봤던 코드처럼 두 개를 위 아래로 분리하여 관리하면 되지 않느냐?란 물음을 할 수 있지만 하나의 jsp파일이 수 백, 수 천 줄이 되는 것은 드문 일이 아니기에 두 코드는 섞여 있는 경우가 많습니다. 따라서 단순 UI 수정을 하는 간단한 일이나 도메인 수정같은 중요한 일 모두 View와 로직을 동시에 손대야합니다.
그런데 모든 개발자가 단순 UI 수정에 100% 집중력을 가지고 임하지 않기에 실수로 UI부분과 비즈니스 로직을 함께 건드렸다고 해봅시다. 요즘이야 테스트 코드 작성이 표준화되고 Component가 모듈화되었기에 디버깅이 순조로운 경우가 많지만 그런게 없이 디버깅을 해야한다고 생각하면....
이제 우리는 단순 JSP만 사용해 View와 비즈니스 로직을 함께 관리하는 것은 한계가 있음을 느꼈습니다. 이제 우리가 해야할 일은 View와 비즈니스 로직의 분리입니다. 그렇기에 먼저 member.jsp 내에 상단에는 비즈니스 로직을 하단에는 뷰 템플릿 부분이 존재하며 그 둘을 각각 분리합니다.
여기서 기존에 name, age 등의 field가 정의된 Member와 그 Member를 저장하는 MemberRepository Class 등 기존 서버에 작성된 유저, 게시글, 상품같은 Cilent 관련 Class들이 존재합니다. 만약 요청이 유저 목록을 보여달라는 것이면 MemberRepository에서 모든 유저 정보를 list로 뽑을텐데 View나 Controller 둘다 이 객체를 저장해두기 위한 Component로는 적절치 않습니다. 그래서 이 객체를 담아두기 위해 Model이라는 Component를 하나 도입합니다. 앞으로는 이 Model에 가공된 데이터를 담아두고 Controller와 View가 필요할 때 전달해주면 되겠네요.
Model에 담는 값들과 Business Logic이 헷갈리신다면 'A가 상품B를 주문하면 C주소로 배달됨'에서 사람 A, 상품 B, 주소 C 정보가 Model에 담을 값들, '사람~가 상품~를 주문하면 주소~로 배달됨'이라는 서비스 흐름이 Business Logic으로 이해하시면 됩니다.
이 Model을 분리했던 View와 Controller와 연결하면 가장 기본적인 MVC 패턴이 완성됩니다. 이제 MVC 패턴에서 각 컴포넌트들이 등장했고 각 컴포넌트의 역할을 어렴풋이 알게되었지만 역할이라는 추상적인 개념만 살펴봤지 이 컴포넌트들의 구현체가 무엇인지는 나오지 않았습니다.
basic 버전에서 각 컴포넌트들의 초기 구현체는 다음과 같습니다. JSP는 원래 템플릿 엔진으로서 뷰 역할을 맡았기에 View Component의 구현체는 변수 등 컨트롤러로 분리되고 남은 간단한 JAVA 코드와 HTML만이 존재하는 .jsp파일입니다. Controller Component의 구현체는 비즈니스 로직을 처리해야하는 JAVA 코드를 담아야하기에 서블릿 하나를 컨트롤러로 만들면 될 것 같습니다. 마지막으로 주의해야할 것은 Model Component의 구현체입니다. Model의 구현체는 jsp나 서블릿처럼 하나의 파일로 저장되지 않고 request에 덤으로 얹혀져 같이 이동하거나 model이라는 빈 객체 덩어리를 생성하는 등 객체 정보를 담을 수 있는 것이면 충분합니다. 쉽게 객체를 담을 수 있는 통 정도로 생각하시면 편합니다. 위 그림처럼 1 기존 Member, MemberRepository 등 Client 관련 Class를 가공한 객체를 만든 후에 2 그 객체를 model이라는 통에 담아두어 Controller나 View에 전달 됩니다.
mvc와 서블릿 - 프론트 컨트롤러의 도입 배경
basic 버전에서 우리가 아는 MVC의 형태를 갖췄기에 각 컴포넌트의 역할이 나누어진 것은 좋습니다만 문제가 남아있습니다. 일단 basic에서 controller 역할을 맡는 유저 목록을 보여주는 서블릿 하나를 살펴보겠습니다.
@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);
String viewPath = "/WEB-INF/views/members.jsp";
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);
}
}
해당 서블릿의 RequestDispatcher를 살펴보겠습니다. 먼저 RequestDispatcher의 경우 생소할 수도 있기에 설명하자면 사용자로부터 요청이 오면 다른 자원으로 그 요청을 넘기거나 요청의 처리 결과를 얻어오는 역할을 하는 클래스입니다. 위 코드에서 viewPath로 지정한 members.jsp를 경로로 하는 dispatcher를 호출했습니다. 그리고 members 객체를 요청에 담아서(이 과정이 members 객체를 model에 담는 것) forward 메서드를 통해 제어권을 JSP파일로 넘겨줍니다.
여기까지만 보면 controller에 요청이 왔을때 model에 객체를 담아 view로 전달해주는 일련의 과정이 잘 이루어지는 것처럼 보입니다. 하지만 이 과정에는 허점이 존재합니다.
우선 해당 서블릿 어디에도 response 객체를 사용하는 코드가 한 줄도 없습니다. 물론 response를 사용하는 서블릿도 당연히 일부 있겠지만 지금 문제는 response를 사용하지 않는 서블릿들이 모두 response 객체를 인자로 받아야한다는 것입니다. 또한 지금은 이해가 안되실 수도 있지만 HttpServletXXX 객체는 테스트 코드를 작성하기도 어렵습니다. 이것은 나중에 다시 다루도록 하겠습니다.
하지만 더 큰 문제는 따로 있습니다. 지금의 mvc 패턴에서 Controller에 요청이 오면 model에 객체를 담은 후 그 model을 RequestDispatcher로 요청과 함께 넘겨줘야합니다. 그리고 그 넘겨주는 행위를 하는 메서드가 forward()입니다. forward 메서드는 서블릿에서 서블릿으로 요청의 제어권을 넘겨주는 메서드이며 우리는 jsp가 결과적으로 서블릿으로 변환된다는 것을 알기에 별 문제가 없어보입니다.
그러나 우리는 지금 멤버 목록 서블릿 하나만 보고 있지 않나요? 지금 상황에서 웹을 만들기 위해선 서블릿이 멤버 목록, 멤버 등록, 멤버 수정 등 엄청 많이 필요합니다. 그런데 멤버 목록 하나의 서블릿을 꼽아서 막상 비즈니스 로직이라고 할 부분은 list를 호출하는 한 줄에 불과한데 반해 요청을 분류하고, viewPath를 작성하고, dispatcher를 생성하고 forward 요청을 하는 코드가 더 깁니다. 이런 서블릿이 여러 개 있으면 그 서블릿의 개수에 비례하여 공통 과정이 중복되는 문제가 발생합니다. 개발자들이 가장 싫어하는 것은 쓸데없는 중복이며 이런 중복은 없애야 마땅합니다. 그래서 고민하던 개발자들은 현재 스프링 MVC의 핵심이 되는 FrontController를 도입하기 시작합니다.
FrontController란 사용자들로부터 요청이 오면 그 요청의 종류에 따라 요청에 맞는 서블릿(컨트롤러)을 호출하는 서블릿이자 요청이 가장 먼저 만나는 Controller입니다. 이 FrontController의 도입 전 basic 버전을 생각해보면 사용자가 여러 명 있을 때 서로 요청이 모두 다르다면 사용자 모두 개별의 컨트롤러를 호출하여 로직이 진행되고 그 로직에서 공통 과정이 중복되는 문제가 존재했습니다.
하지만 FrontController의 도입 이후에는 요청을 공통으로 처리하는 FrontController가 요청을 대신 분류하고 현재 서블릿(컨트롤러)에 존재하던 중복 코드를 대신 도맡아 처리해줄 것이며 앞으로 이 과정을 순서대로 알아보겠습니다. 참고로 이 프론트컨트롤러를 사용하는 MVC 패턴은 MV+C로 생각하면 편한데, 그 이유는 다음 포스트를 보면 아시게 될겁니다.
프론트 컨트롤러의 도입부터 수정되는 과정은 다음 포스트에서 다루도록 하겠습니다.
refer
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-1/dashboard
스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술 - 인프런 | 강의
웹 애플리케이션을 개발할 때 필요한 모든 웹 기술을 기초부터 이해하고, 완성할 수 있습니다. 스프링 MVC의 핵심 원리와 구조를 이해하고, 더 깊이있는 백엔드 개발자로 성장할 수 있습니다., -
www.inflearn.com
'Spring' 카테고리의 다른 글
5_짧_th:field와 th:value (0) | 2022.06.25 |
---|---|
Spring_정리5_Spring의 구조 훑어보기_FrontController (0) | 2022.05.09 |
Spring_정리3_Spring의 구조 훑어보기_서블릿 (0) | 2022.05.05 |
Spring_정리2_Spring 이전 JAVA 웹 개발의 역사 훑어보기 (0) | 2022.04.29 |
Spring_2_Spring Validation 기초 (0) | 2022.04.28 |