2020-11-24 TIL

9 minute read

WAS

image-20201124095314363.png

서블릿의 배포

image-20201124115444883.png

  • «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

서블릿 인터페이스 구현

image-20201124115505647.png

  • 서블릿 인터페이스를 상속받아서 직접 구현하면 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

image-20201124115525338.png

  • 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()를 호출한다.

image-20201124121336576

  • 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()");
  }
}

image-20201124184013893.png

//@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>
  • 필터 객체는 미리 준비된다.
    • 서블릿은 요청이 들어와야 준비된다.

image-20201124184202782.png

  • 필터를 적용한 후 실행을 하면 아래 순서대로 실행된다.
    • 리스너가 먼저 실행된 후 필터가 실행된다.
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()

출력

  • 클라이언트 쪽에서 출력했을 때 인코딩 문제

image-20201124184235701.png

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

image-20201124184253595.png

Categories:

Updated: