서블릿

서블릿이란?

서블릿은 게시판과 같은 프로그램을 만들기 위한 자바 측 기술이다.
java.sql 패키지를 JDBC라고 부르듯이, javax.servlet과 javax.servlet.http 패키지를 서블릿이라 부른다.
서블릿은 네트워크 프로토콜과 무관하게 설계되었지만, 대부분 HTTP 프로토콜을 사용하는 웹 환경에서 동적인 콘텐츠를 만들 때 사용된다.
JSP는 서블릿 기반 기술이다.
JSP는 서블릿보다 화면에 해당하는 애플리케이션 요소를 쉽게 만들 수 있다.

서블릿의 기본 구조

서블릿의 기본 구조는 다음과 같다.

  • 모든 서블릿이 구현해야 하는 javax.servlet.Servlet 인터페이스
  • 대부분의 서블릿이 상속해야 하는 javax.servlet.GenericServlet 추상클래스
  • HTTP 프로토콜을 사용하는 서블릿이 상속해야 하는 javax.servlet.http.HttpServlet 클래스

아래 그림1처럼 GenericServlet은 프로그래머가 사용하기 편하도록 javax.servlet.ServletConfig 인터페이스를 구현하고 있다. Servlets Framework

Servlet 인터페이스

javax.servlet.Servlet 인터페이스는 서블릿 아키텍처의 핵심이다.
모든 서블릿은 Servlet 인터페이스를 구현해야 한다.
이 인터페이스에는 서블릿의 생명 주기 메소드가 선언되어 있다.

  • init(): 서블릿 초기화
  • service(): 클라이언트 요청에 대한 서비스
  • destroy(): 서비스 중지, 자원반납

init() 메소드

서블릿 컨테이너는 서블릿 객체가 생성된 후, 단 한 번 init() 메소드를 호출한다.
서블릿은 init() 메소드가 에러 없이 완료되어야 서비스할 수 있다.
init() 메소드가 완료하기 전의 요청은 차단된다.
init() 메소드가 호출될 때 ServletConfig 인터페이스 타입의 객체를 아규먼트로 전달받는다.
만약 web.xml에서 서블릿 초기화 파라미터를 설정했다면, 전달받은 ServletConfig에는 web.xml에서 설정했던 서블릿 초기화 파라미터를 가지고 있게 된다. 초기화 파라미터가 있다면 init() 메소드에 서블릿의 초기화 작업을 수행하는 코드가 있어야 한다.

void init(ServletConfig config) throws ServletException;

service() 메소드

클라이언트가 서블릿에 요청을 보낼 때마다, 서블릿 컨테이너는 서블릿의 service() 메소드를 호출한다.
service() 메소드는, 첫 번째 아규먼트로 전달받은 ServletRequest 타입의 객체를 통해서 요청 정보를 읽고, 두 번째 아규먼트로 전달받은 ServletResponse 타입의 객체를 사용하여 클라이언트에게 응답한다. 클라이언트가 서블릿을 요청할 때마다 서블릿 컨테이너는 새로운 스레드에서 service() 메소드를 실행한다는 점에 주목해야 한다.

service() 메소드는 별도의 스레드에서 동시에 실행되기에 수많은 클라이언트의 요청을 바로 응답할 수 있다.
그러나 서블릿이 사용하는 자원(예를 들면, 파일이나 네트워크 커넥션, static 변수, 인스턴스 변수 등등)에 임계 영역 문제가 발생할 수 있다. 그러므로 서블릿에서 문제가 될 수 있는 정적 변수나 인스턴스 변수를 만들지 않는 게 좋다. (서블릿이 사용하는 자원을 동기화하려고 애쓰지 말자. 대부분의 경우 좋은 코드가 아니다)

void service(ServletRequest req, ServletResponse res) throws ServletException, IOException;

destroy() 메소드

서블릿이 더는 서비스를 하지 말아야 할 때 서블릿 컨테이너에 의해 호출된다.
이 메소드는 프로그래머가 코드로 호출하는 게 아니다.
destroy() 메소드를 실행하려면 "톰캣 매니저"를 사용하여 애플리케이션을 언로드하거나 톰캣을 셧다운 시켜야 한다.
톰캣 매니저는 http://localhost:8080/manager로 접근할 수 있는 웹 애플리케이션로 웹 애플리케이션을 관리하는 웹 애플리케이션이다.
톰캣 매니저 화면을 보려면, 톰캣을 설치할 때 정해준 관리자와 관리자 비밀번호를 사용하여 로그인해야 한다.
관리자와 관리자 비밀번호가 생각나지 않으면 CATALINA_HOME/conf/tomcat-users.xml 파일을 열어보면 알 수 있다.

void destroy();

GenericServlet 추상클래스

대부분의 서블릿이 GenericServlet 클래스를 상속한다.
GenericServlet 클래스는 ServletConfig 인터페이스를 구현하고 있다.
GenericServlet는 Servlet 인터페이스를 불완전하게 구현하고 있다.
Servlet 인터페이스의 service() 메소드를 구현하지 않았기 때문에, GenericServlet의 service() 메소드는 추상 메소드이고, 그래서 GenericServlet은 추상클래스이다. GenericServlet를 상속하는 서브 클래스는 service() 메소드를 구현해야 한다.

public abstract void service(ServletRequest req, ServletResponse res) throws ServletException, IOException;

GenericServlet의 init(ServletConfig config) 메소드는 다음과 같이 구현되어 있다.

public void init(ServletConfig config) throws ServletException {
	this.config = config;
	this.init();
}

init(ServletConfig config) 구현 부에 마지막에 파라미터가 없는 init() 메소드를 호출한다.
파라미터가 없는 init() 메소드는 편의를 위해 GenericServlet에 추가되었다.
이 메소드는 메소드 몸체에 어떤 코드도 없다.

public void init() throws ServletException {

}

서브 클래스에서 init(ServletConfig config) 메소드를 오버라이딩하기 보다 init() 메소드를 오버라이딩하는 게 편하다.
왜냐하면 ServletConfig 객체를 저장해야 한다는 걱정을 하지 않아도 되기 때문이다.
init(ServletConfig config) 메소드를 오버라이딩하려면 메소드 몸체 첫 줄에 super.init(config); 코드를 추가해야 한다.
이 코드가 없으면 서블릿은 ServletConfig 객체를 저장하지 않는다.

The init(ServletConfig config) 메소드는 아규먼트로 전달받은 ServletConfig 객체를 인스턴스 변수 config에 저장한다.
GenericServlet의 getServletConfig() 메소드는 이 config를 반환한다.

public ServletConfig getServletConfig() {
	return config;
}

getServletContext()는 ServletConfig 인터페이스의 메소드이다.
GenericServlet는 ServletConfig 인터페이스를 구현한다.
GenericServlet의 getServletContext() 메소드는 아래와 같이 ServletContext 타입 객체를 반환한다.

public ServletContext getServletContext() {
	return getServletConfig().getServletContext();
}

ServletConfig 인터페이스의 getInitParameter()와 getInitParameterNames() 메소드는 GenericServlet에서 다음과 같이 구현되어 있다.

public String getInitParameter(String name) {
	return getServletConfig().getInitParameter(name);
}
public Enumeration getInitParameterNames() {
	return getServletConfig().getInitParameterNames();
}   

GenericServlet은 ServletConfig를 구현하고 있다.
이로써 프로그래머가 GenericServlet을 더 편리하게 사용할 수 있다.
예를 들어 보겠다.
서블릿에서 ServletContext 레퍼런스를 얻기 위해서 this.getServletConfig().getServletContext(); 보다는 this.getServletContext();가 더 편리하다.
초기화 파라미터 정보를 얻기 위해서 String driver = this.getServletConfig().getInitParameter("driver"); 보다는 String driver = this.getInitParameter("driver");가 더 편리하다.

HttpServlet 클래스

HTTP 요청을 서비스하는 서블릿은 HttpServlet을 상속해야 한다.
GenericServlet 추상 클래스를 상속하는 HttpServlet 클래스는 HTTP 프로토콜에 특화된 서블릿이다.

HttpServlet 클래스는 HTTP 요청을 처리하는 메소드를 제공한다.
클라이언트의 요청은 HttpServletRequest 객체 타입으로 서블릿에 전달되며, 서블릿은 HttpServletResponse 객체 타입을 사용하여 응답한다.

HttpServlet 클래스는 GenericServlet의 service() 추상 메소드를 구현하고 있다.
메소드 몸체의 내용은 protected void service(HttpServletRequest req, HttpServletResponse resp) 메소드를 호출하는 게 전부다.

public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
	
	HttpServletRequest  request;
	HttpServletResponse response;
	
	try {
		request = (HttpServletRequest) req;
		response = (HttpServletResponse) res;
	} catch (ClassCastException e) {
		throw new ServletException("non-HTTP request or response");
	}
	
	service(request, response);
}

결국, 다음 메소드가 HTTP 요청을 처리한다.

protected void service(HttpServletRequest req, HttpServletResponse resp) 
	throws ServletException, IOException {
	
	String method = req.getMethod();
	
	if (method.equals(METHOD_GET)) {
		long lastModified = getLastModified(req);
		if (lastModified == -1) {
			// servlet doesn't support if-modified-since, no reason
			// to go through further expensive logic
			doGet(req, resp);
		} else {
			long ifModifiedSince = req.getDateHeader(HEADER_IFMODSINCE);
			if (ifModifiedSince < (lastModified / 1000 * 1000)) {
				// If the servlet mod time is later, call doGet()
				// Round down to the nearest second for a proper compare
				// A ifModifiedSince of -1 will always be less
				maybeSetLastModified(resp, lastModified);
				doGet(req, resp);
			} else {
				resp.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
			}
		}
	} else if (method.equals(METHOD_HEAD)) {
		long lastModified = getLastModified(req);
		maybeSetLastModified(resp, lastModified);
		doHead(req, resp);
	} else if (method.equals(METHOD_POST)) {
		doPost(req, resp);
	} else if (method.equals(METHOD_PUT)) {
		doPut(req, resp);
	} else if (method.equals(METHOD_DELETE)) {
		doDelete(req, resp);
	} else if (method.equals(METHOD_OPTIONS)) {
		doOptions(req,resp);
	} else if (method.equals(METHOD_TRACE)) {
		doTrace(req,resp);
	} else {
		//
		// Note that this means NO servlet supports whatever
		// method was requested, anywhere on this server.
		//
		
   		String errMsg = lStrings.getString("http.method_not_implemented");
   		Object[] errArgs = new Object[1];
   		errArgs[0] = method;
   		errMsg = MessageFormat.format(errMsg, errArgs);
   		
		resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED, errMsg);
	}
}

