2020-11-24 TIL
WAS

서블릿의 배포

- «interface»서블릿 <— «concrete»마이서블릿 @WebServlet(경로) 애노테이션으로 배포 정보 설정
- Web.xml <- DDfile 에 배치 정보를 설정해서 배포
서블릿의 메서드 호출
- 서블릿을 실행하고 종료할 때 호출되는 메서드
Servlet01()
Servlet01.init(ServletConfig)
Servlet01.service(ServletRequest,ServletResponse)
Servlet01.service(ServletRequest,ServletResponse)
Servlet01.service(ServletRequest,ServletResponse)
Servlet01.service(ServletRequest,ServletResponse)
Servlet01.service(ServletRequest,ServletResponse)
Servlet01.destroy()
서블릿 이름
- 서블릿 이름은 별명
- 실무에서는 클래스 이름과 동일하게 한다.
서블릿 만들기
<!-- 서블릿 등록 / 서블릿 이름은 별명 -->
<servlet>
<servlet-name>s01</servlet-name>
<servlet-class>com.eomcs.web.ex01.Servlet01</servlet-class>
</servlet>
<!-- 서블릿을 실행할 때 사용할 url path를 설정 -->
<servlet-mapping>
<servlet-name>s01</servlet-name>
<url-pattern>/ohora/haha</url-pattern>
</servlet-mapping>
- 주소가 다음과 같이 바뀐다
- http://localhost:8080/bitcamp-web-project/ohora/haha
서블릿 인터페이스 구현

- 서블릿 인터페이스를 상속받아서 직접 구현하면 init(), service(), destroy(), getServletInfo(), getServletConfig() 메서드를 전부 다 구현해야 한다.
- 이것을 보완하기 위해 미리 service를 빼고 나머지를 다 구현한 추상 메서드 GenericServlet이 서블릿 인터페이스를 상속받아서 만들어졌다.
- service() 는 추상 메서드로 남아있다.
- GenericServlet에 HTTP 프로토콜 처리 기능을 추가해서 만들어진 것이 HttpServlet이다
- 이 추상 클래스는 service()를 구현하고 추가로 service()를 오버로딩, doGet(), doPost() 등의 메서드를 가진다.
- 이 추상 클래스를 상속받아 MyServlet()를 만든다.
- 오버라이딩을 해서 doGet(), doPost(), doHead() 등의 메서드를 만든다.
- HttpServlet에서 호출되는 것은 service() 이다.
- 이 service()가 그 아래의 service(), doGet(), doPost() 등을 호출한다.
GenericServlet 추상 클래스 상송
// GenericServlet 추상 클래스
// => javax.servlet.Servlet 인터페이스를 구현하였다.
// => service() 메서드만 남겨두고 나머지 메서드들은 모두 구현하였다.
// => 따라서 이 클래스를 상속 받는 서브 클래스는 service() 만 구현하면 된다.
//
public class Servlet02 extends GenericServlet {
// GenericServlet 추상 클래스는 java.io.Serialize 인터페이스를 구현하였다.
// => serialVersionUID 변수 값을 설정해야 한다.
private static final long serialVersionUID = 1L;
public Servlet02() {
System.out.println("Servlet02()");
}
@Override
public void service(ServletRequest req, ServletResponse res)
throws ServletException, IOException {
System.out.println("Servlet02.service(ServletRequest,ServletResponse)");
}
}
Servlet과 java.io.Serializable

