< command 기반 개발 패턴 >
// DispatcherServlet.java
@WebServlet("*.do") //뷰 페이지(jsp) 요청을 제외한 모든 요청이 여기로 옴. (* : every)
public class DispatcherServlet extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 요청을 처리한 뒤 뷰페이지 이동 -- 실제 요청 처리는 핸들러 클래스에서 할 거임
// 어떤 핸들러인지에 따라 처리를 다르게 할 것.
String url = request.getServletPath(); // 요청 url 반환
response.getWriter().append("url:" + url);
}
}
- command handler 패턴의 가장 큰 특징은 모든 요청이 *.do로 온다는 것.
- .do로 오는 모든 요청은 DispatcherServlet으로!
- 기능 Servlet은 Handler(interface) 상속받은 기본 클래스 파일로!
1. DispatcherServlet을 서블릿 파일로 생성. (Spring은 기본으로 생성되어 있음)
- 이 파일이 모든 요청을 받고 처리한다.
- url을 "*.do"로 등록했기 때문에 요청 url이 .do로 끝나면 이 요청들은 DispatcherServlet이 받는다.
2. 요청을 처리할 ~Handler 일반 클래스를 만든다. (Handler 인터페이스를 상속받음)
- 요청을 실제로 처리하는 클래스는 ~Handler라는 일반 클래스 들이다.
- 각 Handler 클래스는 요청을 처리하고 뷰페이지 경로를 반환한다. (return view;)
- Handler 클래스들은 /WEB-INF/commands.properties 파일에 url과 함께 등록.
- DispatcherServlet의 init()은 서블릿 실행시 맨 처음에 한 번만 실행된다. (서버에 서블릿이 올라갈 때)
- 서버가 재실행되면 다시 init()이 실행된다.
- init() 메서드에서 commands.properties파일의 데이터를 하나씩 읽어서 매개변수 Map에 저장.
map.put("/member/join.do", new JoinHandler());
map.put("/member/join.do", new LoginHandler());
- Handler handler = map.get("/member/join.do");
- String view = handler.process(req, res);
- /member/a.jsp => forward 로 이동
- redirect: /member/a.jsp => sendRedirect 로 이동
< Handler >
- 스프링에는 다 만들어져있다. 스프링 맛보기로 만들어보는거임.
// String을 반환하는 Handler 인터페이스를 만듦.
Handler(interface)
String process(req, res); -- 추상메서드
위의 핸들러를 상속받는 기능 핸들러를 만듦.
JoinHandler implements Handler()
LoginHandler implements Handler()
1. new -> interface -> Handler
https://intheham.tistory.com/18
[JAVA] 추상클래스, 인터페이스
1. 추상 클래스 상속을 위해 만든 부모 클래스의 메소드는 상속을 목적으로 했을 뿐 해당 클래스에서 별다른 기능을 하진 않음. => 굳이 부모 클래스에 있을 필요가 없음. 메서드 구현 없이 메서
intheham.tistory.com
- interface 는 상수와 추상메서드로만 구성된 상속 목적의 완전추상클래스이다.
- 필요에 따라 객체들을 교체해서 사용하도록 하는 다리 역할을 함.
// 모든 요청 처리 클래스의 부모
public interface Handler {
String process(HttpServletRequest request, HttpServletResponse response);
}
- String process ... 는 public을 안 쓴, process라는 이름의, request와 response를 파라메터로 가지는 추상메서드 이다.
- public을 안쓰면 자동으로 public으로 인식함~
- 추상메서드 : 상속 목적으로 껍데기만 만들어놓은 상속 목적 메서드. {} 없이 ;로 끝남.
- 하위클래스가 자기한테 맞게 구현해서 사용하라고 추상메서드로 만든거임!!
2. Handler를 상속받은 기능 클래스 구현
- 기본 class 파일로 생성
- add를 눌러 상속받을 handler 파일을 선택함
- 껍데기만 만들어놓은 추상메서드의 속에 Servlet에 구현하는 것처럼 씀.
import handler.Handler;
public class JoinHandler implements Handler {
@Override
public String process(HttpServletRequest request, HttpServletResponse response) {
// 요청 처리에 사용할 req, res를 파라메터로 받고, 결과 페이지를 리턴값으로 반환하는 메서드.
String view ="";
if(request.getMethod().equals("GET")) { // 전송방식이 get방식?
view = "/member/join2.jsp";
} else { // 전송방식이 post방식?
// 회원가입 폼에 입력한 값을 변수에 담기
String id = request.getParameter("id");
String pwd = request.getParameter("pwd");
String name = request.getParameter("name");
String email = request.getParameter("email");
// 파라메터로 받은 값을 DB에 넣음
MemberService service = new MemberService();
service.join(new MemberVo(id, pwd, name, email));
//redirect 기능이 있는건 아님. 구분하는 용도로 내가 걍 붙여준 거.
//dispatcherServlet으로 돌아가는 건 같음.
view = "redirect:/index.jsp";
}
return view; //get방식
}
}
- Handler(interface) 에서 상속받은 process 추상메서드를 override함. (수정함!)
- 반환타입은 String. 경로를 반환해야 하기 때문에.
- request.getMethod() -- 요청방식을 리턴함 (GET or POST)
- get 방식으로 들어오면 if 문 실행
- post 방식으로 들어오면 else 문 실행
- view에 이동하고싶은 경로만 적어줌.
- forward 이동일 때에는 view 에 경로만 씀.
- redirect 이동일 때에는 처리하는 방식이 다르기 때문에 구분하기 위해 경로 앞에 "redirect:" 붙여줌.
-----> DispatcherServlet으로 이동해서 처리함.
< commands.properties >
!! 실행될 모든 핸들러 클래스를 다 등록해줘야함. !!
- 띄어쓰기 금지~
url=핸들러 클래스
// key: url, value: handler
/member/join.do=hendler.member.JoinHandler
/member/login.do=hendler.member.LoginHandler
< DispatcherServlet >
0. 총코드
@WebServlet("*.do")
public class DispatcherServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
private Map<String, Handler> map = new HashMap<>();
@Override
public void init() throws ServletException {
Properties prop = new Properties();
String path = this.getServletContext().getRealPath("/WEB-INF/commands.properties");
try {
prop.load(new FileReader(path));
Iterator iter = prop.keySet().iterator();
while (iter.hasNext()) {
String url = (String) iter.next();
String className = prop.getProperty(url);
try {
Class<?> handlerClass = Class.forName(className);
Constructor<?> cons = handlerClass.getConstructor(null);
Handler handler = (Handler) cons.newInstance();
map.put(url, handler);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (SecurityException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (IllegalArgumentException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
public DispatcherServlet() {
super();
}
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String url = request.getServletPath();
String view = null;
Handler handler = map.get(url);
if (handler != null) {
view = handler.process(request, response);
if (view.startsWith("redirect")) {
String[] path = view.split(":");
response.sendRedirect(request.getContextPath() + path[1]);
} else {
RequestDispatcher dis = request.getRequestDispatcher(view);
dis.forward(request, response);
}
} else {
response.getWriter().append("404 not found url");
}
}
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doGet(request, response);
}
}
1. DispatcherServlet으로 이동
<a href="${pageContext.request.contextPath}/member/join.do">회원가입</a><br/>
<a href="${pageContext.request.contextPath}/member/login.do">로그인</a><br/>
<a href="${pageContext.request.contextPath}/member/edit.do?id=${sessionScope.loginId}">내정보확인</a><br/>
<a href="${pageContext.request.contextPath}/member/logout.do">로그아웃</a><br/>
<a href="${pageContext.request.contextPath}/member/out.do">탈퇴</a><br/>
@WebServlet("*.do")
- 뷰페이지(jsp) 요청을 제외한 모든 요청이 여기로 온다.
- jsp에서 .do로 끝나는 링크를 누르면 DispatcherServlet으로 넘어간다.
- * 은 every 임. 모든걸 로드함
2. Map 매개변수 선언
/member/join.do=handler.member.JoinHandler
/member/login.do=handler.member.LoginHandler
/member/edit.do=handler.member.EditHandler
/member/logout.do=handler.member.LogoutHandler
/member/out.do=handler.member.OutHandler
/webapp/WEB-INF 에 만든 commands.properties 파일
url=핸들러주소
private Map<String, Handler> map = new HashMap<>();
map.put("/member/join.do", new JoinHandler());
map.put("/member/join.do", new LoginHandler());
- commands.properties 파일의 url과 명령어를 읽어서 저장할 map 변수 선언
- Map< key, value > 에서
key : url(/member/join.do)
value : 핸들러 주소(handler.member.JoinHandler)
- 각 handler 클래스를 모두 포함해서 업캐스팅할 수 있는 타입이 Handler(interface)임.
3. Source > Override > GenericServlet > init() 생성
@Override
public void init() throws ServletException {
}
- init() : 서블릿 생성시 딱 한 번 실행되는 메서드
처음 서블릿이 실행될 때 한 번 실행되고 그 이후에는 init()으로 로드한 map 값에서 필요한 정보만 불러와 기능 실행함.
if (url.startsWith("/member/join.do")) {
handler = new JoinHandler();
} else if (url.startsWith("/member/login.do")) {
handler = new LoginHandler();
}
처럼 하드코드로 doGet()에 구현할 수 있음.
그러나 기능이 많아지면 수십~수백줄의 코드를 손으로 작성해야 함. => 유지보수 어려움
그래서 while문으로 시스템에서 핸들러를 로드할 수 있게 메서드를 만드는 것임.
---- (1) Map 변수의 key에 넣을 url 지정하기
Properties prop = new Properties();
1) Properties 타입의 변수 선언 - 맵의 한 형태. 프로그램의 설정, 정보 등 기타 데이터를 저장한다.
String path = this.getServletContext().getRealPath("/WEB-INF/commands.properties");
2) 파라메터에 담은 경로를 웹에서 사용하는 실제 경로로 반환해 String path에 담는다.
- getServletContext() : 서블릿 클래스와 서블릿 컨테이너 간 통신을 위한 메소드들을 정의해둔 클래스 (??^^??)
- getRealPath() : getServletContext의 메서드 중 하나. 주어진 디렉토리(폴더)의 실제 실행되는 서버상의 주소를 절대경로로 알려줌.
prop.load(new FileReader(path));
3) .load() : 파일 데이터를 읽어서 Properties 객체에 저장
- commands.properties 파일의 키, 값을 읽어서 prop에 저장한다.
Iterator iter = prop.keySet().iterator();
4) prop에서 .keySet()으로 키값만 뽑아서 반복자에 넣는다.
while (iter.hasNext()) {
String url = (String) iter.next();
Iterator<요소타입> 반복자명 = 리스트명.iterator(); // 생성
while (반복자명.hasNext()) { // 다음에 읽을 요소 있으면 true, 아니면 false 반환.
요소타입 변수명 = iter.next(); // 다음 요소 추출
}
5) 키값의 수만큼 while 루프문을 돌린다. --- map.put(url, handler); 하며 마무리됨.
- 키 값을 url 변수에 넣는다. ---- /member/join.do
- .next로 불러온 iter는 Iterator 타입이므로 (String)으로 강제형변환 해준다.
---- (2) Map 변수의 value에 넣을 handler 지정하기
String className = prop.getProperty(url);
1) getProperty() ( = map의 get) : 키의 값을 잃어오는 메서드.
- url 변수에 넣어진 키와 매칭되는 값을 읽어옴. (commands.properties 파일에서)
- url이 "/member/join.do" 이면 className은 "hendler.member.JoinHandler"
----- 여기서부터는 String으로 불러온 핸들러 이름을 실제로 실행될 Handler 타입으로 변환하는 과정. -------
Class<?> handlerClass = Class.forName(className);
2) 지정한 클래스의 정보를 갖는 Class 객체 반환
- Class 객체 : 클래스 정보(이름, 멤버변수이름 및 타입, 메서드이름, 프로토타입 등)를 가지고 있음. object의 getClass의 반환값.
- forName() : 물리적인 클래스 파일명을 매개변수로 넣어주면 이에 해당하는 Class 객체 반환
--- hendler.member.JoinHandler ==> JoinHandler.java
- <?> : 클래스 종류가 다양해서 하나로 지정할 수 없어서 ? 처리 함.
Constructor<?> cons = handlerClass.getConstructor(null);
3) getConstructor로 지정한 클래스의 객체를 생성할 생성자(Constructor) 반환
- JoinHandler 와 같은 기능 클래스 파일에는 생성한 생성자가 따로 없음
=> 컴파일러가 자동을 생성자를 생성해줌! 그걸 불러오는거임!
==> 근데 생성자를 구현한 건 아니니까 getConstructor(null)로 생성자를 반환함!
Handler handler = (Handler) cons.newInstance();
4) handler 객체 생성
- newInstance : 클래스 이름을 결정할 수 없고 프로그램이 동작하는 시점에 이름이 결정되는 경우에 사용됨.
원래는 new로 생성자를 호출했음. MemberDao dao = new MemberDao(); 처럼!
그치만 이때는 생성자를 호출할 명확한 클래스가 있었음.
이번 경우에는 while 루프문이 돌면서 Class<?> 와 Constructor<?>이 정해지기 때문에
newInstance를 쓰는거임!!
Class<?> handlerClass = Class.forName(className);
Constructor<?> cons = handlerClass.getConstructor(null);
Handler handler = (Handler) cons.newInstance();
위 3줄은 Handler handler = new JoinHandler(); 와 같음
---- (3) 지정한 key와 value를 map에 넣기
map.put(url, handler);
}
- map에 넣으며 while문 종료됨.
Iterator iter = prop.keySet().iterator(); // 키 값만 뽑음
while (iter.hasNext()) { }
map.put("/member/join.do", new JoinHandler());
map.put("/member/join.do", new LoginHandler());
- commands.properties 파일에 있는 키값이 끝날 때까지 while문을 돌림.
--> 서블릿이 실행될 때 다 로드함.
4. DispatcherServlet -- doGet() 실행
---- (4-1) 요청들어온 url과 handler 검색
<!-- index.jsp -->
<a href="${pageContext.request.contextPath}/member/join.do">회원가입</a><br/>
String url = request.getServletPath();
Handler handler = map.get(url);
- request.getServletPath()로 index.jsp에서 요청들어온 url을 알 수 있다. (/member/join.do)
- 그 url과 매칭되는 handler(값)을 불러옴. (JoinHandler.java)
- .get(키) -- 키와 매칭된 값을 뽑아낼 수 있음.
---- (4-2) 검색된 handler가 있을 경우 ---- view 읽어오기
String view = null;
if (handler != null) {
view = handler.process(request, response);
- 구현한 기능 클래스가 맞으면 if문 실행
view = handler.process(request, response);
==> 기능 클래스로 넘어가는 코드!!!!!
- 기능클래스(handler)의 process 요청 처리 메서드 호출 --> 기능 Handler 클래스 파일로 넘어감.!!!!
Handler(interface)에서 상속받아서 각 기능 클래스에서 override 한 process 메서드!
그 메서드가 return한 view를 String view에 담는 과정임!
---- (4-3) 검색된 handler가 없을 경우 ---- 에러메시지 띄우기
} else {
response.getWriter().append("404 not found url");
}
- handler == null 이면 commands.properties에 등록한 handler가 아님.
- 기능 구현을 안했다는 거임
- 그럴 땐 웹뷰에 "404" 메시지를 띄워라.
** 5. 기능 클래스 실행
// JoinHandler.java
String view = "";
if(request.getMethod().equals("GET")) {
view = "/member/join.jsp";
}
return view;
- request.getMethod() -- 요청방식을 리턴함 (GET or POST .. ?)
- get 방식으로 들어오면 if 문 실행
- forward 이동일 때에는 view 에 경로만 씀.
- return을 만나 메서드가 종료되면 메서드를 호출한 페이지로 자동으로 넘어감.
** 6. jsp에서 submit버튼을 누름 -> 기능클래스 재실행
<!-- join.jsp -->
<form name = "f" action="${pageContext.request.contextPath}/member/join.do" method="post">
<table border="1">
<tr><th>ID</th><td><input type="text" name="id"></td></tr>
<tr><th>PWD</th><td><input type="password" name="pwd"></td></tr>
<tr><th>NAME</th><td><input type="text" name="name"></td></tr>
<tr><th>EMAIL</th><td><input type="text" name="email"></td></tr>
<tr><th>가입</th><td><input type="submit" value="가입"></td></tr>
</table>
</form>
// JoinHandler.java
} else {
String id = request.getParameter("id");
String pwd = request.getParameter("pwd");
String name = request.getParameter("name");
String email = request.getParameter("email");
MemberService service = new MemberService();
service.join(new MemberVo(id, pwd, name, email));
view = "redirect:/index.jsp";
}
return view;
- 기존 Servlet의 doPost()에서 처럼 코드를 작성함.
response.sendRedirect(request.getContextPath()+"/index.jsp");
- 대신 "redirect:/index.jsp"로 적어줌.
-- 그냥 String으로 넣는거라 redirect 기능이 있는건 아님. => DispatcherServlet에서 처리할거임.
7. DispatcherServlet -- doGet() 재실행
-------- (4-2-1) view 가 redirect로 시작할 때, (4-2-2) 그렇지 않을 때의 경우 설정
if (view.startsWith("redirect")) {
String[] path = view.split(":");
response.sendRedirect(request.getContextPath() + path[1]);
} else {
RequestDispatcher dis = request.getRequestDispatcher(view);
dis.forward(request, response);
}
- A.startsWith("B") / A.endWith("B") -- A가 B으로 시작하냐/끝나냐? 맞으면 true 반환
- A.equals("B) -- A와 B가 완전히 같을 때에만 true, 하나라도 다르면 false
view = "redirect:/index.jsp" 이면
String[] path = view.split(":"); 할 때
path[0] = redirect
path[1] = /index.jsp 가 된다.
- view가 redirect로 시작하면 ":" 를 기준으로 문자열을 쪼개서 각각을 배열 path에 담아라.
- .split() -- 구분자를 기준으로 문자열 쪼개기. 반환타입은 항상 String[] 이다.
- path[1] 에 담긴 경로로 redirect 보내라.
view = "/member/join.jsp" 이면
- view가 그냥 경로로만 되어 있으면 그 경로로 forward 방식으로 가라!