HttpServlet의 +service() 메소드가 하는 일은 #service() 메소드를 호출하는 게 전부다.
(여기서 +는 public 접근 한정자를, #은 protected 접근 한정자를 의미한다)
HttpServlet 클래스의 #service() 메소드가 호출되면, 이 메소드는 요청객체(HttpServletRequest타입의 객체)안에서 HTTP 메소드 (POST, GET 등등)를 읽고 이 값에 매칭되는 메소드를 호출한다.
예를 들어, HTTP 메소드가 "GET"이면 doGet()을, "POST"이면 doPost() 메소드를 호출한다.
doGet()나 doPost()와 같은 메소드가 오버라이딩해야 할 메소드이다.

HttpServletRequest 인터페이스는 ServletRequest 인터페이스를 상속한다.
HttpServletResponse 인터페이스는 ServletResponse 인터페이스를 상속한다.
서블릿 컨테이너는 클라이언트의 요청이 오면 HttpServletRequest 타입의 객체와 HttpServletResponse 타입의 객체를 만들어서 서블릿의 +service(ServletRequest req,ServletResponse res) 메소드에 아규먼트로 전달한다.
HttpServletRequest, HttpServletResponse 인터페이스를 구현한 클래스는 서블릿 컨테이너 벤더vendor가 만들어야 한다.

서블릿 API 요약

Servlet 인터페이스
init(ServletConfig config)
service(ServletRequest req, ServletResponse res)
destroy()
getServletConfig():ServletConfig
서블릿 초기화에 관련된 변수를 가지고 있는 ServletConfig 객체 반환
getServletInfo():String
서블릿에 대한 간단한 정보를 반환
ServletConfig 인터페이스
getInitParameter(String name):String
name에 해당하는 초기화 파라미터값 반환
getInitParameterNames():Enumeration
서블릿의 초기화 파라미터 이름 모두를 Enumeration 타입으로 반환
getServletContext():ServletContext
ServletContext 반환
getServletName():String
서블릿 인스턴스의 이름 반환
+GenericServlet 추상 클래스
프로토콜에 무관한 기본적인 서비스를 제공하는 클래스로 Servlet, ServletConfig 인터페이스를 구현
+init()
서블릿 초기화 메소드로, GenericServlet의 init(ServletConfig config) 메소드의 의해 호출된다.
<<abstract>> +service(ServletRequest req, ServletResponse res)
+GenericServlet 추상 클래스는 여전히 Servlet 인터페이스의 service() 메소드를 구현하지 않는다.
HttpServlet 추상 클래스
GenericServlet 추상 클래스 상속
#doGet(HttpServletRequest req, HttpServletResponse resp)
HTTP의 GET 요청을 처리하기 위한 메소드
#doPost(HttpServletRequest req, HttpServletResponse resp)
HTTP의 POST 요청을 처리하기 위한 메소드
+service(HttpServletRequest req, HttpServletResponse resp)
GenericServlet 추상클래스의 추상 메소드 service() 구현함. 구현 내용은 #service() 메소드에 호출이 전부다.
#service(HttpServletRequest req, HttpServletResponse resp)
HTTP METHOD에 따라 doGet(req, resp), doHead(req, resp), doPost(req, resp), doGet(req, resp), doDelete(req, resp), doOptions(req, resp), doTrace(req, resp) 중 하나를 호출한다.
ServletContext 인터페이스
이 인터페이스는 서블릿이 서블릿 컨테이너와 통신하기 위해서 사용하는 메소드를 제공한다.
또한, 파일의 MIME 타입, 파일의 전체 경로, RequestDispatcher의 레퍼런스를 얻거나 로그 파일에 로그를 기록하는 기능도 제공한다.
ServletContext 객체는 웹 애플리케이션마다 하나씩 존재하며, 웹 애플리케이션을 구성하는 서블릿이나 JSP 같은 동적 요소들을 위한 공동 저장소를 역할을 한다.
즉, ServletContext 겍체에 저장된 데이터는 같은 웹 애플리케이션에 있는 서블릿이나 JSP에서 자유롭게 접근할 수 있다.
setAttribute(String name, Object value)
데이터를 name|value 쌍으로 저장
getAttribute(String name):Object
주어진 name을 가진 데이터를 반환
removeAttribute(String name)
주어진 name을 가진 데이터를 삭제
getInitParameter(String name):String
주어진 name을 가진 웹 애플리케이션 초기화 파라미터의 값 반환
getRequestDispatcher(String path):RequestDispatcher
주어진 path에 위치하는 자원에 대한 RequestDispatcher 객체를 반환
getRealPath(String path):String
주어진 가상 경로에 대한 실제 경로를 반환
getResource(String path):URL
지정된 경로에 매핑된 리소스의 URL을 반환
RequestDispatcher 인터페이스
클라이언트의 요청을 다른 자원(서블릿, JSP)으로 전달하거나 다른 자원의 내용을 응답에 포함하기 위해 사용한다.
forward(ServletRequest req, ServletResponse res)
클라이언트의 요청을 다른 자원으로 전달
include(ServletRequest req, ServletResponse res)
다른 자원의 내용을 응답에 포함
ServletRequest 인터페이스
클라이언트의 요청 정보을 담고 있음.
setAttribute(String name, Object o)
데이터를 name-value 쌍으로 저장
getAttribute(String name):Object
주어진 name으로 저장된 데이터를 반환
removeAttribute(String name)
주어진 name으로 저장된 데이터를 제거
getInputStream():ServletInputStream
요청 몸체에 있는 바이너리 테이터를 읽기 위한 입력 스트림 반환
getParameter(String name):String
name에 해당하는 HTTP 파라미터의 값 반환
getParameterNames():Enumeration
모든 HTTP 파라미터 이름을 Enumeration 타입으로 반환
getParameterValues(String name):String[]
name에 해당하는 HTTP 파라미터의 모든 값을 String 배열로 반환. 체크박스나 다중 선택 리스트와 같이 하나의 HTTP 파라미터에 값이 여러 개 있을 때 이 메소드를 사용한다.
getServletPath():String
"/"로 시작하는 경로를 반환. 반환되는 경로에 쿼리스트링은 포함되지 않음.
getRemoteAddr():String
클라이언트의 IP 주소를 반환
HttpServletRequest 인터페이스
ServletReqeust 상속
getCookies():Cookie[]
브라우저가 전달한 쿠키 배열을 반환
getSession():HttpSession
현재 세션(HttpSession)을 반환
getSession(boolean created):HttpSession
현재 세션을 반환, 만약 세션이 없는 경우 created가 true면 세션을 생성후 반환하고 created가 false면 null 반환
getContextPath():String
요청 URI에서 컨텍스트를 지시하는 부분을 반환한다.http://localhost:8080/ContextPath/board/list.do?curPage=1를 요청하면 /ContextPath 반환
getRequestURI():String
http://localhost:8080/ContextPath/board/list.do?curPage=1를 요청하면/ContextPath/board/list.do 반환
getQueryString():String
http://localhost:8080/ContextPath/board/list.do?curPage=1를 요청하면curPage=1 반환
ServletResponse 인터페이스
클라이언트에 응답을 보내기 위해 사용.
getOutputStream():ServletOutputStream
응답으로 바이너리 데이터를 전송하기 위한 출력 스트림 반환.
getWriter():PrintWriter
응답으로 문자 데이터를 전송하기 위한 출력 스트림 반환.
setContentType(type:String)
응답 데이터의 MIME 타입을 설정할 때 사용.
MIME은 HTML은 text/html, 일반 텍스트는 text/plain, 바이너리 데이터는 application/octet-stream으로 설정한다.
getWriter() 메소드 전에 호출되어야 한다.
getContentType():String
setContentType() 메소드에서 지정한 MIME 타입 반환. 지정하지 않았다면 null을 반환한다.
setCharacterEncoding(charset:String)
응답 데이터의 캐릭터셋을 설정.
UTF-8로 설정하려면 setCharacterEncoding("UTF-8");와 같이 코딩한다.
이것은 setContentType("text/html; charset=UTF-8");에서의 charset=UTF-8과 동일하다.
getWrite() 메소드가 실행되기 전에 호출되어야 한다.
getCharacterEncoding():String
응답 데이터의 캐릭터셋 반환.
캐릭터셋을 지정하지 않았다면 "ISO-8859-1" 반환한다.
setContentLength(length:int)
응답 데이터의 크기를 int형 값으로 설정.
이 메소드는 클라이언트측에서 서버로부터의 응답 데이터를 어느 정도 다운로드하고 있는지 표시하는데 사용될 수 있다.
HttpServletResponse 인터페이스
ServletResponse 인터페이스 상속.
HTTP 응답을 클라이언트에 보내기 위해 사용한다.
addCookie(cookie:Cookie)
응답에 쿠키를 추가한다.
sendRedirect(String location)
주어진 URL로 리다이렉트한다.
HttpSession 인터페이스
세션유지에 필요한 사용자의 정보를 서버 측에서 저장할 때 사용한다.
getAttribute(String name):Object
setAttribute(String name, Object value)
removeAttribute(String name)
invalidate()
Cookie
쿠키란 세션 유지를 위해 클라이언트 측에서 저장하는 정보이다.
세션 유지를 위해 웹 브라우저는 쿠키를 전송해준 서버로 요청을 전송할 때마다 이 쿠키 정보를 요청에 추가한다.

쿠키는 여러 개의 이름 - 값 쌍을 저장할 수 있다.
또한, 쿠키에는 경로, 도메인, 만료 날짜 및 보안에 대한 선택적 값이 있을 수 있다.

서버 요소에 쿠키를 만들려면 약속된 형식의 문자열을 응답 헤더에 추가하는 코드가 필요하다.
쿠키 정보를 포함한 응답을 받는 웹 브라우저는 요청을 서버에 보낼 때 쿠키 정보를 함께 보내게 된다.
서버 요소에 전달 된 쿠키 정보는 HttpServletRequest의 getCookie() 메소드를 사용하면 배열 유형으로 얻을 수 있다.
쿠키나 세션은 응답 후에 연결을 끊는 HTTP 프로토콜의 한계를 극복하는 기술이다.
Cookie(String name, value:String)
getName():String
getValue():String
setValue(newValue:String)
getPath():String
setPath(uri:String)
getDomain():String
setDomain(pattern:String)
getMaxAge():int
setMaxAge(expiry:int)
getSecure():boolean
setSecure(flag:boolean)

서블릿 예제

아래 나오는 모든 예제는 ROOT 애플리케이션에 작성한다.
웹 애플리케이션 작성 실습에서 도큐먼트 베이스가 C:/www/myapp인 애플리케이션을 ROOT 애플리케이션으로 변경했었다.
JSP는 C:/www/myapp 아래에, 자바는 C:/www/myapp/WEB-INF/src 아래 자바 패키지 이름의 서브디렉터리에 생성한다.
이클립스를 사용하지 않고 에디트플러스와 같은 일반 에디터를 사용한다.

SimpleServlet.java
package example;

import java.io.IOException;
import java.io.PrintWriter;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class SimpleServlet extends HttpServlet {

	@Override
	public void doGet(HttpServletRequest req, HttpServletResponse resp)
			throws ServletException, IOException {
		doPost(req,resp);
	}
	
	@Override
	public void doPost(HttpServletRequest req, HttpServletResponse resp) 
			throws ServletException, IOException {
			
		resp.setContentType("text/html; charset=UTF-8");
		PrintWriter out = resp.getWriter();
    	
		out.println("<html>");
		out.println("<body>");
    	
		//요청한 클라이언트의 IP를 출력
		out.println("당신의 IP 는 " + req.getRemoteAddr() + "입니다.\n");
		out.println("</body></html>");
		out.close();
	}
  
}

SimpleServlet은 서블릿 라이프 사이클 메소드 중 init()과 destroy()는 구현하지 않았다.
이 메소드들은 GenericServlet에 이미 구현되어 있고, 또 특별히 오버라이딩할 이유가 없기 때문이다.
/WEB-INF/web.xml 파일을 열고 web-app 엘리먼트의 자식 엘리먼트로 servlet 엘리먼트와 내용을 아래와 같이 추가한다.

web.xml
<servlet>
	<servlet-name>SimpleServlet</servlet-name>
	<servlet-class>example.SimpleServlet</servlet-class>
</servlet>

<servlet-mapping>
	<servlet-name>SimpleServlet</servlet-name>
	<url-pattern>/simple</url-pattern>
</servlet-mapping>

명령 프롬프트에서 SimpleServlet.java가 있는 소스 폴더로 이동하여 아래와 같이 컴파일한다.

C:\ Command Prompt
javac -d C:/www/myapp/WEB-INF/classes ^
-cp C:/apache-tomcat-9.0.87/lib/servlet-api.jar ^
SimpleServlet.java
package javax.servlet.http does not exist
위 컴파일 에러는 자바 컴파일러가 javax.servlet.http 패키지를 찾지 못했기 때문이다.
원인은 javac의 cp 옵션 값으로 servlet-api.jar 파일의 전체 경로를 잘못 적었기 때문이다.
cp 옵션값으로 주는 경로에 공백이 있다면 경로를 ""로 묶어주어야 한다.

톰캣을 재시작한 후 http://localhost:8080/simple을 방문하여 테스트한다.

SimpleServlet.java 소스 설명

public class SimpleServlet extends HttpServlet

HttpServlet 클래스를 상속받은 서블릿은 public으로 선언해야 한다.

@Override
public void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
	doPost(req,res);
}

