공부함
서블릿과 MVC 패턴과 프론트 컨트롤러 패턴 본문
서블릿이란?
서블릿(Servlet)이란 동적 웹 페이지를 만들 때 사용되는 자바 기반의 웹 애플리케이션 프로그래밍 기술이다. 서블릿은 웹 요청과 응답의 흐름을 간단한 메서드 호출만으로 체계적으로 다룰 수 있게 해준다.
서블릿은 클라이언트의 요청에 따라 동적으로 작동하는 웹 애플리케이션 컴포넌트이다.
서블릿이 필요한 이유
서블릿을 사용하면 개발자는 의미있는 비즈니스 로직 개발에만 집중하면 되므로 효율적인 개발이 가능하다.
@WebServlet(name = "helloServlet", urlPatterns = "/hello")
public class HelloServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 비즈니스 로직
}
}
- urlPatterns에 해당하는 url이 호출되면 해당 서블릿 코드가 실행된다. (service 메서드)
- service 메서드의 파라미터인 HttpServletRequest와 HttpServletResponse를 통해 Http 요청, 응답에 편리하게 접근할 수 있다
- 스프링이 내장 tomcat 서버를 생성하고, 내장 톰캣 서버가 서블릿을 생성해 놓습니다.
- Http 요청이 들어오면 요청을 토대로 request 객체를 생성하고, 서블릿에 의해 요청 처리 후 response 객체를 토대로 Http 응답을 생성해 반환합니다.
서블릿 컨테이너
- 서블릿을 지원하는 WAS를 서블릿 컨테이너라 한다. (ex : tomcat)
- 서블릿 생성-초기화-호출-종료의 생명주기를 관리한다.
- 서블릿 객체는 최초에 하나의 인스턴스를 생성하고 재활용하는 싱글턴 방식이다. 즉 모든 클라이언트 요청은 같은 서블릿 인스턴스에 의해 처리된다.
쓰레드 풀
- WAS는 쓰레드 풀 방식으로 쓰레드를 관리한다. 요청마다 쓰레드를 할당받고 사용 후 반납하는 방식이다.
- 쓰레드 풀에 생성 가능한 최대 쓰레드 수를 튜닝하는 것이 중요하다. (톰캣은 최대 200개)
- WAS는 멀티 쓰레드에 관한 부분을 알아서 처리하기 때문에 개발자는 싱글 쓰레드 상황인 것 처럼 편하게 개발할 수 있다.
템플릿 엔진
@WebServlet(name = "helloServlet", urlPatterns = "/hello")
public class HelloServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 비즈니스 로직
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>");
// .. 등등 복잡한 view를 위한 html 코드
}
}
서블릿과 java 코드만 활용해 화면을 동적으로 표시하려면 위와 같이 복잡한 html 코드를 java로 작성해야 하는 불편함이 있다.
템플릿 엔진이란
- 이러한 복잡성을 해소하기 위해 템플릿 엔진을 활용할 수 있다. 템플릿 엔진은 html 파일상에 필요한 부분에 java 코드를 넣어 동적으로 구성할 수 있다.
- 대표적인 템플릿 엔진으로는 JSP, Thymeleaf 등이 있다.
<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>
예를 들자면 JSP를 활용해 이런 식으로 html 파일에 java 코드를 같이 사용해 동적으로 구성할 수 있다.
MVC 패턴
서블릿+JSP 엔진의 조합으로 개발하면 서블릿만 사용할 때 보다는 편리하지만 여전히 문제점이 있다.
- JSP에 비즈니스 로직이 노출된다 => JSP가 너무 많은 역할을 한다.
- 복잡한 프로젝트라면 JSP 코드가 굉장히 복잡해질 것이고 유지보수성이 떨어진다.
- 비즈니스 로직이나 UI 하나만 변경하려고 해도 둘이 같이 있는 코드를 손대야 한다.
- UI와 비즈니스 로직을 수정하는 라이프 사이클이 다를 가능성이 높다. 라이프 사이클이 다른 부분이 같이 존재하면 유지보수하기 좋지 않다.
이러한 문제를 해결하기 위해 MVC 패턴을 사용할 수 있다.
- Controller : Http 요청을 받아 파라미터 검증 후 비즈니스 로직을 실행한다. 뷰에 전달할 데이터는 모델에 담는다.
- Model : 뷰에 출력할 데이터를 담아둔다. 뷰는 모델 덕분에 비즈니스 로직, 데이터에 관해 모르고 렌더링에만 집중할 수 있다.
- View : 모델에 담겨 있는 데이터로 화면을 렌더링하는 일에 집중한다.
- Service : 비즈니스 로직을 컨트롤러에 두면 컨트롤러의 일이 너무 많아져 일반적으로 서비스 계층에 비즈니스 로직을 두고 컨트롤러는 필요한 비즈니스 로직을 호출한다.
@WebServlet(name = "helloServlet", urlPatterns = "/hello")
public class HelloServlet extends HttpServlet {
private MemberRepository memberRepository = MemberRepository.getInstance();
@Override
protected void service(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String username = request.getParameter("username");
Member member = memberRepository.findByMemberName(username);
reponse.setAttribute("member", member);
String viewPath = "/WEB-INF/views/showMember.jsp";
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);
}
}
- 서블릿(컨트롤러)에서 비즈니스 로직을 처리하고, HttpServletResponse 객체(모델)에 필요한 데이터를 저장해 JSP(뷰)로 넘기는 식으로 MVC 패턴을 적용했다.
프론트 컨트롤러 패턴
MVC 패턴은 유용하지만 컨트롤러에서 viewPath, forward 등의 중복이 발생한다. 또 request, response 객체를 항상 사용하는 것이 아닌 사용할 때도 있고 사용하지 않을 때도 있다.
이러한 문제를 해결하기 위해 컨트롤러 호출 전 수문장 역할을 하는 프론트 컨트롤러를 호출하는 프론트 컨트롤러 패턴을 도입할 수 있다.
- 프론트 컨트롤러가 공통 요청을 처리하고 알맞은 컨트롤러를 찾아서 호출한다.
- 나머지 컨트롤러들은 서블릿을 사용하지 않아도 된다
- Spring Web MVC의 DispatcherServlet이 프론트 컨트롤러 패턴으로 구현된 것이다.
프론트 컨트롤러 패턴 이해하기
김영한님 강의에서는 프론트 컨트롤러를 도입하며 V1~V5까지 발전시켜 왜 지금의 Spring MVC 형태가 되었는지 설명해주신다. 이 과정이 매우 도움되기 때문에 간단하게 정리해보겠다.
V1
- 프론트컨트롤러는 Map으로 컨트롤러를 urlpattern에 대해 매핑한다. 요청 url에 따라 컨트롤러를 찾고 해당 컨트롤러의 process 메서드를 호출한다
- 컨트롤러는 process 메서드를 오버라이딩해야한다. process에서는 비즈니스 로직을 처리하고 forward를 호출해 뷰로 넘어간다.
void process(HttpServletRequest request, HttpServletResponse response);
V2
- V1에서는 컨트롤러의 process에서 forward를 호출하는 부분이 컨트롤러마다 중복된다. 이것을 해결하기 위해 뷰를 처리하는 객체 MyView를 만든다.
- MyView는 뷰 경로 viewPath를 필드로 갖고, forward를 호출하는 render 메서드를 갖는다.
- 컨트롤러는 process 호출 결과 MyView를 반환한다. 프론트컨트롤러에서 반환된 MyView를 통해 render를 호출한다.
MyView process(HttpServletRequest request, HttpServletResponse response);
V3
- 컨트롤러에서 서블릿 종속성을 제거한다.
- HttpServletRequest대신 Java의 Map을 사용해 필요 데이터를 넘긴다.
- HttpServletResponse를 model로 사용하는 대신 Model 객체를 만들어 반환한다.
- 뷰 이름에서 중복을 제거한다.
- 컨트롤러는 뷰의 논리 이름을 반환하고 물리 이름은 프론트 컨트롤러에서 만들어 처리한다.
- 따라서 ModelView를 도입한다.
public class ModelView {
private String viewName;
private Map<String, Object> model = new HashMap<>();
...
}
- ModelView는 viewName과 model을 갖는다.
- HttpServletResponse 대신 ModelView의 model을 사용해 데이터를 view로 넘긴다.
ModelView process(Map<String, String> paramMap);
- 컨트롤러는 서블릿 종속성이 제거되었다.
- process는 ModelView에 viewName과 model 데이터를 담아 반환한다.
// service 메서드
// 생략
Map<String, String> paramMap = createParamMap(request);
ModelView mv = controller.process(paramMap);
String viewName = mv.getViewName();
MyView view = viewResolver(viewName);
view.render(mv.getModel(), request, response);
}
private MyView viewResolver(String viewName) {
return new MyView("/WEB-INF/views/" + viewName + ".jsp");
}
- 프론트 컨트롤러에서는 Java의 Map을 사용해 (paramMap) 컨트롤러로 데이터를 넘긴다.
- 컨트롤러는 process 호출 결과 viewName과 model을 담은 ModelView를 반환한다.
- viewResolver로 view의 물리이름을 얻는다.
- MyView를 통해 render를 호출해 forward를 호출한다.
V4
- 컨트롤러에서 ModelView를 생성, 반환하는 과정이 번거로워 String으로 viewName만 반환하도록 변경한다.
- 모델을 컨트롤러에서 생성하지 않고 프론트컨트롤러에서 생성해 파라미터로 넘겨준다.
Map<String, String> paramMap = createParamMap(request);
Map<String, Object> model = new HashMap<>(); //추가
String viewName = controller.process(paramMap, model);
MyView view = viewResolver(viewName);
view.render(model, request, response);
}
- 프론트컨트롤러에서 model을 생성해 넘겨준다.
@Override
public String process(Map<String, String> paramMap, Map<String, Object> model) {
List<Member> members = memberRepository.findAll();
model.put("members", members);
return "members";
}
- 컨트롤러에서는 비즈니스 로직을 처리하고 model에 필요 데이터를 넣어주고 view의 논리 이름을 반환하기만 하면 된다.
V5
- 어댑터를 사용해 다양한 종류의 핸들러(컨트롤러)를 사용할 수 있게 한다.
- 프론트 컨트롤러가 핸들러를 호출하는 것이 아닌 어댑터가 핸들러를 호출한다.
public interface MyHandlerAdapter {
boolean supports(Object handler);
ModelView handle(HttpServletRequest request, HttpServletResponse response,
Object handler) throws ServletException, IOException;
}
- 어댑터는 handle에서 컨트롤러를 호출하고 결과로 ModelView를 반환해야 한다. 컨트롤러가 ModelView를 반환하지 못한다면 (다양한 핸들러를 호출할 수 있으므로) 어댑터가 만들어서 반환해야 한다.
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
Object handler = getHandler(request);
if (handler == null) {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
MyHandlerAdapter adapter = getHandlerAdapter(handler);
ModelView mv = adapter.handle(request, response, handler);
MyView view = viewResolver(mv.getViewName());
view.render(mv.getModel(), request, response);
}
- 프론트컨트롤러는 먼저 핸들러를 찾고, 핸들러를 처리할 수 있는 핸들러어댑터를 찾고, 핸들러어댑터의 handle을 호출한다.
- 핸들러어댑터는 handle에서 핸들러를 호출해 비즈니스 로직을 처리하고 결과적으로 ModelView를 반환한다.
V5까지 따라왔으면 용어만 살짝 다르다 뿐이지 Spring MVC 구조랑 똑같다. 우왕
결론
서블릿과 Spring MVC에 대해 복습할 겸 정리했는데, 웬일로 완벽히 이해한 것 같다.
그런데 퇴근하고 피자먹고 운동까지하고 쓴거라 넘모 힘들어서 뒤로 갈수록 살짝 글이 성의가 없다. ㅎ.ㅎ
출처
서블릿 관련해서 참고한 개발블로그
https://www.inflearn.com/course/lecture?courseSlug=%EC%8A%A4%ED%94%84%EB%A7%81-mvc-1&unitId=71225
김영한님 spring mvc1 인프런 강의
'스프링' 카테고리의 다른 글
dto를 사용해야 하는 이유 (0) | 2024.09.01 |
---|---|
ApplicationContext (0) | 2024.08.28 |
카카오 로그인 (0) | 2024.08.02 |
@RestControllerAdvice를 활용한 스프링 예외 처리 (0) | 2023.12.19 |
SpringDataJpa 사용자 정의 Repository 적용 (1) | 2023.12.09 |