간단한 서블릿 컨테이너 만들기

서블릿은 클라이언트의 요청을 처리하고, 그 결과를 반환하는 자바의 표준 인터페이스이다. 서블릿 인터페이스는 Java Servlet API 안에 존재한다.

우리는 서블릿 인터페이스를 구현하여 요청을 원하는 방식으로 처리할 수 있다. 서블릿 컨테이너는 HTTP 요청을 받아 필요한 전처리를 한 후, 우리가 구현한 서블릿 클래스를 가져와 실행하고 응답을 받아 전송하게 된다. 스프링 MVC의 시작점인 DispatcherServlet도 서블릿 인터페이스를 구현한 클래스이다.

서블릿 인터페이스를 구현하기 때문에 우리가 구현한 서블릿 클래스를 톰캣이나 제티 등의 서블릿 컨테이너 어디에서든 코드 변경 없이 사용할 수 있다.

Servlet 인터페이스란?

서블릿 프로그래밍은 javax.servlet 패키지와 javax.servlet.http 패키지의 클래스와 인터페이스를 통해 이뤄진다. 이 패키지에서 가장 중요한 인터페이스가 Servlet이라고 할 수 있다. 모든 서블릿은 이 인터페이스를 구현해야 하기 때문이다.

Servlet 인터페이스에는 다음과 같은 5개의 메서드가 있다.

  • public void init(ServletConfig config)
  • public void service(ServletRequest request, ServletResponse response)
  • public void destory()
  • public ServletConfig getServletConfig()
  • public String getServletInfo()

init, service, destroy는 서블릿 생명주기와 관련된 메서드이다.

init 메서드는 서블릿 컨테이너가 서블릿 클래스를 인스턴스화 한 뒤 호출한다. 서블릿 컨테이너가 이 메서드를 호출하면 해당 서블릿이 서비스를 할 수 있는 준비가 됨을 보장해야 한다. 예를들어 데이터베이스 드라이버나 어떤 초기값을 로드하는 일과 같은 코드를 init 메서드 안에 구현해야 한다. 딱히 초기화가 필요하지 않는 경우에는 이 메서드를 비워두는 것이 보통이다.

서블릿 컨테이너는 요청이 있을 때마다 해당 서블릿의 service 메서드를 호출한다. 이 때 서블릿 컨테이너는 ServletRequestServletResponse를 전당한다. ServletRequest에는 클라이언트의 HTTP 요청에 관한 정보를 담고, ServletResponse에는 응답에 관한 정보를 캡슐화한다. service 메서드는 서블릿의 생명주기 동안 자주 호출된다.

서블릿 컨테이너는 서비스 영역으로부터 서블릿 인스턴스를 제거하기에 앞서 해당 서블릿의 destroy 메서드를 호출한다. destroy 메서드는 서블릿이 소유하고 있던 메모리, 파일 핸들, 스레드 등과 같은 자원을 깨끗이 반환할 수 있는 기회를 제공한다.

사전 준비

가장 먼저 서블릿 인터페이스가 포함된 Java Servlet API를 Dependency에 추가해야 한다. 최신 버전의 Java Servlet API는 mvnrepository에서 작성일 기준 4.0.1버전 까지 나온 것으로 보인다.

하지만 이 책에서는 2.4버전을 사용하므로 2.4버전을 사용하자(mvnrepository). build.gradle에 다음과 같이 추가한다.

1
2
3
4
5
dependencies {  
    implementation 'javax.servlet:servlet-api:2.4' // servlet 의존성 추기
    testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1'  
    testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1'  
}

이제 서블릿 관련 클래스들을 사용할 수 있다.

원시 Servlet

우리는 서블릿 컨테이너를 작성할 것이다. 이를 테스트할 때 간단하게 사용할 수 있는 Servlet 클래스를 먼저 만들어 보자.

./webroot에 다음과 같이 작성한다.

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
import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.Servlet;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;

public class PrimitiveServlet implements Servlet {

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

  @Override
  public void service(ServletRequest req, ServletResponse res)
      throws ServletException, IOException {
    System.out.println("from service");
    PrintWriter out = res.getWriter();
    out.println("Hello. Roses are red.");
    out.print("Violets are blue.");
  }

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