@Override
public void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
	//..생략..
}

doGet()과 doPost() 메소드는 HttpServlet의 doGet()와 doPost() 메소드를 오버라이드 한 메소드이다.
HTTP METHOD가 GET으로 요청해오면 doGet() 메소드를 오버라이딩한다.
웹 브라우저의 주소창에서 웹 서버의 자원을 요청하면 GET 방식 요청이다.
위 예제는 doGet() 메소드는 단지 doPost() 메소드를 호출한다.
따라서 GET이든 POST이든 모두 같은 코드가 실행된다.
doGet()과 doPost() 메소드의 두 개 파라미터 타입은 HttpServletRequest와 HttpServletResponse다.

resp.setContentType("text/html; charset=UTF-8");
PrintWriter out = resp.getWriter();

resp.setContentType("text/html; charset=UTF-8");은 응답(HttpServletResponse)의 콘텐츠 타입을 설정한다.
즉, 이 코드는 웹 브라우저에 응답으로 출력될 문서의 MIME2타입을 설정한다.
이 코드는 서블릿에서 단 한 번만 사용할 수 있으며 PrintWriter 객체를 획득하기 전에 실행돼야 한다.
; charset=UTF-8 부분이 빠지면 한글이 깨진다.
PrintWriter 객체는 HttpServletResponse의 getWriter()을 호출하면 얻을 수 있다.

PrintWriter out = resp.getWriter();

out.println("<html>");
out.println("<body>");

//요청한 클라이언트의 IP를 출력한다.
out.println("당신의 IP는 " + req.getRemoteAddr() + "입니다.\n");

PrintWriter의 println() 메소드는 아규먼트로 전달받은 문자열을 클라이언트의 웹 브라우저에 출력한다.
위에서 보듯이 SimpleServlet는 클라이언트에게 HTML을 보내기 위해 PrintWriter의 println()메소드를 사용하고 있다.

req.getRemoteAddr()은 클라이언트의 IP 주소를 반환하는 메소드다.
이처럼 HttpServeltRequest는 클라이언트가 보내거나 클라이언트에 관한 정보를 담고 있다.

SimpleServlet 서블릿이 응답을 보내기까지 과정을 살펴보자.
클라이언트가 웹 브라우저를 이용해서 서버의 SimpleSerlvet을 요청한다.
톰캣은 SimpleServlet의 +service(ServletRequest req, ServletResponse res) 메소드를 호출하면서 클라이언트의 요청을 캡슐화한 객체(HttpSerlvetRequest 인터페이스 구현체)와 응답을 위한 객체(HttpSerlvetResponse 인터페이스 구현체)를 메소드의 아규먼트로 전달한다.
+service(ServletRequest req, ServletResponse res) 메소드는 단지 #service(HttpServletRequest req,HttpServletResponse resp) 메소드를 호출한다.
#service(HttpServletRequest req,HttpServletResponse resp) 메소드는 HTTP 메소드 타입(GET,POST 등)에 따라 doGet()또는 doPost()와 같은 메소드를 호출한다.
위 예제는 웹 브라우저 주소창에서 서블릿 자원을 요청했기 때문에 GET 방식 요청이다.
따라서 doGet() 메소드가 호출된다.

사용자가 문자열 데이터를 서버 측 자원으로 전송하는 방법과 이 데이터를 서버 측 자원에서 수신하는 방법

웹 환경에서 동적인 요소라 하면 클라이언트가 보낸 문자열 데이터에 따라 응답을 하는 요소를 말한다.
웹에서 동적인 요소를 만들어야 하는 웹 프로그래머는, 클라이언트로 하여금 웹 브라우저를 사용하여 문자열 데이터를 서버 측 자원으로 보내게 하는 방법과 서버 측 자원에서 클라이언트가 보낸 문자열 데이터를 획득하는 방법을 알아야 한다.
모든 웹 프로그래머는 클라이언트로 하여금 서버 측 자원으로 문자열 데이터를 전송하게 하려고 주로 form과 그 서브 엘리먼트를 사용한다.3
클라이언트가 전송하는 데이터는 form 엘리먼트의 action 속성으로 지정된 서버 측 자원으로 전달된다.

파라미터 전송 방법과 전송된 파라미터의 값을 얻는 방법

아래 표에서 "HTML 폼" 항목은 사용자로부터 값을 입력받기 위한 HTML 태그를 보여주며, "서블릿" 항목은 서블릿이 클라이언트가 보낸 파라미터의 값을 얻는 방법을 보여준다.

HTML 폼 서블릿
<input type="text" name="addr" />
req.getParameter("addr");
<input type="radio" name="os" value="Windows" />
<input type="radio" name="os" value="Linux" />
req.getParameter("os");
<input type="hidden" name="curPage" value="1" />
req.getParameter("curPage");
<input type="password" name="passwd" />
req.getParamter("passwd");
<textarea name="content" cols="60" rows="12">어쩌고저쩌고</textarea>
req.getParamter("content");
<select name="grade">
	<option value="A">A</option>
	<option value="B">B</option>
	<option value="C">C</option>
	<option value="D">D</option>
	<option value="F">F</option>
</select>
req.getParameter("grade");
<input type="checkbox" name="hw" value="Intel" />
<input type="checkbox" name="hw" value="AMD" />
req.getParameterValues("hw");
<select name="sports" multiple="multiple">
	<option value="soccer">축구</option>
	<option value="baseball">야구</option>
	<option value="basketball">농구</option>
</select>
req.getParameterValues("sports");

getParameter(String name)

ServletRequest의 getParameter(String name) 메소드는 사용자가 보낸 데이터를 얻기 위해 사용하는 가장 보편적인 메소드이다.
클라이언트가 서버로 전달하는 문자 데이터의 형태는 파라미터 이름과 값의 쌍(name-value)이다.
여기서 name은 form의 서브 엘리먼트(input, textarea, select 등등)의 name 속성값이며 value는 사용자가 입력한 값이다.

서버 측 자원의 코드에서 클라이언트가 전달한 매개 변수의 이름이 getParameter(String name) 메소드의 아규먼트로 제공되면, 이 메소드는 사용자가 입력하거나 선택한 값을 반환한다.

type 속성값이 radio인 input 엘리먼트를 라디오 버튼이라 부른다.
name 속성값이 같은 라디오 버튼은 그룹을 형성하고 그룹내에서 단 하나의 항목만 선택할 수 있다.

getParameterValues(String name)

