본문 바로가기
👨‍💻 2. 웹개발_Back end/2-4 JSP & Servlet

[JSP & Servlet] 6장 미니 MVC 프레임워크 만들기 (4) 리플랙션 API를 이용하여 프런트 컨트롤러 개선하기

by 달님🌙 2021. 10. 18.
반응형

 

6.4절 리플랙션 API를 이용하여 프런트 컨트롤러 개선하기

 

1. 학습할 내용

 

- 파라미터 값을 페이지 컨트롤러에게 전달하는 방법을 자동화

 

2. 기존 방식

 

//서블릿 컨텍스트 보관소에서 페이지 컨트롤러를 찾는다.
ServletContext sc = this.getServletContext();
…
//페이지 컨트롤러를 찾아 주면,
Controller pageController = (Controller) sc.getAttribute(servletPath);

//페이지 컨트롤러가 누구냐에 따라
if ("/member/add.do".equals(servletPath)) {
…
} else if ("/member/update.do".equals(servletPath)) {
…
} else if ("/member/delete.do".equals(servletPath)) {
…
} else if ("/auth/login.do".equals(servletPath)) {
…
}

//페이지 컨트롤러가 필요로 하는 값 객체를 준비한다.
new Member()
//파라미터 값을 직접 꺼내 값 객체에 할당
 .setEmail(request.getParameter("email"))
 .setPassword(request.getParameter("password"))
 .setName(request.getParameter("name"))

 

2. 기존 방식의 문제점과 해결책

 

1) 새 페이지 컨트롤러가 추가되면, 프런트 컨트롤러에 값 객체를 준비하는 조건문을 추가해야 한다.

…
} else if ("/member/update.do".equals(servletPath)) {
 if (request.getParameter("email") != null) {
 model.put("member", new Member()…);
 } else {
 model.put("no", new Integer(request.getParameter("no")));
 }
} else if ("/member/delete.do".equals(servletPath)) {
 model.put("no", new Integer(request.getParameter("no")));
} else if ("/auth/login.do".equals(servletPath)) {
 if (request.getParameter("email") != null) {
 model.put("loginInfo", new Member()…);
 }
} else if ("/board/add.do".equals(servletPath)) {
 if (request.getParameter(”title") != null) {
 model.put(”board", new Board()…);
 } else {
 model.put("no", new Integer(request.getParameter("no")));
 }
}

 

 

2) 페이지 컨트롤러를 추가하더라도 프런트 컨트롤러를 변경하지 않으려면? “구조 변경”

- 값 객체를 직접 생성하는 생성하는 코드를 제거!

 

3) 페이지 컨트롤러에게 필요한 데이터가 무엇인지 물어보고 그에 맞추어 준비해 준다.

 

4) 프런트 컨트롤러와 페이지 컨트롤러 사이에 호출 규칙 정의

 

3. 프런트 컨트롤러 변경

 

1) DataBinding을 구현한 페이지 컨트롤러에 대해서만 값 객체를 만들어 준다.

ServletContext sc = this.getServletContext();
 HashMap<String,Object> model = new HashMap<String,Object>();
model.put("session", request.getSession());
 
 Controller pageController = (Controller) sc.getAttribute(servletPath);
 
 if (pageController instanceof DataBinding) {
 prepareRequestData(request, model, (DataBinding)pageController);
 }

 

2) 값 객체를 준비하는 과정

 

 

4. 리플랙션 API 사용

 

1) 인스턴스 생성하기

 

2) 클래스에 선언된 메서드 정보 알아내기

메서드 이름 알아내기

 

3) 메서드 호출하기

 

5. 실습 ( _28_MVC_DataBinding )

 

1) DispatcherServlet.java

- getDataBinders 메서드 이용하여 Object 집합에 키 값과 클래스 타입으로 받아옴

package spms.servlets;

import java.io.IOException;
import java.util.HashMap;

import javax.servlet.RequestDispatcher;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import spms.bind.DataBinding;
import spms.bind.ServletRequestDataBinders;
import spms.controller.Controller;

/**
 * Servlet implementation class DispatcherServlet
 */
