웹 컨텍스트

본 장에서는 JEUS 웹 애플리케이션을 패키징하는 방법과 이를 JEUS 웹 엔진에 deploy하고 모니터링하는 방법에 대해 설명한다.

1. 개요

웹 애플리케이션을 웹 엔진에 deploy하면 웹 컨텍스트가 생성된다. 즉, 웹 애플리케이션과 웹 컨텍스트는 동일한 의미로 봐도 무방하므로 두 용어를 혼용하지 않고 웹 컨텍스트로 통일해서 사용한다.

2. 기본 구조

본 절에서는 웹 컨텍스트가 포함하는 내용과 내부 구조에 대해서 설명한다.

2.1. 웹 컨텍스트 콘텐츠

웹 애플리케이션은 클라이언트의 요청에 의한 웹 기반의 서비스를 수행하기 위한 정적 콘텐츠와 동적 콘텐츠로 구성되어 있다.

  • 정적 콘텐츠

    사전에 이미 완성되어 있는 형태로 웹 엔진에서 어떠한 처리도 하지 않고 반환하는 모든 종류의 데이터들을 의미한다. 정적 콘텐츠의 종류는 다음과 같다.

    • HTML 페이지

    • 텍스트 파일

    • 이미지 파일

    • 비디오

    • 기타

  • 동적 콘텐츠

    서블릿, JSP와 같이 사용자의 요청에 따라 응답을 생성하는 콘텐츠들을 의미한다.

2.2. 웹 컨텍스트 내부 구조(WAR 파일 구조)

웹 컨텍스트는 웹 엔진에 deploy하기 전에 .war 확장자를 가진 JAR 파일 형식의 압축 파일로 패키징해야 한다. 이를 WAR(Web ARchive) 파일이라고 한다.

WAR 파일의 구조는 다음과 같다.

WEB WAR
   |--[T]index.html
   |--static
   |    |--[T].html
   |--jsp
   |    |--.jsp
   |--images
   |    |--[T].jpg, .gif
   |--META-IMF
   |    |--[T]MAINFEST.MF
   |--WEB-IMF
        |--[X]web.xml
        |--[X]jeus-web-dd.xml
        |--[X]ebj-jar.xml
        |--[X]jeus-ejb-dd.xml
        |--classes
        |    |--[C]Servlet.class
        |    |--[C]EJB.class
        |--lib
        |    |--[J]Library JAR
        |--tlds
             |--[T].tld

* Legend
- [01]: binary or executable file
- [X] : XML document
- [J] : JAR file
- [T] : Text file
- [C] : Class file
- [V] : java source file
- [DD] : deployment descriptor
META-INF/

이 디렉터리는 선택 사항이다. 이 디렉터리를 사용하는 경우에는 JAR 포맷에 정의된 Descriptor 파일인 MANIFEST.MF 파일을 포함한다.

WEB-INF/

이 디렉터리는 필수 사항이고 서블릿, 필터, 리스너 클래스들, 라이브러리들이 포함되어 있다. 이 디렉터리 하위에는 다음과 같은 컴포넌트들이 존재한다.

컴포넌트 설명

web.xml

Jakarta EE 표준 웹 애플리케이션 DD 파일로 웹 애플리케이션의 메타 정보를 가지고 있다. Servlet 3.0부터는 web.xml이 반드시 필요하지 않다. 대신 Annotation, Registration API를 통해서 서블릿, 필터, 리스너들을 추가할 수 있다. 자세한 사항은 서블릿 표준을 참고한다.

jeus-web-dd.xml

JEUS의 웹 애플리케이션 DD로 자세한 설명은 jeus-web-dd.xml 설정을 참고한다.

ejb-jar.xml

jeus-ejb-dd.xml

Java EE 6부터 WAR 파일에 EJB를 포함할 수 있다. 서블릿과 마찬가지로 Annotation으로 정의할 수 있다. 이에 대한 자세한 설명은 EJB 표준 또는 JEUS EJB 안내서를 참고한다.

classes/

서블릿 클래스들과 유틸리티 클래스들이 하위 디렉터리에 포함되어 있다.

표준 Java 패키지 구조로 정리되어 있다.

lib/

웹 애플리케이션에 필요한 Java 라이브러리들을 포함한다. 라이브러리들은 .jar 파일들로 패키지되어 이 경로에 저장된다. 이 파일들의 내용들은 자동적으로 모든 서블릿의 클래스 경로에 추가된다.

tlds/

JSP 페이지들을 위한 Custom Tag Library Descriptor들을 포함한다.

기타

JSP, HTML, 이미지 파일들과 같은 콘텐츠를 포함하고 있는 임의의 이름의 디렉터리이다.

3. 웹 컨텍스트 Deploy

본 절에서는 웹 컨텍스트의 Deploy에 대해 설명한다. Deploy는 웹 애플리케이션을 JEUS 웹 엔진에서 서비스할 수 있도록 준비하여 웹 컨텍스트를 생성하는 과정이다.

웹 컨텍스트는 논리적으로 가상 호스트 하위에 존재한다. 가상 호스트에 대한 더 자세한 사항은 가상 호스트를 참고한다.

3.1. jeus-web-dd.xml 설정

반드시 필요한 경우에 한해 WEB-INF/ 디렉터리 아래에 web.xml과 jeus-web-dd.xml을 생성한다.

다음은 jeus-web-dd.xml 파일의 예이다. 몇몇 항목들은 생략되어 있다. 생략된 항목들은 "JEUS XML Reference"의 "13. jeus-web-dd.xml 설정"을 참고한다.

웹 컨텍스트 설정 파일 : <jeus-web-dd.xml>
<jeus-web-dd xmlns="http://www.tmaxsoft.com/xml/ns/jeus" version="9">
    <context-path>/examples</context-path>
    <enable-jsp>true</enable-jsp>
    <auto-reload>
        <enable-reload>true</enable-reload>
        <check-on-demand>true</check-on-demand>
    </auto-reload>
    <added-classpath>
        <class-path>/home/user1/libs/lib.jar</class-path>
    </added-classpath>
    <session-config>
        . . .
    </session-config>
    <thread-pool>
        <base>
            <name>base</name>
            <min>1</min>
            <max>5</max>
        </base>
        <service-group>
            <name>svcg1</name>
            <uri>/service1,/service2</uri>
            <min>10</min>
            <max>20</max>
        </service-group>
        <service-group>
            <name>longJsp</name>
            <uri>/jsp/servlet/asyncContext.jsp</uri>
            <min>5</min>
            <max>10</max>
        </service-group>
        <service-group>
            <name>Jsp</name>
            <uri>/jsp/servlet</uri>
            <min>1</min>
            <max>5</max>
        </service-group>
    </thread-pool>
    <upgrade>
        <upgrade-executor>
            <min>0</min>
            <max>30</max>
            <keep-alive-time>60000</keep-alive-time>
            <queue-size>4096</queue-size>
        </upgrade-executor>
    </upgrade>
    <async-config>
        <async-timeout-millis>600000</async-timeout-millis>
        <background-thread-pool>
            <min>0</min>
            <max>10</max>
        </background-thread-pool>
        <dispatch-thread-pool>
            <min>0</min>
            <max>10</max>
        </dispatch-thread-pool>
    </async-config>
    <properties>
        <property>
            <key>jeus.servlet.jsp.modern</key>
            <value>true</value>
        </property>
    </properties>