클라이언트가 하나의 파라미터로 여러 개의 값을 전송할 때, 서버 측 자원에서 이 값을 얻으려면 HttpServletRequest의 getParamterValues(String name) 메소드가 필요하다.
이 메소드는 사용자가 선택한 값들로 구성된 String 배열을 반환한다.

클라이언트 측에서 하나의 파라미터에 값을 여러 개 보내려면, HTML 코드에 체크박스나 multiple 속성값이 "multiple"인 select 엘리먼트가 필요하다.
(input 엘리먼트의 type 속성값이 "checkbox"이면 체크박스다)
name 속성값을 같게 주면 체크박스는 같은 그룹에 속하게 된다.
체크박스는 라디오 버튼과 달리 그룹 내 여러 개를 선택할 수 있다.

select 요소를 사용하면 일반적으로 하나의 항목만 선택할 수 있다.
그러나 select 엘리먼트의 multiple 속성값이 "multiple"로 설정된 경우 사용자는 Ctrl 또는 Shift 버튼을 사용하여 여러 항목을 선택할 수 있다.

getParamterNames()

사용자가 전송되는 데이터가 어떤 파라미터에 담겨 오는지 알아내려면 서버 측 코드에 HttpServletRequest의 getParamterNames() 메소드가 필요하다.
getParameterNames() 메소드는 파라미터 이름을 담고 있는 Enumeration4 타입 객체를 반환한다.

input type="file"
<input type="file" ../>은 이미지와 같은 바이너리 데이터를 서버로 전송할 때 쓰인다.
부모 엘리먼트인 form은 <form method="post" enctype="multipart/form-data"...>이어야 한다.

업로드하려는 파일뿐 아니라 다른 부가 정보(예를 들면, 이름, 제목, 내용 등등)를 전송해야 한다면, <input type="file" />을 포함하는 form에 문자열을 전송하는 다른 엘리먼트를 추가한다.

submit 버튼을 누르면 문자열만 전송할 때와 다른 전송 규약으로 데이터가 서버 요소로 전송된다.
따라서 getParameter(String name) 메소드를 사용해 전송된 데이터에 접근할 수 없다.
서블릿 API를 사용해서 바이너리 데이터와 바이너리 데이터와 함께 전송된 데이터에 접근하는 메소드를 직접 구현할 순 있지만, 대부분 프로그래머는 이 경우 외부 라이브러리를 사용한다.

문자열 전송 예제

아래 예제를 통해 사용자가 보낸 데이터를 서블릿에서 수신하는 방법을 실습해 보자.
다음 HTML 파일을 도큐먼트 베이스/example 디렉터리에 작성한다.

/example/join.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Sign Up</title>
</head>
<body>
<h3>Sign Up</h3>

<form id="joinForm" action="../RegisterServlet" method="post">
<div>아이디 <input type="text" name="id" /></div>
<div>별명 <input type="text" name="nickname" /></div>
<div>비밀번호 <input type="password" name="passwd" /></div>
<div>이름 <input type="text" name="name" /></div>
<div>성별 Male <input type="radio" name="gender" value="M" /> Female <input type="radio" name="gender" value="F" /></div>
<div>생년월일 <input type="text" name="birthday" /></div>
<div>휴대폰 <input type="text" name="mobile" /></div>
<div>전화 <input type="text" name="tel" /></div>
<div>주소 <input type="text" name="address" /></div>
<div>이메일 <input type="text" name="email" /></div>
<div>
스포츠
<input type="checkbox" name="sports" value="soccer" />soccer
<input type="checkbox" name="sports" value="baseball" />baseball
<input type="checkbox" name="sports" value="basketball" />Basketball
<input type="checkbox" name="sports" value="tennis" />Tennis
<input type="checkbox" name="sports" value="tabletennis" />Tabletennis
</div>
<div>
강좌
<select name="main-menu" multiple="multiple">
	<option value="">-- Multiple Select --</option>
	<option value="java">JAVA</option>
	<option value="jdbc">JDBC</option>
	<option value="jsp">JSP</option>
	<option value="css-layout">CSS Layout</option>
	<option value="jsp-prj">JSP Project</option>
	<option value="spring">Spring</option>
	<option value="javascript">JavaScript</option>
</select>
</div>
<div>
자기 소개
<textarea name="aboutMe" cols="40" rows="7"></textarea>
</div>
<div><input type="submit" value="Submit" /></div>
</form>
</body>
</html>

RegisterServlet.java를 /WEB-INF/src/example 디렉터리에 아래와 같이 작성한다.

RegisterServlet.java
package example;

import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;

public class RegisterServlet extends HttpServlet {
	@Override
	public void doPost(HttpServletRequest req, HttpServletResponse resp)
			throws IOException,ServletException {
		
		resp.setContentType("text/html; charset=UTF-8");
		PrintWriter out = resp.getWriter();
		req.setCharacterEncoding("UTF-8");
		
		String id = req.getParameter("id");
		
		out.println("<html><body>");
		out.println("id : " + id);
		
		String[] sports = req.getParameterValues("sports");
		int len = sports.length;
		
		out.println("<ol>");
		for (int i = 0; i < len; i++) {
			out.println("<li>" + sports[i] + "</li>");
		}
		
		out.println("</ol>");
		
		String path = req.getContextPath();
		out.println("<a href=" + path + "/example/SignUp.html>Join</a>");
		out.println("</body></html>");
	}
}

명령 프롬프트를 열고 /WEB-INF/src/exampe로 이동하여 아래와 같이 컴파일한다.

C:\ Command Prompt
javac -d C:/www/myapp/WEB-INF/classes ^
-cp C:/apache-tomcat-9.0.87/lib/servlet-api.jar ^
RegisterServlet.java

web.xml에 다음을 추가한다.

web.xml
<servlet>			
    <servlet-name>RegisterServlet</servlet-name>
    <servlet-class>example.RegisterServlet</servlet-class>
</servlet>

<servlet-mapping>
    <servlet-name>RegisterServlet</servlet-name>
    <url-pattern>/RegisterServlet</url-pattern>
</servlet-mapping>

톰캣을 재시작한 후, http://localhost:8080/example/SignUp.html를 방문한다. TODO: 아이디와 스포츠 외의 값을 확인하는 소스를 서블릿에 추가한다.

RequestDispatcher 인터페이스

RequestDispathcer 인터페이스는 include()와 forward(), 2개의 메소드를 가지고 있다.
include() 메소드는 제어권을 다른 자원에게 넘겼다가, 다른 자원이 실행을 완료하면, 다시 제어권을 가져오는 메소드로, 응답에 다른 자원이 생산한 메시지를 추가하기 위해 사용한다.
forward() 메소드는 제어권을 다른 자원으로 넘긴다. 그 결과 제어권을 받은 자원이 클라이언트에게 응답하게 된다.

forward() 메소드 예제를 실습해 보자.
ControllerServlet.java 파일을 /WEB-INF/src/example 디렉터리에 다음과 같이 생성한다.

ControllerServlet.java
package example;

import java.io.IOException;

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

public class ControllerServlet extends HttpServlet {

	@Override
	public void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
		doPost(req,resp);
	}

	@Override
	public void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
		req.setCharacterEncoding("UTF-8");
		
		String uri = req.getRequestURI();
		String contextPath = req.getContextPath();
		String command = null;
		String view = null;
		boolean isRedirect = false;
		
		command = uri.substring(contextPath.length());
		
		if (command.equals("/example/SignUp.action")) {
			view = "/example/SignUp.html";
		}
		if (isRedirect == false) {
			ServletContext sc = this.getServletContext();
			RequestDispatcher rd = sc.getRequestDispatcher(view);
			rd.forward(req,resp);
		} else {
			resp.sendRedirect(view);
		}
	}
}

명령 프롬프트를 열고 /WEB-INF/src/exampe로 이동하여 다음과 같이 컴파일한다.

C:\ Command Prompt
javac -d C:/www/myapp/WEB-INF/classes ^
-cp C:/apache-tomcat-9.0.87/lib/servlet-api.jar ^
ControllerServlet.java

web.xml에 다음을 추가한다.

web.xml
<servlet>
    <servlet-name>Controller</servlet-name>
    <servlet-class>example.ControllerServlet</servlet-class>
    <load-on-startup>1</load-on-startup>
</servlet>

<servlet-mapping>
    <servlet-name>Controller</servlet-name>
    <url-pattern>*.action</url-pattern>
</servlet-mapping>

톰캣을 재시작한 다음 http://localhost:8080/example/SignUp.action를 방문하여 /example/SignUp.html이 응답하는지 확인한다.
TODO: ControllerServlet.java에서 isRedirect를 true로 변경한 후 다시 컴파일하고 테스트한다.

우리는 ControllerServlet이 .action으로 끝나는 모든 요청을 담당하도록 web.xml에 ControllerServlet 서블릿 선언과 매핑을 추가했다.
.action로 끝나는 요청이 오면 톰캣은 web.xml에서 매핑정보를 해석해서 ControllerServlet 서블릿의 +service(ServletRequest req, ServletResponse res) 메소드를 호출한다.
+service(ServletRequest req, ServletResponse res) 메소드는 #service(HttpServletRequest req, HttpServletResponse resp) 메소드를 호출한다.
#service(HttpServletRequest req, HttpServletResponse resp) 메소드에서는 요청의 HTTP METHOD가 무엇인지 판단하고 그에 맞는 메소드를 호출한다.

웹 브라우저의 주소창에 http://localhost:8080/example/SignUp.action를 입력하여 요청했으므로 GET 방식이다.
따라서 이 경우는 doGet() 메소드를 호출된다.
ControllerServlet서블릿의 doGet() 메소드는 단순히 doPost()을 호출한다.
다음 표에서 doPost()의 구현 부에 사용된 HttpServletRequest의 메소드를 정리했다.

getRequestURI()
웹 브라우저로 http://localhost:8080/example/SignUp.action 요청하면, 이 메소드는 "/example/SignUp.action"을 반환한다.
getContextPath()
컨텍스트 파일의 path 속성값을 반환한다.(이 값을 ContextPath라 한다.)
우리는 ROOT 애플리케이션에서 작업하므로 이 메소드를 통해 얻을 수 있는 값은 ""이다.
req.getRequestURI().substring(req.getContextPath().length())
위 코드는 "/example/SignUp.action"을 반환한다.