  @Override
  public ServletConfig getServletConfig() {
    return null;
  }

  @Override
  public String getServletInfo() {
    return null;
  }
}

아주 간단하게 하드 코딩된 메시지를 프린트하는 클래스이다. 응답의 마지막 줄은 body로 끝나야 하므로 마지막 줄엔 print메서드를 사용했다.

이제 이 클래스를 컴파일 해야한다. PrimitiveServlet클래스는 Servlet에 의존하고 있기 때문에 Servlet클래스를 가지고 있는 jar파일을 클래스 패스에 추가해야 컴파일 할 수 있다.

jar파일의 경로를 알기 위해 다음과 같이 복사한다.

여기서 Absolute Path를 복사하면 된다.

이제 복사한 경로를 -cp옵션에 넣고 PrimitiveServlet을 컴파일하자.

1
$ java -cp "javac -cp "[복사한 경로]" PrimitiveServlet.java

그러면 아래와 같이 class파일이 생성되고 우리의 애플리케이션에서 불러와 실행할 수 있다.

첫 번째 애플리케이션

완전한 서블릿 컨테이너는 다음과 같이 HTTP 요청을 처리해야한다.

  • 어떤 서블릿을 처음으로 요청받았을 떄, 해당 서블릿 클래스를 로드하고 서블릿의 init 메서드를 (딱 한 번) 호출한다.
  • 각 요청에서 ServletRequestServletResponse 인스턴스를 생성한다.
  • ServletRequestServletResponse를 전달해 서블릿의 service 메서드를 호출한다.
  • 서블릿 클래스를 종룟하면서 서블릿의 destroy 메서드를 호출하고 서블릿 클래스를 언로드해야 한다.