</jeus-web-dd>

다음은 설정 태그에 대한 설명이다.

태그 설명

<context-path>

웹 컨텍스트에 접근하기 위한 Root Path이다. 이것은 반드시 '/'로 시작해야 하며 가상 호스트 내에서 유일해야 한다.

"http://www.foo.com/examples/index.html"과 같은 URL에서 <context path>는 "/examples"가 된다.

<enable-jsp>

false로 설정했을 때 JSP 기능을 사용하지 않는다. 기본적으로 JSP를 지원한다.

<user-log>

웹 컨텍스트가 남기는 로그이다. 유저 로그에 대한 자세한 내용은 "유저 로그 설정"을 참고한다.

<auto-reload>

디스크의 서블릿 클래스가 변경되었을 때 자동적으로 로딩할지 여부를 결정하는 옵션이다. 자세한 내용은 클래스 동적 반영을 참고한다.

<added-classpath>

서블릿을 실행할 때 참조할 라이브러리들의 리스트이다.

디렉터리도 설정 가능하다.

<allow-indexing>

클라이언트가 요청했을 때 실제 디렉터리 목록이 출력되는 URL 경로의 목록이다. 클라이언트에서 요청한 파일이 디렉터리에 없을 경우에 디렉터리의 목록이 출력된다.

대신에 디렉터리 목록의 파일을 하나 선택하면 해당 파일의 내용이 출력된다.

<deny-download>

어떤 경우에라도 직접적으로 다운로드받거나 접근할 수 없는 자원들을 규정하기 위한 필터이다. 이 필터들은 파일 이름의 확장자로 파일 이름과 함께 경로로 지정할 수 있다.

필터의 파일 이름과 디렉터리들은 context document base 디렉터리의 상대적인 경로로 설정된다.

<aliasing>

Custom URL 경로에 대한 디렉터리 매핑을 의미한다.

다른 위치의 디렉터리를 URL 요청 경로에 매핑시킬 필요가 있는 경우에 aliasing 기능을 사용한다.

<file-caching>

응답 속도 향상을 위해 런타임 메모리에 어떤 정적 콘텐츠를 Cache해야 할지 결정한다.

이 항목의 하위 설정들은 Cache 메모리의 최대 크기(단위: MB), 요청되지 않을 때 Cache 내에 머무를 수 있는 정적 콘텐츠의 최대 크기, caching되어야 할 콘텐츠의 경로가 있다.

<jsp-engine>

웹 컨텍스트에 포함된 JSP 페이지에 대한 설정이다. 자세한 내용은 jeus-web-dd.xml 설정을 참고한다.

<session-config>

웹 컨텍스트 레벨의 세션을 설정한다.

이 설정은 웹 엔진에 설정한 것보다 우선순위가 높다. 세션 설정에 대한 자세한 내용은 세션 설정을 참고한다.

<webinf-first>

클래스 로딩 우선순위에 관한 옵션으로 true로 설정하면 해당 애플리케이션에 한해서 WEB-INF 하위의 클래스를 우선적으로 로딩한다.

true로 설정하였을 때 클래스 로딩 관련 충돌이 발생할 경우 아래와 같이 특정 패키지 또는 클래스만 제외할 수 있는 기능이 JEUS 7 Fix#4부터 추가되었다.

<webinf-first>
    <enabled>true</enabled>
    <excluded-package>package.name.</excluded-package>
    ...
</webinf-first>

<attach-stacktrace-on-error>

서버에 문제가 발생했을 때 오류의 상세 내역을 브라우저로 보여줄 것인지에 대한 설정이다. 이 메시지는 개발하는 경우에는 유용하지만 운영하는 경우에는 제거하는 것이 좋다.

<keep-alive-error-response-codes>

에러 응답 코드 중에서 Connection: Close 대신 Keep-Alive 응답을 주길 원하는 값을 설정한다. 웹 애플리케이션이 직접 에러 응답을 내보낼 때 적용되며, 엔진 내부적으로 커넥션을 끊어야 할 필요가 있다고 판단한 경우에는 적용되지 않는다.

예를 들어 서블릿이 response.sendError(500)을 수행한 경우에는 적용되지만 서블릿에서 exception이 발생해서 엔진이 500 에러를 보내는 경우에는 적용되지 않는다. 여러 개의 응답 코드를 설정하려면 "404,503"과 같이 콤마(,)로 구분한다.

<encoding>

Request, Response에 대한 인코딩 설정이다. 웹 엔진 설정(domain.xml)과 동일하지만 동시에 설정될 경우 jeus-web-dd.xml의 <request-encoding> 설정과 <response-encoding> 설정이 각각 우선적으로 적용된다. 자세한 내용은 인코딩 설정을 참고한다.

  • Request Encoding : URL Query String으로 application/x-www-form-urlencoded 형식의 바디이다. getReader()로 호출된다. JEUS 7 Fix#4부터 <request-encoding><url-mapping> 설정이 추가되었다. 이는 특정 Servlet Path에 매핑된 인코딩을 적용하는 설정이다. 단, <forced> 또는 <client-override> 설정이 있는 경우에는 무시된다.

  • Response Encoding : 응답 HTTP 바디에 적용되는 인코딩이다.

<cookie-policy>

HTTP Request Header에 있는 쿠키를 읽거나 애플리케이션 요청에 의해 새로운 쿠키를 HTTP Response Header를 사용할 때 적용하는 정책을 설정할 수 있다. 자세한 내용은 쿠키 정책 설정을 참고한다.

<async-config>

Servlet 3.0부터 추가된 Async Processing에 관한 설정이다. 하위에는 Async Timeout, Background Thread Pool, Dispatch Thread Pool에 대한 설정을 할 수 있다. Async Processing에 관한 자세한 내용은 서블릿 표준을 참고한다.

<upgrade>

Upgrade Inbound Message를 처리하기 위해서 Container 내부적으로 사용하는 쓰레드 풀 관련 설정이다.

<web-security>

sendRedirect할 경우에 주소값 검증에 대한 정책 설정 등 웹 애플리케이션에 국한된 보안 정책들의 설정 그룹이다.

<websocket>

