리플렉션이란 객체를 통해 클래스의 정보를 분석해 내는 프로그램 기법을 말한다. 투영, 반사 라는 사전적인 의미를 지니고 있다.
자바는 스크립트 언어가 아닌 컴파일 언어이다. 물론 .java -> .class -> 실행이라는 2단계의 메커니즘을 가지고 있지만 컴파일 언어로 분리하는 게 옳다. 원래 자바에서는 동적으로 객체를 생성하는 기술이 없었다. 그리고 동적으로 인스턴스를 생성하는 Reflection으로 그 역활을 대신하게 된다.
즉 클래스나 메서드의 내부 구조를 들여다 볼 때 사용하는 도구라는 뜻이다.
MVC구조에서 좀 더 유지보수가 쉬운 구조로 개선 한 것으로 두개의 컨트롤러를 사용하여 웹 브라우저 요청을 처리한다. 프론트 컨트롤러는 VO 객체의 준비, 뷰 컴포넌트의 위임, 오류 처리 등과 같은 공동 작업을 담당하고, 페이지 컨트롤러는 이름 그대로 요청한 페이지만을 위한 작업을 수행한다.
DispatcherServlet
은 회원 목록 요청 처리를 담당하는 페이지 컨트롤러를 호출한다. 이때 데이터를 주고받을 바구니 역할을 할 Map 객체를 넘긴다.MemverListController
는 Dao에게 회원 목록 데이터를 요청한다.프런트 컨트롤러가 페이지 컨트롤러에게 작업을 위임할때 execute()
메서드를 호출한다. 이렇게 호출하기 위해서 페이지 컨트롤러 클래스를 대표하는 Interface를 만들고 execute()
메서드를 상속받게 만든다. 그러면 일관성있는 사용을 보장할 수 있다.
페이지 컨트롤러를 서블릿이 아닌 자바파일로 만들었기 때문에, Dao에서 리턴받은 결과값을 Map을 사용하여 데이터 저장하여, 프런트 컨트롤러로 전달한다. 프런트 컨트롤러에서는 jsp에서 이 데이터들을 사용하기 위해 서블릿 보관소에 저장한다. 이런 과정을 프런트 컨트롤러 패턴이라고 한다.
의존 객체를 사용하는 쪽과 의존 객체(또는 보관소) 사이의 결합도가 높아져서 의존 객체나 보관소에 변경이 발생하면 바로 영향을 받는다. 그리고 의존 객체를 다른 객체로 대체하기 가 어렵다. 이런 문제를 해결하기 위해 의존 객체를 외부에서 주입받는 방식이 등장하였다.
이의존객체를 전문으로 관리하는 빈컨테이너(Java Beans Container)
를 사용한다. 빈컨테이너는 객체가 실행되기 전에 그 객체가 필요로 하는 의존 객체를 주입해주는 역할을 수행한다. 이런방식으로 의존 객체를 관리하는 것을 의존성주입(DI:Dependency Injection)
좀 더 일반적으로 역제어(IoC:Inversion of Control)
라고 한다. 즉 역제어 방식의 한 예가 의존성 주입이다.
하지만 이 방법으로는 DispatcherServlet
의 경우 페이지 컨트롤러가 추가 될 때마다 조건문을 변경해야하고, ContextLoaderListener
도 Dao나 페이지 컨트롤러가 추가될 때마다 변경해야하는 문제가 있다.
// 의존 객체 주입을 위해 인스턴스 변수와 셋터 메서드 추가
//- 또한 의존 객체를 꺼내는 기존 코드 변경
public class MemberListController implements Controller {
MemberDao memberDao;
public MemberListController setMemberDao(MemberDao memberDao) {
this.memberDao = memberDao;
return this;
}
@Override
public String execute(Map<String, Object> model) throws Exception {
model.put("members", memberDao.selectList());
return "/member/MemberList.jsp";
}
}
// ServletContext에 보관된 페이지 컨트롤러를 사용
@SuppressWarnings("serial")
@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");
String servletPath = request.getServletPath();
try {
ServletContext sc = this.getServletContext();
// 페이지 컨트롤러에게 전달할 Map 객체를 준비한다.
HashMap<String,Object> model = new HashMap<String,Object>();
model.put("session", request.getSession());
Controller pageController = (Controller) sc.getAttribute(servletPath);
if ("/member/add.do".equals(servletPath)) {
if (request.getParameter("email") != null) {
model.put("member", new Member()
.setEmail(request.getParameter("email"))
.setPassword(request.getParameter("password"))
.setName(request.getParameter("name")));
}
} else if ("/member/update.do".equals(servletPath)) {
if (request.getParameter("email") != null) {
model.put("member", new Member()
.setNo(Integer.parseInt(request.getParameter("no")))
.setEmail(request.getParameter("email"))
.setName(request.getParameter("name")));
} 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()
.setEmail(request.getParameter("email"))
.setPassword(request.getParameter("password")));
}
}
// 페이지 컨트롤러를 실행한다.
String viewUrl = pageController.execute(model);
// Map 객체에 저장된 값을 ServletRequest에 복사한다.
for (String key : model.keySet()) {
request.setAttribute(key, model.get(key));
}
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);
}
}
}
@WebListener
public class ContextLoaderListener implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent event) {
try {
ServletContext sc = event.getServletContext();
InitialContext initialContext = new InitialContext();
DataSource ds = (DataSource)initialContext.lookup(
"java:comp/env/jdbc/studydb");
MemberDao memberDao = new MemberDao();
memberDao.setDataSource(ds);
sc.setAttribute("/auth/login.do",
new LogInController().setMemberDao(memberDao));
sc.setAttribute("/auth/logout.do", new LogOutController());
sc.setAttribute("/member/list.do",
new MemberListController().setMemberDao(memberDao));
sc.setAttribute("/member/add.do",
new MemberAddController().setMemberDao(memberDao));
sc.setAttribute("/member/update.do",
new MemberUpdateController().setMemberDao(memberDao));
sc.setAttribute("/member/delete.do",
new MemberDeleteController().setMemberDao(memberDao));
} catch(Throwable e) {
e.printStackTrace();
}
}
@Override
public void contextDestroyed(ServletContextEvent event) {}
}
DAO는 데이터 처리를 전문으로 하는 객체이다. 데이터베이스나 파일, 메모리 등을 이용하여 애플리케이션 데이터를 생성, 조회, 변경, 삭제하는 역할을 수행한다. DAO는 보통 하나의 DB테이블이나 DB뷰에 대응하고, 여러개의 테이블을 조인(join)한 데이터도 다룬다.
예>
public class MemberDao {
Connection connection;
public void setConnection(Connection connection) {
this.connection = connection;
}
public List<Member> selectList() throws Exception {
Statement stmt = null;
ResultSet rs = null;
try {
stmt = connection.createStatement();
rs = stmt.executeQuery(
"SELECT MNO,MNAME,EMAIL,CRE_DATE" +
" FROM MEMBERS" +
" ORDER BY MNO ASC");
ArrayList<Member> members = new ArrayList<Member>();
while(rs.next()) {
members.add(new Member()
.setNo(rs.getInt("MNO"))
.setName(rs.getString("MNAME"))
.setEmail(rs.getString("EMAIL"))
.setCreatedDate(rs.getDate("CRE_DATE")) );
}
return members;
} catch (Exception e) {
throw e;
} finally {
try {if (rs != null) rs.close();} catch(Exception e) {}
try {if (stmt != null) stmt.close();} catch(Exception e) {}
}
}
}
위의 예에서 두가지를 배울 수 있다.
첫번째는 프로그래밍의 유연성
이다. selectList()
에서 리턴 타입이 List인터페이스인 것이다. 하지만 실제 리턴값은 ArrayList객체이다. 이런것을 프로그래밍의 유연성이라고 한다.
두번째는 의존성주입(DI, Dependency Injection)
다른말로 역제어(IoC, Inversion of Control)
이다. connection 인스턴스 변수와 셋터 메서드는 MemberDao에서 ServletContext에 접근할 수 없기 때문에, ServletContext에 보관된 DB Connectoin 객체를 꺼낼 수 없어 필요하다. 외부로 부터 Connection객체를 connection 인스턴스변수와 셋터 메서드에 주입한다. 이런 것을 의존성 주입 또는 역제어라고 한다.
서블릿 컨테이너는 웹 어플리케이션의 상태를 모니터링 할 수 있도록 웹어플리케이션의 시작에서 종료까지 주요한 사건에 대한 알람기능을 제공한다. 이를 사용하기 위해 규칙에 따라 객체를 만들어 DD파일에 등록하면 된다.
액션 | 설명 |
---|---|
<jsp:useBean> | 자바 인스턴스를 준비한다. 보관소에서 자바 인스턴스를 꺼내거나, 자바 인스턴스를 새로 만들어 보관소에 저장하는 코드를 생성한다. 자바인스턴스를 자바 빈(Java Bean)이라고 한다. |
<jsp:setProperty> | 자바빈의 프로퍼티 값을 설정합니다. 자바 객체의 셋터 메서드를 호출하는 코드를 생성한다. |
<jsp:getProperty> | 자바빈의 프로퍼티 값을 꺼낸다. 자바 객체의 겟터메서드를 호출하는 코드를 생성한다. |
<jsp:include> | 정적 또는 동적자원을 인클루딩 하는 자바 코드를 생성한다. |
<jsp:forward> | 현재 페이지의 실행을 멈추고 다른 정적자원이나 동적자원으로 포워딩하는 자바 코드를 생성한다. |
<jsp:param> | jsp:include, jsp:forward, jsp:params의 자식 태그로 사용할 수 있다. ServletRequest 객체에 매게변수를 추가하는 코드를 생성한다. |
<jsp:plugin> | Object 또는 Embed HTML태그를 생성한다. |
<jsp:element> | 임의의 xml태그나 html태그를 생성한다. |
<jsp:useBean id = '이름' scope="page|request|session|application"
class="클래스명" type="타입명" />
<jsp:useBean id="members"
scope="request"
class="java.util.ArrayList"
type="java.util.ArrayList<spms.vo.Member>"/>
웹 애플리케이션이 시작 될 때 생성되어 웹 애플리케이션 종료될 때까지 유지된다. 이 보관소에 데이터를 보관하면 웹 애플리케이션이 실행되는 동안에는 모든 서블릿이 사용 할 수 있다. application
변수를 통해 이 보관소를 참조할 수 있다.
@Override
public void init(ServletConfig config) throws ServletException {
System.out.println("AppInitServlet 준비…");
super.init(config);
try {
ServletContext sc = this.getServletContext();
Class.forName(sc.getInitParameter("driver"));
Connection conn = DriverManager.getConnection(
sc.getInitParameter("url"),
sc.getInitParameter("username"),
sc.getInitParameter("password"));
sc.setAttribute("conn", conn);
} catch(Throwable e) {
throw new ServletException(e);
}
}
@Override
public void destroy() {
System.out.println("AppInitServlet 마무리...");
super.destroy();
Connection conn =
(Connection)this.getServletContext().getAttribute("conn");
try {
if (conn != null && conn.isClosed() == false) {
conn.close();
}
} catch (Exception e) {}
}
AppInitServlet클래스를 만들어 init()과 destroy()에서 DB커넥션과 릴리즈를 할 수 있다. 이 때 클래스의 URL 맵핑정보가 없는데, 이를 DD파일에서 다음과 같이 입력한다.
<servlet>
<servlet-name>AppInitServlet</servlet-name>
<servlet-class>spms.servlets.AppInitServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<load-on-startup> </load-on-startup>
태그를 사용해 클라이언트 요청이 없어도 해당 서블릿이 웹어플리케이션이 시작 될 때 자동으로 생성된다. 이 태그의 값은 생성 순서를 의미한다.
클라이언트의 최초 요청 시 생성되어 브라우저를 닫을 때까지 유지된다. 보통 로그인 할 때 이 보관소를 초기화하고, 로그아웃하면 이 보관소에 저장된 값들을 비운다. 이 보관소에 값을 보관하면 서블릿이나 JSP 페이지에 상관없이 로그아웃 하기 전까지 계속 값을 유지 가능하다. session
변수를 통해서 이 보관소에 참조할 수 있다.
// HttpSession변수에 값을 저장한다.
HttpSession session = request.getSession();
session.setAttribute("member", member);
// HttpSession변수에 저장된 값을 가져다 사용한다.
Member member = (Member)session.getAttribute("member");
// HttpSession 객체를 제거한다.
session.invalidate();
클라이언트의 요청이 들어올 때 생성되어, 클라이언트에게 응답 할 때까지 유지된다. 이 보관소는 포워딩이나 인클루딩하는 서블릿들 사이에서 값을 공유 할 때 유용하다. request
변수를 통해 이 보관소에 참조할 수 있다.
// JSP로 출력을 위임한다.
RequestDispatcher rd = request.getRequestDispatcher("/member/MemberList.jsp");
rd.include(request, response);
JSP 페이지를 실행하는 동안만 유지. 실제로 잘 쓸 일이 없다. pageContext
변수를 통해서 이 보관소를 참조할 수 있다.
JSP 기술의 가장 중요한 목적은 콘텐츠를 출력하는 코딩을 단순화 하는 것이다.
JSP엔진
을 통해서 JSP파일을 해석하여 서블릿 자바소스를 생성한다.service()
메서드가 호출되고, 출력 메서드를 통해 서블릿이 생성한 HTML 화면을 웹브라우저로 보낸다.결론은 JSP가 직접 실행되는 것이 아니라 JSP로부터 만들어진 서블릿이 실행된다.
JSP엔진은 JSP 파일로 부터 서블릿 클래스를 생성할 때 HttpJspPage
인터페이스를 구현한 클래스를 만든다.
상속관계는 다음과 같다.
- Servlet(Interface)
- init()
- service()
- destory()
- getServletConfig()
- getServletInfo()
- JspPage(Interface)
- jspInit()
- jspDestory()
- HttpJspPage(Interface)
- _jspSerivce()
결론적으로 HttpJspPage는 Servlet 인터페이스도 구현한다는 것이기 때문에 해당 클래스는 서블릿이 될 수 있다.
jspInit()
jspDestory()
_jspService()
page
지시자는 JSP페이지와 관련된 속성을 정의할 때 사용하는 태그이다. JSP 지시자에는 page
, taglib
, include
가 있다.
<%@page import="spms.vo.Member"%>
<%@page import="java.util.ArrayList"%>
<%@ page
language="java"
contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
language속성
은 스크립트릿(Scriptlet)이나, 표현식(Expression element), 선언부(Declaration element)를 작성할때 사용한다.contenttype속성
은 출력할 데이터의 MIME타입과 문자 집합을 지정한다.pageEncoding속성
은 출력할 데이터의 문자 집합을 지정한다. 기본값은 ISO-8859-1
이다. 만약 이속성을 생략하면 contentType
에 설정된 값을 사용한다. contentType속성
에도 없다면 기본값 ISO-8859-1
을 사용한다.JSP 페이지 안에서 자바코드를 넣을 때는 스크립트릿(Scriptlet Elements)태그 <% %>
안에 작성한다. <% 자바 코드 %>
<%
String v1 = "";
String v2 = "";
String result = "";
String[] selected = {"", "", "", ""};
//값이 있을 때만 꺼낸다.
if (request.getParameter("v1") != null) {
v1 = request.getParameter("v1");
v2 = request.getParameter("v2");
String op = request.getParameter("op");
result = calculate(
Integer.parseInt(v1),
Integer.parseInt(v2),
op);
if ("+".equals(op)) {
selected[0] = "selected";
} else if ("-".equals(op)) {
selected[1] = "selected";
} else if ("*".equals(op)) {
selected[2] = "selected";
} else if ("/".equals(op)) {
selected[3] = "selected";
}
}
%>
위 코드의 자바 서블릿 소스에서 _jspService() 메서드를 보면 스크립트릿 태그안의 자바소스가 그대로 입력된 것을 알 수 있다.
JSP페이지에서 스크립트릿 <% %>
이나 표현식 <%= %>
을 작성할 때 별도의 선언 없이 사용하는 자바 객체를 JSP 내장객체(Implicit Objects)라 한다. _jspService()
는 javax.servlet.jsp.HttpJspPage
인터페이스에 선언된 메서드이다. JSP페이지로 서블릿을 만들때 반드시 이 인터페이스를 구현하도록 정의 되어 있다.
JSP 기술 사양서는 JSP페이지 작성자가 별도 선언 없이 즉시 이용할 수 있는 9개 객체를 정의 한다.
request
, response
, pageContext
, session
, application
, config
, out
, page
, exception
객체가 _jspservice()
메서드에 선언되어 있어 별도의 선언 없이 스크립트릿과 표현식에 JSP내장객체를 사용할 수 있다.
JSP 선언문(Declarations) <%! %>
은 서블릿 클래스의 맴버(변수나 메서드)를 선언 할 때 사용하는 태그이다. JSP페이지에서 선언문 <%! %>
을 작성하는 위치는 위, 아래, 가운데 어디든 상관 없다. 왜나햐면 선언문은 _jspService()
메서드 안에 복사되는 것이 아니라, _jspService()
밖의 클래스 블록 안에 복사 되기 때문이다.
<%! 맴버 변수 및 메서드 선언 %>
<%!
private String calculate(int a, int b, String op) {
int r = 0;
if ("+".equals(op)) {
r = a + b;
} else if ("-".equals(op)) {
r = a - b;
} else if ("*".equals(op)) {
r = a * b;
} else if ("/".equals(op)) {
r = a / b;
}
return Integer.toString(r);
}
%>
표현식(Expressions)태그는 문자열을 출력할 때 사용한다. <%= %>
안에는 결과를 반환하는 자바코드가 와야한다. 표현식도 스크립트릿과 같이 _jspService()
안에 순서대로 복사한다.
<body>
<h2>JSP 계산기</h2>
<form action="Calculator.jsp" method="get">
<input type="text" name="v1" size="4" value="<%=v1%>">
<select name="op">
<option value="+" <%=selected[0]%>>+</option>
<option value="-" <%=selected[1]%>>-</option>
<option value="*" <%=selected[2]%>>*</option>
<option value="/" <%=selected[3]%>>/</option>
</select>
<input type="text" name="v2" size="4" value="<%=v2%>">
<input type="submit" value="=">
<input type="text" size="8" value="<%=result%>"><br>
</form>
</body>
표현식 태그에 작성한 자바코드가 서블릿 파일안에서는 out.print()
출력의 인자값(argument)으로 복사된다. 출력문을 만들 때 out.print()
만 사용하는 것은 아니다. out.write()
도 가능하다 즉 JSP엔진에 따라 만들어지는 자바코드가 달라 질 수 있다. 톰켓에서는 JSP 표현식에 대해 out.print()
메서드를 만든다.
데이터베이스에서 가져온 정보를 JSP페이지에 전달하려면 그 정보를 담을 객체가 필요하다. 이렇게 값을 담는 용도로 사용하는 객체를 값 객체(value object)
라고한다. VO는 계층간 또는 객체 간에 데이터를 전달하는 데 용이하므로 데이터 수송 객체(Data Transfer Object)
라고도 한다.
보통 데이터베이스 테이블에 대응하여 값 객체를 정의한다.
package spms.vo;
import java.util.Date;
public class Member {
protected int no;
protected String name;
protected String email;
protected String password;
protected Date createdDate;
protected Date modifiedDate;
public int getNo() {
return no;
}
public Member setNo(int no) {
this.no = no;
return this;
}
public String getName() {
return name;
}
public Member setName(String name) {
this.name = name;
return this;
}
public String getEmail() {
return email;
}
public Member setEmail(String email) {
this.email = email;
return this;
}
}
setXXX()
메소드의 return값을 this로 하는 이유는 다음과 같다.
//return this를 사용한 경우
while(rs.next()) {
members.add(new Member()
.setNo(rs.getInt("MNO"))
.setName(rs.getString("MNAME"))
}
//set메서드 반환값이 없는 경우
Member member = new Member();
member.setNo(rs.getInt("MNO"));
member.setName(rs.getString("MNAME"));
member.setEmail(rs.getString("EMAIL"));
객체를 생성한 즉시, setter메서드를 연속으로 호출하여 값을 할당한다. 이런 코딩 방식은 스크립팅 언어에서 널리 사용되는 방식이다.
화면생성을 위해 JSP로 작업을 위임해야한다. 이렇게 다른 서블릿이나 JSP로 작업을 위임할 때 사용하는 객체가 RequestDispatcher
이다. 이 객체는 HttpservletRequest
를 통해서 얻을 수 있다.
RequestDispatcher rd = request.getRequestDispatcher(
"/member/MemberList.jsp");
예)
// request에 회원 목록 데이터 보관한다.
request.setAttribute("members", members);
// JSP로 출력을 위임한다.
RequestDispatcher rd = request.getRequestDispatcher(
"/member/MemberList.jsp");
rd.include(request, response);
<%@page import="spms.vo.Member"%>
<%@page import="java.util.ArrayList"%>
<%@ page
language="java"
contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
"http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>회원 목록</title>
</head>
<body>
<h1>회원목록</h1>
<p><a href='add'>신규 회원</a></p>
<%
ArrayList<Member> members = (ArrayList<Member>)request.getAttribute(
"members");
for(Member member : members) {
%>
<%=member.getNo()%>,
<a href='update?no=<%=member.getNo()%>'><%=member.getName()%></a>,
<%=member.getEmail()%>,
<%=member.getCreatedDate()%>
<a href='delete?no=<%=member.getNo()%>'>[삭제]</a><br>
<%} %>
</body>
</html>
page
지시자로 import
를 처리한다. 그리고 setAttribute
로 넘겨준 값을 getAttribute()
를 호출하여 사용한다.
포워드 방식은 작업을 한 번 위임하면 다시 이전 서블릿으로 제어권이 돌아오지 않는다. 포워딩은 예외가 발생하면 화면 내용보다 좀 더 부드러운 안내 문구를 출력하는데 보통 사용한다.
RequestDispatcher rd = request.getRequestDispatcher("/error.jsp");
rd.forward(request, response);
<jsp:forward page="/header.jsp"/>
인클루드 방식은 다른 서블릿으로 작업을 위임한 후 , 그 서블릿의 실행이 끝나면 다시 이전 서블릿으로 제어권이 넘어온다. 한 jsp에 여러 페이지를 포함할 때 사용한다.
RequestDispatcher rd = request.getRequestDispatcher("/Header.jsp");
rd.include(request, response);
<jsp:include page="/header.jsp"/>