-
Load Balancer 가 여러 Servlet Container에 객체를 분배한다.
- 실행 중에 다른 서블릿에 객체를 이관해야 하는 상황이 온다면 객체를 복제해서 보낼 때 시리얼 넘버를 부여해서 복제한 다음에 보낸다.
- 그렇기 때문에 서블릿은 시리얼라이즈 인터페이스를 구현한다.
- 실행 중에 다른 서블릿에 객체를 이관해야 하는 상황이 온다면 객체를 복제해서 보낼 때 시리얼 넘버를 부여해서 복제한 다음에 보낸다.
-
또는 Active한 서블릿 컨테이너(예 : 사용중인 타이어)와 InActive한 서블릿 컨테이너(예 : 스페어 타이어)가 있을 때 (시스템 2중화)
-
Active한 서블릿 컨테이너에 문제가 생겼을 경우 Active한 서블릿 컨테이너에서 실행하고 있던 객체를 InActive한 서블릿 컨테이너로 옮겨야 한다.
-
실행 중인 값을 그대로 복제해야 하기 때문에 시리얼라이즈 해야한다.
-
-
서블릿은 Serial 버전관리를 해야한다.
- 그래서 각 서블릿은 serialVersionUID 스태틱 변수의 값을 설정해야 한다.
HttpServlet 추상 클래스 상속
- 요청 -> 서블릿 컨테이너 -service()->MyServlet 클래스에는 service가 없기 때문에 그 수퍼 클래스에서 service()를 호출
// HttpServlet 추상 클래스
// => javax.servlet.GenericServlet 추상 클래스를 상속 받았다.
// => service() 메서드도 구현했다.
// => service() 메서드는 웹브라우저가 요청한 명령에 따라
// doGet(), doPost(), doHead(), doPut() 등을 호출하게 프로그램 되어 있다.
// => HTTP 프로토콜을 다루려면 GenericServlet을 상속 받지 말고
// HttpServlet을 상속 받아 서블릿 클래스를 만들라!
//
public class Servlet03 extends HttpServlet {
// GenericServlet 추상 클래스가 java.io.Serialize 인터페이스를 구현하였고,
// HttpServlet 클래스가 GenericServlet 추상 클래스를 상속 받았으니
// HttpServlet 클래스를 상속 받는 이 클래스도 마찬가지로
// serialVersionUID 변수 값을 설정해야 한다.
private static final long serialVersionUID = 1L;
// service()를 오버라이딩 하는 대신에
// doGet(), doPost(), doHead() 등을 오버라이딩 하라.
// 호출과정:
// => 웹브라우저
// => 톰캣 서버
// => Servlet03.service(ServletRequest, ServletResponse)
// => Serlvet03.service(HttpServletRequest, HttpServletResponse)
// => Servlet03.doGet(HttpServletRequest, HttpServletResponse)
@Override
public void doGet(HttpServletRequest req, HttpServletResponse res)
throws ServletException, IOException {
System.out.println("Servlet03.doGet(HttpServletRequest,HttpServletResponse)");
}
}
- 수퍼 클래스인 HttpServlet에서 service가 호출된다.
public abstract class HttpServlet extends GenericServlet
.
.
.
@Override
public void service(ServletRequest req, ServletResponse res)
throws ServletException, IOException
{
HttpServletRequest request;
HttpServletResponse response;
if (!(req instanceof HttpServletRequest &&
res instanceof HttpServletResponse)) {
throw new ServletException("non-HTTP request or response");
}
request = (HttpServletRequest) req;
response = (HttpServletResponse) res;
service(request, response);
}
}
- HttpServlet에서 service(ServletRequest, ServletResponse) 를 오버로딩 해서 내부적으로 service(HttpServletRequest, HttpServletResponse)를 호출한다.
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) {
// 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);
}
}
- get을 요청받았다면 service(HttpServletRequest, HttpServletResponse)에서 자식 클래스의 doGet()을 호출한다.
- post를 요청받으면 doPost(), head를 요청받으면 doHead()를 호출한다.

