Posts 학원 #86일차:서블릿 프로그래밍, 프로젝트를 WAS 아키텍처로 전환
Post
Cancel

학원 #86일차:서블릿 프로그래밍, 프로젝트를 WAS 아키텍처로 전환

서블릿 컨테이너관리하는 컴포넌트서블릿, 필터, 리스너로 3가지이다.

Servlet 만들기

image

1. javax.servlet.Servlet 인터페이스 구현

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
//@WebServlet("/ex01/first")
public class Servlet01 implements Servlet {

  ServletConfig config;

  public Servlet01() {
    System.out.println("Servlet01()");
  }

  @Override
  public void init(ServletConfig config) throws ServletException {
    this.config = config;
    System.out.println("Servlet01.init(ServletConfig)");
  }

  @Override
  public void service(ServletRequest req, ServletResponse res)
      throws ServletException, IOException { System.out.println("Servlet01.service(ServletRequest,ServletResponse)");
  }

  @Override
  public void destroy() {
    System.out.println("Servlet01.destroy()");
  }

  @Override
  public ServletConfig getServletConfig() {
    System.out.println("Servlet01.getServletConfig()");
    return this.config;
  }

  @Override
  public String getServletInfo() {
    System.out.println("Servlet01.getServletInfo()");
    return "Servlet01";
  }

}

2. javax.servlet.GenericServlet 추상클래스 상속

만약 Servlet 인터페이스를 구현한다면 Servlet의 모든 메서드를 구현해야 할 것이다. 그러나 5개의 메서드를 각 Servlet마다 구현하는 것은 번거로운 작업이다. 이를 위해서 자바는 GenericServlet 추상클래스를 제공한다. GenericServlet의 특징은 다음과 같다.

  • java.servlet.Servlet 인터페이스를 구현하였다.
  • service() 메서드만 남겨두고 나머지 메서드들은 모두 구현하였다.
  • 따라서 이 클래스를 상속받는 서브 클래스는 service() 만 구현하면 된다.

GenericServlet