웹 애플리케이션의 WebSocket Container에 대한 설정이다. 자세한 내용은 WebSocket 컨테이너 설정을 참고한다.

<properties>

웹 애플리케이션에 적용되는 프로퍼티를 설정한다. 여기에 설정된 프로퍼티는 다른 웹 애플리케이션에는 적용되지 않고, 설정된 웹 애플리케이션에만 적용된다.

<async-config><async-timeout-millis>의 기본값은 30초이다. 만약 Background Thread에서 수행하는 작업이 너무 오래 걸리는 경우가 있어서 타임아웃 에러가 발생한다면 jeus-web-dd.xml에 이 값을 설정하거나 jakarta.servlet.AsyncContext#setTimeout()으로 프로그램에서 조절한다.

다음은 Thread Pool 태그에 대한 설명이다.

태그 설명

<base>

Service Group의 uri에 매칭되지 않는 요청들을 처리할 default Thread Pool이다. 상위의 Thread Pool인 Virtual Host 또는 Web-Connection Thread Pool에서 처리하게 하려면 설정하면 안된다.

<service-group>

uri에 매칭되는 service들을 처리할 Thread Pool이다.

<name>

base와 service-group들을 구분하기 위한 이름이다.

<uri>

service-group Thread Pool에서 처리할 요청들의 uri이다. "," 로 구분해서 하나 이상의 resource를 나타낼 수 있으며 각 uri는 '/' 로 시작해야 한다.

3.2. 웹 컨텍스트 Redeploy(Graceful Redeploy)

웹 컨텍스트의 Redeploy에 대한 내용은 본 안내서에서는 생략한다. 자세한 내용은 JEUS Applications & Deployment 안내서의 Graceful Redeployment를 참고한다.

3.3. Shared Library Reference 추가

공유 라이브러리(Shared Library)는 애플리케이션별로 필요한 라이브러리를 WEB-INF/lib가 아닌 JEUS_HOME/lib/shared 아래에 추가한 후에 여러 애플리케이션에서 공유하여 사용할 수 있다. JEUS의 재기동 없이도 libraries.xml 파일 수정 후 모듈을 다시 deploy하면 변경된 라이브러리가 반영된다.

공유 라이브러리를 추가하려면 다음과 같이 JEUS_HOME/lib/shared/libraries.xml 파일에서 <library> 태그를 추가하고 공유할 JAR 파일들을 지정한다. 버전은 스펙 버전과 구현체 버전을 각각 지정할 수 있다.

Shared Library Reference 추가 : <libraries.xml>
<libraries xmlns="http://www.tmaxsoft.com/xml/ns/jeus">
    <!-- DO NOT MODIFY OUR SYSTEM libraries!!! -->
    <library>
        <library-name>jsf</library-name>
        <specification-version>1.2</specification-version>
        <implementation-version>1.2.15</implementation-version>
        <files dir="jsf_ri_1.2"/>
    </library>
    <library>
        <library-name>jsf</library-name>
        <specification-version>2.2</specification-version>
        <implementation-version>2.2.13</implementation-version>
        <files dir="jsf_ri_2.2"/>
    </library>
    <library>
        <library-name>jsf-jeusport</library-name>
        <files dir=".">
            <include name="jsf-injection-provider.jar"/>
    </library>
    <library>
        <library-name>jsf-weld-integration</library-name>
        <files dir=".">
            <include name="jsf-weld-integration.jar"/>
    </library>
    <library>
        <library-name>jstl</library-name>
        <specification-version>1.2</specification-version>
        <implementation-version>1.2</implementation-version>
        <files dir="jstl_1.2"/>
    </library>
    <library>
        <library-name>jstl</library-name>
        <specification-version>1.2</specification-version>
        <implementation-version>1.2</implementation-version>
        <files dir="jstl_1.2"/>
    </library>
    <library>
        <library-name>spring-support</library-name>
        <specification-version>4.2.0.RELEASE</specification-version>
        <implementation-version>4.2.0.RELEASE</implementation-version>
        <files dir="spring-support/4.2.0.RELEASE"/>
    </library>
    <library>
        <library-name>spring-support</library-name>
        <specification-version>4.0.0.RELEASE</specification-version>
        <implementation-version>4.0.0.RELEASE</implementation-version>
        <files dir="spring-support/4.0.0.RELEASE"/>
    </library>

    <!-- Add user libraries from here -->
    <library>
        <library-name>myLibrary</library-name>
        <specification-version>2.0</specification-version>
        <implementation-version>2.1</implementation-version>
        <files dir=".">
            <include name="commons-logging.jar"/>
            <include name="commons-util.jar"/>
        </files>
        <files dir="myLib-2.1"/>
    </library>
</libraries>

설정한 공유 라이브러리를 참조하려면 jeus-web-dd.xml에 다음과 같이 <library-ref>를 설정한다. 버전이 명시되지 않은 경우는 libraries.xml에 등록된 동일한 이름의 라이브러리 중에서 최상위 버전을 참조한다.

   <library-ref>
       <library-name>myLibrary</library-name>
       <specification-version>
          <value>2.0</value>
       </specification-version>
   </library-ref>

JSF, JSTL 라이브러리 설정

Shared Library Reference 추가 : <libraries.xml>에 설명한 libraries.xml 파일에는 JSF와 JSTL이 공유 라이브러리로 추가되어 있다. Jakarta EE 표준은 JSF와 JSTL을 포함하고 있기 때문에 JEUS에서 기본적으로 제공한다.

JEUS 시스템 라이브러리로 제공할 수도 있지만 하나의 JSF, JSTL 구현체만 사용할 수 있는 제약 사항이 발생한다. JSF 구현체는 2.0뿐만 아니라 1.2 버전도 있고 Sun RI가 아닌 Apache의 MyFaces도 있기 때문에 JEUS에서는 여러 가지 버전의 구현체를 사용할 수 있도록 공유 라이브러리 형태로 JSF, JSTL을 제공한다.

구현체의 위치와 종류에 따라 사용되는 구현체는 다음과 같이 구분된다.

  • WEB-INF/lib 아래에 포함한 JSF나 JSTL 구현체

  • Shared Library로 포함시킨 구현체(위의 구현체보다 우선 순위가 낮다.)

[참고]

다음은 JEUS 7 Fix#2 버전부터 더이상 JEUS_HOME/lib/shared 아래의 Oracle JSF RI를 자동으로 추가해주지 않는다. 이를 사용하려면 명시적으로 jeus-web-dd.xml에서 <library-ref>로 참조하도록 설정해야 한다.

다음과 같이 선언하면 JEUS_HOME/lib/shared에 기본으로 포함된 라이브러리를 사용할 수 있다.

<library-ref>
    <library-name>jsf</library-name>
</library-ref>