- httpServlet을 상속받아야지 http 기능을 사용할 수 있다.
@애노테이션
// @WebServlet 애노테이션
// => web.xml 에 서블릿 정보를 설정하는 대신에
// 이 애노테이션을 사용하여 서블릿을 설정할 수 있다.
// => 이 애노테이션을 활성화시키려면
// web.xml의 <web-app> 태그에 다음 속성을 추가해야 한다.
// metadata-complete="false"
// 속성의 값은 false 여야 한다.
//
@WebServlet("/ex01/s04")
public class Servlet04 extends GenericServlet {
private static final long serialVersionUID = 1L;
@Override
public void service(ServletRequest req, ServletResponse res)
throws ServletException, IOException {
System.out.println("Servlet04.service(ServletRequest,ServletResponse)");
}
}
metadata-complete=”true”
- web.xml의 메타데이터가 true일 경우 @WebServlet을 인식하지 못한다.\
- 배포가 안될 경우 아예 빼버리거나 false로 변경한다.
리스너 (옵저버 패턴)
- 보고를 받는 컨테이너
// 서블릿 컨테이너가 관리하는 컴포넌트
// => 서블릿, 필터, 리스너
//
// 리스너 만들기
// => 서블릿 컨테이너 또는 서블릿, 세션 등의 객체 상태가 변경되었을 때 보고 받는 옵저버
// => "Observer" 디자인 패턴이 적용된 것이다.
// => ServletContextListener
// - 서블릿 컨테이너를 시작하거나 종료할 때 보고 받고 싶다면 이 인터페이스를 구현하라.
// => ServletRequestListener
// - 요청이 들어오거나 종료될 때 보고 받고 싶다면 이 인터페이스를 구현하라.
// => HttpSessionListener
// - 세션이 생성되거나 종료될 때 보고 받고 싶다면 이 인터페이스를 구현하라.
// => XxxListener
// - 기타 다양한 인터페이스가 있다. 문서를 참고하라.
//
// 리스너 배포하기
// => DD 파일(web.xml)에 설정하거나 @WebListenr 애노테이션으로 설정하면 된다.
//
// ------------------------------------------------------------
// web.xml
// <!-- 리스너 등록 -->
// <listener>
// <listener-class>com.eomcs.web.ex02.Listener01</listener-class>
// </listener>
// ------------------------------------------------------------
//
// 리스너의 용도
// => 서블릿 컨테이너나, 세션 등이 특별한 상태일 때 필요한 작업을 수행한다.
// => ServletContextListener
// - 웹 애플리케이션을 시작할 때 Spring IoC 컨테이너 준비하기
// - 웹 애플리케이션을 시작할 때 DB 커넥션 풀 준비하기
// - 웹 애플리케이션을 종료할 때 DB 커넥션 풀에 들어 있는 모든 연결을 해제하기
// => ServletRequestListener
// - 요청이 들어 올 때 로그 남기기
//
//
@WebListener // 이걸 붙이면 DD파일에 설정하지 않아도 된다.
public class Listener01 implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent sce) {
// 웹 애플리케이션이 시작될 때 호출된다.
System.out.println("Listener01.contextInitialized()");
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
// 웹 애플리케이션이 종료될 때 호출된다.
System.out.println("Listener01.contextDestroyed()");
}
}