1
2
3
public abstract class GenericServlet 
    implements Servlet, ServletConfig, java.io.Serializable
{

주목해야 할 점은 GenericServletjava.io.Serializable을 구현했다는 것이다. 따라서 serialVersionUID 변수 값을 설정해야 한다.

image

시스템이 멈춰야 하는 경우, 서블릿의 인스턴스 필드 값이 날아가는 것을 막기 위해서 Serializable 인터페이스를 구현하였다.

보통 서블릿에는 인스턴스 필드가 없지만 비유를 하자면 PMS 프로젝트에서의 Service 객체가 해당된다.

객체를 바이트 배열로 만들어서 상대편에게 보낼 때 버전이 일치하는 지를 확인하기 위해서 시리얼라이즈 넘버가 필요하다. 서비스 중에 다른 시스템에 이관해야 하는 상황일 때 이 방법을 사용한다. 보통 서비스는 이러한 상황을 대비해 시스템을 이중화시킨다. 실제 실행하는 active 시스템과 예비용인 inactive 시스템을 똑같은 스펙으로 만들 수도 있고, 여유가 없다면 inactive 시스템은 성능이 좀 떨어져도 괜찮다. 어쨌든 inactive 시스템이 없는 것보다는 성능이 떨어지더라도 있는 것이 낫다. 시스템에 문제가 생겨서 멈출 때 inactive 시스템으로 이관할 수 있기 때문이다. 기존에 사용자들이 일을 시키고 있던 것을 inactive 시스템으로 이관한다. 갑자기 다운되면 사용자가 active로 가다가 inactive 로 가게 된다. 그럴 때 기존에 하던 작업은 그대로 가져와야 한다. 실행 중인 값을 그대로 다른 서버로 객체를 보낸다. 다른 서버로 객체를 보낼 때 직렬화를 하고, 바이트배열로 전송한 후 받는 쪽에서는 역직렬화하여 사용한다. 이를 대비해 GenericServlet은 serializable 인터페이스를 구현하였다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Servlet02 extends GenericServlet {

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

GenericServlet은 구현은 하였으나 다음과 같이 구현 부분이 비어있다. 이 필요하다면 이를 오버라이딩하면 된다.

1
2
3
4
5
public void destroy() {
}
public String getServletInfo() {
  return "";
}

3. javax.servlet.http.HttpServlet 추상 클래스 상속

image

  • javax.servlet.GenericServlet 추상 클래스를 상속 받았다.

    1
    2
    
    public abstract class HttpServlet
    extends GenericServlet
    
  • service() 메서드도 구현했다.

  • service() 메서드는 웹 브라우저가 요청한 명령에 따라 doGet(), doPost(), doHead(), doPut() 등을 호출하게 프로그램되어 있다.

  • 특별히 HTTP 프로토콜 처리 기능을 원할 때는 HttpServlet을 상속받아 서블릿 클래스를 만든다.

  • GenericServlet 추상 클래스가 java.io.Serialize 인터페이스를 구현하였고, HttpServlet 클래스가 GenericServlet 추상 클래스를 상속받았으니 HttpServlet 클래스를 상속 받는 이 클래스도 마찬가지로 serialVersionUID 변수 값을 설정해야 한다.

  • service()를 오버라이딩하는 대신에 doGet(), doPost(), doHead() 등을 오버라이딩한다.

호출 과정

  • 웹 브라우저
    • 톰캣 서버
      • Servlet03.service(ServletRequest, ServletResponse)
        • Servlet03.service(HttpServletRequest, HttpServlet Response)
          • Servlet03.doGet(HttpServletReqeust, HttpServletResponse)

javax는 java extension(extension)의 약자이다.

1
2
3
4
5
6
7
8
9
public class Servlet03 extends HttpServlet {
  private static final long serialVersionUID = 1L;

  @Override
  public void doGet(HttpServletRequest req, HttpServletResponse res)
      throws ServletException, IOException {
    System.out.println("Servlet03.doGet(HttpServletRequest,HttpServletResponse)");
  }
}

다음은 HttpServletservice() 메서드이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public void service(ServletRequest req, ServletResponse res)
  throws ServletException, IOException
{
  HttpServletRequest  request; // ServletRequest의 자식 인터페이스
  HttpServletResponse response; // ServletResponse의 자식 인터페이스

  // HttpServletRequest의 객체가 아니거나 HttpServletServletResponse가 아니라면 예외를 던진다.
  if (!(req instanceof HttpServletRequest &&
        res instanceof HttpServletResponse)) {
    throw new ServletException("non-HTTP request or response");
  }

  // 맞다면 인스턴스 필드에 할당한다.
  // 이때 형변환을 해야 한다. 
  request = (HttpServletRequest) req;
  response = (HttpServletResponse) res;

  // 
  service(request, response);
}

실제로는 service(ServletRequest, ServletResponse)의 파라미터에는 구현체가 넘어온다. 실제로는 HttpServletRequest와 HttpServletResponse를 받는다. 따라서 형변환을 하고 써야 한다.

1
2
3
4
// 서블릿 컨테이너가 넘기는 빵은 실제로 소보루 빵이다.
먹어라 () {
  소보루빵 obj = (소보루빵) ;
}

HttpServlet 클래스는 service(ServletRequest, ServletResponse) 외에도 파라미터 이름이 다른 메서드를 추가하였다(오버로딩). 이 메서드는 규칙에 맞지 않기 때문에 service(HttpServletRequest, HttpServletResponse)를 추가하였다. service(ServletRequest, ServletResponse)는 이 메서드를 내부적으로 호출한다. 그리고 이 메서드는 클라이언트 요청 메서드에 따라 각기 다른 메서드를 호출한다.

HTTP 요청 메서드(명령어)

  • GET
  • HEAD: 콘텐츠의 부가 정보만 줘라
  • POST
  • PUT
  • DELETE
  • CONNECT
  • OPTIONS

다음은 HttpServlet 추상 클래스의 service(HttpServletRequest req, HttpServletResponse resp)의 소스코드이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
protected void service(HttpServletRequest req, HttpServletResponse resp)
  throws ServletException, IOException
{
  // HTTP: 메서드
  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);
  }
}

다음은 doGet() 메서드의 소스코드이다.

1
2
3
4
5
6
7
8
9
10
11
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
  throws ServletException, IOException
{
  String protocol = req.getProtocol();
  String msg = lStrings.getString("http.method_get_not_supported");
  if (protocol.endsWith("1.1")) {
    resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED, msg);
  } else {
    resp.sendError(HttpServletResponse.SC_BAD_REQUEST, msg);
  }
}

만약 클라이언트로부터 GET요청이 들어온다면, HttpServlet을 상속받은 자식 객체에서 오버라이딩한 doGet()을 최종적으로 호출한다. HTTP기능을 사용하기 위해서는 HttpServlet을 상속받는 것이 좋다.

리스너 만들기