/example/join.action을 요청한 사용자는 /example/SignUp.html의 응답을 받게 된다.

데이터베이스를 사용하는 서블릿

JDBC 장에서 실습했던 오라클 JDBC 연동 테스트 파일인, GetEmp.java를 서블릿으로 변환해보자.
ROOT 애플리케이션의 /WEB-INF/src/example 디렉터리에 GetEmpServlet.java 파일을 생성한다.

GetEmpServlet.java
package example;

import java.io.IOException;
import java.io.PrintWriter;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class GetEmpServlet extends HttpServlet {
	
	private String DB_URL = "jdbc:oracle:thin:@127.0.0.1:1521:XE";
	private String DB_USER = "scott";
	private String DB_PASSWORD = "tiger";
	
	/*
	 * GenericServlet의 init() 메소드
	 * init(ServletConfig config) 메소드 구현 부에서 이 메소드를 호출하도록 구현되어 있다.
	 * 따라서 굳이 init(ServletConfig config) 메소드를 오버라이딩하지 않아도 된다.
	 */
	@Override
	public void init() throws ServletException {
		try {
			Class.forName( "oracle.jdbc.driver.OracleDriver" );
		} catch (ClassNotFoundException e) {
			e.printStackTrace();
		}
	}
	

	@Override
	protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
		
		resp.setContentType("text/html; charset=UTF-8");
		PrintWriter out = resp.getWriter();
		
		Connection con = null;
		Statement stmt = null;
		ResultSet rs = null;
		
		String sql = "select * from emp";
		
		try {
			con = DriverManager.getConnection(DB_URL, DB_USER, DB_PASSWORD);
			stmt = con.createStatement();
			rs = stmt.executeQuery(sql);
			
			while (rs.next()) {
				String empno = rs.getString(1);
				String ename = rs.getString(2);
				String job = rs.getString(3);
				String mgr = rs.getString(4);
				String hiredate = rs.getString(5);
				String sal = rs.getString(6);
				String comm = rs.getString(7);
				String depno = rs.getString(8);
				
				out.println( empno + " : " + ename + " : " + job + " : " + mgr + 
				" : " + hiredate + " : " + sal + " : " + comm+" : " + depno + "<br />" );
			}

		} catch (SQLException e) {
			e.printStackTrace(out);
		} finally {
			if (rs != null) {
				try {
					rs.close();
				} catch (SQLException e) {
					e.printStackTrace();
				}
			}
			if (stmt != null) {
				try {
					stmt.close();
				} catch (SQLException e) {

					e.printStackTrace();
				}
			}
			if (con != null) {
				try {
					con.close();
				} catch (SQLException e) {
					e.printStackTrace();
				}
			}
		}
	}
}

명령 프롬프트에서 ROOT 애플리케이션의 /WEB-INF/src/example 디렉터리로 이동한 후 다음과 같이 컴파일한다.

C:\ Command Prompt
javac -d C:/www/myapp/WEB-INF/classes ^ 
-cp C:/apache-tomcat-9.0.87/lib/servlet-api.jar ^
GetEmpServlet.java

web.xml에 다음을 추가한다.

web.xml
<servlet>
    <servlet-name>GetEmpServlet</servlet-name>
    <servlet-class>example.GetEmpServlet</servlet-class>
</servlet>

<servlet-mapping>
    <servlet-name>GetEmpServlet</servlet-name>
    <url-pattern>/empList</url-pattern>
</servlet-mapping>

JDBC 드라이버 파일을 CATALINA_HOME/lib 복사한다.
톰캣을 재시작한 후, http://localhost:8080/empList를 방문한다.

7369 : SMITH : CLERK : 7902 : 1980-12-17 00:00:00.0 : 800 : null : 20
7499 : ALLEN : SALESMAN : 7698 : 1981-02-20 00:00:00.0 : 1600 : 300 : 30
7521 : WARD : SALESMAN : 7698 : 1981-02-22 00:00:00.0 : 1250 : 500 : 30
7566 : JONES : MANAGER : 7839 : 1981-04-02 00:00:00.0 : 2975 : null : 20
7654 : MARTIN : SALESMAN : 7698 : 1981-09-28 00:00:00.0 : 1250 : 1400 : 30
7698 : BLAKE : MANAGER : 7839 : 1981-05-01 00:00:00.0 : 2850 : null : 30
7782 : CLARK : MANAGER : 7839 : 1981-06-09 00:00:00.0 : 2450 : null : 10
7788 : SCOTT : ANALYST : 7566 : 1987-04-19 00:00:00.0 : 3000 : null : 20
7839 : KING : PRESIDENT : null : 1981-11-17 00:00:00.0 : 5000 : null : 10
7844 : TURNER : SALESMAN : 7698 : 1981-09-08 00:00:00.0 : 1500 : 0 : 30
7876 : ADAMS : CLERK : 7788 : 1987-05-23 00:00:00.0 : 1100 : null : 20
7900 : JAMES : CLERK : 7698 : 1981-12-03 00:00:00.0 : 950 : null : 30
7902 : FORD : ANALYST : 7566 : 1981-12-03 00:00:00.0 : 3000 : null : 20
7934 : MILLER : CLERK : 7782 : 1982-01-23 00:00:00.0 : 1300 : null : 10

원하는 결과가 나오지 않을 때는 아래 리스트를 점검한다.

  • web.xml 파일에 서블릿 선언과 서블릿 매핑이 올바르게 추가되었는가?
  • /WEB-INF/classes/example 디렉터리에 GetEmpServlet 바이트 코드 있는가?
  • CATALINA_HOME/lib에 오라클 JDBC 드라이버 파일(ojdbc6.jar)이 있는가?
  • 루트 웹 애플리케이션이 성공적으로 로드되었는가?

ServletConfig 초기화 파라미터 사용하기

GetEmpServlet.java 예제를 서블릿 초기화 파라미터를 사용하는 예제로 수정해 보자.
아래 서블릿을 /WEB-INF/src/example 디렉터리에 만든다.

InitParamServlet.java
package example;

import java.io.IOException;
import java.io.PrintWriter;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class InitParamServlet extends HttpServlet {
	
	private String url;
	private String user;
	private String passwd;
	private String driver;
	
	@Override
	public void init() throws ServletException {
		url = this.getInitParameter("url");
		user = this.getInitParameter("user");
		passwd = this.getInitParameter("passwd");
		driver = this.getInitParameter("driver");
		
		try {
			Class.forName(driver);
		} catch (ClassNotFoundException e) {
			e.printStackTrace();
		}
	}
	
	@Override
	public void doGet(HttpServletRequest req, HttpServletResponse resp)
			throws IOException, ServletException {
		
		resp.setContentType("text/html; charset=UTF-8");
		PrintWriter out = resp.getWriter();
		
		Connection con = null;
		PreparedStatement stmt = null;
		ResultSet rs = null;
		
		String sql = "SELECT * FROM emp";
		
		try {
			con = DriverManager.getConnection(url, user, passwd);
			stmt = con.prepareStatement(sql);
			rs = stmt.executeQuery();
			
			while (rs.next()) {
				String empno = rs.getString(1);
				String ename = rs.getString(2);
				String job = rs.getString(3);
				String mgr = rs.getString(4);
				String hiredate = rs.getString(5);
				String sal = rs.getString(6);
				String comm = rs.getString(7);
				String depno = rs.getString(8);
				
				out.println(empno + " : " + ename + " : " + job + " : " + mgr + 
				  " : " + hiredate + " : " + sal + " : " + comm+" : " + depno + "<br />");
			}
		} catch (SQLException e) {
			e.printStackTrace(out);
		} finally {
			if (rs != null) {
				try {
					rs.close();
				} catch (SQLException e) {
					e.printStackTrace();
				}
			}
			if (stmt != null) {
				try {
					stmt.close();
				} catch (SQLException e) {
					e.printStackTrace();
				}
			}
			if (con != null) {
				try {
					con.close();
				} catch (SQLException e) {
					e.printStackTrace();
				}
			}
		}
	}
}

web.xml에 다음을 추가한다.

web.xml
<servlet>
  <servlet-name>InitParamServlet</servlet-name>
  <servlet-class>example.InitParamServlet</servlet-class>

  <init-param>
    <param-name>driver</param-name>
    <param-value>oracle.jdbc.driver.OracleDriver</param-value>
  </init-param>
  <init-param>
    <param-name>url</param-name>
    <param-value>jdbc:oracle:thin:@127.0.0.1:1521:XE</param-value>
  </init-param>
  <init-param>
    <param-name>user</param-name>
    <param-value>scott</param-value>
  </init-param>
  <init-param>
    <param-name>passwd</param-name>
    <param-value>tiger</param-value>
  </init-param>
</servlet>

<servlet-mapping>
  <servlet-name>InitParamServlet</servlet-name>
  <url-pattern>/initParam</url-pattern>
</servlet-mapping>

위에서 설정한 ServletConfig의 초기화 파라미터의 값은 ServletConfig의 getInitParameter(String name) 메소드를 이용하면 얻어진다.
톰캣을 재시작한 후에, http://localhost:8080/initParam을 방문한다.

ServletContext 초기화 파라미터 이용하기

ServletConfig 초기화 파라미터는 해당 서블릿에서만 참조 할 수 있다.
ServletContext 초기화 파라미터는 웹 애플리케이션 내 모든 서블릿과 JSP에서 참조할 수 있다.
ServletContext 초기화 파라미터는 web.xml에 context-param 엘리먼트를 사용하여 설정한다.

schema와 dtd는 지정된 순서가 있다.
다음은 http://java.sun.com/dtd/web-app_2_3.dtd에서 발췌했다.

<!ELEMENT web-app (icon?, display-name?, description?, distributable?, context-param*, filter*, filter-mapping*, listener*, servlet*, servlet-mapping*, session-config?, mime-mapping*, welcome-file-list?, error-page*, taglib*, resource-env-ref*, resource-ref*, security-constraint*, login-config?, security-role*, env-entry*, ejb-ref*, ejb-local-ref*)>