//@WebListener
public class Listener02 implements ServletRequestListener {
@Override
public void requestInitialized(ServletRequestEvent sre) {
// 요청이 들어 왔을 때 호출된다.
System.out.println("Listener02.requestInitialized()");
// 서블릿 컨테이너가 넘기는 것은 httpServletRequest와
// httpServletResponse이다.
// 왜 타입캐스팅을 하느냐?
// 원래는 Http 전용이 아니라 범용으로 설계된 것이기 때문이다.
HttpServletRequest request = (HttpServletRequest) sre.getServletRequest();
System.out.println("클라이언트 IP: " + request.getRemoteAddr());
System.out.println("요청 URL: " + request.getServletPath());
}
@Override
public void requestDestroyed(ServletRequestEvent sre) {
// 요청 처리를 완료할 때 호출된다.
System.out.println("Listener02.requestDestroyed()");
}
}
- 리스너는 무조건 실행한다.
- 필터는 조건에 따라 실행이 달라진다.
필터 연결
// 서블릿 컨테이너가 관리하는 컴포넌트
// => 서블릿, 필터, 리스너
//
// 필터 만들기
// => javax.servlet.Filter 인터페이스 규칙에 따라 작성한다.
//
// 필터 배포하기
// => DD 파일(web.xml)에 설정하거나 애노테이션으로 설정하면 된다.
//
// 필터의 용도
// => 서블릿을 실행하기 전후에 필요한 작업을 수행
// => 서블릿 실행 전
// - 웹브라우저가 보낸 암호화된 파라미터 값을 서블릿으로 전달하기 전에 암호 해제하기
// - 웹브라우저가 보낸 압축된 데이터를 서블릿으로 전달하기 전에 압축 해제하기
// - 서블릿의 실행을 요청할 권한이 있는지 검사하기
// - 로그인 사용자인지 검사하기
// - 로그 남기기
// => 서블릿 실행 후
// - 클라이언트로 보낼 데이터를 압축하기
// - 클라이언트로 보낼 데이터를 암호화시키기
//
public class Filter01 implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
// 필터 객체를 생성한 후 제일 처음으로 호출된다.
// 필터가 사용할 자원을 이 메서드에서 준비한다.
// => 웹 애플리케이션을 시작할 때 필터는 자동 생성된다.
System.out.println("Filter01.init()");
}
@Override
public void destroy() {
// 웹 애플리케이션이 종료될 때 호출된다.
// init()에서 준비한 자원을 해제한다.
System.out.println("Filter01.destroy()");
}
@Override
public void doFilter(ServletRequest request,
ServletResponse response,
FilterChain chain) // 다음 필터
throws IOException, ServletException {
// 요청이 들어 올 때 마다 호출된다.
// => 단 필터를 설정할 때 지정된 URL의 요청에만 호출된다.
// => 서블릿이 실행되기 전에 필터가 먼저 실행된다.
// => 서블릿을 실행한 후 다시 필터로 리턴한다.
System.out.println("Filter01.doFilter() : 시작");
// 다음 필터를 실행한다.
// 만약 다음 필터가 없으면,
// 요청한 서블릿의 service() 메서드를 호출한다.
// service() 메서드 호출이 끝나면 리턴된다.
chain.doFilter(request, response);
// 체인에 연결된 필터나 서블릿이 모두 실행된 다음에
// 다시 이 필터로 리턴될 것이다.
System.out.println("Filter01.doFilter() : 종료");
}
}
- 필터를 적용할 url을 지정할 수 있다.
<!-- 필터 등록 -->
<filter>
<filter-name>Filter01</filter-name>
<filter-class>com.eomcs.web.ex02.Filter01</filter-class>
</filter>
<!-- 필터를 적용할 URL을 지정 -->
<filter-mapping>
<filter-name>Filter01</filter-name>
<url-pattern>/ex02/s1</url-pattern>
</filter-mapping>
- 필터 객체는 미리 준비된다.
- 서블릿은 요청이 들어와야 준비된다.

- 필터를 적용한 후 실행을 하면 아래 순서대로 실행된다.
- 리스너가 먼저 실행된 후 필터가 실행된다.
Listener02.requestInitialized()
클라이언트 IP: 0:0:0:0:0:0:0:1
요청 URL: /ex02/s1
Filter01.doFilter() : 시작
/ex02/s1 서블릿 실행!
Filter01.doFilter() : 종료
Listener02.requestDestroyed()
- ex02/s1 실행 시
Listener02.requestInitialized()
클라이언트 IP: 0:0:0:0:0:0:0:1
요청 URL: /ex02/s1
Filter01.doFilter() : 시작
Filter02.doFilter() : 시작
/ex02/s1 서블릿 실행!
Filter02.doFilter() : 종료
Filter01.doFilter() : 종료
Listener02.requestDestroyed()
출력
- 클라이언트 쪽에서 출력했을 때 인코딩 문제