//모든 .do url 호출에 대한 처리를 프론트컨트롤러인 Dispatcher에서 처리
/*
 * 1) 의미 : tomcat으로부터 바로 제어 컨트롤러를 전송하는 방식
 * 			-> tomcat은 FrontController로 요청을 전송
 * 			   FrontController에서 각 PageController분기해서 처리하도록
 * 			   FrontController : DispatcherServlet
 * 			   BackController(PageController) : 각 페이지 별 서블릿
 * 
 * 2) 이유 : 공통된 중복코드를 한 곳에 모아서 관리하기 편하게 하기 위함 -> 프레임워크의 기초 개념
 *          FrontController만 HttpServlet을 상속받고
 *          PageController들은 일반 자바 클래스로 변환
 *          
 * 3) FrontController의 역할
 *    3-1) 요청에 따른 페이지별 분기 처리
 *    3-2) 페이지 컨트롤러로 전송할 VO 객체 생성
 *    3-3) 서블릿/JSP 페이지 이동 처리
 *    3-4) PageController에서 발생하는 모든 예외 처리
 *    3-5) 신규 페이지나 기존 페이지 관리
 *    
 * 4) 장점
 * 	  4-1) FrontController만 HttpServlet을 상속받기 때문에 다른 WAS로 이식할 때 FrontController만 수정하면 됨
 *    4-2) 브라우저에 노출하고 싶지 않은 페이지 감춤
 *    4-3) 공통역할을 FrontController에 모아놓기 때문에 자동화(Framework화)하기 편함
 * 
 * */
@WebServlet("*.do")
public class DispatcherServlet extends HttpServlet {
	@Override
	protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		response.setContentType("text/html;charset=UTF-8");
		
		//분기 처리를 위한 servlet(요청url) 경로 얻음
		String servletPath = request.getServletPath();
		try {
			ServletContext sc = this.getServletContext();
			//서블릿 경로에 따른 페이지컨트롤러 경롤 담을 변수
			//서블릿 컨텍스트 데이터 보관소 영역에 저장한 컨트롤러를 꺼내 씀(키 값 : servletpath, value : 해당 컨트롤러(DAO의존성 주입 상태))
			Controller pageController = (Controller)sc.getAttribute(servletPath);
			HashMap<String, Object> model = new HashMap<String, Object>();
			model.put("session", request.getSession());
			
			//pageController객체가 DataBinding인터페이스를 상속받았다면
			//만들어야 할 객체가 존재하는  pageController라면
			if(pageController instanceof DataBinding) {
				//pageController에 전달할 객체에 자동 주입하여
				//브라우저가 보내는 파라미터를 객체에 주입하여 
				//model객체에 저장
				prepareRequestData(request, model, (DataBinding)pageController);
			}
			
			//POJO Page Controller
			String viewUrl = "";
			if(pageController != null) {
				viewUrl = pageController.execute(model);
				for(String key : model.keySet()) {
					request.setAttribute(key, model.get(key));
				}
			} else {
				System.out.println("주소 대상  Controller를 찾을 수 없습니다.");
			}
			
			//반환받은 값에 redirect가 포함되어 있으면 바로 페이지 이동
			if(viewUrl.startsWith("redirect:")) {
				response.sendRedirect(viewUrl.substring(9));
				return;
			} else {
				RequestDispatcher rd = request.getRequestDispatcher(viewUrl);
				rd.include(request, response);
			}
		} catch(Exception e) {
			e.printStackTrace();
			request.setAttribute("error", e);
			RequestDispatcher rd = request.getRequestDispatcher("/Error.jsp");
			rd.forward(request, response);
		}
	}
	
	private void prepareRequestData(HttpServletRequest request, 
									HashMap<String, Object> model, 
									DataBinding dataBinding) throws Exception {
		Object[] dataBinders = dataBinding.getDataBinders();
		String dataName = null;  //key값
		Class<?> dataType = null;//생성할 객체의 클래스 정보
		Object dataObj = null;   //생성할 객체
		//dataBinding[0] : 키값, dataBinders[1] : value값
		for(int i = 0; i < dataBinders.length; i+=2) {
			dataName = (String)dataBinders[i];
			dataType = (Class<?>)dataBinders[i+1]; //홀수 값을 하나씩 넣어줌
			
			/*
			 * request : 매개변수 추출을 위한 정보 전송
			 * dataType : 생성할 객체의 클래스 정보 전송
			 * dataName : 매개변수(키 값) 이름
			 * */
			
			//객체 생성
			dataObj = ServletRequestDataBinders.bind(request, dataType, dataName);
			
			//만들어진 객체를 model에 저장
			//model("member", "MemberVO");
			model.put(dataName, dataObj);
		}
	}
}

 

