Leeyebin의 블로그

5장 MVC 아키텍쳐 본문

공부 기록실/JAVA 웹 개발 워크북 요약정리

5장 MVC 아키텍쳐

안되면될때까지 2017. 4. 17. 12:30

서블릿의 단점을 보완하기 위해 등장한 JSP(JavaServer Page)


5.1 MVC 이해하기


올인원 All-in-one 방식과 문제점

-이전에는 클라이언트와 요청 처리를 서블릿 홀로 담당하는 올인원 방식이었다.(서블릿 혼자 북치고 장구치고)

-올인원 방식은 규모가 작거나 업무 변경이 많지 않은 경우에 적합하지만, 규모가 크거나 업무 변경이 잦은 경우에는 오히려 유지 보수가 어려워 운영 비용이 증가하게 된다.


글로벌 환경과 MVC 아키텍처

-시스템 변경이 잦은 상황에서 유지 보수를 보다 쉽게 하려면, 중복 코드의 작성을 최소화 하고, 코드 변경이 쉬워야 한다. 그래서 기존 코드의 재사용성을 높이는 방향으로 설계해야한다. 특히 객체지향의 특성을 활용하여 좀 더 역할을 세분화하고 역할 간 의존성을 최소화해야한다.



MVC의 각 컴포넌트 역할

컨트롤러(controller) 컴포넌트의 역할은, 클라이언트의 요청을 받았을 때 그 요청에 대해 실제 업무를 수행하는 모델 컴포넌트를 호출하는 일이다. 또 클라이언트가 보낸 데이터가 있다면, 모델을 호출할 때 데이터를 적절히 가공하는 일을 한다.


모델(model) 컴포넌트의 역할은, 데이터 저장소와 연동하여 사용자가 입력한 데이터나 사용자에게 출력할 데이터를 다루는 일을 한다.(트랜젝션을 다루는 일)


뷰(view) 컴포넌트의 역할은, 모델이 처리한 데이터나 그 작업 결과를 갖고 사용자에게 출력할 화면을 만드는 일을 한다.


MVC 이점

높은 재사용성, 넓은 융통성

  • look and feel을 쉽게 교체할 수 있다.
  • one source multi use를 구현할 수 있다.
  • 코드를 재사용할 수 있다.
빠른 개발, 저렴한 비용
  • 다른 프로젝트에서도 모델 컴포넌트를 재사용할 수 있어서 개발 속도가 빨라진다.
  • 소스 코드를 역할에 따라 여러 컴포넌트로 쪼개게 되면 전체적인 개발 및 유지보수 비용은 줄 수 있다.(낮은 수준의 개발자 투입 가능)

MVC 구동 원리

이미지 출처 : 자바 웹 개발 워크북 p249 글씨 윤형원 우정출연


  1. 웹 브라우저가 웹 애플리케이션 실행을 요청하면, 웹 서버가 그 요청을 받아서 서블릿 컨테이너에 넘겨준다. 서블릿 컨테이너는 URL을 확인하여 그 요청을 처리할 서블릿을 찾아서 실행한다.
  2. 서블릿은 실제 업무를 처리하는 모델 자바 객체의 메서드를 호출한다. 만약 웹 브라우저가 보낸 데이터를 저장하거나 변경해야 한다면 그 데이터를 가공하여 값 객체(VO)를 생성하고, 모델 객체의 메서드를 호출할 때 인자값으로 넘긴다. 모델 객체는 엔터프라이즈 자바빈(EJB)일 수도 있고, 일반 자바 객체(POJO)일 수도 있다.
  3. 모델 객체는 JDBC를 사용하여 매개변수로 넘어온 값 객체를 데이터베이스에 저장하거나, 데이터베이스로부터 질의 결과를 가져와서 값 객체로 만들어 반환한다. 이렇게 값 객체는 객체와 객체 사이에 데이터를 전달하는 용도로 사용하기 때문에 '데이터 전송 객체(DTO)'라고도 부른다.
  4. 서블릿은 모델 객체로부터 반환받은 값을  JSP에 전달한다.
  5. JSP는 서블릿으로부터 전달받은 값 객체를 참조하여 웹 브라우저가 출력할 결과 화면을 만든다. 그리고 웹 브라우저에 출력함으로써 요청 처리를 완료한다.
  6. 웹 브라우저는 서버로부터 받은 응답 내용을 화면에 출력한다.


5.2 뷰 컴포넌트와 JSP


JSP를 사용하는 이유
기존 방식으로  HTML을 out.println()을 사용하여 출력한다면 너무 복잡할 것이다. 이런 부분을 해소하고자 나온 기술이 JSP이다.

JSP 구동 원리
JSP 기술의 가장 중요한 목적은 컨텐츠를 출력하는 코딩을 단순화 하는 것이다.