리스너는 서블릿 컨테이너 또는 서블릿, 세션 등의 객체 상태가 변경되었을 때 보고 받는 옵저버이다. Observer 패턴이 적용된 것이다.

  • ServletContextListener: 서블릿 컨테이너를 시작하거나 종료할 때 보고받고 싶다면 이 인터페이스를 구현한다.
  • ServletRequestListener: 요청이 들어오거나 종료할 때 보고받고 싶다면 이 인터페이스를 구현한다.
  • HttpSessionListener: 세션이 생성되거나 종료될 때 보고받고 싶다면 이 인터페이스를 구현한다.
  • XxxListener: 기타 다양한 인터페이스가 있다.

리스너 배포하기

DD 파일(web.xml)에 설정하거나 @WebListener 애노테이션으로 설정하면 된다. 애노테이션으로 설정할 때 Servlet과 달리 이름을 붙이지 않아도 된다. 리스너는 경로로 접근하여 실행할 필요 없이 특정 시점일 때 자동으로 실행되기 때문이다.

리스너의 용도

리스너는 서블릿 컨테이너세션 등이 특별한 상태일 때 필요한 작업을 수행한다.

ServletContextListener

  • 웹 애플리케이션를 시작할 때 Spring IoC 컨테이너를 준비한다.
  • 웹 애플리케이션을 시작할 때 DB 커넥션 풀을 준비한다.
  • 웹 애플리케이션을 종료할 때 DB 커넥션 풀에 있는 모든 연결을 해제한다.

DB 커넥션 풀? 시스템에 접근하는 사용자가 많아 서버에 부하가 생기는 경우, 시스템의 상황에 따라 성능 이슈가 발생할 확률이 높고 구축한 시스템에 대해 최적화를 해야 하는 상황이 발생하게 되는데 보통 성능 최적화를 위해 테스트하고 확인하는 것 중 하나가 DB connection pool과 Thread 개수를 조절하는 것이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@WebListener
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()");
  }
  
}

ServletRequestListener

요청이 들어올 때 로그 남기기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@WebListener
public class Listener02 implements ServletRequestListener {

  @Override
  public void requestInitialized(ServletRequestEvent sre) {
    // 요청이 들어 왔을 때 호출된다.
    System.out.println("Listener02.requestInitialized()");
    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()");
  }
}
1
2
3
4
5
Listener02.requestInitialized()
클라이언트 IP: 0:0:0:0:0:0:0:1
요청 URL: /ex01/s03
Servlet03.doGet(HttpServletRequest,HttpServletResponse)
Listener02.requestDestroyed()

왜 요청이 들어올 때 로그를 남겨야 할까? 기록을 남겨야지 나중에 누가 해킹했는지 알 수 있기 때문이다.

리스너는 무조건 특정 객체 상태가 변경될 때 보고를 받는다. 필터에서 로그를 찍어도 된다. 필터와 리스너의 차이점은 필터는 조건을 걸 수 있다는 것이다. 모든 요청에 대해서 로그를 남겨야 할 때는 리스너에서 해당 기능을 구현하고, 특정 요청에 특정 로그를 남겨야 할 때는 필터를 구현한다.

getServletRequest() 메서드는 ServletRequest 객체를 리턴하도록 되어 있다. 그러나 실제 객체는 ServletRequest의 서브 클래스인 HttpServletRequest 객체이다. getServletPath()HttpServletRequest 객체의 메서드이기 때문에 getServletRequest()의 리턴값을 그대로 사용하면 메서드를 쓸 수 없고, 실제 가리키는 객체 타입으로 형변환을 한 후 메서드를 호출해야 한다.

그렇다면 왜 굳이 service(ServletRequest, ServletResponse) 메서드는 ServletRequestServletResponse를 파라미터로 받도록 정의되어 있을까? 애초에 인터페이스를 설계할 때 컨테이너가 특정 프로토콜에 구애받지 않고 service()를 호출하기 위해서 service(ServletRequest, ServletResponse)로 정의하였다. 그러나 실제로는 HttpServletRequestHttpServletResponse만 넘어온다. 서블릿 컨테이너 앞쪽에 통신하는 프로토콜은 99.9% HTTP 프로토콜을 사용하기 때문이다. 즉 어떤 프로토콜이든 마음대로 응용해서 사용할 수 있도록 범용환경에서 사용하게 기획하고 설계하였는데 실제로는 사람들이 HTTP 프로토콜로 소통할 때만 사용한다. 그렇지 않은 경우는 거의 없다.