하지만 첫번째 서블릿 컨테이너에서는 init메서드와 destroy메서드 호출을 없애기로 한다. 또한 /servlet/**에 대한 요청은 서블릿을 로드하여 처리하고, 나머지 요청은 정적 리소스를 반환하기로 한다.

먼저 전체적인 구조를 UML로 확인해 보자.

애플리케이션의 진입점은 HttpServer1 클래스에 위치하며 main메서드는 HttpServer1의 인스턴스를 생성하고 await 메서드를 호출한다. await 메서드를 HTTP 요청을 기다리고, 모든 요청에 대해 RequestResponse 인스턴스를 생성한다. 그리고 정적 자원의 요청인지 서블릿 요청인지에 따라 두 객체를 StaticResourceProcessorServletProcessor 인스턴스에 디스패치한다.

HttpServer1 클래스

이 클래스는 chap01HttpServer 클래스와 거의 유사하다. 다른 점은 정적 자원 말고도 서블릿 요청에 대한 처리도 할 수 있다는 점이다.

정적 자원에 요청하려면 다음과 같은 URL로 요청하면 된다.

1
$ curl http://localhost:8080/staticResourse --http0.9

우리가 만든 원시 Servlet에 요청하려면 다음과 같이 요청하면 된다.

1
$ curl http://localhost:8080/servlet/PrimitiveServlet --http0.9

이제 코드를 살펴보자.

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
60
61
62
63
64
65
66
package com.dukcode.chap02;  
  
import java.io.IOException;  
import java.io.InputStream;  
import java.io.OutputStream;  
import java.net.InetAddress;  
import java.net.ServerSocket;  
import java.net.Socket;  
  
public class HttpServer1 {  
  
  private static final String SHUTDOWN_COMMAND = "/SHUTDOWN";  
  
  private boolean shutdown = false;  
  
  public static void main(String[] args) {  
    HttpServer1 server = new HttpServer1();  
    server.await();  
  }  
  
  private void await() {  
    ServerSocket serverSocket = null;  
    int port = 8080;  
    try {  
      serverSocket = new ServerSocket(port, 1, InetAddress.getByName("127.0.0.1"));  
    } catch (IOException e) {  
      e.printStackTrace();  
      System.exit(1);  
    }  
  
    while (!shutdown) {  
      Socket socket = null;  
      InputStream input = null;  
      OutputStream output = null;  
  
      try {  
        socket = serverSocket.accept();  
        input = socket.getInputStream();  
        output = socket.getOutputStream();  
  
        Request request = new Request(input);  
        request.parse();  
  
        Response response = new Response(output);  
        response.setRequest(request);  
  
        if (request.getUri().startsWith("/servlet/")) {  
          ServletProcessor1 processor = new ServletProcessor1();  
          processor.process(request, response);  
        } else {  
          StaticResourceProcessor processor = new StaticResourceProcessor();  
          processor.process(request, response);  
        }  
  
        socket.close();  
  
        shutdown = request.getUri().equals(SHUTDOWN_COMMAND);  
  
      } catch (IOException e) {  
        e.printStackTrace();  
        continue;  
      }  
    }  
  }  
  
}

기본적인 동작 방식은 chap01HttpServer과 유사하다. 하지만 다른점은 Response 인스턴스에게 직접 응답을 전송하도록 시키는 것이 아닌 경우에 따라 ServletProcessor1StaticResourceProcessor에게 프로세스를 맡긴다.

/servlet/으로 시작하는 경우에 ServletProcessor1에게 프로세스를 맡기고 아닌 경우에 StaticResourceProceccor에게 프로세스를 맡긴다.

또 다른 점은 기존의 WEB_ROOTConstants라는 새로운 클래스로 옮겨 갔다는 것이다. WEB_ROOTServletProcessor1에서 동적으로 클래스를 로드할때 필요한 경로이므로 외부 클래스에서 상수를 관리하는 것이 타탕해 보인다.

RequestResponse 클래스

우리는 Servlet 표준이 제공하는 RequestReponse를 사용해 서블릿 요청을 처리할 것이므로 두 클래스 모두 ServletRequest, ServletResponse라는 인터페이스를 구현하도록 변경한다. 두 클래스의 로직은 chap01의 로직과 대부분 유사하다.

인스턴스에서 구현하도록 요구하는 메서드들은 일단 구현을 미루도록 빈 구현을 해놓도록 하자.

[Request]

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
60
61
62
63
64
65
66
67
68
69
70
package com.dukcode.chap02;  
  
import java.io.BufferedReader;  
import java.io.IOException;  
import java.io.InputStream;  
import java.io.UnsupportedEncodingException;  
import java.util.Enumeration;  
import java.util.Locale;  
import java.util.Map;  
import javax.servlet.RequestDispatcher;  
import javax.servlet.ServletInputStream;  
import javax.servlet.ServletRequest;  
  
public class Request implements ServletRequest {  
  
  private InputStream input;  
  private String uri;  
  
  public Request(InputStream input) {  
    this.input = input;  
  }  
  
  public void parse() {  
    StringBuilder request = new StringBuilder(2048);  
    int i;  
    byte[] buffer = new byte[2048];  
    try {  
      i = input.read(buffer);  
    } catch (IOException e) {  
      e.printStackTrace();  
      i = -1;  
    }  
    for (int j = 0; j < i; j++) {  
      request.append((char) buffer[j]);  
    }  
    System.out.print(request.toString());  
    uri = parseUri(request.toString());  
  }  
  
  private String parseUri(String requestString) {  
    int index1;  
    int index2;  
  
    index1 = requestString.indexOf(' ');  
    if (index1 != -1) {  
      index2 = requestString.indexOf(' ', index1 + 1);  
      if (index2 > index1) {  
        return requestString.substring(index1 + 1, index2);  
      }  
    }  
    return null;  
  }  
  
  public String getUri() {  
    return uri;  
  }  
  
  /* 이하 SerlvetRequest의 빈 구현 */  
  @Override  
  public Object getAttribute(String name) {  
    return null;  
  }  
  
  // 생략...
  
  @Override  
  public int getLocalPort() {  
    return 0;  
  }  
}

Request 클래스는 ServletRequest를 구현하면서 기존의 로직은 그대로 가지고 있도록 구현한다.

[Response]

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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
package com.dukcode.chap02;  
  
