06. MVC와 Model2
- -
이번 포스트에서는 MVC 패턴과 Servlet에서의 Model2 구조에 대해 살펴보자.
MVC 패턴
MVC 패턴이란?
애플리케이션 개발에 가장 흔하게 적용되는 패턴으로 MVC 패턴이 있다. 이는 애플리케이션을 역할에 따라 Model-View-Controller로 모듈화하는 패턴이다.
- Model: 업무 즉 business logic을 담당하는 모듈로 필요하다면 DB를 연동(persistence logic)하기도 한다.
- View: 화면에 보여주기 위한 presentation logic 처리하는 모듈이다.
- Controller: 전체적인 흐름을 제어하는 모듈이다.
이렇게 모듈을 분리하는 목적은 필요에 따라 모듈을 쉽게 교체하기 위해서이다. 만약 현재 View가 A라는 기술로 작성되어있는 상황에서 B라는 기술로 대체해야 한다고 생각해보자. 만약 3개의 모듈이 섞여 있다면 변경하기가 힘들지만 MVC로 쪼개져 있다면 C와 M은 전혀 건드리지 않고도 V만 교체할 수 있다.
그럼 M.V.C는 각각 어떤 기술로 작성해야 할까? Model 영역은 비지니스 로직을 작성하는 부분이기 때문에 당연히 Java로 작성해야 한다. 화면을 만들 때 Servlet에서 HTML 태그를 작성하는 것은 아주 힘든 일이다. 따라서 View는 JSP로 작성한다. 마지막 Controller는 Servlet과 JSP가 각각 모두 담당할 수 있다. 과연 누가 이 영역에 적합할까?
Model 1과 Model 2
Java의 Web Application에서는 Controller의 역할을 누가 맡느냐에 따라 Model 1 방식과 Model 2 방식으로 나눈다.
|
|
Model 1 | Model 2 |
Model1 방식은 JSP가 Controller 역할을 담당하고 Model2 방식에서는 Servlet이 Controller 역할을 담당한다. 각각의 장/단점은 다음과 같다.
구분 | 장점 | 단점 |
Model 1 |
- 구현이 간단하고 작업 속도가 빠르다.
- 작은 규모의 프로젝트에 적합하다. |
- 비지니스 로직과 프레젠테이션 로직이 결합되어 코드가 복잡하다.
- 테스트와 유지보수가 어렵다. |
Model 2 |
- 각 열할에 따라 명확히 분리된다.
- 코드 재사용성, 유지 보수성이 좋다. |
- 학습 비용이 크다.
- 초기 설정 및 구조가 복잡하다. |
일반적으로 Model 1은 유지보수할 일이 별로 없고 빨리 개발해야 하는 작은 규모의 사이트 개발에 용이하다. 그런데 이런 프로젝트는 거의 없다. Model 2의 경우는 시스템은 복잡해보이지만 유지보수에 용이해서 엔터프라이즈 환경에 적합하다. 따라서 앞으로 우리가 나아갈 방향도 Model 2라고 볼 수 있다.
Model 2 기반으로 웹 애플리케이션을 만들다 보면 Controller(Servlet)가 요청을 받아서 결국 View(JSP)가 응답하게 해야 한다. 즉 2개 이상의 웹 컴포넌트가 연계해서 동작한다. 그냥 객체의 사용이 아니라 URL 기반으로 동작하는 웹 컴포넌트들을 내부적으로 어떤 방식으로 연결할 수 있을까?
이런 흐름을 제어하기 위해서는 forward와 redirect 방법이 사용된다.
forward
forward?
forward 방식은 A 컴포넌트에서 B 컴포넌트를 호출할 때 A 컴포넌트가 가지고 있던 HttpServletRequest와 HttpServletResponse를 그대로 B 컴포넌트에 넘겨주는 방식이다.(email의 forward와 동일한 개념이다.)
위 그림에서 처음 사용자의 요청을 받아들인 것은 "/gugu" 서블릿이다. 이 Servlet(C)이 뒷단의 GuguService(M)을 이용해서 Gugu단을 생성 후 결과를 리턴한다.(이때는 자바 -> 자바 호출이다.) 다시 C에서 V(guguResult.jsp)를 호출하려면 이제 URL 기반의 호출이 필요하다. 여기서는 forward를 사용할 껀데 forward는 내부적으로 호출되면서 servlet이 가지고 있던 request, response를 그대로 전달해준다.
forward를 사용하기 위해서는 RequestDispatcher 객체를 사용한다.
RequestDispatcher dispatcher = req.getRequestDispatcher("대상 컴포넌트 path");
dispatcher.forward(req, resp);
여기서 [대상 컴포넌트 path]를 URL 기반으로 작성하는데 절대경로를 나타내는 '/'가 context root임을 기억해두자.
추가적인 정보 전달
A 컴포넌트에서 B 컴포넌트가 내부적으로 호출되면서 추가적으로 생성된 데이터를 전달할 필요가 있는데 이 데이터를 attribute라고 한다. forward 과정에서는 내부적으로 request가 그대로 전달되기 때문에 request를 통해 attribute를 전달한다.
다음은 attribute와 관련된 HttpServletRequest의 메서드들이다.
public abstract void setAttribute(String name, Object o )
public abstract Object getAttribute(String name)
public abstract void removeAttribute(String name)
어떤 '정보를 request가 완료될 때까지 저장'하기 위해 attribute와 parameter가 사용되는데 차이점을 정리해보자.
구분 | Parameter | Attribute |
정의 | 클라이언트가 최초 요청 시 전송한 데이터 | 서버 측에서 생성해서 저장된 데이터 |
유효 범위 | 요청이 완료될 때까지 유효 | 요청이 완료될 때까지 유효 |
데이터 타입 | 문자열로만 저장됨 | 다양한 객체 타입 저장 가능 |
접근 방법 | request.getParameter(name)으로 접근 | request.getAttribute(name)으로 접근 |
변경 가능성 | 읽기 전용 | 추가/수정/삭제 가능 |
용도 | 사용자 입력 데이터 처리 | 요청 처리 중 필요한 데이터 저장 |
forward를 이용한 model2 기반의 MVC 프로그래밍
이제 구구단을 처리하던 기존의 Servlet을 M/V/C로 쪼개서 작성해보자.
먼저 구구단을 작성하는 Model은 다음과 같이 작성할 수 있다. 웹과 전혀 상관 없는 그냥 평범한 자바 코드다!
package com.doding.simpleweb.model.service;
public class SimpleService {
public String getGugudan(int dan) {
StringBuilder data = new StringBuilder();
for (int i = 1; i < 10; i++) {
data.append(dan).append("*").append(i).append("=").append(i * dan).append("\n");
}
return data.toString();
}
}
이제 결과를 보여줄 View인 guguResult.jsp를 만들어보자. request의 result라는 attribute를 사용하고 있다는 것을 명심하자. 구구단 호출 결과가 담겨야 할 속성의 이름이 바로 result 이다.
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>구구단 호출 결과</title>
</head>
<body>
<%@include file="/include/header.jsp"%>
<!-- request 내장 객체에 result라는 이름으로 담긴 attribute를 가져와서 출력한다. -->
<textarea cols="30" rows="10"><%=request.getAttribute("result")%></textarea>
<!-- session 내장 객체에 result라는 이름으로 담긴 attribute를 가져와서 출력한다. -->
<textarea cols="30" rows="10"><%=session.getAttribute("result")%></textarea>
<br>
<a href="<%=root%>/static/gugu.html">다시하기</a>
</body>
</html>
다음으로 사용자의 요청을 받아들이는 Controller인 Servlet이다. Servlet은 언제나 3가지 일을 처리한다.
- 사용자의 요청 검증
- 비지니스 로직 전문가(Model)활용 - 요청 사항 처리
- 2의 결과를 출력할 화면 전문가(View)활용
forward로 처리할 때 redirect로 처리할지를 구분하기 위해서 forward라는 이름으로 parameter를 추가로 받아보자. 이제 비지니스 로직을 직접 처리 하지 않고 SimpleService를 이용하고 있고 결과를 result라는 attribute에 담았다. 또한 결과를 직접 출력하지 않고 guguResult.jsp를 활용한다.
private String gugu(HttpServletRequest request, HttpServletResponse response)
throws IOException {
String forward = request.getParameter("forward");
String danStr = request.getParameter("dan"); // 1. 파라미터 확인 및 검증
StringBuilder data = new StringBuilder();
if (danStr == null || danStr.isEmpty()) {
data.append("dan을 입력하세요.");
} else {
int dan = Integer.parseInt(danStr); // 2. 비지니스 로직 처리
data.append(new SimpleService().getGugudan(dan));
}
if(forward!=null){
request.setAttribute("result", data); // 2-2. 추가적인 데이터 설정
return "/guguResult.jsp"; // 3. 화면 연결
}else{
return null;
}
}
doGet에서는 위의 return 경로를 forward로 호출해준다.
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
logging(req);
String path = req.getServletPath();
String target = "index.jsp";
if(path.equals("/gugu")){
target = gugu(req,resp);
}
// 화면 연결
move(target, req, resp);
}
private void move(String target, HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
RequestDispatcher dispatcher = req.getRequestDispatcher(target);
dispatcher.forward(req, resp);
}
마지막으로 gugu.html에서 forward를 이용할지, redirect를 이용할 지 설정할 수 있도록 화면을 변경해보자. (gugu.html은 jsp가 아니므로 include를 사용할 수 없다.)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>구구단출력</title>
</head>
<body>
<form action="/simpleweb/gugu">
<label for="dan">단: </label><input type="text" name="dan" id="dan">
<label for="isForward">forward?<input type="checkbox" name="forward" id="isForward"></label>
<button type="submit" formmethod="get">전송:get</button>
<button type="submit" formmethod="post">전송:post</button>
</form>
</body>
</html>
이제 각자의 영역에서 각자 잘 하는 코드를 작성하는 것을 알 수 있다.
redirect
redirect?
redirect는 새롭게(re) 지시하는(direct)것으로 현재 실행중인 페이지의 실행을 중단하고 다른 웹 자원을 대신 호출하도록 한다. 즉 다음 요청에서처럼 일단 "요청 1"을 받아서 서버에서 처리하다가 일단 클라이언트로 응답을 보내는데 이 응답에는 화면에 보여줄 데이터가 있는 것이 아니라 새롭게 요청할 정보가 담겨있다.
그래서 이 2번째 요청을 하고 그 결과를 화면에 출력하게 된다. 클라이언트의 입장에서는 결국 2번의 요청과 응답이 오고간다. redirect를 사용하기 위해서는 response 객체의 sendRedirect 메서드를 사용한다.
response.sendRedirect("대상의 location");
추가로 [대상의 location]을 URL 기반으로 작성하는데 절대경로를 나타내는 '/'가 froward와 달리 container root다. 따라서 내부 리소스를 호출하더라도 context path를 개입시켜야 한다.
response.sendRedirect("/guguResult.jsp"); // http://localhost:8080/guguResult.jsp => 404
response.sendRedirect("/simpleweb/guguResult.jsp"); // http://localhost:8080/simpleweb/guguResult.jsp
추가적인 정보 전달
forward와의 차이점은 request와 response를 넘겨주지 않는다는 점이다. 즉 (요청1, 응답1)과 (요청2, 응답2)는 전혀 연결고리가 없다. 따라서 요청1에서 저장해놓은 attribute를 요청2에서 확인할 수 있는 방법이 없다. 그럼 요청2에서 정보를 확인하려면 어떻게 해야 할까? 정답은 request 보다 좀 더 오래 유지되는 영역에 값을 저장하는 것이다.
웹에서 데이터를 저장하기 위한 공간의 유효기간은 page > request > session > application으로 확장된다. 즉 request보다 좀 더 정보를 유지할 수 있는 공간은 session이다. 상세한 것은 뒤에 살펴보고 일단 사용 예를 보자. attribute를 관리하는 방법은 request의 attribute를 처리하는 방법과 동일하다.
HttpSession session = req.getSession(); // HttpServletRequest에서 HttpSession 획득
session.setAttribute("result", data); // session 영역에 자료 저장
redirect를 이용한 model2 기반의 MVC 프로그래밍
일단 servlet 영역은 다음과 같이 변경해볼 수 있다. gugu와 doGet에서 각각 redirect를 위한 처리만 추가 되었다. gugu에서는 service의 처리 결과를 session에 request 속성으로 담아줬으며 redirect:를 접두어로 다음 jsp를 지정했다. doGet에서는 접두어를 기준으로 redirect/forward를 판단하고 처리한다.
private String gugu(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// 1. 파라미터 확인 및 검증
String forward = req.getParameter("forward");
. . .
// 2-2. 결과 활용
if (forward != null) { }
else {
HttpSession session = request.getSession();
session.setAttribute("result", data);
return "redirect:/guguResult.jsp";
}
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
logging(req);
...
move(target, req, resp);
}
private void move(String target, HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
if (target.startsWith("redirect:")) {
target = target.substring(9);
resp.sendRedirect(req.getContextPath() + target);
} else {
RequestDispatcher dispatcher = req.getRequestDispatcher(target);
dispatcher.forward(req, resp);
}
}
이제 새로 추가된 데이터가 session에 들어있으므로 JSP는 다음과 같이 변경되어야 한다. JSP에서는 session이 이미 내장 객체로 포함되어있으니 바로 사용하면 된다.
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>구구단 호출 결과</title>
</head>
<body>
<!-- session 내장 객체에 result라는 이름으로 담긴 attribute를 가져와서 출력한다. -->
<textarea cols="30" rows="10"><%=session.getAttribute("result")%></textarea>
</body>
</html>
forward vs redirect
forward vs redirect
forward와 redirect는 중요하니까 그 차이점을 다시 한번 정리하고 가자.
구분 | forward | redirect |
호출 |
- 웹 서버 내에서 직접 호출
- 기존의 request와 response 전달
|
- URL을 웹 브라우저로 보내서 간접 호출
- 새로운 request 와 response 생성
|
URL 변화 |
- 클라이언트 입장에서는 1번의 요청/응답
- 브라우저의 URL은 처음 호출한 URL 유지 |
- 2번의 요청/응답
- 브라우저의 URL이 변경됨 |
호출 범위 |
- 동일 웹 애플리케이션 자원만 호출
|
- 다른 웹 서버의 자원 호출 가능
|
데이터 전달 |
- request parameter는 물론 attribute를 이용해 객체 형태 전달 가능
|
- request 레벨은 파라미터로 텍스트 데이터만 전달 가능
- 필요 시 session attribute 사용, session 관리 부담
|
/ 의 의미 |
context root
|
container root: context root 개입 필요
|
상태 코드 | 200 |
302
|
동작 추측
다음과 같은 시나리오가 주어졌을 때 servlet 2에서 동작하는 method는 doGet일까? 아니면 doPost일까?
FrontController Pattern 활용 연습
두 수(num1, num2)를 받아서 합을 반환하는 동작 처리
숫자가 제대로 넘어왔을 때와 그렇지 않을때를 고민해서 처리해보자.
- 동작 방식은 get으로 하고 요청 url은 /add로 설정
- 숫자가 넘어왔을 때: 결과를 페이지에 출력함
- 숫자가 넘어오지 않아 계산하지 못할 때: 오류 내용을 전달하고 화면에서 JavaScript로 경고창 출력하기(header.jsp)
// SimpleService.java
public int add(int a, int b){
return a + b;
}
// FrontController.java
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
logging(req); // 전처리
String path = req.getServletPath();
String target = "/index.jsp";
if (path.equals("/gugu")) {
target = gugudan(req, resp);
} else if (path.equals("/add")) {
target = add(req, resp);
}
// 페이지 이동
...
}
private String add(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
String param1 = req.getParameter("num1");
String param2 = req.getParameter("num2");
try {
int num1 = Integer.parseInt(param1);
int num2 = Integer.parseInt(param2);
int result = new GuguService().add(num1, num2);
req.setAttribute("result", result);
} catch (RuntimeException e) {
req.setAttribute("error", e.getMessage());
}
return "addResult.jsp";
}
<!-- addResult.jsp -->
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>덧샘 결과</title>
</head>
<body>
<%@include file="/include/header.jsp"%>
<%=request.getParameter("num1")%> + <%=request.getParameter("num2")%> = ?
<%
Object result = request.getAttribute("result");
if (result != null) {
out.print(result);
}
%>
</body>
</html>
<%@ page import="com.doding.simpleweb.model.service.dto.User" %>
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%
String root = request.getContextPath();
%>
<h1 >Simple Web App</h1>
<script>
<%
Object error = request.getAttribute("error");
if(error != null) {
out.print("alert('"+error+"')");
}
%>
</script>
# 요청 테스트
http://localhost:8080/simpleweb/add -> 오류 발생
http://localhost:8080/simpleweb/add?num1=10&num2=20 -> 결과 표시
'Spring MVC > 01.WebProgramming' 카테고리의 다른 글
07. cookie와 session (7) | 2024.09.08 |
---|---|
05. JSP (0) | 2024.08.12 |
04. Front Controller Pattern (0) | 2024.08.08 |
03. Servlet 분석 (0) | 2024.08.05 |
02. Hello Servlet (0) | 2024.03.05 |
소중한 공감 감사합니다