필터 만들기

리스너와 필터의 차이점: 필터는 더 정교하게 조건을 걸어 꼽을 수 있다.

필터의 용도

  • 서블릿을 실행하기 전후에 꼭 필요한 작업을 수행
  • 서블릿 실행 전
    • 웹 브라우저가 보낸 암호화된 파라미터 값서블릿으로 전달하기
    • 웹 브라우저가 보낸 압축된 데이터서블릿으로 전달하기 전압축 해제하기
    • 서블릿의 실행을 요청할 권한이 있는지 검사하기
    • 로그인 사용자인지 검사하기
    • 로그 남기기
  • 서블릿 실행 후
    • 클라이언트로 보낼 데이터압축하기
    • 클라이언트로 보낼 데이터를 암호화하기
  • init(FilterConfig filterConfig) throws ServletException

    • 필터 객체를 생성한 후 제일 처음으로 호출된다.
    • 필터가 사용할 자원을 이 메서드에서 준비한다.
    • 웹 애플리케이션을 시작할 때 필터는 자동 생성된다.
  • destroy()

    • 웹 애플리케이션이 종료될 때 호출된다.
    • init()에서 준비한 자원을 해제한다.
  • doFilter(SerlvetRequest, ServletResponse, FilterChain) throws ServletException

    • 요청이 들어올 때마다 호출된다.

    • 단 필터를 실행할 때 지정된 URL의 요청에만 호출된다.

    • 서블릿이 실행되기 전에 필터가 먼저 실행된다.

    • 서블릿을 실행한 후 다시 필터로 리턴한다.

    • chain.doFilter(request, response);

      다음 필터를 실행한다. 만약 다음 필터가 없으면 요청한 서블릿의 service() 메서드를 호출한다. service() 메서드 호출이 끝나면 리턴된다