import java.io.File;  
import java.io.FileInputStream;  
import java.io.IOException;  
import java.io.OutputStream;  
import java.io.PrintWriter;  
import java.util.Locale;  
import javax.servlet.ServletOutputStream;  
import javax.servlet.ServletResponse;  
  
public class Response implements ServletResponse {  
  
  private static final int BUFFER_SIZE = 1024;  
  Request request;  
  OutputStream output;  
  
  public Response(OutputStream output) {  
    this.output = output;  
  }  
  
  public void setRequest(Request request) {  
    this.request = request;  
  }  
  
  public void sendStaticResource() throws IOException {  
    byte[] bytes = new byte[BUFFER_SIZE];  
    FileInputStream fis = null;  
    try {  
      File file = new File(Constants.WEB_ROOT, request.getUri());  
      if (file.exists()) {  
        fis = new FileInputStream(file);  
        int ch = fis.read(bytes, 0, BUFFER_SIZE);  
        while (ch != -1) {  
          output.write(bytes, 0, ch);  
          ch = fis.read(bytes, 0, BUFFER_SIZE);  
        }  
      } else {  
        String errorMessage = """  
            HTTP/1.1 404 File Not Found\r  
            Content-Type: text/html\r  
            Content-Length: 23\r  
            \r  
            <h1>File Not Found</h1>""";  
        output.write(errorMessage.getBytes());  
      }  
    } catch (IOException e) {  
      e.printStackTrace();  
    } finally {  
      if (fis != null) {  
        fis.close();  
      }  
    }  
  }  
  
  @Override  
  public PrintWriter getWriter() throws IOException {  
    PrintWriter writer = new PrintWriter(output, true);  
    return writer;  
  }  
  
  /* 이하 SerlvetResponse의 빈 구현 */  
  @Override  
  public String getCharacterEncoding() {  
    return null;  
  }  
  
  // 생략...
  
  @Override  
  public void setLocale(Locale loc) {  
  
  }  
}

Response도 마찬가지고 ServletResponse를 구현한다. 하지만 거의 빈 구현이고 유일하게 getWriter 메서드를 구현한다.

PrintWriter 생성자의 두 번째 파라미터는 autoFlush 여부이다. 인스턴스의 println, printf, format 메서드는 자동적으로 플러시 하지만, print 메서드는 자동적으로 플러시하지 않는다. 따라서 PrimitiveServlet의 마지막 줄인 out.print("Violets are blue.");는 플러시 하지 않아서 요청 시에 볼 수 없다. (HTTP 응답의 마지막 줄은 body로 끝나야 해서 print메서드를 사용함)

이 문제는 추후 애플리케이션을 개발하면서 해결할 것이다.

StaticResourceProcessor 클래스

정적 자원에 대한 처리를 하는 간단한 클래스이다. ResponsesendStaticResource메서드를 호출 하는 기능이 전부이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.dukcode.chap02;  
  
import java.io.IOException;  
  
public class StaticResourceProcessor {  
  
  public void process(Request request, Response response) {  
    try {  
      response.sendStaticResource();  
    } catch (IOException e) {  
      e.printStackTrace();  
    }  
  }  
}

ServletProcessor1 클래스

ServletProcessor1클래스의 process 메서드의 역할은 WEB_ROOT 경로에서 URI 경로와 맞는 서블릿 클래스를 로드해서 service메서드를 호출하는 기능을 한다.

클래스를 동적으로 로드하는 코드는 처음 짜봐서 난해할 수 있지만, 사용법을 익힌다고 생각해보고 직접 작성해보자. 클래스로더는 chap08에서 자세히 다룰 예정이다.

샘플 코드와 다른 점은 클래스를 읽어오는 부분이다. Class.newInstance는 예외 처리가 난해해 JAVA 9부터 Deprecated 되었다. Class.getContructor 메서드를 기본 생성자를 가져오고 기본 생성자를 통해 클래스를 로드하는 방식으로 변경했다.

로드한 클래스를 Servlet으로 캐스팅해 service 메서드를 호출한다.

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
package com.dukcode.chap02;  
  