2) DataBinding.java

package spms.bind;

public interface DataBinding {
	//자동으로 생성해야될 VO객체 항목을 반환
	public Object[] getDataBinders();
}

 

3) ServletRequestDataBinders

package spms.bind;

import java.lang.reflect.Method;
import java.sql.Date;
import java.util.Set;

import javax.servlet.ServletRequest;

//클라이언트가 보낸 매개변수 값을 객체에 담아주는 클래스
public class ServletRequestDataBinders {
	public static Object bind(ServletRequest request, Class<?> dataType, String dataName) throws Exception {
		//생성해야 될 대상이 PrimitiveType일 경우
		if(isPrimitiveType(dataType)) {
			return createValueObject(dataType, request.getParameter(dataName));
		} //dataType이 일반 VO객체일 경우
		else {
			//브라우저가 보낸 매개변수들의 이름을 set에 담음
			//email, password, no, name....이 Set에 String타입으로 담기게 됨
			Set<String> paramNames = request.getParameterMap().keySet();
			//클래스 타입대로 객체 생성
			//리플렉션 API newInstance() : 해당 타입의 클래스 인스턴스 생성
			//dataType == spms.vo.Member => dataObject = spms.vo.Member;
			Object dataObject = dataType.newInstance();
			//브라우저가 보낸 매개변수를 객체의 필드를 찾아서 저장
			//setter를 찾아서 담아줄 Method 변수 
			Method m = null; //VO의 setter 담을 변수
			for(String paramName : paramNames) {
				//매개변수에 해당하는 setter 메소드 찾기
				//ex) no이 오면 m = setNo()
				//	name : m = setName()
				//	password : m = setPassword()
				m = findSetter(dataType, paramName);
				if(m != null) {
					//dataObject객체의 m메소드를 호출
					//첫 번째 매개변수에 클라이언트의 매개변수 값을 대입
					//리플렉션 API invoke() : 해당 메소드 실행
					m.invoke(dataObject, 
							createValueObject(m.getParameterTypes()[0], //setter 메소드의 매개변수 타입 
											 request.getParameter(paramName) /*요청 매개변수 값*/ ));
				}
			}
			//브라우저가 보낸 매개변수 값까지 객체에 저장한 후 생성된 객체 리턴
			return dataObject;
		}
	}
	
	private static Method findSetter(Class<?> type, String name) {
		//해당 클래스 타입이 가진 모든 메소드 추출
		//spms.vo.Member
		Method[] methods = type.getMethods();
		
		String propName = null;
		for(Method m : methods) {
			//메소드 시작이름이 set 아니면 돌아감
			if(!m.getName().startsWith("set")) {
				continue;
			}
			
			//set을 포함한 이름 ==> 프로퍼티
			//setName() -> propName = Name;
			propName = m.getName().substring(3);
			//propName = name();
			propName = propName.toLowerCase();
			//name으로 넘겨준 이름과 같으면 해당 프로퍼트의  setter를 찾음
			if(propName.equals(name.toLowerCase())) {
				return m;
			}
		} 
		return null;
	}
	
	private static boolean isPrimitiveType(Class<?> type) {
		if(type.getName().equals("int") || type == Integer.class ||
		   type.getName().equals("long") || type == Long.class ||
		   type.getName().equals("float") || type == Float.class ||
		   type.getName().equals("double") || type == Double.class ||
		   type.getName().equals("boolean") || type == Boolean.class ||
		   type == Date.class || type == String.class) {
			return true;
		}
		return false;
	}
	