이미지 출처 : 웹 개발 워크북 p253


  1. 개발자는 서버에 JSP 파일을 작성해 둔다. 클라이언트가 JSP를 실행해 달라고 요청하면, 서블릿 컨테이너는 JSP파일에 대응하는 자바 서블릿을 찾아서 실행한다.
  2. 만약 JSP에 대응하는 서블릿이 없거나 JSP파일이 변경되었다면, JSP 엔진을 통해 JSP 파일을 생성하여 서블릿 자바 소스를 생성한다.
  3. 서블릿 자바 소스는 자바 컴파일러를 통해 서블릿 클래스 파일로 컴파일된다. JSP 파일을 바꿀 때마다 이 과정을 반복한다.
  4. JSP로부터 생성된 서블릿은 서블릿 구동 방식에 따라 실행된다. 서블릿의 service() 메서드가 호출되고, 출력 메서드를 통해 서블릿이 생성한 HTML화면을 웹 브라우저로 보낸다.
JSP는 서블릿 자바 파일을 만들기 위한 템플릿으로 사용된다.

JSP 파일로부터 자동 생성된 자바 소스 파일 확인
(예 C:\javalec\workspace\.metadata\.plugins\org.eclipse.wst.server.core\tmp0\work\Catalina\localhost\web05\org\apache\jsp)


<%@ page language="java" contentType="text/html; charset=EUC-KR"
    pageEncoding="EUC-KR"%>
<!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=EUC-KR">
<title>Insert title here</title>
</head>
<body>
<p>안녕하세요</p>
</body>
</html>

위의 Hello.jsp를 만들고 서버에 올려본 후, 자바 파일을 확인해보았다.


/*
 * Generated by the Jasper component of Apache Tomcat
 * Version: Apache Tomcat/7.0.68
 * Generated at: 2017-04-17 05:34:48 UTC
 * Note: The last modified time of this file was set to
 *       the last modified time of the source file after
 *       generation to assist with modification tracking.
 */
package org.apache.jsp;

import javax.servlet.*;
import javax.servlet.http.*;
import javax.servlet.jsp.*;

public final class Hello_jsp extends org.apache.jasper.runtime.HttpJspBase
    implements org.apache.jasper.runtime.JspSourceDependent {

  private static final javax.servlet.jsp.JspFactory _jspxFactory =
          javax.servlet.jsp.JspFactory.getDefaultFactory();

  private static java.util.Map<java.lang.String,java.lang.Long> _jspx_dependants;

  private volatile javax.el.ExpressionFactory _el_expressionfactory;
  private volatile org.apache.tomcat.InstanceManager _jsp_instancemanager;

  public java.util.Map<java.lang.String,java.lang.Long> getDependants() {
    return _jspx_dependants;
  }

  public javax.el.ExpressionFactory _jsp_getExpressionFactory() {
    if (_el_expressionfactory == null) {
      synchronized (this) {
        if (_el_expressionfactory == null) {
          _el_expressionfactory = _jspxFactory.getJspApplicationContext(getServletConfig().getServletContext()).getExpressionFactory();
        }
      }
    }
    return _el_expressionfactory;
  }

  public org.apache.tomcat.InstanceManager _jsp_getInstanceManager() {
    if (_jsp_instancemanager == null) {
      synchronized (this) {
        if (_jsp_instancemanager == null) {
          _jsp_instancemanager = org.apache.jasper.runtime.InstanceManagerFactory.getInstanceManager(getServletConfig());
        }
      }
    }
    return _jsp_instancemanager;
  }

  public void _jspInit() {
  }

  public void _jspDestroy() {
  }

  public void _jspService(final javax.servlet.http.HttpServletRequest request, final javax.servlet.http.HttpServletResponse response)
        throws java.io.IOException, javax.servlet.ServletException {

    final javax.servlet.jsp.PageContext pageContext;
    javax.servlet.http.HttpSession session = null;
    final javax.servlet.ServletContext application;
    final javax.servlet.ServletConfig config;
    javax.servlet.jsp.JspWriter out = null;
    final java.lang.Object page = this;
    javax.servlet.jsp.JspWriter _jspx_out = null;
    javax.servlet.jsp.PageContext _jspx_page_context = null;


    try {
      response.setContentType("text/html; charset=EUC-KR");
      pageContext = _jspxFactory.getPageContext(this, request, response,
      			null, true, 8192, true);
      _jspx_page_context = pageContext;
      application = pageContext.getServletContext();
      config = pageContext.getServletConfig();
      session = pageContext.getSession();
      out = pageContext.getOut();
      _jspx_out = out;

      out.write("\r\n");
      out.write("<!DOCTYPE html PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\" \"http://www.w3.org/TR/html4/loose.dtd\">\r\n");
      out.write("<html>\r\n");
      out.write("<head>\r\n");
      out.write("<meta http-equiv=\"Content-Type\" content=\"text/html; charset=EUC-KR\">\r\n");
      out.write("<title>Insert title here</title>\r\n");
      out.write("</head>\r\n");
      out.write("<body>\r\n");
      out.write("<p>안녕하세요</p>\r\n");
      out.write("</body>\r\n");
      out.write("</html>");
    } catch (java.lang.Throwable t) {
      if (!(t instanceof javax.servlet.jsp.SkipPageException)){
        out = _jspx_out;
        if (out != null && out.getBufferSize() != 0)
          try {
            if (response.isCommitted()) {
              out.flush();
            } else {
              out.clearBuffer();
            }
          } catch (java.io.IOException e) {}
        if (_jspx_page_context != null) _jspx_page_context.handlePageException(t);
        else throw new ServletException(t);
      }
    } finally {
      _jspxFactory.releasePageContext(_jspx_page_context);
    }
  }
}