context-param은 servlet 보다는 앞서 선언해야 한다.
web.xml을 열고 다음을 추가한다.

web.xml
<context-param>
    <param-name>url</param-name>
    <param-value>jdbc:oracle:thin:@127.0.0.1:1521:XE</param-value>
</context-param>

ServletContext 객체의 레퍼런스는 서블릿에서 getServletContext() 메소드를 이용하면 얻을 수 있다.
위에서 선언한 ServletContext의 초기화 파라미터인 url 값은 ServletContext 의 getInitParameter(String name) 메소드를 이용하여 구한다.
SimpleSerlvet.java의 적당한 위치에 다음 코드를 추가한다.

SimpleServlet.java 편집
ServletContext sc = getServletContext();
String url = sc.getInitParameter("url");
out.println(url);

톰캣을 재실행하고, http://localhost:8080/simple을 방문한다.
TODO: InitParamServlet.java에서 url이 ServletContext 초기화 파라미터를 이용해서 설정되도록 수정하자.

리슨너

리슨너는 웹 애플리케이션의 이벤트에 실행된다.
웹 애플리케이션 이벤트는 서블릿 2.3 스펙에 추가되었다.
웹 애플리케이션 이벤트는 다음과 같이 나뉜다.

  • 애플리케이션 스타트업과 셧다운
  • 세션 생성 및 세션 무효

애플리케이션 스타트업 이벤트는 톰캣과 같은 서블릿 컨테이너에 의해 웹 애플리케이션이 처음 로드되어 스타트될 때 발생한다.
애플리케이션 셧다운 이벤트는 웹 애플리케이션이 셧다운 될 때 발생한다.

세션 생성 이벤트는 새로운 세션이 생성될 때 발생한다.
세션 무효 이벤트는 세션이 무효화 될때 매번 발생한다.

이벤트를 이용하기 위해서는 리슨너라는 클래스를 작성해야 한다.
리슨너 클래스는 순수 자바 클래스로 다음의 인터페이스를 구현한다.

  • javax.servlet.ServletContextListener
  • javax.servlet.http.HttpSessionListener

애플리케이션 스타트업 또는 셧다운 이벤트를 위한 리슨너를 원한다면 ServletContextListener 인터페이스를 구현한다.
세션 생성 및 세션 무효 이벤트를 위한 리슨너를 원한다면 HttpSessionListener 인터페이스를 구현한다.
ServletContextListener 인터페이스는 다음 2개의 메소드로 구성되어 있다.

  • public void contextInitialized(ServletContextEvent sce);
  • public void contextDestroyed(ServletContextEvent sce);

HttpSessionListener 인터페이스는 다음 2개의 메소드로 구성되어 있다.

  • public void sessionCreated(HttpSessionEvent se);
  • public void sessionDestroyed(HttpSessionEvent se);

다음 Listener 클래스를 작성한다.
이 클래스는 웹 애플리케이션 시작 시 OracleConnectionManager 객체를 생성하고 ServletContext에 저장한다.

MyServletContextListener.java
package net.java_school.listener;

import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;

import net.java_school.db.dbpool.OracleConnectionManager;

public class MyServletContextListener implements ServletContextListener {

	@Override
	public void contextInitialized(ServletContextEvent sce) {
		ServletContext sc = sce.getServletContext();
		OracleConnectionManager dbmgr = new OracleConnectionManager();
		sc.setAttribute("dbmgr", dbmgr);
	}

	@Override
	public void contextDestroyed(ServletContextEvent sce) {
		ServletContext sc = sce.getServletContext();
		sc.removeAttribute("dbmgr");
	}

}

web.xml에 다음을, context-param 아래에 servlet보다는 위에, 추가한다.

web.xml
<listener>
    <listener-class>net.java_school.listener.MyServletContextListener</listener-class>
</listener>

사용자 정의 커넥션 풀 사용하기

JDBC 장의 ConnectionPool절 자바 소스를 모두 ROOT 애플리케이션의 WEB-INF/src 디렉터리에 복사한다.
Log.java 파일을 열고 다음과 같이 수정한다.

public String logFile = "C:/www/myapp/WEB-INF/myapp.log";

ConnectionPool절 orcale.properties 파일을 ROOT 애플리케이션의 WEB-INF 디렉터리에 복사한다.
ConnectionManager.java 파일을 열고 다음과 같이 수정한다.

configFile = "C:/www/myapp/WEB-INF/" + poolName + ".properties";

이제 ROOT 웹 애플리케이션이 시작되면 OracleConnectionManager 객체가 생성되고 그 레퍼런스가 ServletContext에 저장된다.
테스트를 위해 GetEmpServlet.java 파일을 아래와 같이 수정한다.

GetEmpServlet.java
package example;

import java.sql.*;
import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;

import net.java_school.db.dbpool.*;

public class GetEmpServlet extends HttpServlet {

	private OracleConnectionManager dbmgr;
	
	@Override
	public void init() throws ServletException {
		ServletContext sc = getServletContext();
		dbmgr = (OracleConnectionManager) sc.getAttribute("dbmgr");
	}
	
	@Override
	public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException {
			
		resp.setContentType("text/html; charset=UTF-8");
		PrintWriter out = resp.getWriter();
		
		Connection con = null;
		PreparedStatement stmt = null;
		ResultSet rs = null;
		
		String sql = "SELECT * FROM emp";
		
		try {
			con = dbmgr.getConnection();
			stmt = con.prepareStatement(sql);
			rs = stmt.executeQuery();
			
			while (rs.next()) {
				String empno = rs.getString(1);
				String ename = rs.getString(2);
				String job = rs.getString(3);
				String mgr = rs.getString(4);
				String hiredate = rs.getString(5);
				String sal = rs.getString(6);
				String comm = rs.getString(7);
				String depno = rs.getString(8);
				
				out.println( empno + " : " + ename + " : " + job + " : " + mgr +
					" : " + hiredate + " : " + sal + " : " + comm+" : " + depno + "<br>" );
			}
		} catch (SQLException e) {
			e.printStackTrace(out);
		} finally {
			if (rs != null) {
				try {
					rs.close();
				} catch (SQLException e) {
					e.printStackTrace();
				}
			}
			if (stmt != null) {
				try {
					stmt.close();
				} catch (SQLException e) {
					e.printStackTrace();
				}
			}
			if (con != null) {
				try {
					con.close();
				} catch (SQLException e) {
					e.printStackTrace();
				}
			}
		}
	}
}

사용자 정의 커넥션 풀 소스와 GetEmpServlet.java를 컴파일한다.
톰캣을 재가동 후 http://localhost:8080/empList를 방문한다. (GetEmpServlet 서블릿에 대한 선언과 매핑은 이미 이전 실습에서 설정했다)

HttpSessionListener 인터페이스는 2개의 메소드로 구성된다.
하나는 세션 생성 이벤트에 동작하는 기능이고, 다른 하나는 세션 무효화 이벤트에 동작하는 기능이다.

  • public void sessionCreated(HttpSessionEvent se);
  • public void sessionDestroyed(HttpSessionEvent se);

다음은 HttpSessionListener 에 대한 예제이다.

SessionCounterListener.java
package net.java_school.listener;

import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;

public class SessionCounterListener implements HttpSessionListener {
	public static int totalCount;
	
	@Override
	public void sessionCreated(HttpSessionEvent event) {
		totalCount++;
		System.out.println("세션증가 총세션수:" + totalCount);
	}

	@Override
	public void sessionDestroyed(HttpSessionEvent event) {
		totalCount--;
		System.out.println("세션감수 총세션수:" + totalCount);
	}

}

web.xml에 다음을 추가한다.

<listener>
    <listener-class>net.java_school.listener.SessionCounterListener</listener-class>
</listener>

톰캣을 재실행한 후, http://localhost:8080/simple를 방문한다.
다른 웹 브라우저로 http://localhost:8080/simple를 방문한다.
톰캣 로그 파일의 로그 메시지를 확인한다.

Filter

필터란 사용자의 요청이 서버 자원에 전달되기 전에 언제나 수행되어야 하는 코드 조각이 있을 때 사용한다.
필터가 작동하려면 web.xml에 필터 선언과 필터 매핑을 추가해야 한다.
web.xml에 필터1 다음에 필터2가 순서대로 선언되고 매핑되었다면 필터1 - 필터2 - 서버 자원 - 필터2 - 필터1 - 웹 브라우저 순으로 실행된다.
필터 클래스를 작성하기 위해서는 javax.servlet.Filter 인터페이스를 구현해야 한다.

다음은 필터 매카니즘을 흉내 낸 순수 자바 애플리케이션 예이다.

ChainFilter.java
package net.java_school.filter;

import java.util.ArrayList;
import java.util.Iterator;

public class ChainFilter {
	private ArrayList<Filter> filters;
	private Iterator<Filter> iterator;

	public void doFilter() {
		if (iterator.hasNext()) {
			iterator.next().doFilter(this);
		} else {
			System.out.println("Run Server resource");
		}
	}

	public ArrayList<Filter> getFilters() {
		return filters;
	}

	public void setFilters(ArrayList<Filter> filters) {
		this.filters = filters;
		this.iterator = filters.iterator();
	}
	
}
Filter.java
package net.java_school.filter;

public interface Filter {
	
	public void doFilter(ChainFilter chain);

}
Filter1.java
package net.java_school.filter;

public class Filter1 implements Filter {

	@Override
	public void doFilter(ChainFilter chain) {
		System.out.println("Run Filter 1 before the server resource runs");
		chain.doFilter();
		System.out.println("Run Filter 1 after the server resource runs");
	}

}
Filter2.java
package net.java_school.filter;

public class Filter2 implements Filter {

	@Override
	public void doFilter(ChainFilter chain) {
		System.out.println("Run Filter 2 before the server resource runs");
		chain.doFilter();
		System.out.println("Run Filter 2 after the server resource runs");
	}

}
Tomcat.java
package net.java_school.filter;

import java.util.ArrayList;

public class Tomcat {

