kh day 080
07장 서블릿 고급 이어서 나가자!
MVC 패턴 (Model View Controller)
Model : 비지니스 로직 수행결과 데이터
View : Model 데이터를 이용한 응답화면을 생성
Controller : MVC 흐름을 제어
: 하나의 Servlet이다
: 모든 Request Message를 받는 집중화 수행
(적용 패턴 : Front Controller Pattern)
이를 위해, url-pattern mapping에 달려있다!
(1) 디렉토리 패턴 : 반드시 '/'로 시작해야함 ex) '/*'
(2) 확장자 패턴 : 반드시 *.확장자로 끝나야 함 ex) '*.do'
---
Command 패턴
사용자의 요청을 객체인 클래스로 처리하는 것을 의미한다. 객체 형태로 사용하면 서로 다른 사용자의 요청값을 필요에 의해서 저장하거나 또는 취소가 가능하고, 요청을 처리할 작업을 일반화시켜 요청의 종류와 무관하게 프로그램 작성이 가능
하게 구현할 수 있다. 구현방법은 다음과 같이 Command패턴을 적용한 Service 이름의 클래스를 추가한다. Service 클래스는 작업 수행을 요청하는 객체인 서블릿과 실제 작업을 수행하는 객체로 분리시켜주기 때문에 시스템간의 결합도를 낮출 수 있다. 일반적으로 의존성을 낮추기 위해서 인터페이스를 사용하여 구현하고,서블릿과 DAO간의 의존성도 감소시키는 역할을 담당한다
//FrontControllerServlet.java -> 이게 핵심이다
//런온 서버 후 브라우저에서 *.do 입력후 콘솔창 확인
//사진01
//사진02
domain패키지에 EmpDTO 클래스를 만들자
@Data // 필요한 메소드들을 다 만들어준다 (Outline 확인)
public class EmpDTO { // POJO : Plain Old Java Object
private Integer empno;
private String ename;
private Double sal;
private Integer deptno;
} // end class
EmpDTO.java
pesistence패키지를 만들어서 EmpDAO를 만들자
@Log4j2
@NoArgsConstructor
public class EmpDAO {
// public static DataSource dataSource;
private static DataSource dataSource;
static { // JNDI lookup을 통해, DataSource 자원객체를 필드에 초기화 시키자
// WAS가 생성한 DataSource 객체의 획득방법
// WAS의 표준 API인, JNDI API를 이용해서 설정에 의해 자동생성된 데이터 소스 획득
try {
// 1. JNDI tree의 뿌리에 접근하게 해주는 객체를 획득
Context ctx = new InitialContext(); // 100% 성공 (Web Application 안에서 수행된다면...)
// 2. Context 객체를 가지고, 지정된 이름을 가지는 리소스 열매를 찾아서 획득
EmpDAO.dataSource = (DataSource) ctx.lookup("java:comp/env/jdbc/OracleCloudATP");
log.info("\t+ this.dataSource : {}", EmpDAO.dataSource);
} catch(Exception e) {
e.printStackTrace();
} // try-catch
} // static initializer
public List<EmpVO> selectAll() throws SQLException {
log.trace("select() invoked.");
// Scott 스키마의 'emp' 테이블을 모두 조회해서, 리스트 컬렉션으로 반환
// 리스트 컬렉션의 요소는 EmpVO이어야 함.
Connection conn = EmpDAO.dataSource.getConnection();
String sql = "SELECT * FROM emp ORDER BY empno";
PreparedStatement pstmt = conn.prepareStatement(sql);
ResultSet rs = pstmt.executeQuery();
List<EmpVO> list = new Vector<>();
try (conn; pstmt; rs;) { // 순서를 신경써서 닫으려면 try-with로 닫자
while(rs.next()) {
Integer empno = rs.getInt("empno");
String ename = rs.getString("ename");
String job = rs.getString("job");
Integer mgr = rs.getInt("mgr");
Date hireDate = rs.getDate("hireDate");
Double sal = rs.getDouble("sal");
Double comm = rs.getDouble("comm");
Integer deptno = rs.getInt("deptno");
EmpVO vo = new EmpVO(empno, ename, job, mgr, hireDate, sal, comm, deptno);
list.add(vo);
} // while
} // try-with-resources
return list;
} // selectAll
public int insert(EmpDTO dto) throws SQLException {
log.trace("insert({}) invoked.", dto);
Connection conn = EmpDAO.dataSource.getConnection();
String sql = "INSERT INTO emp (empno, ename, sal, deptno) VALUES (?,?,?,?)";
PreparedStatement pstmt = conn.prepareStatement(sql);
pstmt.setInt(1, dto.getEmpno());
pstmt.setString(2, dto.getEname());
pstmt.setDouble(3, dto.getSal());
pstmt.setInt(4, dto.getDeptno());
try (conn; pstmt;) { // 자원객체 순서대로 닫기 (오른쪽부터)
return pstmt.executeUpdate();
} // try-with-resources
} // insert
public int update(EmpDTO dto) throws SQLException {
log.trace("update({}) invoked.", dto);
Connection conn = EmpDAO.dataSource.getConnection();
String sql = "UPDATE emo SET ename =?, sal =? deptno =? WHERE empno = ?";
PreparedStatement pstmt = conn.prepareStatement(sql);
pstmt.setString(1, dto.getEname());
pstmt.setDouble(2, dto.getSal());
pstmt.setInt(3, dto.getDeptno());
pstmt.setInt(4, dto.getEmpno());
try (conn; pstmt;) { // 자원객체 순서대로 닫기 (오른쪽부터)
return pstmt.executeUpdate();
} // try-with-resources
} // update
public int delete(EmpDTO dto) throws SQLException {
log.trace("delete({}) invoked.", dto);
Connection conn = EmpDAO.dataSource.getConnection();
String sql = "DELETE FROM emp WHERE empno = ?";
PreparedStatement pstmt = conn.prepareStatement(sql);
pstmt.setInt(1, dto.getEmpno());
try (conn; pstmt;) { // 자원객체 순서대로 닫기 (오른쪽부터)
return pstmt.executeUpdate();
} // try-with-resources
} // delete
} // end class
EmpDAO.java
서비스 패키지를 만들어서 그 아래에 서비스 인터페이스 만들자
public interface Service {
public static final String DTO = "__DTO__";
public static final String MODEL = "__MODEL__";
public abstract void execute(HttpServletRequest req, HttpServletResponse res)
throws BusinessException;
} // end class
Service.java
BusinessException이라는 예외를 직접 정의해 만들어주자
exception 패키지에 Exception을 상속 하는 놈을 만들면 된다.
// User-defined Exception으로, 비지니스 로직 수행시 오류가 발생하면
// 이 예외를 던지도록 설정
@NoArgsConstructor
public class BusinessException extends Exception {
private static final long serialVersionUID = 1L;
// public BusinessException() {
// ;;
// } // default constructor
public BusinessException(String message) {
super(message);
} // constructor
public BusinessException(Exception e) {
super(e);
} // constructor
} // end class
BusinessException.java
서비스 패키지에 InsertServiceImpl, DeleteServiceImpl, SelectServiceImpl, UpdateServiceImpl, UnknownServiceImpl, 를 각각 만들어주자!
@Log4j2
@NoArgsConstructor
public class InsertServiceImpl implements Service {
@Override
public void execute(HttpServletRequest req, HttpServletResponse res)
throws BusinessException {
log.trace("execute(req, res) invoked.");
// 비지니스 로직을 수행하고, 그 결과 데이터인 Model을 생성하여 Req.Scope에 바인딩
try {
// step1. Req.Scope에서 DTO 객체 획득
EmpDTO dto = (EmpDTO) req.getAttribute(Service.DTO);
// step2. 획득한 DTO를 이용해서 DAO의 메소드 호출
EmpDAO dao = new EmpDAO();
// step3. 비지니스 로직 수행 (신규 사원 등록)
int insertedRows = dao.insert(dto); // insertedRows : 비지니스 수행결과 데이터
req.setAttribute(Service.MODEL, insertedRows); // Auto-boxing : Integer 객체로 저장
} catch (Exception e) {
throw new BusinessException(e);
} // try-catch
} // execute
} // end class
InsertServiceImpl
@Log4j2
@NoArgsConstructor
public class DeleteServiceImpl implements Service {
@Override
public void execute(HttpServletRequest req, HttpServletResponse res)
throws BusinessException {
log.trace("execute(req, res) invoked.");
// 비지니스 로직을 수행하고, 그 결과 데이터인 Model을 생성하여 Req.Scope에 바인딩
try {
// step1. Req.Scope에서 DTO 객체 획득
EmpDTO dto = (EmpDTO) req.getAttribute(Service.DTO);
// step2. 획득한 DTO를 이용해서 DAO의 메소드 호출
EmpDAO dao = new EmpDAO();
// step3. 비지니스 로직 수행 (특정사원삭제)
int deletedRows = dao.delete(dto); // deletedRows : 비지니스 수행결과 데이터
req.setAttribute(Service.MODEL, deletedRows); // Auto-boxing : Integer 객체로 저장
} catch (Exception e) {
throw new BusinessException(e);
} // try-catch
} // execute
} // end class
DeleteServiceImpl
@Log4j2
@NoArgsConstructor
public class SelectServiceImpl implements Service {
@Override
public void execute(HttpServletRequest req, HttpServletResponse res)
throws BusinessException {
log.trace("execute(req, res) invoked.");
// 비지니스 로직을 수행하고, 그 결과 데이터인 Model을 생성하여 Req.Scope에 바인딩
try {
// step1. Req.Scope에서 DTO 객체 획득할 필요 없음
// EmpDTO dto = (EmpDTO) req.getAttribute(Service.DTO);
// step2. 획득한 DTO를 이용해서 DAO의 메소드 호출
EmpDAO dao = new EmpDAO();
// step3. 비지니스 로직 수행 (모든 사업 정보 획득)
List<EmpVO> list = dao.selectAll(); // list : 비지니스 수행결과 데이터
req.setAttribute(Service.MODEL, list);
} catch (Exception e) {
throw new BusinessException(e);
} // try-catch
} // execute
} // end class
SelectServiceImpl
@Log4j2
@NoArgsConstructor
public class UpdateServiceImpl implements Service {
@Override
public void execute(HttpServletRequest req, HttpServletResponse res)
throws BusinessException {
log.trace("execute(req, res) invoked.");
// 비지니스 로직을 수행하고, 그 결과 데이터인 Model을 생성하여 Req.Scope에 바인딩
try {
// step1. Req.Scope에서 DTO 객체 획득
EmpDTO dto = (EmpDTO) req.getAttribute(Service.DTO);
// step2. 획득한 DTO를 이용해서 DAO의 메소드 호출
EmpDAO dao = new EmpDAO();
// step3. 비지니스 로직 수행 (기존 사원 업데이트)
int updatedRows = dao.update(dto); // updatedRows : 비지니스 수행결과 데이터
req.setAttribute(Service.MODEL, updatedRows);
} catch (Exception e) {
throw new BusinessException(e);
} // try-catch
} // execute
} // end class
UpdateServiceImpl
@Log4j2
@NoArgsConstructor
public class UnknownServiceImpl implements Service {
@Override
public void execute(HttpServletRequest req, HttpServletResponse res)
throws BusinessException {
log.trace("execute(req, res) invoked.");
try {
req.setAttribute(Service.MODEL, "Bad Request");
} catch (Exception e) {
throw new BusinessException(e);
} // try-catch
} // execute
} // end class
UnknownServiceImpl
FrontControllerServlet.java를 만들어준다
@Log4j2
@NoArgsConstructor
@WebServlet("*.do")
public class FrontControllerServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
@Override
protected void service(HttpServletRequest req, HttpServletResponse res)
throws ServletException, IOException {
log.trace("service(req, res) invoked.");
// String requestURI = req.getRequestURI();
// String contextPath = req.getContextPath();
// log.info("\t+ requestURI : {}, contextPath: {}", requestURI, contextPath);
// step 1. 모든 전송파라미터 획득 및 DTO 객체로 수집
// req.setCharacterEncoding("utf8"); // MyFilter에서 이미 처리
String empno = req.getParameter("empno");
String ename = req.getParameter("ename");
String sal = req.getParameter("sal");
String deptno = req.getParameter("deptno");
log.info("\t+ empno : {}, ename : {}, sal : {}, deptno : {}", empno, ename, sal, deptno);
// ---
// 수집된 모든 전송 파라미터를 DTO객체의 필드로 수집(저장)
// 이 DTO (Data Transfer Object, 데이터 전달 객체)는 웹 3계층에서 앞 -> 뒤로 전달됨
EmpDTO dto = new EmpDTO();
if(empno != null && !"".equals(empno)) {
dto.setEmpno(Integer.parseInt(empno));
} // if
dto.setEname(ename);
if(sal != null && !"".equals(sal)) {
dto.setSal(Double.parseDouble(sal));
} // if
if(deptno != null && !"".equals(deptno)) {
dto.setDeptno(Integer.parseInt(deptno));
} // if
log.info("\t+ dto : {}", dto);
// ---
// 현재의 Request에 대한, Response가 나가기 전까지 살아있는
// Request Scope에 DTO 객체를 binding 해주어서, 웹 3계층의 어디에서든 사용할 수 있도록 공유
req.setAttribute(Service.DTO, dto); // 공유속성 이름 : __DTO__
// step 2. Reqeust URI를 획득하여 요청을 식별
String command = req.getRequestURI();
log.info("\t+ command : {}", command);
// step 3. 얻어낸 command를 처리할 비지니스 수행 객체(=서비스 객체)를
// 식별하고 처리를 위임 (Delegation) => Command Pattern 적용
try { // 비지니스 계층의 서비스 객체가 로직을 수행하고 나면 반드시 Model이 생성됨
// Command Pattern에 따라, 각 Command를 처리할 객체가 반드시 구현할 메소드를
// 인터페이스로 규격화하고, 이에 맞게 Command 처리객체를 생성할 수 있도록 클래스 구현하는 패턴
switch(command) { // 유형별로 처리를 위임 (Delegation)
case "/insert.do": new InsertServiceImpl().execute(req, res); break;
case "/select.do": new SelectServiceImpl().execute(req, res); break;
case "/update.do": new UpdateServiceImpl().execute(req, res); break;
case "/delete.do": new DeleteServiceImpl().execute(req, res); break;
default : new UnknownServiceImpl().execute(req, res); break;
} // switch
} catch (BusinessException e) {
throw new ServletException(e);
} // try-catch
// step 4. 비지니스 수행결과 데이터인 Model을 함께 전달하여
// 응답결과 화면을 동적으로 생석하도록 View 호출
RequestDispatcher dispatcher = req.getRequestDispatcher("/View"); // View는 맵핑된 URI
dispatcher.forward(req, res);
log.info("\t+ forwarded request into /View");
} // service
} // end class
FrontControllerServlet.java
---
***요청 포워딩 ( request forwarding )
Model1 Architecture ==> Worst Practice
1개의 Request에 대해서 1개의 서블릿이 받고 처리 응답을 생성 전송
Model2 Architecture => Best Practice (MVC 패턴)
1개의 Request에 대해서, 다수의 서블릿/JSP가 처리를 나누어 수행
처리위임 (요청 포워딩)
브라우저 -> Request -> Servlet1 -> Servlet2 -> JSP1 -> JSP2 : 응답생성 및 전송
요청 포워딩
사용자의 요청을 받은 서블릿 또는 JSP에서 다른 컴포넌트( 서블릿, JSP, html )로 요청을 위임할 수 있는 방법이다. 포워드(forward) 하는 이유는 처리 작업을 모듈화하기 위해서이다. 직접 요청받은 서블릿 또는 JSP에서 모든 작업을 처리하지 않고 모듈화 시킨 다른 컴포넌트로 요청을 위임하여 처리할 수 있다. 재사용성도 높아지고 유지보수가 쉬워진다. 일반적으로 MVC 패턴에서 서블릿이 JSP를 포워딩할 때 주로 사용된다
일반적으로 요청을 처리하는 웹 컴포넌트는 FrontController 패턴을 적용한 서블릿으로 구현하고, 응답을 처리하기 위한 웹 컴포넌트는 JSP로 구현할 수 있으며, 이런 아키텍쳐를 MVC 패턴 또는 Model 2 Architecture라고 한다
요청 포워딩을 구현하는 방법은 다음과 같이 2가지 방법이 제공된다. => 화면전환기법!
- RequestDispatcher 객체의 forward 메소드를 이용하는 방법
ex) RequestDispatcher dispatcher = req.getReauestDispatcher(target);
dispatcher.forward(req, res);
- HttpServletResponse 객체의 redirect 메소드를 이용하는 방법 (리다이렉션 기법)
ex) response.sendRedirect(target);
* 위 방법들에서 인자값인 'target'은, 요청을 위임받아 나머지 역할을 처리할 웹 컴포넌트를 의미
MVC 패턴에 따라 1개의 요청을 다수의 웹 컴포넌트가 나누어 처리하는 방식 => Model2 Architecture
위와 같이 하려면, 반드시 요청을 나누어 처리하기 위해서 요청을 위임하는 방법이 있어야한다. => 요청 포워딩
---
1) RequestDispatcher 클래스를 이용한 forward 방법
웹 브라우저를 통하여 사용자가 서블릿에 요청을 하면 HttpServletRequest 객체가 자동으로 생성된다. 이때 생성된 request 객체를 사용하여 응답 처리하는 웹 컴포넌트(서블릿, JSP, html)로 재요청하는 방식을 의미한다. 이 request 객체의 주된 용도는 사용자의 입력 파라미터값을 얻어오거나 또는 한글 인코딩 처리 및 request scoe에 해당되는 속성(attribute)설
정에 사용 가능하다. 요청받은 서블릿에서 다른 컴포넌트로 포워드하는 방법은 다음과 같다. 지정된 target에는 서블릿 맵핑명 또는 JSP파일 및 html파일을 지정할 수 있다.
RequestDistatcher dispatcher = request.getRequestDispatcher(target);
dispatcher.forward( request, response );
forward의 전체적인 아키텍쳐는 다음과 같다
@Log4j2
@NoArgsConstructor
@WebServlet("/Request")
public class RequestServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
@Override
protected void service(HttpServletRequest req, HttpServletResponse res)
throws ServletException, IOException {
log.trace("service(req, res) invoked.");
// Step 1. 비지니스 로직 수행
// Step 2. 비지니스 데이터(=Model)를 request scope 공유데이터 영역에 올려 놓음
req.setAttribute("name", "홍길동");
req.setAttribute("address", "서울");
//응답을 만들어 낼 웹 컴포넌트(예: Servlet)에 요청을 위임 (View)
//step 3. 요청위임 (by request forwarding)
// 두번째 응답 서블릿으로 포워딩
RequestDispatcher dispatcher = req.getRequestDispatcher("/Response");
dispatcher.forward(req, res);
log.info("Forwarded request to the /Response");
} // service
} // end class
RequestServlet.java
@Log4j2
@NoArgsConstructor
@WebServlet("/Response")
public class ResponseServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
@Override
protected void service(HttpServletRequest req, HttpServletResponse res)
throws ServletException, IOException {
log.trace("service(req, res) invoked.", req, res);
// step1. 공유데이터 영역(Req.Scope)에서 Model 얻어내기
String name = (String) req.getAttribute("name");
String address = (String) req.getAttribute("address");
log.info("Model - name : {}, address : {}", name, address);
// step2. Model을 이용한 응답문서의 생성 및 전송
res.setContentType("text/html; charset=utf8");
@Cleanup
PrintWriter out = res.getWriter();
out.println("<h1>/Response</h1>");
out.println("<hr>");
out.println("<h2> 1. name : " + name + "<h2>");
out.println("<h2> 2. address : " + address + "<h2>");
out.flush();
} // service
ResponseServlet.java
RequestServlet을 실행시키자
---
2) HttpServletResponse 클래스를 이용한 redirect 방법
forward 방식과 마찬가지로 다른 웹 컴포넌트에게 재요청을 하는 방법이다. 차이점은 응답을 먼저 하고 재요청이 되기 때문에, 동일한 HttpServletRequest가 아닌 새로운 request 객체가 생성된다. 따라서 웹 브라우저의 URL 값이 변경되고 속성(attribute)에 설정된 값을 가져오지 못한다. 요청받은 서블릿에서 다른 컴포넌트로 redirect 하는 방법은 다음과 같다. 지정된 target에는 서블릿 맵핑명 또는 JSP파일 및 html파일을 지정할 수 있다.
response.sendRedirect(target);
redirect의 전체적인 아키텍쳐는 다음과 같다.
@Log4j2
@NoArgsConstructor
@WebServlet("/RequestRedirect")
public class RequestRedirectServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
@Override
protected void service(HttpServletRequest req, HttpServletResponse res)
throws ServletException, IOException {
log.trace("service(req, res) invoked.");
// Step 1. 비지니스 데이터(=Model)를 request scope에 속성 바인딩
req.setAttribute("name", "홍길동");
req.setAttribute("address", "서울");
//step 2. Redirect 응답 전송
res.sendRedirect("/ResponseRedirect");
log.info("Succeed to send a Redirection Response to the web browser");
} // service
} // end class
RequestRedirectServlet.java
@Log4j2
@NoArgsConstructor
@WebServlet("/ResponseRedirect")
public class ResponseRedirectServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
@Override
protected void service(HttpServletRequest req, HttpServletResponse res)
throws ServletException, IOException {
log.trace("service(req, res) invoked.");
// step1. 공유데이터 영역(Req.Scope)에서 Model 얻어내기
String name = (String) req.getAttribute("name");
String address = (String) req.getAttribute("address");
log.info("Model - name : {}, address : {}", name, address);
// 이미 응답이 나가면서 Req.Scope이 파괴되었기 때문에, Null이 나온다!
// step2. Model을 이용한 응답문서의 생성 및 전송
res.setContentType("text/html; charset=utf8");
@Cleanup
PrintWriter out = res.getWriter();
out.println("<h1>/ResponseRedirect</h1>");
out.println("<hr>");
out.println("<h2> 1. name : " + name + "<h2>");
out.println("<h2> 2. address : " + address + "<h2>");
out.flush();
} // service
} // end class
ResponseRedirectServlet.java
RequestRedirectServlet를 실행시키자마자 ResponseRedirectServlet로 넘기는걸 확인할 수 있다
왜 Null이 나오죠?
이미 응답이 나가는 순간에 Req.Scope이 파괴되기 떄문에 Req.Scope에 저장한 값을 찍어봤자 값이 이미 파괴되어 없으므로 Null로 나온다!!
---
'국비학원' 카테고리의 다른 글
[국비지원] KH 정보교육원 82-83일차 (0) | 2022.07.26 |
---|---|
[국비지원] KH 정보교육원 81일차 (0) | 2022.07.22 |
[국비지원] KH 정보교육원 79일차 (0) | 2022.07.20 |
[국비지원] KH 정보교육원 78일차 (0) | 2022.07.20 |
[국비지원] KH 정보교육원 77일차 (0) | 2022.07.16 |