난 body태그 안에 '안녕하세요'만 넣었을 뿐인데, 엄청나게 길어졌다. 그리고 내가 JSP로 작성한 내용은 out.write에 고스란히 입력이 되어있는 것을 확인할 수 있다. 그렇다 JSP는 JSP가 실행되는 것이 아니라 JSP로부터 만들어진 서블릿이 실행된다.


HttpJspPage 인터페이스

JSP 엔진은 JSP파일로부터 서블릿 클래스를 생성할 때 HttpJspPage 인터페이스를 구현한 클래스를 만든다.



출처 : HttpJspBase.class를 디컴파일한 부분중


jspInit()

JspPage에 선언된 jspInit()는 JSP 객체(JSP로부터 만들어진 서블릿 객체)가 생성될 때 호출된다. 만약 JSP 페이지에서 init()을 오버라이딩할 일이 있다면 init() 대신 jspInit()를 오버라이딩 해야한다.


jspDestroy()

JSP페이지에서 destroy()를 오버라이딩 할 일이 있다면 jspDestroy()를 오버라이딩 해야한다.


_jspService()

JSP파일에 작성된 대부분의 내용이 이 메서드 안으로 들어간다.


JSP 객체의 실체 분석


자동 생성된 서블릿 클래스의 이름

Hello.jsp가 서블릿 클래스가 되면 이름이 톰캣의 경우 Hello_jsp.java로 변환된다.(서블릿 컨테이너마다 다르다.)


HttpJspBase 클래스

HttpJspBase는 톰캣 서버에서 제공하는 클래스이다. 이 클래스는 HttpServlet클래스를 상속받았고 HttpJspPage인터페이스를 구현하였다. 즉 HttpJspBase를 상속받은 Hello_jsp는 서블릿이라는 뜻이다.

public abstract class HttpJspBase 
    extends HttpServlet 
    implements HttpJspPage

JSP 프리컴파일

JSP 실행 요청이 들어 왔을 때, 곧바로 서블릿을 호출할 수 있기 때문에 실무에서는 웹 애플리케이션을 서버에 배치할 때 모든 JSP파일에 대해 자바 서블릿 클래스를 미리 생성하기도 한다. 단, 이방식의 문제는 JSP를 편집하면 서버를 다시 시작해야 한다. 안정화된 이후에 JSP프리컴파일을 고려해야한다.


5.3 JSP의 주요 구성 요소


JSP를 구성하는 요서 두가지

  • 템플릿 데이터(클라이언트로 출력되는 컨텐츠(EX: HTML, javascript, 스타일 시트, JSON, 형식 문자열, XML, 일반 텍스트 등)
  • JSP 전용 태그(특정 자바 명령문으로 바뀌는 태그)
JSP 전용 태그

지시자	: <%@ 지시자 속성="값" 속성="값" ... %>	: JSP 페이지와 관련된 속성을 정의할 때 사용하는 태그. page, taglib, include가 있다.
주석	: <%--	 --%>
선언문	: <%!	   %>	: 변수, 메소드 선언
표현식	: <%= 결과를 반환하는 자바 표현식 %>	: 결과값 출력
스크립트릿 : <%	   %>	: JSP 페이지 안에 자바 코드를 넣을 때 작성. 이 태그 안에 작성한 내용은 서블릿 파일을 만들 때 그대로 복사된다.
액션태그    : <jsp:action>	 </jsp:action> : 자바빈 연결
//JSP 내장 객체
//위 Hello_jsp.java의 일부분
final javax.servlet.jsp.PageContext pageContext;
javax.servlet.http.HttpSession session = null;
final javax.servlet.ServletContext application;
final javax.servlet.ServletConfig config;
javax.servlet.jsp.JspWriter out = null;
final java.lang.Object page = this;
javax.servlet.jsp.JspWriter _jspx_out = null;
javax.servlet.jsp.PageContext _jspx_page_context = null;


<!-- 페이지 지시자 -->
<%@ page 
	language="java" 
	contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>

<!-- 스크립트릿 -->
<%
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";
	}
}
%>    
<!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>
<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>
</html>

<!-- 선언문 -->
<%! 
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);
}
%>

 5.4 서블릿에서 뷰 분리하기


값 객체(VO) = 데이터 수송 객체(DTO)

데이터베이스에서 가져온 정보를 JSP 페이지에  전달하려면  그 정보를 담을 객체가 필요하다. 이 때 값을 담는 용도로 사용하는 객체를 VO라고 부른다. VO는 계층 간 또는 객체 간에 데이터를 전달하는 데 이용하므로 DTO라고도 부른다. 또한 업무영역의 데이터를 표현하기 때문에 도메인 객체(domain object)라고도 한다.


package spms.servlets;

import java.io.IOException;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.Statement;
import java.util.ArrayList;

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.vo.Member;

@WebServlet("/member/list")
public class MemberListServlet extends HttpServlet {
	private static final long serialVersionUID = 1L;