	public static void main(String[] args) {
		ChainFilter chain = new ChainFilter();
		ArrayList<Filter> filters = new ArrayList<Filter>();
		Filter f1 = new Filter1();
		Filter f2 = new Filter2();
		filters.add(f1);
		filters.add(f2);
		chain.setFilters(filters);
		chain.doFilter();
	}

}
Run Filter 1 before the server resource runs
Run Filter 2 before the server resource runs
Run Server resource.
Run Filter 2 after the server resource runs
Run Filter 1 after the server resource runs

Filter 인터페이스

  • init(FilterConfig filterConfig) throws ServletException 서블릿 컨테이너에 의해 호출되면 필터는 서비스 상태가 됨
  • doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException 서블릿 컨테이너에 의해 호출되어 필터링 작업을 수행
  • destroy() 서블릿 컨테이너에 의해 호출되면 해당 필터는 더 이상 서비스를 할 수 없음. 주로 자원 반납을 위해 사용한다.

doFilter 메소드의 아규먼트는, 필터가 요청이나 응답을 가로챌 때, 필터가 ServletRequest, ServletResponse 그리고 javax.servlet.FilterChain 객체에 접근할 수 있음을 보여준다.
FilterChain 객체는 순서대로 호출되어야 하는 필터의 리스트를 담고 있다.

필터 클래스의 doFilter() 메소드에서 FilterChain의 doFilter() 메소드를 호출하기 전까지가 요청 전에 실행되는 필터링 코드이며 FilterChain의 doFilter() 메소드 호출 다음이 응답 전에 호출되는 필터링 코드이다.

Filter 예제

다음은 모든 서버 자원이 실행되기 전에 req.setCharacterEncoding("UTF-8"); 코드를 실행하는 예제이다.
아래와 같이 CharsetFilter.java 파일을 작성한다.

/WEB-INF/src/net/java_school/filter/CharsetFilter.java
package net.java_school.filter;

import java.io.IOException;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;

public class CharsetFilter implements Filter {

	private String charset = null;
	
	@Override
	public void init(FilterConfig config) throws ServletException {
		this.charset = config.getInitParameter("charset");
	}
	
	@Override
	public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) 
		throws IOException, ServletException {
		
		if (req.getCharacterEncoding() == null) {
			req.setCharacterEncoding(charset);
			chain.doFilter(req,resp);
		}
	}

	@Override
	public void destroy() {
		//반납할 자원이 있다면 작성한다.
	}

}

명령 프롬프트에서 /WEB-INF/src/net/java_school/filter/ 폴더로 이동한 다음 아래와 같이 컴파일한다.

C:\ Command Prompt
javac -d C:/www/myapp/WEB-INF/classes ^
-cp C:/apache-tomcat-9.0.87/lib/servlet-api.jar ^
CharsetFilter.java

다음 web.xml 파일을 열고 다음을 추가한다.
기존 엘리먼트와의 순서에 주의한다.
다음 코드는 context-param 엘리먼트와 listener 엘리먼트 사이에 두어야 한다.

web.xml
<filter>
   <filter-name>CharsetFilter</filter-name>
   <filter-class>net.java_school.filter.CharsetFilter</filter-class>
   <init-param>
      <param-name>charset</param-name>
      <param-value>UTF-8</param-value>
   </init-param>
</filter>