@WebServlet("/ex03/s1") // http://localhost:9999/bitcamp-java-web/ex03/s1
public class Servlet01 extends GenericServlet {
private static final long serialVersionUID = 1L;
@Override
public void service(ServletRequest req, ServletResponse res)
throws ServletException, IOException {
// 출력 스트림을 꺼내기 전에
// 출력할 때 사용할 문자표(charset)를 지정하지 않으면
// 리턴 받은 출력 스트림은 기본 문자표 ISO-8859-1을 사용한다.
// 즉, 자바의 유니코드 문자를 ISO-8859-1 문자표에 따라
// 변환하여 출력한다.
// 자바(Unicode2;UTF-16) ===> 출력문자(ISO-8859-1)(ASCII)
PrintWriter out = res.getWriter();
// 다음 영어 유니코드 문자는 ISO-8850=9-1에 있기 때문에 제대로 변환된다.
out.println("Hello!");
// 그러나 다음 유니코드 문자들은 ISO-8859-1 문자표에 없기 때문에
// 없다는 의미에서 '?' 문자로 바뀌어 출력된다.
out.println("안녕하세요!");
out.println("こんにちは");
out.println("您好");
out.println("مع السلامة؛ إلى اللقاء!");
}
}
- 화면에서 다음과 같이 깨져서 나온다.
Hello!
?????!
?????
??
?? ???????? ??? ??????!
- 해결책은 다음과 같다
- 출력 스트림을 꺼내기 전에 출력할 때 사용할 문자표를 지정하면 된다.
@WebServlet("/ex03/s2")
public class Servlet02 extends GenericServlet {
private static final long serialVersionUID = 1L;
@Override
public void service(ServletRequest req, ServletResponse res)
throws ServletException, IOException {
// 한글 깨짐 처리하기
// => 출력 스트림을 꺼내기 전에 출력할 때 사용할 문자표(charset)를 지정하라.
// => 반드시 출력 스트림을 얻기 전에 설정해야 한다.
// res.setContentType("MIME Type;charset=문자표이름");
//
res.setContentType("text/plain;charset=UTF-8"); // UCS2(UTF-16) ==> UTF-8
PrintWriter out = res.getWriter();
out.println("Hello!");
// 한글, 일본문자, 한자, 아랍문자는
// utf-8 문자표에 문자표에 정의되어 있기 때문에
// utf-8 문자로 변환할 수 있다.
out.println("안녕하세요!");
out.println("こんにちは");
out.println("您好");
out.println("مع السلامة؛ إلى اللقاء!");
// MIME Type : Multi-purpose Internet Mail Extension
// => 콘텐트의 형식을 표현
// => 콘텐트타입/상세타입
// => 예) text/plain, text/css, text/html 등
// => 웹 브라우저는 콘텐트를 출력할 때 서버가 알려준 MIME 타입을 보고
// 어떤 방식으로 출력할 지 결정한다.
}
}
- 헤더
- 부가적인 정보를 알려주는 메타 데이터
- 바디
- 본문
@WebServlet("/ex03/s3")
public class Servlet03 extends GenericServlet {
private static final long serialVersionUID = 1L;
@Override
public void service(ServletRequest req, ServletResponse res)
throws ServletException, IOException {
// HTML 출력할 때 MIME 타입에 HTML을 지정하지 않으면
// 웹 브라우저는 일반 텍스트로 간주하여 출력한다.
res.setContentType("text/html;charset=UTF-8"); // UTF-16 ==> UTF-8
PrintWriter out = res.getWriter();
out.println("<!DOCTYPE html>");
out.println("<html>");
out.println("<head><title>servlet03</title></head>");
out.println("<body><h1>안녕하세요</h1></body>");
out.println("</html>");
}
}
html을 적용해서 테이블 만들기
- 출력되는 내용이 테이블로 나오도록 한다.
@WebServlet("/board/list")
public class BoardListCommand extends HttpServlet {
private static final long serialVersionUID = 1L;
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
ServletContext ctx = request.getServletContext();
BoardService boardService =
(BoardService) ctx.getAttribute("boardService");
response.setContentType("text/html;charset=UTF-8");
PrintWriter out = response.getWriter();
out.println("<!DOCTYPE html>");
out.println("<html>");
out.println("<head><title>게시글 목록</title></head>");
out.println("<body>");
try {
out.println("<h1>게시물 목록</h1>");
out.println("<tr>");
out.println("[게시물 목록]");
out.println("</tr>");
List<Board> list = boardService.list();
out.println("<table border='1'>");
out.println("<tr>"
+ "<th>번호</th>"
+ "<th>제목</th>"
+ "<th>작성자</th>"
+ "<th>등록일</th>"
+ "<th>조회수</th>"
+ "</tr>");
for (Board board : list) {
out.printf("<tr>"
+ "<td>%d</td>"
+ "<td>%s</td>"
+ "<td>%s</td>"
+ "<td>%s</td>"
+ "<td>%d\n</td>"
+ "</tr>\n",
board.getNo(),
board.getTitle(),
board.getWriter().getName(),
board.getRegisteredDate(),
board.getViewCount());
}
out.println("</table>");
} catch (Exception e) {
out.printf("<p>게시글 목록 조회 중 오류 발생 -%s</p>\n", e.getMessage());
StringWriter errOut = new StringWriter();
e.printStackTrace(new PrintWriter(errOut));
out.printf("<pre?%s</pre>\n", errOut.toString());
}
out.println("</body>");
out.println("</html>");
}
}