	@Override
	public void doGet(
			HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {
		Connection conn = null;
		Statement stmt = null;
		ResultSet rs = null;

		try {
			ServletContext sc = this.getServletContext();
			Class.forName(sc.getInitParameter("driver"));
			conn = DriverManager.getConnection(
						sc.getInitParameter("url"),
						sc.getInitParameter("username"),
						sc.getInitParameter("password")); 
			stmt = conn.createStatement();
			rs = stmt.executeQuery(
					"SELECT MNO,MNAME,EMAIL,CRE_DATE" + 
					" FROM MEMBERS" +
					" ORDER BY MNO ASC");
			
			response.setContentType("text/html; charset=UTF-8");
			
			//JSP에 전달할 회원 목록 데이터 준비
			//회원 목록을 담을 Array 객체를 준비한다.
			ArrayList<Member> members = new ArrayList<Member>();
			
			//rs(ResultSet) 객체를 통해 데이터베이스로부터 받은 레코드를 값 객체 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"))
						);
			}
			
			//MemberListServlet과 MemberList.jsp는 request와 response를 공유한다.
			//setAttribute() - 값을 보관
			//getAttribute() - 보관한 값을 꺼내기
			request.setAttribute("members",  members);
			
			//JSP로 작업을 위임해야 한다.
			//다른 서블릿이나 JSP로 작업을 위임할 때 사용 하는 객체가 RequestDispatcher(HttpServletRequest를 통해 얻는다)
			RequestDispatcher rd = request.getRequestDispatcher("/member/MemberList.jsp");
			
			//RequestDispatcher 객체를 얻었으면 Forward하거나 Include하면 된다.
			//Forward로 위임하면 해당 서블릿으로 제어권이 넘어간 후 다시 돌아오지 않는다.
			//Include를 하면 해당 서블릿으로 제어권을 넘기고 그 서블릿 작업을 끝내면 다시 제어권이 넘어온다.
			rd.include(request, response);
			
		} catch (Exception e) {
			throw new ServletException(e);
			
		} finally {
			try {if (rs != null) rs.close();} catch(Exception e) {}
			try {if (stmt != null) stmt.close();} catch(Exception e) {}
			try {if (conn != null) conn.close();} catch(Exception e) {}
		}

	}
}


<!-- Member 클래스와 ArrayList 클래스를 사용해야 하므로 지시자 처리됨 -->
<%@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>

19Line에 request.getAttribute가 request객체를 통해서 MemberListServlet의 데이터를 받아온다.


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;
	}
	public String getPassword() {
		return password;
	}
	public Member setPassword(String password) {
		this.password = password;
		return this;
	}
	public Date getCreatedDate() {
		return createdDate;
	}
	public Member setCreatedDate(Date createdDate) {
		this.createdDate = createdDate;
		return this;
	}
	public Date getModifiedDate() {
		return modifiedDate;
	}
	public Member setModifiedDate(Date modifiedDate) {
		this.modifiedDate = modifiedDate;
		return this;
	}
}


5.5 포워딩과 인클루딩


이미지 : p295

//forward
...
} catch (Exception e) {
			//throw new ServletException(e);
			request.setAttribute("error", e);
			RequestDispatcher rd = request.getRequestDispatcher("/Error.jsp");
			rd.forward(request, response);
			
		} finally {
...


이미지 : p295


MemberList.jsp 중

//forward
...
<body>
<jsp:include page="/Header.jsp"/>
<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>
<%} %>
<jsp:include page="/Tail.jsp"/>
</body>
...

4, 18 Line


 서블릿으로 바뀐 MemberList_jsp.java

...
out.write("<!-- Member 클래스와 ArrayList 클래스를 사용해야 하므로 지시자 처리됨 -->\n");
      out.write("\n");
      out.write("\n");
      out.write("\n");
      out.write("<!DOCTYPE html PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\" \n");
      out.write("\t\"http://www.w3.org/TR/html4/loose.dtd\">\n");
      out.write("<html>\n");
      out.write("<head>\n");
      out.write("<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\">\n");
      out.write("<title>회원 목록</title>\n");
      out.write("</head>\n");
      out.write("<body>\n");
      org.apache.jasper.runtime.JspRuntimeLibrary.include(request, response, "/Header.jsp", out, false);
      out.write("\n");
      out.write("<h1>회원목록</h1>\n");
      out.write("<p><a href='add'>신규 회원</a></p>\n");

ArrayList<Member> members = (ArrayList<Member>)request.getAttribute("members");

for(Member member : members) {

      out.write('\n');
      out.print(member.getNo());
      out.write(",\n");
      out.write("<a href='update?no=");
      out.print(member.getNo());
      out.write('\'');
      out.write('>');
      out.print(member.getName());
      out.write("</a>,\n");
      out.print(member.getEmail());
      out.write(',');
      out.write('\n');
      out.print(member.getCreatedDate());
      out.write("\n");
      out.write("<a href='delete?no=");
      out.print(member.getNo());
      out.write("'>[삭제]</a><br>\n");
} 
      out.write('\n');
      org.apache.jasper.runtime.JspRuntimeLibrary.include(request, response, "/Tail.jsp", out, false);
      out.write("\n");
      out.write("</body>\n");
      out.write("</html>");
    }