import java.io.File;  
import java.io.IOException;  
import java.lang.reflect.Constructor;  
import java.net.URL;  
import java.net.URLClassLoader;  
import java.net.URLStreamHandler;  
import javax.servlet.Servlet;  
import javax.servlet.ServletRequest;  
import javax.servlet.ServletResponse;  
  
public class ServletProcessor1 {  
  
  public void process(Request request, Response response) {  
    String uri = request.getUri();  
    String servletName = uri.substring(uri.lastIndexOf("/") + 1);  
    URLClassLoader loader = null;  
    try {  
      URL[] urls = new URL[1];  
      URLStreamHandler streamHandler = null;  
      File classPath = new File(Constants.WEB_ROOT);  
  
      String repository = new URL("file", null,  
          classPath.getCanonicalPath() + File.separator).toString();  
      urls[0] = new URL(null, repository, streamHandler);  
      loader = new URLClassLoader(urls);  
    } catch (IOException e) {  
      e.printStackTrace();  
    }  
  
    Class myClass = null;  
    try {  
      myClass = loader.loadClass(servletName);  
    } catch (ClassNotFoundException e) {  
      e.printStackTrace();  
    }  
  
    Servlet servlet = null;  
    try {  
      Constructor constructor = myClass.getConstructor();  
      servlet = (Servlet) constructor.newInstance();  
      servlet.service((ServletRequest) request, (ServletResponse) response);  
    } catch (Throwable e) {  
      e.printStackTrace();  
    }  
  }  
  
}

URLCLassLoader의 가장 간단한 생성자는 URL의 배열을 파라미터로 받는다. 이 때, URL의 경로가 /로 끝나는 경우에는 디렉토리를 참조하는 것으로 간주되고 , /로 끝나지 않으면 jar파일을 참조하는 것으로 간주된다.

일반적으로 서블릿 컨테이너에서 클래스 로더가 서블릿 클래스를 찾는 위치를 repository라고 명한다. repository/로 끝나는 것을 눈여겨 보자.

실제로 톰캣은 URL를 만들 때, public URL(URL context, String spec, URLStreamHandler handler) 생성자를 사용한다. 또한 다른 생성자도 존재한다. public URL(String protocol, String host, String file)의 형태이다.

우리는 톰캣의 구현을 따라 첫 번째의 생성자를 사용하고 싶다. 하지만 new URL(null, repository, null)의 형태로 작성한다면 컴파일러 입장에서 어떤 생성자를 사용할지 판단할 수 없다. 따라서 URLStreamHandlernull로 선언해 넣어준 것이다.

Constants 클래스

단순한 상수 클래스이다.

1
2
3
4
5
6
7
8
9
10
package com.dukcode.chap02;  
  
import java.io.File;  
  
public class Constants {  
  
  public static final String WEB_ROOT =  
      System.getProperty("user.dir") + File.separator + "webroot";  
  
}

실행해보기

이제 정적 리소스와 서블릿 모두를 호출해보자.

정적 리소스를 호출하기 위해 다음과 같이 명령어를 입력한다.

1
$ curl http://localhost:8080/index.html --http0.9

아래와 같이 정적 리소스를 잘 불러오는 것을 확인할 수 있다.

이제 서블릿을 호출해 보자. 다음과 같은 명령어로 서블릿을 호출한다.

1
$ curl http://localhost:8080/servlet/PrimitiveServlet --http0.9

아래와 같이 잘 작동하는 것을 확인할 수 있다. PrimitiveServlet의 마지막 print메서드는 플러시 되지 못한 것도 확인할 수 있다.

두 번째 애플리케이션

첫번째 애플리케이션은 심각한 문제가 존재한다. ServletProcessor1 클래스의 process메서드를 확인해 보면 Request 인스턴스와 Response 인스턴스를 각각 ServletRequestServletResponse로 업캐스팅해 전달하고 있다.

이런 방식은 보안과 관련한 문제를 야기한다. 서블릿 컨테이너의 내부를 알고있는 서블릿 프로그래머라면 서블릿 내부에서 ServletRequestServletResponse를 각각 RequestResponse로 다운캐스팅해 퍼블릭 메서드를 호출할 수 있다. 예를들어 서블릿 내부에서 Requestparse메서드를 호출하고, Response에서 sendStaticResource를 호출하는 등, 서블릿 컨테이너 개발자가 의도하지 않은대로 서블릿을 작성할 수 있게 된다.