	private static Object createValueObject(Class<?> type, String value) {
		if(type.getName().equals("int") || type == Integer.class) {
			return new Integer(value);
		} else if(type.getName().equals("float") || type == Float.class) {
			return new Float(value);
		} else if(type.getName().equals("double") || type == Double.class) {
			return new Double(value);
		} else if(type.getName().equals("long") || type == Long.class) {
			return new Long(value);
		} else if(type.getName().equals("boolean") || type == Boolean.class) {
			return new Boolean(value);
		} else if(type == Date.class) {
			return java.sql.Date.valueOf(value);
		} else {
			return value;
		}
	}
}

 

4) MemberAddController.java

package spms.controller;

import java.util.Map;

import spms.bind.DataBinding;
import spms.dao.MySqlMemberDAO;
import spms.vo.Member;

//HttpServlet은 GenericServlet을 상속받은 클래스
//service() 메소드를 호출방식에 따라 doGet(), doPost(), doPut(), doDelete()로 분리해놓음
public class MemberAddController implements Controller, DataBinding {
	MySqlMemberDAO memberDAO = null;
	
	public MemberAddController setMemberDAO(MySqlMemberDAO memberDAO) {
		this.memberDAO = memberDAO;
		return this;
	}
	
	@Override
	public Object[] getDataBinders() {
		//key 값과 이름으로 매핑하여 자동으로 생성해야 되는 클래스 타입 지정
		return new Object[] {
				"member", spms.vo.Member.class
		};
	}
	
	@Override
	public String execute(Map<String, Object> model) throws Exception {
		Member member = (Member)model.get("member");
		
		if(member.getEmail() == null) {
			return "/member/MemberForm.jsp";
		} else {
			memberDAO.insert(member);
			
			return "redirect:list.do";
		}
	}
}

 

5) MemberDeleteController.java

package spms.controller;

import java.util.Map;

import spms.bind.DataBinding;
import spms.dao.MySqlMemberDAO;

public class MemberDeleteController implements Controller, DataBinding {
	MySqlMemberDAO memberDAO = null;
	
	public MemberDeleteController setMemberDAO(MySqlMemberDAO memberDAO) {
		this.memberDAO = memberDAO;
		return this;
	}
	
	@Override
	public Object[] getDataBinders() {
		//key 값과 이름으로 매핑하여 자동으로 생성해야 되는 클래스 타입 지정
		return new Object[] {
				"no", Integer.class
		};
	}
	
	@Override
	public String execute(Map<String, Object> model) throws Exception {		
		Integer no = (Integer)model.get("no");
		memberDAO.delete(no);
		
		return "redirect:list.do";
	}
}

 

6) MemberUpdateController.java

package spms.controller;

import java.util.Map;

import spms.bind.DataBinding;
import spms.dao.MySqlMemberDAO;
import spms.vo.Member;

public class MemberUpdateController implements Controller, DataBinding {
	MySqlMemberDAO memberDAO = null;
	
	public MemberUpdateController setMemberDAO(MySqlMemberDAO memberDAO) {
		this.memberDAO = memberDAO;
		return this;
	}
	
	@Override
	public Object[] getDataBinders() {
		//key 값과 이름으로 매핑하여 자동으로 생성해야 되는 클래스 타입 지정
		return new Object[] {
				"no", Integer.class,
				"member", spms.vo.Member.class
		};
	}
	
	@Override
	public String execute(Map<String, Object> model) throws Exception {
		Member member = (Member)model.get("member");
		
		if(member.getEmail() == null) {
			Integer no = (Integer)model.get("no");
			Member detailInfo = memberDAO.selectOne(no);
			model.put("member", detailInfo);
			return "/member/MemberUpdateForm.jsp";
		} else {
			memberDAO.update(member);
			return "redirect:list.do";
		}
	}

}

 

7) 실행 화면

- 로그인, 로그아웃, 회원등록, 회원수정, 회원삭제 잘 되는지 확인

 

 

반응형

댓글