또한 JEUS 내부적으로 판단해서 Oracle JSF RI 2.x에 포함된 웹 리스너를 자동으로 추가하지 않는다. 명시적으로 web.xml에서 <listener>로 설정해야 한다.

<listener>
    <listener-class>com.sun.faces.config.ConfigureListener</listener-class>
</listener>

동일한 JSF나 JSTL 라이브러리를 여러 애플리케이션에서 참조해서 사용하려면 직접 JEUS_HOME/lib/shared/libraries.xml에 <library>를 등록하고 jeus-web-dd.xml에서 <library-ref>로 참조하도록 설정한다.

JSF의 경우 Managed Bean에서 @EJB나 @Resource와 같은 Annotation으로 WAS가 제공하는 리소스에 접근할 수 있고 WAS는 Annotation으로 명시된 리소스들을 injection할 수 있어야 한다.

JEUS에서는 JSF RI에 JEUS Injection을 지원하기 위해서 공유 라이브러리로 jsf-injection-provider.jar를 제공하고 있다. 공유 라이브러리는 다음의 디렉터리에 존재한다.

JEUS_HOME/lib/shared

이는 web.xml에 jakarta.faces.webapp.FacesServlet이 포함된 경우 자동으로 포함된다.

Spring Support 라이브러리 설정

Shared Library Reference 추가 : <libraries.xml>에 설명한 libraries.xml 파일에는 Spring Support 라이브러리가 공유 라이브러리로 추가되어 있다.

Spring Support 라이브러리는 Spring Framework을 이용해 만든 애플리케이션이 JEUS의 기능을 사용하기 위해 필요하다. 이 절에서는 WebSocket 관련 기능만 설명한다.

Spring Framework을 사용하여서 WebSocket 기능을 구현 할 경우 spring-websocket 모듈을 사용하게 된다. 하지만, 이 모듈에서 JEUS의 WebSocket Handshake를 지원하고 있지 않기 때문에 JEUS에서 정상동작 되지 않는 문제가 있다. Spring Support 라이브러리를 사용하면 JEUS의 WebSocket 기능을 정상적으로 사용할 수 있다.

Spring Support 라이브러리의 WebSocket 기능을 사용하기 위해서는 jeus-web-dd.xml에서 <library-ref>로 참조 설정을 한 후 다음과 같이 Spring 설정에 관련 bean을 추가해 주어야 한다.

<beans ...>
    <websocket:handlers>
        <websocket:mapping .../>
        <websocket:handshake-handler ref="handshakeHandler"/>
    </websocket:handlers>

    <bean id="handshakeHandler" class="org.springframework.web.socket.server.support.DefaultHandshakeHandler">
        <constructor-arg ref="upgradeStrategy"/>
    </bean>
    <bean id="upgradeStrategy" class="jeus.spring.websocket.JeusRequestUpgradeStrategy"/>
</beans>

4.2.0.RELEASE 버전 라이브러리를 사용할 경우 아래와 같이 설정할 수 있다.

<beans ...>
    <websocket:handlers>
        <websocket:mapping .../>
        <websocket:handshake-handler ref="handshakeHandler"/>
    </websocket:handlers>

    <bean id="handshakeHandler" class="jeus.spring.websocket.JeusHandshakeHandler"/>
</beans>

3.4. 호환성을 위한 웹 컨텍스트 레벨의 옵션 설정

이전 JEUS 버전이나 타 WAS와 호환성에 문제가 발생한 경우에 웹 컨텍스트 레벨에서 옵션을 설정하여 해결할 수 있다.

이전 JEUS와의 호환성

Servlet 2.4 표준 이전에는 표준 자체에 제약 사항이 제대로 기술되지 않은 경우가 많았다. 이는 Servlet 2.4, 2.5를 거치면서 보완되었고 JEUS에서도 자연스럽게 제약 사항이 생기게 되었다. 또한 JEUS도 여러 가지 문제점을 경험하고 이를 수정하면서 표준을 따르지 않는 동작들을 지원하지 않게 되었다. 이에 따라 JEUS 4에서 작성한 웹 애플리케이션이 JEUS 6에서 제대로 동작하지 않는 문제들이 발생할 수 있다.

JEUS는 Servlet 및 JSP 표준을 준수해야 할 책임이 있지만 기존 애플리케이션의 동작 호환성을 보장해야 할 책임도 있다. 하지만 이 두 가지 책임은 서로 상충하는 면이 있다. 주로 표준 준수를 위해서 jakarta.servlet 표준 API, 내부 동작을 수정했을 때, 기존의 잘못된 동작에 기반하여 작성한 애플리케이션이 있을 때 문제가 발생한다. 이런 상황에서는 기본적으로 표준 준수 요구 사항이 우선순위가 더 높기 때문에 해당 애플리케이션을 수정해야 할 필요가 있다. 왜냐하면 그 애플리케이션을 다른 웹 컨테이너에서는 실행할 수 없기 때문이다. 또한 시간이 지남에 따라 애플리케이션을 업그레이드하는 과정에서 서블릿 표준을 준수하는 프레임워크들과도 충돌하는 문제가 발생할 수 있다.

하지만 이러한 애플리케이션의 문제점을 초기에 파악하지 못하고 서비스 개시일을 앞두고 발견한 경우 애플리케이션의 수정 대신 JEUS의 동작을 수정하는 요구 사항이 발생한다. 이를 지원하기 위해서 JEUS에서는 하위 호환성을 위한 프로퍼티를 제공한다.

JSP 엔진에 관한 호환성 문제는 JSP 하위 호환성을 위한 웹 컨텍스트 레벨의 옵션 설정을 참고하고, 하위 호환성을 위해 JEUS에서 제공하는 프로퍼티에 대한 자세한 내용은 JEUS Reference 안내서의 호환성 제공 프로퍼티를 참고한다.

다른 WAS와의 호환성

Servlet 2.4 표준 이전에는 Servlet과 JSP를 직접 사용해서 웹 애플리케이션을 개발했지만 현재는 웹 프레임워크를 사용하는 추세이고, Java EE 5부터는 표준 웹 프레임워크로 JSF와 JSTL이 추가되었다. 이러한 웹 프레임워크는 주로 Servlet, JSP 스펙에 대한 참조 구현체인 Tomcat을 사용해서 개발하고 테스트하기 때문에 웹 프레임워크들이 비표준적인 기능을 지원할 경우에 다양한 호환성 관련 문제가 발생할 수 있다.

호환성에 문제가 발생했을 때 필요한 경우에 한해서 jeus-web-dd.xml에 설정할 수 있다. jeus-web-dd.xml의 설정에 대한 자세한 내용은 JEUS Reference 안내서의 웹 엔진 프로퍼티를 참고한다.

3.5. 부가 기능 사용을 위한 설정