이를 해결하기 위한 방법이 있다. FACADE 패턴을 사용하는 것이다. 기존 클래스 메서드를 퍼블릭으로 유지하면서 의도되지않은 서블릿 개발자의 사용을 막을 수 있다.

RequestFacadeResponseFacade 클래스

RequestFacadeResponseFacade 클래스는 각각 RequestResponse를 인스턴스로 가지고 있는 FACADE 클래스이다. 외부에 공개하고 싶은 메서드만 위임 메서드로 구현한다. 코드는 다음과 같다.

[RequestFacade]

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
package com.dukcode.chap02;  
  
import java.io.BufferedReader;  
import java.io.IOException;  
import java.io.UnsupportedEncodingException;  
import java.util.Enumeration;  
import java.util.Locale;  
import java.util.Map;  
import javax.servlet.RequestDispatcher;  
import javax.servlet.ServletInputStream;  
import javax.servlet.ServletRequest;  
  
public class RequestFacade implements ServletRequest {  
    
  private Request request;  
  
  public RequestFacade(Request request) {  
    this.request = request;  
  }  
  
  /* 이하 SerlvetRequest의 위임 메서드 구현 */  
  @Override  
  public Object getAttribute(String name) {  
    return request.getAttribute(name);  
  }  

  // 생략...

  @Override  
  public int getLocalPort() {  
    return request.getLocalPort();  
  }  
}

[ResponseFacace]

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
package com.dukcode.chap02;  
  
import java.io.IOException;  
import java.io.PrintWriter;  
import java.util.Locale;  
import javax.servlet.ServletOutputStream;  
import javax.servlet.ServletResponse;  
  
public class ResponseFacade implements ServletResponse {  
    
  private Response response;  
  
  public ResponseFacade(Response response) {  
    this.response = response;  
  }  
  
  /* 이하 위임 메서드 구현 */  
  @Override  
  public PrintWriter getWriter() throws IOException {  
    return response.getWriter();  
  }  
    
  // 생략...
  
  @Override  
  public Locale getLocale() {  
    return response.getLocale();  
  }  
}

이제 RequestparseUri 메서드와 ResponsesendStaticResource 메서드는 안전해졌다.

HttpServer2 클래스

HttpServer2 클래스는 await 메서드에서 ServletProcessor1대신 ServletProcessor2를 사용한다는 점만 제외하면 HttpServer1 클래스와 같다.

[다른 부분]

1
2
3
4
5
6
7
8
        if (request.getUri().startsWith("/servlet/")) {
          // ServeletProcessor2 사용
          ServletProcessor2 processor = new ServletProcessor2();
          processor.process(request, response);
        } else {
          // ...
        }

ServletProcessor2 클래스

ServletProcessor2 클래스는 RequestResponse를 각각 ServletRequestServletResponse로 바로 업캐스팅 하지 않고 RequestFacadeResponseFacade를 생성하고 업캐스팅하여 전달한다. 나머지 부분은 같다.

[다른 부분]

1
2
3
4
5
6
7
8
9
10
11
12
13
    try {
      Constructor constructor = myClass.getConstructor();
      servlet = (Servlet) constructor.newInstance();

      // FACADE 클래스를 생성해 전달
      RequestFacade requestFacade = new RequestFacade(request);
      ResponseFacade responseFacade = new ResponseFacade(response);
      
      servlet.service((ServletRequest) requestFacade, (ServletResponse) responseFacade);
    } catch (Throwable e) {
      e.printStackTrace();
    }

실행해보기

이제 두 번째 애플리케이션도 첫 번째 애플리케이션과 같은 방법으로 실행할 수 있다. 다른 점은 서블릿 클래스에서 RequestResponse의 허용되지 않는 퍼블릭 메서드에 접근할 수 없다는 것이다.

[정적 리소스 접근]

[서블릿 접근]

댓글남기기