...

14, 42 Line


5.6 데이터 보관소


ServletContext

-웹 애플리케이션 시작 시 준비됨

-웹 애플리케이션 당 1개


HttpSession

-최초 요청 시 생성 후 브라우저 닫거나 혹은 타임웃하기 전까지유지(세션이 무효화 되기 전까지)

-세션 ID를 통해 사용자 별 세션 식별됨

-서블릿 컨테이너는 세션 식별자를 쿠키에 담아 클라이언트에 보낸 -> 클라이언트는 세션 식별자를 메모리에 보관 -> 이후 요청부터는 서버에 요청할 때마다 세션 식별자를 보냄


ServletRequest

-매 요청 때마다 생성되고 응답 전까지 유지됨

-포워드, 인클루드 서블릿끼리 데이터 공유할 때 적합


JspContext

-JSP 페이지를 실행하는 동안만 유지된다.

-태그 핸들러와 데이터를 공유할 때 사용한다.


사용법

객체.setAttribute(키, 값); //값 저장

객체.getAttribute(키); //값 조회


이미지 출처 : 저자 강의 중 https://youtu.be/-Gcw2DvKIHM?list=PLTEeGHE5dNEO7fGBYIhZN59dcWufaCSok



ServletContext의 활용


AppInitServlet.java(ServletContext)

package spms.servlets;

import java.sql.Connection;
import java.sql.DriverManager;

import javax.servlet.ServletConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;

@SuppressWarnings("serial")
public class AppInitServlet extends HttpServlet {

	//클라이언트에서 호출할 서블릿이 아니므로 init()만 오버라이딩
	@Override
	public void init(ServletConfig config) throws ServletException {
		System.out.println("AppInitServlet 준비…");
		super.init(config); //슈퍼클래스 HttpServlet으로부터 상속받은 init()의 기능을 그대로 수행하겠다.
		try {
			ServletContext sc = this.getServletContext(); //ServletContext 객체
			Class.forName(sc.getInitParameter("driver"));
			Connection conn = DriverManager.getConnection(
						sc.getInitParameter("url"),
						sc.getInitParameter("username"),
						sc.getInitParameter("password"));
			
			sc.setAttribute("conn", conn);//ServletContext객체에 세팅
		} 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) {}
		
	}
}


web.xml

    ...
    <!-- 서블릿 선언 -->
	<servlet>
    <servlet-name>AppInitServlet</servlet-name>
    <servlet-class>spms.servlets.AppInitServlet</servlet-class>
    <load-on-startup>1</load-on-startup><!-- 클라이언트 요청이 없더라도 생성되도록 한다. 웹 애플리케이션이 시작될 때 자동으로 생성된다. -->
 	</servlet>
    ...