JEUS에서 제공하는 다음의 부가 기능을 사용하려면 별도의 설정이 필요하다.

  • 정적 리소스를 처리하는 서블릿 매핑

    서블릿 표준에서는 Servlet 또는 JSP가 아닌 경우 WAS에서 제공하는 Default Servlet으로 처리하도록 되어 있다. 이를 ResourceServlet이라고 한다.

    다음과 같이 web.xml에서 <servlet-mapping> 설정에 jeus.servlet.servlets.ResourceServlet으로 매핑한다.

    정적 리소스를 처리하는 서블릿 매핑 : <web.xml>
    <servlet-mapping>
        <servlet-name>jeus.servlet.servlets.ResourceServlet</servlet-name>
        <url-pattern>/static/*</url-pattern>
    </servlet-mapping>
  • Spring 3.0 이상에서 mvc:default-servlet-handler 등록 방법

    JEUS 9에서는 Spring 3.0 이상에서 mvc:default-servlet-handler을 더이상 등록할 필요가 없다. Tomcat과 마찬가지로 "default"라는 이름으로 JEUS의 default servlet을 제공한다.

  • Servlet Multipart 기능 사용하는 경우 ProgressListener 등록 방법

    Servlet 3.0부터는 서블릿별로 multipart-config 설정을 등록해서 POST 요청의 multipart/form-data 처리를 웹 컨테이너에 맡길 수 있다. 이때 처리 진행 상황에 대한 이벤트를 받고 싶은 경우 ProgressListener를 등록한다.

    1. jeus-servlet.jar에 포함된 jeus.servlet.engine.multipart.ProgressListener 인터페이스를 구현해야 한다. 인터페이스에 관한 내용은 Servlet API 문서를 참고한다.

    2. 위 인터페이스 구현 클래스의 인스턴스를 HttpServletRequest attribute에 추가한다. 이름은 인터페이스 이름과 마찬가지로 jeus.servlet.engine.multipart.ProgressListener 이다.이때 주의할 사항은 반드시 getPart(), getParts() 또는 getParameter()를 호출하기 이전이어야 한다.

3.6. 웹 보안 설정

jeus-web-dd.xml을 통해 애플리케이션별로 적용할 웹 보안 설정에 대해 설명한다.

Redirect Location 보안 설정

애플리케이션은 jakarta.servlet.http.HttpServletResponse.sendRedirect(String location) 표준 API를 통해서 "302 Found" 응답을 보낼 수 있다. 이때 기본적으로 Location으로 넘겨주는 문자열에 대해서 아무런 확인을 하지 않고 그대로 URL로 전환해서 Location 헤더로 설정한다. 그렇기 때문에 악의적인 사용자가 CRLF injection과 같은 공격을 할 수 있다. 이를 방지하기 위해 애플리케이션은 jeus.servlet.security.RedirectStrategy 인터페이스를 구현해서 jeus-web-dd.xml에 설정할 수 있다.

다음은 jeus.servlet.security.RedirectStrategy 인터페이스를 설정한 예이다.

Redirect Location 보안 설정 인터페이스
package jeus.servlet.security;

import jakarta.servlet.http.HttpServletRequest;

public interface RedirectStrategy {
    /**
     * Makes the redirect URL.
     *
     * @param location the target URL to redirect to, for example "/login"
     */
    String makeRedirectURL(HttpServletRequest request, String location)
        throws IllegalArgumentException;
}

설정한 인터페이스를 다음과 같이 구현할 수 있다.

Redirect Location 보안 설정 구현 예
package jeus.servlet.security;
import jakarta.servlet.http.HttpServletRequest;

public class RejectCrlfRedirectStrategy implements RedirectStrategy {
    private Pattern CR_OR_LF = Pattern.compile("\\r|\\n");

    @Override
    String String makeRedirectURL(HttpServletRequest request, String location)
       throws IllegalArgumentException {
        if (CR_OR_LF.matcher(location).find()) {
           throw new IllegalArgumentException("invalid characters (CR/LF) in redirect location");
        }
       return makeAbsolute(location);
    }

    private String makeAbsolute(String location) {
       // make code for make absolute path
    }
}

jeus-web-dd.xml의 설정 방법은 다음과 같다.

Redirect Location 보안 설정 : <jeus-web-dd.xml>
...
<web-security>
    <redirect-strategy-ref>
    jeus.servlet.security.RejectCrlfRedirectStrategy
    </redirect-strategy-ref>
</web-security>
..

<redirect-strategy-ref>는 jeus.servlet.security.RedirectStrategy 인터페이스를 구현한 클래스 이름이다. 이 클래스는 웹 애플리케이션의 클래스 패스에 포함시키면 된다. 위에서 제시한 jeus.servlet.security.RejectCrlfRedirectStrategy의 경우 기본적으로 제공하는 RedirectStrategy이며 CR, LF 또는 CRLF가 있는 Location이 sendRedirect API로 넘어올 경우 500 에러가 발생한다.

그 외에도 CR, LF 또는 CRLF 문자열을 빈 문자열로 치환해주는 jeus.servlet.security.RemoveCrlfRedirectStrategy를 제공한다.

3.7. 보안 Role 매핑

web.xml에 정의된 보안 Role을 실제 시스템 사용자와 사용자 그룹에 설정해야 한다. 이 매핑은 jeus-web-dd.xml에 기술된다.

다음의 내용을 web.xml에 정의했다고 가정한다.