<filter-mapping>
   <filter-name>CharsetFilter</filter-name>
   <url-pattern>/*</url-pattern>
</filter-mapping>

위에서 실행했던 회원가입 예제의 RegisterServlet.java 소스에서 req.setCharacterEncoding("UTF-8");을 주석처리 한 후 RegisterServlet 서블릿을 재컴파일한다.
http://localhost:8080/example/SignUp.jsp를 방문하여 아이디에 한글을 입력하고 좋아하는 운동을 선택한 후 전송을 클릭한다.
req.setCharacterEncoding("UTF-8");이 실행된다면 사용자가 입력한 한글 아이디가 제대로 출력된다.

필터 초기화 파라미터는 web.xml에서 filter의 서브 엘리먼트, init-param를 사용하여 설정한다.
필터 초기화 파라미터를 읽기 위해선 FilterConfig의 getInitParameter() 메소드나 getInitParameters() 메소드를 사용한다.

web.xml에서 filter-mapping 엘리먼트는 필터링 될 자원을 지정한다.

필터는 배치 정의자에 나와 있는 순으로 FilterChain에 추가된다. 이때 서블릿 이름과 매핑된 필터는 URL 패턴에 매칭되는 필터 다음에 추가된다.
FilterChain.doFilter() 메소드는 FilterChain의 다음 아이템을 호출한다.

파일 업로드

대부분 프로그래머는 외부 라이브러리를 사용하여 파일을 업로드한다.

MultipartRequest

MultipartRequest 패키지는 파일 업로드에 널리 이용되고 있는 패키지이다.
http://www.servlets.com/cos/index.html에서 cos-26Dec2008.zip를 다운로드한다.
압축을 푼 후, cos.jar 파일을 ROOT 애플리케이션의 /WEB-INF/lib 디렉터리에 복사한다.
MultipartRequest 클래스의 생성자는 8개가 있다.
자세한 사항은 다음 주소에서 참조한다.
http://www.servlets.com/cos/javadoc/com/oreilly/servlet/MultipartRequest.html
다음 생성자는 한글 인코딩 문제를 해결할 수 있고 업로드하는 파일 이름이 중복될 때 자동으로 파일명을 바꾼다.

public MultipartRequest(
	HttpServletRequest request,
	String saveDirectory,
	int maxPostSize,
	String encoding,
	FileRenamePolicy policy) throws IOException

MultipartRequest의 메소드

업로드는 성공하면 MultipartRequest 객체가 생성된다.
아래 표는 서버 시스템에 파일을 업로드된 후 이용하는 MultipartRequest의 멤버 메소드를 보여준다.
<input type="file" name="photo"/>으로 logo.gif를 업로드했다고 가정한다.

getContentType("photo");
업로드된 파일의 MIME 타입 반환, 예를 들어 확장자가 gif 이미지라면 "image/gif" 가 반환
getFile("photo");
업로드되어 서버에 저장된 파일의 File 객체 반환
getFileNames();
폼 요소 중 input 태그 속성이 file 로 된 파라미터의 이름을 Enumeration 타입으로 반환
getFilesystemName("photo");
업로드되어 서버 파일시스템에 존재하는 실제 파일명을 반환
getOriginalFileName("photo");
원래의 파일명을 반환
HttpServletRequest와 같은 인터페이스를 제공하기 위한 메소드
getParameter(String name);
name에 해당하는 파라미터의 값을 String 타입으로 반환
getParameterNames();
모든 파라미터의 이름을 String으로 구성된 Enumeration 타입으로 반환
getParameterValues(String name);
name 에 해당하는 파라미터의 값들을 String[] 타입으로 반환

MultipartRequest 예제

다음 HTML 파일을 ROOT 애플리케이션의 최상위 디렉터리의 example 서브 디렉터리에 작성한다.

/example/MultipartRequest.html
<!DOCTYPE html>
<html>
<head>
	<title>MultipartRequest Servlet Example</title>
	<meta charset="UTF-8" />
</head>
<body>

<h1>MultipartRequest를 이용한 파일 업로드 테스트</h1>

<form action="../servlet/UploadTest" method="post" enctype="multipart/form-data">
<div>Name: <input type="text" name="name" /></div>
<div>파일 1: <input type="file" name="file1" /></div>
<div>파일 2: <input type="file" name="file2" /></div>
<div><input type="submit" value="전송" /></div>
</form>

</body>
</html>

다음 서블릿을 작성하고 컴파일한다.
컴파일할 때에 자바 컴파일러가 cos.jar 파일 경로를 알아야 한다.

UploadTest.java
package example;

import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Enumeration;

import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.oreilly.servlet.MultipartRequest;
import com.oreilly.servlet.multipart.DefaultFileRenamePolicy;

public class UploadTest extends HttpServlet {
	
	@Override	
	public void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException {
			
		resp.setContentType("text/html; charset=UTF-8");
		PrintWriter out = resp.getWriter();
		
		ServletContext cxt = getServletContext();
		String dir = cxt.getRealPath("/upload");
		
		try {
			MultipartRequest multi = new MultipartRequest(req, dir, 5*1024*1024, "UTF-8", new DefaultFileRenamePolicy());
					
			out.println("<html>");
			out.println("<body>");
			out.println("<h1>사용자가 전달한 파라미터</h1>");
			out.println("<ol>");
			Enumeration<?> params = multi.getParameterNames();
			
			while (params.hasMoreElements()) {
				String name = (String) params.nextElement();
				String value = multi.getParameter(name);
				out.println("<li>" + name + "=" + value + "</li>");
			}
			
			out.println("</ol>");
			out.println("<h1>업로드한 파일</h1>");
			
			Enumeration<?> files = multi.getFileNames();
			
			while (files.hasMoreElements()) {
				out.println("<ul>");
				String name = (String) files.nextElement();
				String filename = multi.getFilesystemName(name);
				String orginalname =multi.getOriginalFileName(name);
				String type = multi.getContentType(name);
				File f = multi.getFile(name);
				out.println("<li>파라미터 이름 : "  + name + "</li>");
				out.println("<li>파일 이름 : " + filename + "</li>");
				out.println("<li>원래 파일 이름 : " + orginalname + "</li>");
				out.println("<li>파일 타입 : " + type + "</li>");
				
				if (f != null) {
					out.println("<li>크기: " + f.length() + "</li>");
				}
				out.println("</ul>");
			}
		} catch(Exception e) {
			out.println("<ul>");
			e.printStackTrace(out);
			out.println("</ul>");
		}
		out.println("</body></html>");
	}
}

web.xml에 다음을 추가한다.

web.xml
<servlet>
    <servlet-name>UploadTest</servlet-name>
    <servlet-class>example.UploadTest</servlet-class>
</servlet>

<servlet-mapping>
    <servlet-name>UploadTest</servlet-name>
    <url-pattern>/servlet/UploadTest</url-pattern>
</servlet-mapping>

예제를 실행하기 전에 ROOT 웹 애플리케이션의 최상위 디렉터리에 upload 서브 디렉터리를 생성한다.
톰캣을 재가동하고 http://localhost:8080/example/upload.html를 방문한다.
중복된 파일을 업로드한 후 upload 폴더에서 파일명을 확인한다.
중복된 파일을 업로드하면 확장자를 제외한 파일 이름의 끝에 숫자가 붙어서 업로드되고 있음을 확인할 수 있다.
만약 테스트가 실패했다면 아래 리스트를 점검한다.

  1. UploadTest 서블릿의 바이트 코드가 생성되었는가?
  2. ROOT 애플리케이션의 최상위 디렉터리에 upload 서브 디렉터리가 있는가?
  3. 톰캣 클래스로더가 찾을 수 있도록 cos.jar 파일이 ROOT 애플리케이션의 /WEB-INF/lib에 복사되어 있는가?
  4. web.xml 파일에 UploadTest 서블릿을 선언하고 매핑했는가?
  5. ROOT 웹 애플리케이션이 로드되었는가?

commons-fileupload

Commons-fileupload는 오픈 소스로 유명한 아파치에서 제공하는 파일 업로드 라이브러리이다.
다음 주소에서 최신 바이너리 파일을 내려받는다.
http://commons.apache.org/proper/commons-fileupload/download_fileupload.cgi
http://commons.apache.org/proper/commons-io/download_io.cgi
압축을 풀고 commons-fileupload-x.x.jar와 commons-io-x.x.jar를 ROOT 애플리케이션의 /WEB-INF/lib 디렉터리에 복사한다.

/example/commons-fileupload.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>commons-fileupload 테스트</title>
</head>
<body>

<h1>commons-fileupload를 사용하여 파일 업로드하기</h1>

<form action="../CommonsUpload" method="post" enctype="multipart/form-data">
<div>File : <input type="file" name="upload" /></div>
<div><input type="submit" value="Submit" /></div>
</form>

</body>
</html>
CommonsUpload.java
package example;

import java.io.*;

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

import java.util.Iterator;
import java.io.File;
import java.util.List;

import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;

public class CommonsUpload extends HttpServlet {

	@Override
	public void doPost(HttpServletRequest req, HttpServletResponse resp) 
			throws IOException, ServletException {
			
		resp.setContentType("text/html; charset=UTF-8");
		PrintWriter out = resp.getWriter();
		//Check that we have a file upload request
		boolean isMultipart = ServletFileUpload.isMultipartContent(req);
		//Create a factory for disk-based file items
		DiskFileItemFactory factory = new DiskFileItemFactory();
		
		//Configure a repository (to ensure a secure temp location is used)
		ServletContext servletContext = this.getServletConfig().getServletContext();
		File repository = (File) servletContext.getAttribute("javax.servlet.context.tempdir");
		factory.setRepository(repository);
		
		//Create a new file upload handler
		ServletFileUpload upload = new ServletFileUpload(factory);
		upload.setHeaderEncoding("UTF-8");//한글파일명 처리위해 추가
		try {
			//Parse the request
			List<FileItem> items = upload.parseRequest(req);
			//Process a file upload
			Iterator<FileItem> iter = items.iterator();
			while (iter.hasNext()) {
				FileItem item = iter.next();
				String fileName = null;
				if (!item.isFormField()) {
					String fieldName = item.getFieldName();
					out.println(fieldName);
					fileName = item.getName();
					out.println(fileName);
					String contentType = item.getContentType();
					out.println(contentType);
					boolean isInMemory = item.isInMemory();
					out.println(isInMemory);
					long sizeInBytes = item.getSize();
					out.println(sizeInBytes);
				}
				// Process a file upload
				ServletContext cxt = getServletContext();
				String dir = cxt.getRealPath("/upload");
				File uploadedFile = new File(dir + "/" + fileName);
				item.write(uploadedFile);
			}
		} catch (Exception e) {
			out.println("<ul>");
			e.printStackTrace(out);
			out.println("</ul>");
		}
		out.println("<a href=/"example/commons-fileupload.html\">파일 업로드 폼으로</a>");
	}
}

web.xml에 다음을 추가한다.

web.xml
<servlet>
    <servlet-name>commonsUpload</servlet-name>
    <servlet-class>example.CommonsUpload</servlet-class>
</servlet>

<servlet-mapping>
	<servlet-name>commonsUpload</servlet-name>
	<url-pattern>/CommonsUpload</url-pattern>
</servlet-mapping>

톰캣을 재가동하고 http://localhost:8080/example/commons-fileupload.html를 방문한다.
중복된 파일을 업로드한 후 upload 폴더에서 파일명을 확인한다.
중복된 파일을 업로드하면 cos.jar와는 달리 기존 파일을 덮어쓴다.
업로드된 파일을 보여주는 예제는 JSP에서 다룬다.

쿠키

HTTP 프로토콜은 상태를 유지할 수 없는 프로토콜이다.
쿠키는 세션을 유지하지 못하는 HTTP 프로토콜을 극복하기 위한 기술이다.
서버가 쿠키를 전송하면 웹 브라우저는 요청마다 쿠키값을 서버로 전달하여 사용자 정보를 유지할 수 있게 한다.

서버가 웹 브라우저로 쿠키 전송

쿠키가 작동하려면 서버에서 쿠키를 만들어 웹 브라우저로 전송해야 한다.
이때 쿠키의 모양은 다음과 같다.

Set-Cookie : name = value ; expires = date ; path = path ; domain = domain ; secure

쿠키는 웹 브라우저가 관리하는 파일에 저장된다.

웹 브라우저에서 서버로 쿠키 전송

쿠키가 웹 브라우저에 세팅되면, 웹 브라우저는 쿠키를 전달해준 서버로 자원을 요청할 때마다 아래와 같은 쿠키 문자열을 서버로 보내게 된다.
이때 쿠키의 모양은 다음과 같다.

Cookie ; name = value1 ; name2 = value2 ;

쿠키 이름과 값에 []()="/?@:; 같은 문자는 사용할 수 없다.

쿠키 클래스의 메소드

setValue(String value)
생성된 쿠키의 값을 재설정할 때 사용한다.
setDomain(String pattern)
쿠키는 기본적으로 쿠키를 생성한 서버에만 전송된다.
같은 도메인을 사용하는 서버에도 쿠키를 보내려면 이 메소드를 사용한다.
서버와 관련이 없는 도메인을 setDomain()으로 설정할 수 없다.
setMaxAge(int expiry)
쿠키의 유효기간을 초 단위로 설정한다.
음수를 입력하면 웹 브라우저 종료시 쿠키가 삭제된다.
setPath(String uri)
쿠키가 적용될 경로 정보를 설정한다.
경로가 설정되면 해당하는 경로로 방문하는 경우에만 웹 브라우저는 쿠키를 웹 서버에 전송한다.
setSecure(boolean flag)
flag가 true이면 보안 채널을 사용하는 서버로만 쿠키를 전송한다.

쿠키 클래스의 생성자를 호출하고 쿠키 클래스의 메소드를 적절히 사용해 쿠키를 만들었다면 쿠키를 웹 브라우저로 보내야 한다.
다음은 웹 브라우저로 쿠키를 전송하는 코드이다.

resp.addCookie(cookie);

서버 자원에서 웹 브라우저가 보낸 쿠키에 접근하는 방법

Cookie[] cookie = req.getCookies();

HttpServletRequest의 getCookies() 메소드를 사용해서 쿠키 배열을 얻는다.
(만약 쿠키가 없다면 getCookies() 메소드는 null을 반환한다)
다음의 메소드를 사용하면 쿠키에 대한 정보를 얻을 수 있다.
이중 getName()과 getValue()가 주로 쓰인다.

getName()
쿠키의 이름을 구한다.
getValue()
쿠키의 값을 구한다.
getDomain()
쿠키의 도메인을 구한다.
getMaxAge()
쿠키의 유효시간을 구한다.

다음은 서버 자원에서 쿠키값을 구하는 코드조각이다.

String id = null;
Cookie[] cookies = request.getCookies();

if (cookies != null) {

	for (int i = 0; i < cookies.length; i++) {
		String name = cookies[i].getName();
		
		if (name.equals("id")) {
			id = cookies[i].getValue();
			break;
		}
	}
}

아래는 쿠키를 삭제하는 예이다.
방법은 삭제하고자 하는 쿠키와 같은 이름의 쿠키를 생성하고 setMaxAge(0)을 호출한 다음 쿠키를 웹 브라우저로 보낸다.

Cookie cookie = new Cookie("id","");
cookie.setMaxAge(0);
resp.addCookie(cookie);

쿠키에 대한 실습은 JSP에서 다룬다.

세션

세션은 쿠키 기반 기술로 쿠키의 보안상 약점을 극복하기 위해 만들어졌다.
웹 브라우저는 서버가 정해준 세션 ID만을 쿠키값으로 저장한다.
웹 브라우저에 저장된 세션 ID 쿠키와 이와 매핑되는 서버의 세션 객체(HttpSession)로 사용자의 상태를 유지한다.
다음은 세션을 생성하는 코드이다.

HttpSession session = req.getSession(true); //세션이 없으면 생성
HttpSession session = req.getSession(false); //세션이 없다면 null반환

세션 객체가 생성되었으면 세션에 정보를 아래 코드처럼 저장할 수 있다.

User user = new User("홍길동","1234");
session.setAttribue("user", user);

세션에 대한 실습은 JSP에서 다룬다.

주석
  1. 그림(서블릿 기본골격 클래스 다이어그램)이 GenericServlet, HttpServlet 의 모든 속성과 메소드를 모두 나타내고 있지는 않다.
    서블릿을 쉽게 이해하려면, Servlet, ServletConfig, GenericServlet, HttpServlet의 상속 관계를 그릴 수 있어야 한다.
  2. MIME(Multipurpose Internet Mail Extensions)
    .html 또는 .htm의 MIME은 text/html, .txt는 text/plain .gif는 image/gif이다.
  3. 쿼리 스트링(Query tring)이란 URL 뒤 ? 다음에 나오는 문자열로, URL에 해당하는 서버 측 자원에 전달되는 데이터이다.
    쿼리 스트링의 정보가 1개 이상일 때는 두 번째부터 &를 사용한다. (예, http://localhost:8080/list.jsp?board=chat&page=1)
  4. Enumeration 인터페이스는 hasMoreElements()와 nextElement() 2개의 메소드를 이용하여 데이터를 순서대로 접근할 수 있다.
참고