필터 만들기

  • javax.servlet.Filter 인터페이스 규칙에 따라 작성한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class Filter01 implements Filter {

  @Override
  public void init(FilterConfig filterConfig) throws ServletException {
    // 필터 객체를 생성한 후 가장 먼저 호출된다.
    // 필터가 사용할 자원 준비
    System.out.println("Filter01.init()");
  }

  @Override
  public void destroy() {
    // 웹앱 종료 시 호출된다. 자원 해제
    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() : 종료");
  }

image

이전 PMS 프로젝트에서 필터를 구현하였을 때, 무조건 요청에 따라 무조건 필터를 실행하도록 만들었으나, Servlet API를 이용하면 필터는 요청에 따라 실행 여부를 결정할 수 있다.

필터 배포하기

DD 파일(web.xml)에 설정하거나 애노테이션으로 설정하면 된다.

DD 파일에 설정

1
2
3
4
5
6
7
8
9
10
<!-- 필터 등록 -->
  <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>

위의 코드에서 필터는 클라이언트가 /ex02/s1 요청을 할 때에만 이 필터를 실행한다.

web.xml 변경 시 서버를 재시작해야 한다.

필터는 클라이언트가 요청하기 전에 필터 객체를 미리 준비한다. 이는 서블릿과 구분되는 차이점이다. 딱 한번만 호출된다.

1
2
Listener01.contextInitialized()
Filter01.init()

필터를 적용하지 않은 URL에 접근했을 때 실행 결과

1
2
3
4
5
Listener02.requestInitialized()
클라이언트 IP: 0:0:0:0:0:0:0:1
요청 URL: /ex02/a/s2
/ex02/a/s2 서블릿 실행!
Listener02.requestDestroyed()

필터를 적용한 URL에 접근했을 때 실행 결과

1
2
3
4
5
6
7
Listener02.requestInitialized()
클라이언트 IP: 0:0:0:0:0:0:0:1
요청 URL: /ex02/s1
Filter01.doFilter() : 시작
/ex02/s1 서블릿 실행!
Filter01.doFilter() : 종료
Listener02.requestDestroyed()

필터 실행 순서

  • 필터의 실행 순서를 임의로 조정할 수 없다.
  • 필터를 정의할 때 필터 순서에 의존하는 방식으로 프로그래밍하지 말자.
  • 필터의 실행 순서에 상관없이 각 필터가 독립적으로 동작하도록 작성하자.

필터의 에노테이션에 와일드카드를 지정할 수 있다.

1
2
@WebFilter("/ex02/*")
public class Filter02 implements Filter {
1
2
3
4
5
6
7
8
9
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

출력 스트림을 꺼내기 전출력할 때 사용할 문자표(charset)를 지정하지 않으면 리턴받은 출력 스트림은 기본 문자표 ISO-8859-1을 사용한다. 즉 자바의 유니코드 문자를 ISO-8859-1문자표에 따라 변환하여 출력한다.영어 유니코드 문자는 ISO-8859-1 문자표에 있기 때문에 제대로 변환된다. 그 아래 유니코드 문자는 ISO-8859-1 문자표에 없기 때문에 없다는 의미에서 ‘?’ 문자로 바뀌어 출력된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@WebServlet("/ex03/s1")
public class Servlet01 extends GenericServlet {

  private static final long serialVersionUID = 1L;

  @Override
  public void service(ServletRequest req, ServletResponse res)
      throws ServletException, IOException {

    PrintWriter out = res.getWriter();
    out.println("Hello!");
    out.println("안녕하세요!");
    out.println("こんにちは");
    out.println("您好");
    out.println("مع السلامة؛ إلى اللقاء!");
  }

}

image

res.setContentType("MIME Type;charset=문자표이름");

따라서 출력 스트림을 꺼내기 전에 출력 스트림이 사용할 문자표(charset)를 지정해야 한다. 한글이나 아랍문자, 중국문자, 일본문자는 UTF-8 문자표에 정의되어 있기 때문에 UTF-8 문자로 변환할 수 있다. MS949로도 할 수 있지만 국제 표준이 아니기 때문에 사용하지 말자.

1
2
res.setContentType("text/plain;charset=UTF-8"); // UCS2(UTF-16) ==> UTF-8
PrintWriter out = res.getWriter();

image

MIME Type: Multi-purpose Internet Mail Extension

  • 콘텐트의 형식을 표현
  • 콘텐트타입/상세타입
  • 예: text/plain, text/css, text/html

웹 브라우저는 콘텐츠를 출력할 때 서버가 알려준 MIME 타입을 보고 어떤 방식으로 출력할 지 결정한다.

윈도우 운영체제에서는 파일 확장자를 통해 해당 파일이 어떤 파일인지 알수 있다. 확장자와 마찬가지로 MIME type은 파일 안에 들어 있는 콘텐트가 어떤 콘텐트인지 표현하는 문법이다. 초창기에 메일을 주고받을 때 보내는 파일이 어떤 파일인 지 알려주기 위해서 사용하기 시작했다. 그러나 오늘날에는 파일을 주고받는 모든 곳에 사용한다(확장되었다). 인터넷 상에서 주고받는 콘텐트가 무엇인지를 지정한다.

앞에서는 MIME타입을 text/plain으로 지정했는데 MIME타입을 html로 바꾸어보자.

1
res.setContentType("text/plain;charset=UTF-8")

서버가 콘텐츠 타입을 html로 설정한다면 브라우저도 어떤 방식으로 출력할 지 결정한다. 이렇게 보내는 데이터에 앞서서 부가적인 데이터를 보내주는 것을 헤더라고 한다.

image

소스는 바뀌지 않았지만 출력 형식이 달라졌다. 줄바꿈 기호가 들어가지 않은 이유는 HTML에서는 특별히 태그를 지정하지 않는 이상 줄바꿈기호를 출력 때 반영하지 않기 때문이다.

HTML 출력하기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@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/pain;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>");
  }
}

image

MIME 타입을 html로 바꿔 HTML규칙에 맞춰서 렌더링하라고 요청하자. 결과는 다음과 같다.

image

바이너리 데이터 출력하기

각각의 객체를 사람으로 생각해라. 담당자에게 물어본다. /photo.jpeg에서 /(루트)는 애플리케이션 루트를 의미한다. .metadata.org~. tmp1.wtpwebapps/...

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
@WebServlet("/ex03/s4")
public class Servlet04 extends GenericServlet {

  private static final long serialVersionUID = 1L;