MemberListServlet.java

    ...
    try {
			ServletContext sc = this.getServletContext();
			/*Class.forName(sc.getInitParameter("driver"));
			conn = DriverManager.getConnection(
						sc.getInitParameter("url"),
						sc.getInitParameter("username"),
						sc.getInitParameter("password")); */
			conn = (Connection)sc.getAttribute("conn");
			stmt = conn.createStatement();
			rs = stmt.executeQuery(
					"SELECT MNO,MNAME,EMAIL,CRE_DATE" + 
					" FROM MEMBERS" +
					" ORDER BY MNO ASC");
    ...


HttpSession의 활용 - 로그인


LogInServlet.java

package spms.servlets;

import java.io.IOException;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;

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 javax.servlet.http.HttpSession;

import spms.vo.Member;

@WebServlet("/auth/login")
public class LogInServlet extends HttpServlet {
	private static final long serialVersionUID = 1L;

	//doGet일 때는 로그인 폼으로 가도록
	@Override
	protected void doGet(
			HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {
		RequestDispatcher rd = request.getRequestDispatcher(
				"/auth/LogInForm.jsp");
		rd.forward(request, response);
	}
	
	//doPost일 때는 계정 검증
	@Override
	protected void doPost(
			HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {
		Connection conn = null;
		PreparedStatement stmt = null;
		ResultSet rs = null;
		
		try {
			ServletContext sc = this.getServletContext();
			conn = (Connection) sc.getAttribute("conn");  
			stmt = conn.prepareStatement(
					"SELECT MNAME,EMAIL FROM MEMBERS"
					+ " WHERE EMAIL=? AND PWD=?");
			stmt.setString(1, request.getParameter("email"));
			stmt.setString(2, request.getParameter("password"));
			rs = stmt.executeQuery();
			if (rs.next()) {
				Member member = new Member()
						.setEmail(rs.getString("EMAIL"))
						.setName(rs.getString("MNAME"));
				HttpSession session = request.getSession(); //계정 확인이 되었다면 HttpSession 객체를 세팅한다.
				session.setAttribute("member", member); //session에 member를 세팅한다.
				
				response.sendRedirect("../member/list");
			} else {
				//디비에 계정이 없다면, 로그인 실패 페이지를 안내한다.
				RequestDispatcher rd = request.getRequestDispatcher(
						"/auth/LogInFail.jsp");
				rd.forward(request, response);
			}
		} catch (Exception e) {
			throw new ServletException(e);
			
		} finally {
			try {if (rs != null) rs.close();} catch (Exception e) {}
			try {if (stmt != null) stmt.close();} catch (Exception e) {}
		}
	}
}


LogOutServlet.java

package spms.servlets;

import java.io.IOException;

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 javax.servlet.http.HttpSession;

@WebServlet("/auth/logout")
public class LogOutServlet extends HttpServlet {
	private static final long serialVersionUID = 1L;

	@Override
	protected void doGet(
			HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {
		HttpSession session = request.getSession();
		session.invalidate(); //HttpSession 객체를 제거한다.
		
		response.sendRedirect("login");
	}
}


ServletRequest의 활용

-인클루드나 포워드하는 서블릿들끼리 데이터를 공유할 때 쓴다.


JspContext의 활용

-JSP 페이지 내부에서만 사용될 데이터를 공유할 때 사용한다. 

JSP 페이지에 선언된 로컬 변수는 태그 핸들러에서 접근할 수 없다. 그래서 태그 핸들러에게 전달할 데이터가 아니라면 JspContext에 값을 보관할 필요는 없다.


5.7 JSP 액션 태그의 사용

JSP 전용 태그


이미지 출처 : 자바 웹 개발 워크북 p335


<jsp:useBean>의 사용 예


5.8 EL 표기법

EL은 콤마(.)와 대괄호([])를 사용하여 자바 빈의 프로퍼티나 맵, 리스트, 배열의 값을 보다 쉽게 꺼내게 해주는 기술이다.


EL 표기법

EL은 ${}와 #{}를 사용하여 값을 표현한다. 

${} - JSP페이지에 즉시 반영된다. '즉시 적용'이라 부른다. 

#{} - 시스템에서 필요하다고 판단될 때 그 값을 사용한다. '지연적용'이라 부른다.


${member.no} 또는 ${member["no"}}

EL에서 검색 범위 지정

EL도 <jsp:useBean>처럼 보관소에서 값을 꺼낼 수 있다. 다만 차이점은 EL로는 객체를 생성할 수 없다.

  • pageScope -> JspContext
  • requestScope -> ServletRequest
  • sessionScope -> HttpSession
  • applicationScope -> ServletContext


${requestScope.member.no}

//위의 EL 코드를 자바 코드로 나타내면 아래와 같다.

Member obj = (Member)request.getAttribute("member");
int value = obj.getNo();


JSP에서 제공하는 EL 기본 객체

이미지 출처 : 자바 웹 개발 워크북 p340


연산자-산술연산자, 논리연산자, 관계연산자, empty, 조건

OperatorDescription
.Access a bean property or Map entry
[]Access an array or List element
( )Group a subexpression to change the evaluation order
+Addition
-Subtraction or negation of a value
*Multiplication
/ or divDivision
% or modModulo (remainder)
== or eqTest for equality
!= or neTest for inequality
< or ltTest for less than
> or gtTest for greater than
<= or leTest for less than or equal
>= or geTest for greater than or equal
&& or andTest for logical AND
|| or orTest for logical OR
! or notUnary Boolean complement
emptyTest for empty variable values

표 출처 : https://www.tutorialspoint.com/jsp/jsp_expression_language.htm


예약 키워드 - 식별자로 사용하면 안됨

and, or, not, eq, ne, lt, gt, le, ge, true, false, null, instanceof, empty, div, mod


5.9 JSTL 사용하기

JSP Standard Tag Library

http://jstl.java.net


JSTL 주요 태그의 사용법

사용할 태그 라이브러리를 선언

 JSTL 확장 태그를 사용하려면 그 태그의 라이브러리를 선언해야 한다.

이미지 출처 : 자바 웹개발 워크북 p356


(참고 : https://www.tutorialspoint.com/jsp/jsp_standard_tag_library.htm)

설명과 예제들이 참 잘되어있다.


5.10 DAO  만들기

데이터 처리를 전문으로 하는 객체를 'DAO '라고 부른다.



package spms.dao;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;

import spms.vo.Member;

public class MemberDao {
	//MemberDao 클래스에는 ServletContext에 접근할 수 없기 때문에, ServletContext에 보관된 DBConnection 객체를 꺼낼 수 없다.
	//그리서 외부로부터 Connection 객체를 주입받기 위한 setter메서드와 인스턴스 변수가 필요하다.
	//selectList()가 호출되기 전에, Connection 객체가 먼저 설정되어야 한다.
	//이를 DI라고 부르고 IOC역제어라고도 부른다.
	Connection connection;
	
	public void setConnection(Connection connection) {
		this.connection = connection;
	}

	public List 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 members = new ArrayList();

			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) {
			}
		}
	}
}


5.11 ServletContextListener와 객체 공유

서블릿 컨테이너는 웹 애플리케이션의 상태를 모니터링 할 수 있도록 웹 애플리케이션의 시작에서 종료까지 주요한 사건에 대해 알림 기능을 제공한다. 규칙에 따라 객체를 만들어 DD파일(web.xml)에 등록하면 된다. 이렇게 사건이 발생했을 때 알림을 받는 객체를 '리스너'라고 부른다.


리스너 ServletContextListener 만들기


package spms.listeners;

import java.sql.Connection;
import java.sql.DriverManager;

import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
//import javax.servlet.annotation.WebListener;

import javax.servlet.annotation.WebListener;

import spms.dao.MemberDao;

@WebListener
public class ContextLoaderListener implements ServletContextListener {
  Connection conn;
  
  @Override
  public void contextInitialized(ServletContextEvent event) {
    try {
      ServletContext sc = event.getServletContext();

      Class.forName(sc.getInitParameter("driver"));
      conn = DriverManager.getConnection(
          sc.getInitParameter("url"),
          sc.getInitParameter("username"),
          sc.getInitParameter("password"));

      //MemberDao 객체를 준비하여 ServletContext에 보관한다.
      MemberDao memberDao = new MemberDao();
      memberDao.setConnection(conn);

      sc.setAttribute("memberDao", memberDao);

    } catch(Throwable e) {
      e.printStackTrace();
    }
  }

  @Override
  public void contextDestroyed(ServletContextEvent event) {
    try {
      conn.close();
    } catch (Exception e) {}
  }
}


ContextLoaderListener의 배치

-애노테이션을 사용하는 방법

-web.xml을 사용하는 방법


5.12 DB 커넥션 풀

DB 커넥션 객체를 여러 개 생성하여 풀에 담아 놓고, 필요할 때 꺼내 쓰는 방식

풀링(pooling) - 자주 쓰는 객체를 미리 만들어 두고, 필요할 때마다 빌리고, 사용한 다음 반납하는 방식

객체 풀(object pool) - 여러 개의 객체를 모아둔 것

DB 커넥션 풀 - 여러 개의 DB 커넥션을 관리하는 객체


기존(싱글 커넥션 사용)의 문제점

이미지 출처: 자바 웹 개발 워크북 p397


롤백은 Statement객체에는 기능이 없다. 커넥션 객체를 통해서만 롤백을 수행할 수 있다. 하지만 위의 그림처럼 커넥션 객체의 롤백을 호출하면 다른 모든 작업도 롤백 된다는 문제가 생긴다. 이를 해결한 것이 DB 커넥션풀이다. DB 커넥션풀을 이용하면, 각 요청에 대해 별도의 커넥션 객체를 사용하기 때문에 다른 작업에 영향을 주지 않는다. 또한 사용한 DB 커넥션 객체는 버리지 않고 풀에 보관했다가 다시 사용하기 때문에, 가비지가 생성되지 않고 속도도 빨라진다.


DB커넥션풀 만들기(DAO, ContextLoaderListener도 수정)

package spms.util;

import java.sql.Connection;
import java.sql.DriverManager;
import java.util.ArrayList;

public class DBConnectionPool {
  String url;
  String username;
  String password;
  
  //Connection 객체를 보관할 ArrayList를 준비한다.
  ArrayList connList = new ArrayList(); 
  
  //생성자에서는 DB커넥션 생성에 필요한 값을 매개변수로 받는다.
  public DBConnectionPool(String driver, String url, 
      String username, String password) throws Exception {
    this.url = url;
    this.username = username;
    this.password = password;
    
    Class.forName(driver);
  }
  
  //DB 커넥션을 달라고 요청받으면, ArrayList에 들어 있는 것을 꺼내 준다.(DB 커넥션 객체도 일정 시간 후면 연결이 끊어지기 때문에 유효성체크)
  public Connection getConnection() throws Exception {
    if (connList.size() > 0) {
      Connection conn = connList.remove(0);
      if (conn.isValid(10)) {
        return conn;
      }
    }
    //ArrayList에 보관된 객체가 없다면, DriverManager를 통해 새로 만들어 반환한다.
    return DriverManager.getConnection(url, username, password);
  }
  
  //커넥션 객체를 쓰고 난 다음에는 이 메서드를 호출하여 커넥션 풀에 반환한다.
  public void returnConnection(Connection conn) throws Exception {
    connList.add(conn);
  }
  
  //이 메서드를 호출하여 데이터베이스와 연결된 것을 모두 끊어야 한다.
  public void closeAll() {
    for(Connection conn : connList) {
      try{conn.close();} catch (Exception e) {}
    }
  }
}


5.13 DataSource와 JNDI

DataSource는 JDBC 확장 API를 정의한 javax.sql 패키지에 들어 있다.


javax.sql 확장 패키지

javax.sql 패키지는 java.sql 패키지의 기능을 보조하기 위해 만든 확장 패키지이다.


javax.sql 패키지가 제공하는 주요기능

  • DriverManager를 대체할 수 있는 DataSource 인터페이스 제공
  • Connection 및 Statement 객체의 풀링
  • 분산 트랜잭션 처리
  • Rowsets의 지원

DataSource

  • DataSource는 서버에서 관리하기 때문에 데이터베이스나 JDBC 드라이버가 변경되더라도 애플리케이션을 바꿀 필요가 없다.
  • DataSource를 사용하면 Connection과 Statement 객체를 풀링할 수 있고, 분산 트랜잭션을 다룰 수 있다.(DataSource 자체적으로 커넥션풀 기능을 구현하기 때문에 웹 애플리케이션 쪽에서 따로 작업할 것이 없어 매우 편리하다.)




MemberDao.java

package spms.dao;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;

import javax.sql.DataSource;

import spms.vo.Member;

public class MemberDao {
	DataSource ds;

	public void setDataSource(DataSource ds) {
		this.ds = ds;
	}

	public List selectList() throws Exception {
		Connection connection = null;
		Statement stmt = null;
		ResultSet rs = null;

		try {
			//DataSource로부터 커넥션 객체를 꺼낸다.
			connection = ds.getConnection();
			stmt = connection.createStatement();
			rs = stmt.executeQuery("SELECT MNO,MNAME,EMAIL,CRE_DATE" + " FROM MEMBERS" + " ORDER BY MNO ASC");

			ArrayList members = new ArrayList();

			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) {
			}
			try {
				//커넥션 객체 닫는 close()메서드 호출한다.                
				if (connection != null) {
					connection.close();
				}
			} catch (Exception e) {
			}
		}
	}
}


DataSource의 Connection


try {
				//커넥션 객체 닫는 close()메서드 호출한다.                
				if (connection != null) {
					connection.close();
				}
			} catch (Exception e) {
			}


close()를 하는 이유는 실제로 커넥션 객체를 닫는게 아니다.

DataSource가 만들어 주는 connection 객체는 DriverManager가 만들어주는 커넥션 객체를 한번 더 포장한 것이다.

1. MemberDao가 DataSource에게 커넥션을 달라고 요청한다.

2. DataSource는 DriverManager가 생성한 커넥션을 리턴하는 것이 아니라, 커넥션 대행 객체를 리턴한다. 아파치 DBCP 컴포넌트의 BasicDataSource를 사용할 경우, 앞의 그림과 같이 PoolableConnection 객체를 반환한다. 이 객체가 커넥션의 대행 객체이다. 이객체에는 진짜 커넥션 객체를 가리키는 참조 변수(_conn) 와 커넥션풀을 가리키는 참조 변수(_pool)가 들어 있다.

그래서 DataSource가 만들어 준 커넥션 대행 객체에 대해 close()를 호출하면, 커넥션 대행 객체는 진짜 커넥션 객체를 커넥션풀에 반납한다. 그래서 커넥션이 닫힐까 걱정할 필요가 없다.


서버에서 제공하는 DataSource 사용하기

<?xml version="1.0" encoding="UTF-8"?>
    <!-- Default set of monitored resources -->
    <WatchedResource>WEB-INF/web.xml</WatchedResource>
    
	<Resource name="jdbc/studydb" auth="Container" type="javax.sql.DataSource"
	    maxActive="10" maxIdle="3" maxWait="10000" 
	    username="study"
	    password="study" 
	    driverClassName="com.mysql.jdbc.Driver"
	    url="jdbc:mysql://localhost/studydb" 
	    closeMethod="close"/>
</Context>


<Resource>태그의 속성들

이미지 출처 : 자바웹개발 워크북 p419


<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns="http://java.sun.com/xml/ns/javaee"
	xsi:schemaLocation="http://java.sun.com/xml/ns/javaee 
		http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
	id="WebApp_ID" version="3.0">
	<display-name>web05</display-name>
    
    <!-- resource-ref 문법
    <resource-ref>
    <res-ref-name>JNDI 이름(context.xml에 선언한 자원의 타입과 같아야 한다.)</res-ref-name>
    <res-type>리턴될 자원의 클래스 이름(패키지명 포함)</res-type>
    <res-auth>자원 관리의 주체</res-auth>
    </resource-ref>
    -->
    
	<resource-ref>
	<res-ref-name>jdbc/studydb</res-ref-name>
	<res-type>javax.sql.DataSource</res-type>
	<res-auth>Container</res-auth>
	</resource-ref>
	
	<welcome-file-list>
		<welcome-file>index.html</welcome-file>
		<welcome-file>index.htm</welcome-file>
		<welcome-file>index.jsp</welcome-file>
	</welcome-file-list>
</web-app>
package spms.listeners;

import javax.naming.InitialContext;
import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener;
import javax.sql.DataSource;

import spms.dao.MemberDao;

@WebListener
public class ContextLoaderListener implements ServletContextListener {
	//BasicDataSource ds;
	

	@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");

//			ds = new BasicDataSource();
//			ds.setDriverClassName(sc.getInitParameter("driver"));
//			ds.setUrl(sc.getInitParameter("url"));
//			ds.setUsername(sc.getInitParameter("username"));
//			ds.setPassword(sc.getInitParameter("password"));

			MemberDao memberDao = new MemberDao();
			//DataSource를 주입
			memberDao.setDataSource(ds);

			sc.setAttribute("memberDao", memberDao);

		} catch (Throwable e) {
			e.printStackTrace();
		}
	}

	@Override
	public void contextDestroyed(ServletContextEvent event) {
		//context.xml에 이미 closeMethod 속성이 있어서 톰캣 서버가 종료되면 close를 알아서 해준다.
		/*try {
			if (ds != null)
				//커넥션 객체 닫는다.
				ds.close();
		} catch (SQLException e) {
		}*/
	}
}


Comments