나의 부트캠프에서의 첫 프로젝트인 Shopping mall 프로젝트 정기 리팩터링이 모두 끝났다.
쇼핑몰을 정말 쇼핑몰 답게 만들기 위해 고민하다 보니 이제부터 진짜 시작이라는 생각이 든다.
기능을 추가할 때마다 코드와 함께 나의 느낀 점 등을 기록하려 한다.
앞으로 새로운 기능이나, 현재 로직을 손 대서 완성도를 높여 볼 예정이다.
이전의 학습 여정에서 알게 된 새로운 지식과 느낀 점 부터 차례로 작성해야겠다.
메인 프로젝트에 들어가기 전이니, 최대한 빨리 기록하자..
첫 프로젝트를 진행하면서 수 많은 에러들과 좌절을 맛보았고, 그러다가 기능 하나가 제대로 돌아갈 때의 행복을 느끼는 수순으로 흘러갔는데, 기능이 돌아갈 때만 행복감을 느끼면 이 일을 오래 하기 힘들 것 같다고 생각이 들었다.
Servlet 은 Java 의 HTTP 요청을 처리하는 Web 기술이다.
Servlet 과 뗄 수 없는 Lifecycle 과 Singleton
일단 Servlet 의 생명주기에 대해 먼저 정리하자면 외부에서 초기 요청이 오면,
Servlet Container 가 init() method 호출해서 상태값을 세팅해 준다.
그와 동시에 service()
method 를 호출하고, 이는 doGet()
doPost()
등을 호출해준다.
이후 동일한 요청이 들어오면 service()
호출만 반복되고 새 생성자는 만들어지지 않는다.
동일한 작업은 instance 하나로 모두 처리하게 된다.
앱이 종료되는 시점, 혹은 일정한 앱 내 구현된 로직에 따라 destroy()
method 가 호출되며 작업에서 없앤다.
이와 같이 한 번의 상태값 세팅으로 여러 곳에서 사용하게 관리되는 패턴을 Singleton
이라 한다.
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class LifecycleServlet extends HttpServlet {
// init 메서드
@Override
public void init() throws ServletException {
System.out.println("Servlet is being initialized.");
}
// GET 요청 처리 메서드
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
resp.getWriter().write("Hello from LifecycleServlet!");
}
// 서블릿 소멸 메서드
@Override
public void destroy() {
System.out.println("Servlet is being destroyed.");
}
}
Singleton 이 아닌 일반적인 경우 특정 클래스 내의 기능을 사용하기 위해서는 매 번 생성자로 메모리에 인스턴스화 해줘야 하는데, 요청이 너무 많으면 메모리 부하가 걸릴 가능성이 있다.
그러나 Singleton 의 경우 메모리 영역에 하나의 객체만 인스턴스화 하고 이후는 method 호출만 이뤄지니, Server 메모리 관리가 용이해 진다.
Servlet 을 MVC 패턴 개발을 용이하게 해준다.
첫 프로젝트 진행에서 가장 기억에 남는 것은, Action 클래스가 많았다는 것이다.
package com.model2.mvc.framework;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public abstract class Action {
private ServletContext servletContext;
public Action(){
}
public ServletContext getServletContext() {
return servletContext;
}
public void setServletContext(ServletContext servletContext) {
this.servletContext = servletContext;
}
public abstract String execute(HttpServletRequest request, HttpServletResponse response) throws Exception ;
}
일처리(Action) 자체다. 고객 요청이 오면 무언가를 한다. (
execute
)
근데 추상적인 일처리 라서 구체적으로 만들어 줘야한다. (후술 예정
)
package com.model2.mvc.framework;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.model2.mvc.common.util.HttpUtil;
public class ActionServlet extends HttpServlet {
private RequestMapping mapper;
@Override
public void init() throws ServletException {
super.init();
String resources=getServletConfig().getInitParameter("resources");
mapper=RequestMapping.getInstance(resources);
// 초기 properties 를 RequestMapping 으로 전달되며, 인스턴스 생성 시 값 전달됨.
}
@Override
protected void service(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String url = request.getRequestURI();
String contextPath = request.getContextPath();
String path = url.substring(contextPath.length());
System.out.println(path);
try{
Action action = mapper.getAction(path);
action.setServletContext(getServletContext());
String resultPage=action.execute(request, response);
String result=resultPage.substring(resultPage.indexOf(":")+1);
if(resultPage.startsWith("forward:"))
HttpUtil.forward(request, response, result);
else
HttpUtil.redirect(response, result);
}catch(Exception ex){
ex.printStackTrace();
}
}
}
ActionServlet 은 사장님이다. 고객의 요청이 오면 사장님은 RequestMapping 이라는 부장님에게 일을 맡긴다. 사장님은 .do 라는 요청이 들어왔을 때만 움직인다. (
단일인입점 구성
)
ex) /product/addProduct.do
아래는 프로젝트 메타데이터를 저장해 두는 web.xml
파일 내용 일부이다.
url 이 *.do 로 왔을 때 ActionServlet
사장님이 호출된다.
<servlet>
<servlet-name>action</servlet-name>
<servlet-class>com.model2.mvc.framework.ActionServlet</servlet-class>
<!-- 나중에 RequestMapping 이 참고할 경로가 있는 곳 -->
<init-param>
<param-name>resources</param-name>
<param-value>com/model2/mvc/resources/actionmapping.properties</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>action</servlet-name>
<url-pattern>*.do</url-pattern>
</servlet-mapping>
init-param 태그 내의 경로에 있는
actionmapping.properties
라는 사내연락망에서 *.do 의 요청 이름에 따라 일 할 직원을 찾는다. 그 예는 아래 처럼 기록 되어있다.
/addProduct.do = com.model2.mvc.view.product.AddProductAction
/getProduct.do = com.model2.mvc.view.product.GetProductAction
/updateProductView.do = com.model2.mvc.view.product.UpdateProductViewAction
/updateProduct.do = com.model2.mvc.view.product.UpdateProductAction
/listProduct.do = com.model2.mvc.view.product.ListProductAction
package com.model2.mvc.framework;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
public class RequestMapping {
private static RequestMapping requestMapping;
private Map<String, Action> map;
private Properties properties;
private RequestMapping(String resources) {
map = new HashMap<String, Action>();
InputStream in = null;
try{
in = getClass().getClassLoader().getResourceAsStream(resources);
properties = new Properties();
properties.load(in);
}catch(Exception ex){
System.out.println(ex);
throw new RuntimeException("actionmapping.properties 파일 로딩 실패 :" + ex);
}finally{
if(in != null){
try{ in.close(); } catch(Exception ex){}
}
}
}
// 본 method 는 synchronized 로 한 번에 하나의 thread 만 실행한다.
public synchronized static RequestMapping getInstance(String resources){
if(requestMapping == null){
requestMapping = new RequestMapping(resources);
}
return requestMapping;
}
public Action getAction(String path){
Action action = map.get(path);
if(action == null){
String className = properties.getProperty(path);
System.out.println("prop : " + properties);
System.out.println("path : " + path);
System.out.println("className : " + className);
className = className.trim();
try{
Class c = Class.forName(className);
Object obj = c.newInstance();
if(obj instanceof Action){
map.put(path, (Action)obj);
action = (Action)obj;
}else{
throw new ClassCastException("Class형변환시 오류 발생 ");
}
}catch(Exception ex){
System.out.println(ex);
throw new RuntimeException("Action정보를 구하는 도중 오류 발생 : " + ex);
}
}
return action;
}
}
RequestMapping 은 부장님이다. Class 명을 보면 알 수 있듯이 요청이 오면 사장님이
resources
를 파라미터로 주면서 일을 시킨다. (getAction
)
부장님은 상술한actionmapping.properties
로 가서 목적에 맞는 직원 (**Action
) 에게 일을 시킨다.
package com.model2.mvc.view.product;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.model2.mvc.framework.Action;
import com.model2.mvc.service.product.ProductService;
import com.model2.mvc.service.product.impl.ProductServiceImpl;
import com.model2.mvc.service.product.vo.ProductVO;
public class AddProductAction extends Action {
@Override
public String execute(HttpServletRequest request, HttpServletResponse response) throws Exception {
ProductVO productVO = new ProductVO();
productVO.setProdName(request.getParameter("prodName"));
productVO.setProdDetail(request.getParameter("prodDetail"));
productVO.setManuDate(request.getParameter("manuDate").replace("-", ""));
productVO.setPrice(Integer.parseInt(request.getParameter("price")));
productVO.setFileName(request.getParameter("fileName"));
System.out.println(productVO);
ProductService service = new ProductServiceImpl();
service.addProduct(productVO);
return "redirect:/product/addProductView.jsp";
}
}
**Action 은 사무를 담당하는 직원이다.
무언가를 하라고 지시가 내려오면 본인이 맡은 업무를execute
한다.
AddProductAction 이라는 사무 직원을 예로 들겠다.
AddProductAction 은 제품을 신규 등록하는 업무를 하는데, ProductVO 라는 제품 정보를 담는 양식에 정보를 각 항목에 맞게 적어 넣고 ( setter
),
제품 등록 service 주문을 넣는다. ( service.addProduct(productVO)
)
사무 직원은 service 주문서에 주문을 넣고 결과를 받는 것만 하지, 현장에서 실제 등록은 어떻게 이뤄지는지 모른다. (3-tier architecture
, 후술)
package com.model2.mvc.service.product.dao;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.ArrayList;
import java.util.HashMap;
import com.model2.mvc.common.SearchVO;
import com.model2.mvc.common.util.DBUtil;
import com.model2.mvc.service.product.vo.ProductVO;
public class ProductDAO {
//Constructor
public ProductDAO() {
}
//Method
public void insertProduct(ProductVO productVO) throws Exception {
Connection con = DBUtil.getConnection();
String sql = "insert into PRODUCT values ( seq_product_prod_no.nextval, ?, ?, ?, ?, ?, sysdate ) ";
PreparedStatement pstmt = con.prepareStatement(sql);
//prod_no = seq_product_prod_no.nextval (다음 번호 시퀀스 진행)
pstmt.setString(1, productVO.getProdName()); //prod_name
pstmt.setString(2, productVO.getProdDetail()); //prod_detail
pstmt.setString(3, productVO.getManuDate()); //manufacture_day
pstmt.setInt(4, productVO.getPrice()); // price
pstmt.setString(5, productVO.getFileName()); // image_file
// regDate 는 sysdate
pstmt.executeUpdate();
con.close();
} //end of insertProduct
**DAO 는 업무가 진행되는 현장이다. ( 물류창고 느낌 )
ProductDAO 를 예로 들겠다.
제품등록 주문서를 받아서 여기서 실제 등록을 처리 한다.