  @Override
  public void service(ServletRequest req, ServletResponse res)
      throws ServletException, IOException {

    // /WEB-INF/photo.jpeg 파일의 실제 경로 알아내기
    // 1) 서블릿의 환경 정보를 다루는 객체를 먼저 얻는다.
    ServletContext ctx = req.getServletContext();

    // 2) ServletContext를 통해 웹 자원의 실제 경로를 알아낸다.
    // => getRealPath(현재 웹 애플리케이션의 파일 경로) : 실제 전체 경로를 리턴한다.
    String path = ctx.getRealPath("/photo.jpeg");
    System.out.println(path);

    FileInputStream in = new FileInputStream(path);

    // 바이너리를 출력할 때 MIME 타입을 지정해야 웹 브라우저가 제대로 출력할 수 있다.
    // => 웹 브라우저가 모르는 형식을 지정하면 웹 브라우저는 처리하지 못하기 때문에
    // 그냥 다운로드 대화상자를 띄운다.
    res.setContentType("image/jpeg");

    BufferedOutputStream out = new BufferedOutputStream(res.getOutputStream());

    int b;
    while ((b = in.read()) != -1) {
      out.write(b);
    }

    out.flush(); // 버퍼 데코레이터에 보관된 데이터를 클라이언트로 방출한다.
    out.close();
    in.close();
  }
}

받는 쪽에서 다운로드가 나온다면 MIME Type을 실수한 것이다. 만약 MIME타입을 image/ohora 처럼 잘못 설정한다면 다운로드 창이 나오거나 다운로드가 된다. 그리고 텍스트 파일을 text/ohora처럼 잘못 설정한다면 텍스트 그 자체가 나온다. (html형식으로 되어 있다면 html코드가 그대로 출력된다) 그리고 만약 toxt/ohora로 잘못 입력하면 text인지 조차 모르기 때문에 화면에 출력하는 것이 아니라 다운로드가 된다. MIME타입은 브라우저에게 지금 컨텐츠의 타입이 어떤지 알려주는 것이다. 브라우저는 타입대로 출력을 할 뿐이다.

코드를 읽으면 안된다. 코드의 의미를 파악할 수 있어야 한다. 도구나 사람같이 비유하자. 여기서 무언가 얻을 때 (getServletContext, getRealPath) 이것을 관리하는 담당자 객체를 사용하여 얻었다.

PMS

PMS 프로젝트에서 BoardListCommand부터 HttpServlet 객체를 상속받도록 만든다. execute(Request)Servlet API에 정의된 대로 service(HttpServletRequest request, HttpServletResponse response)로 바꾼다. 이전에 Map<String, Object> contextRequest 객체에서 꺼내 썼듯이, 여기서 context도 SerlvetRequest 객체에서 꺼내 쓴다. 다만 여기서는 context의 타입이 Map이 아니라 ServletContext 타입이다. 그리고 ctx에서 boardService라는 속성으로 저장했던 객체 BoardService를 거낸다. ctx.getAttribute() 메서드를 사용하면 된다.

ServletResponse 객체에서 getWriter()를 호출해 PrintWriter out 객체를 꺼내기 전에, 반드시 한글이 깨지지 않도록 reponse.setContentType()를 호출하여 UTF-8로 charset을 정의하고, MIME type을 html로 지정한다. 그리고 이전에는 콘솔에 출력했기 때문에 그대로 출력하면 되었지만, 브라우저에서 사용하는 HTML 형식에 맞춰 작성해야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
@WebServlet("/board/list")
public class BoardListCommand extends HttpServlet {
  private static final long serialVersionUID = 1L;

  @Override
  public void service(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>");
    try {
      out.println("<head><title>게시글목록</title></head>");
      out.println("<body><h1>게시글목록</h1></body>");

      List<Board> list = boardService.list();

      out.println("<body>");
      
      out.println("<table><tr>");
      out.println("<th>번호</th><th>제목</th><th>작성자</th><th>등록일</th><th>조회수</th>");
      out.println("</tr>");
      for (Board board : list) {
        out.printf("<td>%d</td><td>%s</td><td>%s</td><td>%s</td><td>%d</td>\n",
            board.getNo(),
            board.getTitle(),
            board.getWriter().getName(),
            board.getRegisteredDate(),
            board.getViewCount());
        out.println("</tr>");
      }
      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>");
  }

}

project와 배포 폴더

image

자바 프로젝트에서 src/main/java 밑 소스 파일들은 컴파일되고 클래스파일이 생성된다. 이 클래스 파일들과 src/main/resources에 있는 파일(mapper 파일 등)은 배포 폴더(wtpwebapp)의 /project/WEB-INF/class 폴더에 놓인다. src/main/webapp/ 밑의 파일(HTML, images 파일 등)은 /project 폴더=에 바로 위치한다. 또한 의존 라이브러리는 /WEB-INF/lib/ 폴더 아래에 놓인다.

This post is licensed under CC BY 4.0 by the author.

DEVIL: mysql의 datatime 타입과 java.util.Date 클래스

학원 #87일차: HTML 태그 사용법

Loading comments from Disqus ...