보안 Role 매핑 : <web.xml>
<web-app>
    <security-role>
        <role-name>manager</role-name>
    </security-role>
    <security-role>
        <role-name>developer</role-name>
    </security-role>
    <servlet>
        . . .
    </servlet>
    <security-constraint>
        <web-resource-collection>
            <web-resource-name>
                MyResource
            </web-resource-name>
            <url-pattern>/jsp/*</url-pattern>
            <http-method>GET</http-method>
            <http-method>POST</http-method>
        </web-resource-collection>
        <auth-constraint>
            <role-name>manager</role-name>
        </auth-constraint>
    </security-constraint>
</web-app>

위의 예제에서는 2개의 보안 Role이 선언되어 있다(굵은 글씨로 표현된 부분). 세 번째 굵은 글씨로 표현된 부분은 manager role이 <security-constraint> 태그에 어떻게 사용되는지 나타낸다.

이 매핑을 jeus-web-dd.xml에 기술하면 다음과 같다.

보안 Role 매핑 : <jeus-web-dd.xml>
<jeus-web-dd  xmlns="http://www.tmaxsoft.com/xml/ns/jeus" version="9">
     . . .
     <role-mapping>
         <role-permission>
             <principal>Peter</principal>
             <role>manager</role>
         </role-permission>
         <role-permission>
             <principal>Linda</principal>
             <role>developer</role>
         </role-permission>
     </role-mapping>
     . . .
</jeus-web-dd>

웹 애플리케이션이 실제의 시스템에 deploy될 때 "manager"와 "developer" Role을 시스템의 특정한 사용자로 바인딩해야 한다. 이 매핑은 <context><role-mapping>에 정의되어 있다. <role-mapping> 태그는 <role-permission> 태그를 포함한다. 이 태그를 이용하여 <principal-to-role> 매핑을 정의한다.

다음의 하위 태그들과 함께 바인딩된다. 예제에서 2개의 Role "manager"와 "developer"가 "Peter"와 "Linda"로 각각 매핑되어 있다.

태그 설명

<role>(1개, 필수)

web.xml에 정의된 <role-name> 값이며, 위의 예에서는 <role-name>이 "manager"이다.

<principal>(0개 이상)

<role-name>과 연계되어야 할 JEUS가 관리하는 사용자의 이름이다.

자세한 내용은 JEUS Security 안내서의 웹 모듈 보안 설정을 참고한다.

3.8. Symbolic Reference 매핑

Role이 실제 사용자에 매핑되듯이 EJB Reference, Resource Reference, 관리되는 객체의 Reference를 실제 시스템 자원에 매핑할 필요가 있다.

다음은 Symbolc Reference 매핑의 예이다.

Symbolc Reference 매핑 : <web.xml>
<web-app>
    . . .
    <ejb-ref>
        <ejb-ref-name>ejb/account</ejb-ref-name>
        <ejb-ref-type>Entity</ejb-ref-type>
        <home>com.mycompany.AccountHome</home>
        <remote>com.mycompany.Account</remote>
    </ejb-ref>
    <resource-ref>
        <res-ref-name>jdbc/EmployeeAppDB</res-ref-name>
        <res-type>javax.sql.DataSource</res-type>
        <res-auth>Container</res-auth>
        <res-sharing-scope>Shareable</res-sharing-scope>
    </resource-ref>
    <resource-env-ref>
        <resource-env-ref-name>
            jms/StockQueue
        </resource-env-ref-name>
        <resource-env-ref-type>
            jakarta.jms.Queue
        </resource-env-ref-type>
    </resource-env-ref>
    . . .
</web-app>

이 웹 애플리케이션을 등록하기 위해 web.xml의 <ejb-ref>, <resource-ref>, <resource-env-ref> 아래의 모든 Symbolc Reference 이름들이 실제 의도하는 자원의 JNDI 이름과 매핑되어야 한다. 이 매핑은 jeus-web-dd.xml에서 해당 <ejb-ref>, <res-ref>, <res-env-ref>를 추가하면 완성된다.

이 태그들은 다음의 하위 태그들을 포함한다.

  • <jndi-info>

    태그 설명

    <ref-name>

    Reference의 이름으로 web.xml과 서블릿 코드에 정의된 것과 같은 것이다.

    <export-name>

    JNDI Export Name은 <ref-name>이 가리키는 실제 리소스의 Export Name이다.

다음은 JNDI 이름이 위의 3개의 Reference와 매핑된 jeus-web-dd.xml의 예이다.

Symbolc Reference 매핑 : <jeus-web-dd.xml>
<jeus-web-dd  xmlns="http://www.tmaxsoft.com/xml/ns/jeus" version="9">
    . . .
    <ejb-ref>
        <jndi-info>
            <ref-name>ejb/account</ref-name>
            <export-name>AccountEJB</export-name>
        </jndi-info>
    </ejb-ref>
    <res-ref>
        <jndi-info>
            <ref-name>jdbc/EmployeeAppDB</ref-name>
            <export-name>EmployeeDB</export-name>
        </jndi-info>
    </res-ref>
    <res-env-ref>
        <jndi-info>
            <ref-name>jms/StockQueue</ref-name>
            <export-name>StockQueue</export-name>
        </jndi-info>
    </res-env-ref>
    . . .
</jeus-web-dd>

4. 웹 컨텍스트 모니터링

모니터링은 웹 컨텍스트의 정보를 확인하는 것이다. 웹 엔진에 배치되어 있는 웹 컨텍스트의 목록 및 현재 상태 등을 콘솔 툴을 사용하여 조회할 수 있다.

콘솔 툴 사용

다음과 같이 application-info 명령어를 수행하면 deploy된 웹 컨텍스트의 모든 정보가 조회된다. 명령어에 대한 자세한 내용은 JEUS Reference 안내서의 application-info를 참고한다.

application-info -type WAR

특정 웹 컨텍스트의 정보를 조회하려면 다음과 같이 -id <application-id> 옵션을 설정하여 명령어를 수행한다. 해당 ID의 웹 컨텍스트 정보가 조회된다.

application-info -id <application-id>

5. 웹 컨텍스트 제어

웹 컨텍스트를 제어한다는 것은 웹 컨텍스트의 상태를 변경하여 서비스를 변경하는 것을 의미한다. 콘솔 툴을 사용하여 웹 컨텍스트를 리로드하거나 중지 및 재개하는 등의 제어가 가능하다.

5.1. 웹 컨텍스트 리로드

배치되어 있는 웹 컨텍스트가 변경이 있거나 특별한 이유로 다시 초기화해야 할 필요가 있을 경우에 웹 컨텍스트를 제거한 후 다시 배치하지 않고, 변경사항을 반영하여 웹 컨텍스트를 리로드한다. 콘솔 툴을 사용하여 웹 컨텍스트를 리로드할 수 있다.

콘솔 툴 사용

다음과 같이 reload-web-context 명령어를 수행한다. 명령어에 대한 자세한 내용은 JEUS Reference 안내서의 reload-web-context를 참고한다.

reload-web-context -ctx <context-name>
seccessfully reloaded

5.2. 웹 컨텍스트의 비동기 요청에 대한 Thread Pool 모니터링

웹 컨텍스트가 비동기 요청(Asynchronous Request)을 처리할 때 JEUS가 제공하는 Thread Pool을 사용하려면 jeus-web-dd.xml의 <async-config>에 관련 정보를 설정해야 한다. <async-config>를 설정한 후에 JEUS에서 제공하는 Async Thread Pool 정보를 모니터링할 수 있다.

다음은 jeus-web-dd.xml의 설정 예이다. 설정된 정보에 따른 Async Thread Pool 정보는 콘솔 명령을 통해 확인할 수 있다.

<async-config> 설정 : <jeus-web-dd.xml>
...
<async-config>
    <dispatch-thread-pool>
        <min>5</min>
        <max>20</max>
    </dispatch-thread-pool>
    <background-thread-pool>
        <min>5</min>
        <max>20</max>
    </background-thread-pool>
    <async-timeout-millis>120000</async-timeout-millis>
</async-config>
...

다음은 설정 태그에 대한 설명이다.

태그 설명

<dispatch-thread-pool>

AsyncContext#dispatch를 호출할 때 사용할 Thread Pool의 최소 Thread 값과 최대 Thread 값을 설정한다.

<background-thread-pool>

AsyncContext#start를 호출할 때 사용할 Thread Pool의 최소 Thread 값과 최대 Thread 값을 설정한다.

<async-timeout-millis>

Asynchronous 작업을 수행할 때 웹 컨테이너가 타임아웃을 처리하는 데 기준이 되는 시간을 설정한다. 애플리케이션이 AsyncContext#setTimeout을 호출하지 않은 경우 AsyncContext#getTimeout일 때 설정된 값을 리턴한다.

(기본값: 30000, 단위: ms)

콘솔 툴 사용

콘솔을 사용한 웹 컨텍스트의 Thread 정보 조회 방법은 다음과 같이 thread-info 명령어를 수행한다. thread-info 명령어에 대한 자세한 내용은 JEUS Reference 안내서의 thread-info를 참고한다.

thread-info -server <server-name>

5.3. 웹 컨텍스트 중지

배치되어 서비스되고 있는 웹 컨텍스트를 관리자가 특정 시간이나 특별한 사유로 웹 엔진 내의 특정 웹 컨텍스트의 서비스를 잠깐 중지할 수 있다. 중지된 웹 컨텍스를 다시 리로드할 수도 있다. 웹 컨텍스트 리로드 방법에 대한 자세한 내용은 웹 컨텍스트 재개를 참고한다.

콘솔 툴 사용

다음과 같이 suspend-web-component 명령어를 수행한다. suspend-web-component 명령어에 대한 자세한 내용은 JEUS Reference 안내서의 suspend-web-component를 참고한다.

suspend-web-component -server <server-name> -ctx <context> -svl <servlet>

또는

suspend-web-component -cluster <cluster-name> -ctx <context> -svl <servlet>

5.4. 웹 컨텍스트 재개

중지된 웹 컨텍스트를 콘솔 툴을 사용하여 다시 재개할 수 있다.

콘솔 툴 사용

콘솔 툴을 사용하여 중지된 웹 컨텍스트를 재개하려면 다음과 같이 resume-web-component 명령어를 수행한다. resume-web-component 명령어에 대한 자세한 내용은 JEUS Reference 안내서의 resume-web-component를 참고한다.

resume-web-component -server <server-name> -ctx <context> -svl <servlet>

또는

resume-web-component -cluster <cluster-name> -ctx <context> -svl <servlet>

6. 웹 컨텍스트 튜닝

최고의 성능을 위해 웹 컨텍스트의 설정(jeus-web-dd.xml)을 튜닝할 때 다음과 같은 사항들을 고려해야 한다.

  • 유저 로그는 버퍼의 크기를 증가시키고 "stdout"의 사용을 자제한다.

  • JSP 엔진은 사용하지 않으면 OFF시킨다.

  • <enable-reload>와 <check-on-demand> 옵션은 항상 OFF시킨다. 이 옵션은 클래스 변경이 잦은 개발 환경에서만 사용한다.

  • 가능하면 파일 Caching 기능을 사용한다. 최대한 많은 양의 정적 콘텐츠 디렉터리를 파일 Caching 태그에 설정한다. Cache의 <max-idle-time>과 <max-cache-memory>의 값을 크게 설정한다(무한대로 설정해도 무방하다).

7. 비동기식 처리 프로그래밍 가이드

본 절에서는 Servlet 3.0부터 추가된 비동기식 처리(Asynchronous Processing)에 관해서 애플리케이션 개발자가 반드시 숙지해야 할 내용에 대해 설명한다.

7.1. Servlet 표준에 따른 주의사항

애플리케이션 개발자가 직접 Thread를 다뤄야 하는 비동기식 처리 프로그래밍의 특성으로 인해 다음과 같은 실수가 발생할 수 있다.

이 중에는 테스트할 때는 발견하지 못하다가 실제 서비스 중에 부하가 많이 발생했을 때 나타나는 문제들이 있다. 이는 JEUS에서 보장할 수 없는 문제들로 이에 대한 책임은 애플리케이션 개발자에게 있다.

본 절에서 설명하는 내용은 서블릿 표준을 기반으로 작성한 것이다(JEUS에만 해당하는 것이 아니다).

Thread의 과다 생성

Thread를 과도하게 많이 생성하면 JVM의 성능이 떨어지거나 OutOfMemoryError 상태에 빠진다. 다음은 Thread를 과도하게 많이 생성한 잘못된 비동기식 처리 프로그램밍의 작성 예이다.

잘못된 비동기식 처리 프로그래밍 작성 예 (1) : <WrongAsyncServlet1.java>
import jakarta.servlet.AsyncContext;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;

@WebServlet(urlPatterns = "/WrongAsyncServlet1", asyncSupported = true)
public class WrongAsyncServlet1 extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {
        AsyncContext asyncContext = req.startAsync();
        Thread thread = new Thread(new TestRunnable(asyncContext));
        thread.start();
    }

    private class TestRunnable implements Runnable {
        private final AsyncContext asyncContext;

        public TestRunnable(AsyncContext asyncContext) {
            this.asyncContext = asyncContext;
        }

        @Override
        public void run() {
        }
    }
}

비동기식 처리가 필요한 요청은 서비스 상황에 따라서 동시에 수천 개가 될 수도 있다. 그때마다 새로운 Thread를 생성하면 동시에 수천 개의 Thread가 존재하게 된다. 이로 인해서 JVM의 성능이 심각하게 저하되거나 "OutOfMemoryError: unable to create new native thread"와 같은 에러가 발생할 수 있다. 따라서 가능하면 jakarta.servlet.AsyncContext#start(Runnable) 메소드를 사용하거나 직접 관리하는 Thread Pool을 사용할 것을 권장한다.

AsyncContext asyncContext = req.startAsync();
asyncContext.start(new TestRunnable(asyncContext));
서로 다른 Thread 간에 공유하면 안 되는 객체의 공유

서로 다른 Thread 간에 공유하면 안 되는 멀티 스레드에 안전하지 않은 객체를 함께 사용하면 성능 저하, 교착 상태 또는 데이터의 무결성을 침해할 수 있다.

다음은 서로 다른 Thread 간에 공유하면 안 되는 객체를 공유한 잘못된 비동기식 처리 프로그램밍의 작성 예이다.

잘못된 비동기식 처리 프로그래밍 작성 예 (2) : <WrongAsyncServlet2.java>
import jakarta.servlet.AsyncContext;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletInputStream;
import jakarta.servlet.ServletOutputStream;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;

@WebServlet(urlPatterns = "/WrongAsyncServlet2", asyncSupported = true)
public class WrongAsyncServlet2 extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {
        AsyncContext asyncContext = req.startAsync();
        asyncContext.start(new TestRunnable(asyncContext));

        byte[] buffer = new byte[1024];
        ServletInputStream inputStream = req.getInputStream();
        inputStream.read(buffer);

        ServletOutputStream outputStream = resp.getOutputStream();
        outputStream.write(buffer);
    }

    private class TestRunnable implements Runnable {
        private final AsyncContext asyncContext;

        public TestRunnable(AsyncContext asyncContext) {
            this.asyncContext = asyncContext;
        }

        @Override
        public void run() {
            byte[] buffer = new byte[1024];
            try {
              ServletInputStream inputStream = asyncContext.getRequest().getInputStream();
              inputStream.read(buffer);
            } catch (IOException e) {
                //log error
                return;
            }

            ServletOutputStream outputStream;
            try {
                outputStream = asyncContext.getResponse().getOutputStream();
              outputStream.write(buffer);
            } catch (IOException e) {
                //log error
                return;
            }
        }
    }
}

위의 예제는 Request 객체로부터 얻을 수 있는 ServletInputStream이나 Reader 객체 또는 Response 객체로부터 얻을 수 있는 ServletOutputStream이나 Writer 객체를 서로 다른 Thread에서 함께 사용하는 경우이다. Servlet 표준에 의거하여 JEUS는 이러한 동작에 대해 안정성을 보장하지 않는다. 필터 또는 서블릿에서 비동기식 처리를 시작해서 다른 Thread로 동작을 넘겼다면 그 이후 필터 및 서블릿 코드에서는 가능하면 모든 동작을 비동기식 처리를 수행하는 Thread 측에 맡겨야 한다. 이는 멀티 스레드 프로그래밍에 대한 잘못된 이해로 인한 성능 저하, 교착 상태(Dead Lock)나 데이터 무결성 침해를 피하는 방법이기도 하다.

startAsync()로 직접 호출하지 않은 필터 또는 서블릿에서 비동기식 처리가 시작됐는지 확인하려면 jakarta.servlet.ServletRequest#isAsyncStarted()를 체크한다.

비동기식으로 처리하는 Thread에서 jakarta.servlet.RequestDispatcher 사용

비동기식으로 처리하는 Thread에서 jakarta.servlet.RequestDispatcher를 사용하면 원하는 데로 프로그램이 동작하지 않거나 Exception이 발생할 수 있다.

다음은 Thread에서 jakarta.servlet.RequestDispatcher를 사용한 잘못된 비동기식 처리 프로그램밍의 작성 예이다.

잘못된 비동기식 처리 프로그래밍 작성 예 (3) : <WrongAsyncServlet3.java>
import jakarta.servlet.AsyncContext;
import jakarta.servlet.RequestDispatcher;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;

@WebServlet(urlPatterns = "/WrongAsyncServlet3", asyncSupported = true)
public class WrongAsyncServlet3 extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {
        AsyncContext asyncContext = req.startAsync();
        asyncContext.start(new TestRunnable(asyncContext));
    }

    private class TestRunnable implements Runnable {
        private final AsyncContext asyncContext;

        public TestRunnable(AsyncContext asyncContext) {
            this.asyncContext = asyncContext;
        }

        @Override
        public void run() {
            // ...... do something
            RequestDispatcher requestDispatcher =
                asyncContext.getRequest().getRequestDispatcher("/views/report.jsp");
            try {
                requestDispatcher.forward(asyncContext.getRequest(),
                 asyncContext.getResponse());
            } catch (ServletException e) {
                // log error
            } catch (IOException e) {
                // log error
            }
        }
    }
}

비동기식 처리를 진행하는 Thread에서 다른 서블릿이나 JSP로 dispatch하기 위한 방법은 jakarta.servlet.AsyncContext에서 제공하고 있다.

asyncContext.dispatch("/views/report.jsp");

7.2. 기타 주의사항

비동기식 처리 프로그래밍에 대해 표준에서 설명하고 있는 내용 외에도 주의해야 할 사항이 존재한다.

  • AsyncContext.start(Runnable)를 호출했을 때 Thread에서 java.util.concurrent.RejectedExecutionException이 발생하는 경우

    AsyncContext.start는 새로운 태스크(Task)를 나타낸다. 이 태스크를 수행하기 위해서는 Thread가 필요한데, 일반적으로 Thread는 제한된 리소스이기 때문에 Thread Pool을 사용한다. 새로운 태스크를 Thread Pool에 할당했을 때 해당 태스크를 수행할 수 있는 Thread가 하나도 없다면 큐에 쌓이게 된다.

    일반적으로 오랜 수행시간을 필요로하는 태스크의 경우에 Thread의 처리가 지연되면 태스크들은 큐에 적채된다. 큐 역시 사이즈(기본값(최댓값): 4096)가 제한되어 있으므로 결국에는 새로운 태스크를 할당할 수 없는 상태가 된다. 이러한 상황에 도달하면 java.util.concurrent.RejectedExecutionException이 발생한다. 그러므로 애플리케이션 프로그래머는 이러한 상태를 고려해서 비동기식 처리를 프로그래밍해야 한다.

  • WebtoB, Apache 등 웹 서버와 통신하는 방식의 리스너에서 비동기식 처리를 하는 경우

    WebtoB, AJP13 리스너는 제한된 수의 TCP 커넥션들을 사용한다. 웹 서버에서 WAS로 전달해야 하는 HTTP 요청이 한꺼번에 몰릴 경우 WAS의 처리 용량를 넘어서 결국 서비스 불능 상태가 될 수 있기 때문이다. 이러한 방식은 본래의 동기식 처리를 타겟으로 디자인된 것이다. 웹 서버로부터 HTTP 요청이 전달되면 WAS는 그 요청이 전달된 TCP 커넥션에 대한 소유권을 Servlet에게 넘기는데 해당 Servlet이 최대한 빠르게 HTTP 응답을 보내고 TCP 커넥션을 리턴할 거라고 가정하고 있다.

    하지만 비동기식 처리는 Servlet이 빠르게 HTTP 응답을 보낸다고 가정할 수 없다. 만약 웹 서버와의 TCP 커넥션들을 모두 비동기식 처리에 사용하게 되는 경우 일반적인 HTTP 요청 처리를 일시적으로 하지 못하게 된다.

    이러한 문제를 해결하기 위해서는 다음과 같은 방법들을 고려해볼 수 있다.

    • 웹 서버에서 비동기식 요청에 대한 URL 매핑 등을 통해 Reverse Proxy로 처리한다. Reverse Proxy의 타겟은 HTTP 리스너이다.

    • 웹 서버와의 TCP 커넥션 수를 충분히 지정해서 모든 커넥션이 비동기식 처리를 하는 상황이 없도록 만든다.

      WebtoB는 4.1.6 이상을 사용하길 권장한다.