Outsider's Dev Story

Stay Hungry. Stay Foolish. Don't Be Satisfied.
RetroTech 팟캐스트 44BITS 팟캐스트

[Spring 레퍼런스] 22장 JMS (Java Message Service) #2

이 문서는 개인적인 목적이나 배포하기 위해서 복사할 수 있다. 출력물이든 디지털 문서든 각 복사본에 어떤 비용도 청구할 수 없고 모든 복사본에는 이 카피라이트 문구가 있어야 한다.



22.4 메시지 수신

22.4.1 동기 수신

JMS가 보통 비동기 처리와 관련되어 있지만 동기적으로 메시지를 처리할 수도 있다. receive(..) 메서드를 오버로드해서 동기적으로 처리할 수 있다. 동기적으로 메시지를 받는 동안 호출하는 스레드는 메시지를 이용할 수 있을 때까지 블럭킹된다. 호출하는 스레드가 불명확하게 블럭킹될 가능성이 있으므로 이는 위험한 작업이 될 수 있다. receiveTimeout 프로퍼티는 리시버가 메시지를 얼마나 오랫동안 기다려야 하는지를 지정한다.

22.4.2 비동기 수신 - 메시지 주도 POJO

EJB의 메시지 주도 빈(MDB, Message-Driven Bean)과 비슷한 방법으로 메시지 주도 POJO(MDP, Message-Driven POJO)는 JMS 메시지에 대해서 리시버로 동작한다. javax.jms.MessageListener 인터페이스를 반드시 구현해야 한다는 점이 MDP가 가진 하나의 제약사항(하지만 아래의 MessageListenerAdapter 클래스에 대한 논의를 참고해라.)이다. POJO가 여러 스레드에서 메시지를 받는 경우라면 스레드세이프하게 구현해야 한다는 점이 중요하다는 것을 기억해야 한다.

아래는 MDP 구현체의 간단한 예시이다.

import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.MessageListener;
import javax.jms.TextMessage;

public class ExampleListener implements MessageListener {

  public void onMessage(Message message) {
    if (message instanceof TextMessage) {
      try {
        System.out.println(((TextMessage) message).getText());
      }
      catch (JMSException ex) {
        throw new RuntimeException(ex);
      }
    }
    else {
      throw new IllegalArgumentException("Message must be of type TextMessage");
    }
  }
}

MessageListener를 구현했으면 메시지 리스너 컨테이너를 만들 차례다.

스프링이 제공하는 메시지 리스너 컨테이너중 하나를 어떻게 정의하고 설정했는지 아래 예제를 참고해라.(이 경우에는 DefaultMessageListenerContainer)


<bean id="messageListener" class="jmsexample.ExampleListener" />


<bean id="jmsContainer" class="org.springframework.jms.listener.DefaultMessageListenerContainer">
  <property name="connectionFactory" ref="connectionFactory"/>
  <property name="destination" ref="destination"/>
  <property name="messageListener" ref="messageListener" />
</bean>

다양한 메시지 리스너 컨테이너의 각 구현체가 지원하는 기능과 자세한 내용은 스프링 자바독을 참고해라.

22.4.3 SessionAwareMessageListener 인터페이스

SessionAwareMessageListener 인터페이스는 스프링에 특화된 인터페이스로 JMS MessageListener와 비슷한 계약(contract)을 제공하지만 Message를 받은 JMS Session에 접근해서 메시지를 처리할 수 있는 메서드도 제공한다.

package org.springframework.jms.listener;

public interface SessionAwareMessageListener {
  void onMessage(Message message, Session session) throws JMSException;
}

구현한 MDP가 어떤 메시지에도 응답할 수 있기를 (onMessage(Message, Session) 메서드에서 제공된 Session을 사용해서) 원한다면 MDP가 이 인터페이스 (표준 JMS MessageListener 인터페이스를 선호하는 경우)를 구현하도록 할 수 있다. 스프링이 제공하는 모든 메시지 리스너 컨테이너 구현체는 MessageListener나 SessionAwareMessageListener 인터페이스를 구현한 MDP를 지원한다. SessionAwareMessageListener를 구현한 클래스는 이 인터페이스로 스프링에 의존성을 가진다는 것을 유의해야 한다. 이를 사용할 것인지 아닌지는 어플리케이션 개발자나 아키텍쳐의 선택이다.

SessionAwareMessageListener 인터페이스의 'onMessage(..)' 메서드가 JMSException를 던진다는 것을 기억해라. 표준 JMS MessageListener 인터페이스와는 달리 SessionAwareMessageListener 인터페이스를 사용했을 때 던저진 예외를 처리하는 것은 클라이언트 코드의 몫이다.

22.4.4 MessageListenerAdapter

MessageListenerAdapter 클래스는 스프링에서 비동기 메시지를 지원하는 마지막 컴포넌트다. 간단히 말하면 이 클래스는 거의 모든 클래스를 MDP로 노출하도록 해준다. (물론 몇가지 제약사항이 있다.)

다음의 인터페이스 정의를 보자. 이 인터페이스가 MessageListener나 SessionAwareMessageListener를 상속하지 않았음에도 MessageListenerAdapter를 사용해서 MDP로 사용할 수 있다. 다양한 메시지 처리 메서드가 어떻게 받아서 처리할 수 있는 다양한 Message의 내용에 따라 타입을 지정하는지도 참고해라.

public interface MessageDelegate {
  void handleMessage(String message);

  void handleMessage(Map message);

  void handleMessage(byte[] message);

  void handleMessage(Serializable message);
}
public class DefaultMessageDelegate implements MessageDelegate {
  // 구현부는 생략했다...
}

특히 MessageDelegate 인터페이스의 구현체 (위의 DefaultMessageDelegate 클래스)가 어떻게 JSM에 대한 의존성은 전혀 갖지 않는지 봐라. 이는 다음의 설정으로 MDP로 만들 POJO다.


<bean id="messageListener" class="org.springframework.jms.listener.adapter.MessageListenerAdapter">
  <constructor-arg>
    <bean class="jmsexample.DefaultMessageDelegate"/>
  </constructor-arg>
</bean>


<bean id="jmsContainer" class="org.springframework.jms.listener.DefaultMessageListenerContainer">
  <property name="connectionFactory" ref="connectionFactory"/>
  <property name="destination" ref="destination"/>
  <property name="messageListener" ref="messageListener" />
</bean>

다음은 또다른 MDP의 예시로 JMS TextMessage 메시지만 받아서 처리할 수 있다. 어떻게 메시지 처리 메서드가 실제로는 'receive'로 (MessageListenerAdapter의 메시지 처리 메서드의 이름은 기본적으로 'handleMessage'이다.) 호출되게 설정하는지 참고해라. 'receive(..)' 메서드가 JMS TextMessage 메시지에만 받고 응답하도록 타입을 지정하는 방법도 참고해라.

public interface TextMessageDelegate {
  void receive(TextMessage message);
}
public class DefaultTextMessageDelegate implements TextMessageDelegate {
  // 구현부는 생략했다...
}

MessageListenerAdapter의 설정은 다음과 같을 것이다.

<bean id="messageListener" class="org.springframework.jms.listener.adapter.MessageListenerAdapter">
  <constructor-arg>
    <bean class="jmsexample.DefaultTextMessageDelegate"/>
  </constructor-arg>
  <property name="defaultListenerMethod" value="receive"/>
  <!-- 자동 메시지 컨텍스트 추출을 원하지 않는다 -->
  <property name="messageConverter">
    <null/>
  </property>
</bean>

위의 'messageListener'가 TextMessage가 아닌 다른 타입의 JMS Message를 받는다면 IllegalStateException가 던져질 것이다.(그리고 이어진 메시지는 무시할 것이다.) 핸들러 메서드가 void가 아닌 값을 반환하는 경우 자동적으로 응답 Message를 다시 보내는 것은 MessageListenerAdapter클래스의 또다른 기능이다. 다음 인터페이스와 클래스를 보자.

public interface ResponsiveTextMessageDelegate {
  // 반환 타입을 잘 봐라...
  String receive(TextMessage message);
}
public class DefaultResponsiveTextMessageDelegate implements ResponsiveTextMessageDelegate {
  // 구현부는 생략했다...
}

위의 DefaultResponsiveTextMessageDelegate를 MessageListenerAdapter와 함께 사용한 경우 'receive(..)' 메서드의 실행후 반환된 null이 아닌 모든 값은 TextMessage로 변환될 것이다.(기본 설정에 따라) 그 다음 변환된 TextMessage는 원래의 Message의 JMS Reply-To 프로퍼티에 정의된(존재하는 경우) Destination이나 MessageListenerAdapter에 설정된 기본 Destination(설정되었다면)으로 보내진다. Destination가 없다면 InvalidDestinationException가 던져질 것이다.(이 예외는 무시되지 않고 호출 스택에 추가될 것이다.)

22.4.5 트랜잭션에서의 메시지 처리

트랜잭션에서 메시지 리스너를 호출하려면 리스너 컨테이너만 재설정하면 된다.

리스너 컨테이너 정의의 sessionTransacted 플래그로 로컬 리소스 트랜잭션은 활성화할 수 있다. 로컬 리소스 트랜잭션을 활성화하면 활성화된 JMS 트랜잭션내에서 각 메시지 리스너 호출을 할 수 있고 리스너 실행이 실패한 경우 메시지 수신이 롤백된다. 응답 메시지 전송 (SessionAwareMessageListener로)도 같은 로컬 트랜잭션에 포함되어 있지만 다른 리소스에 대한 작업(데이터베이스 접근 같은)은 독립적으로 수행될 것이다. 이는 데이터베이스 처리는 커밋되었지만 메시지 처리는 커밋이 실패한 경우를 포함해서 리스너 구현체에서 중복 메시지 탐지를 해야 한다.

<bean id="jmsContainer" class="org.springframework.jms.listener.DefaultMessageListenerContainer">
  <property name="connectionFactory" ref="connectionFactory"/>
  <property name="destination" ref="destination"/>
  <property name="messageListener" ref="messageListener"/>
  <property name="sessionTransacted" value="true"/>
</bean>

외부에서 관리하는 트랜잭션에 참여하려면 트랜잭션 관리자를 설정하고 외부 관리 트랜잭션을 지원하는 리스너 컨테이너(보통 DefaultMessageListenerContainer)를 사용해야 한다.

XA 트랜잭션에 참가하도록 메시지 리스너 컨테이너를 설정하려면 JtaTransactionManager(이 트랜잭션 관리자는 기본적으로 Java EE 서버의 트랜잭션 하위시스템에 위임한다.)를 설정해야 한다. 여기서 의존 JMS ConnectionFactory는 XA 기능이 있어야 하고 JTA 트랜잭션 코디네이터(JTA transaction coordinator)가 적절히 등록되어 있어야 한다! (Java EE 서버의 JNDI 리소스 설정을 확인해라.) 이는 메시지 수신이 데이터베이스 접근과 함께 같은 트랜잭션에 있도록 한다.(통일된 커밋 개념을 가지지만 XA 트랜잭션 로그 비용이 크다.)

<bean id="transactionManager" class="org.springframework.transaction.jta.JtaTransactionManager"/>

앞에서 본 컨테이터 설정에 위 부분을 추가하면 된다. 컨테이너가 나머지를 관리할 것이다.

<bean id="jmsContainer" class="org.springframework.jms.listener.DefaultMessageListenerContainer">
  <property name="connectionFactory" ref="connectionFactory"/>
  <property name="destination" ref="destination"/>
  <property name="messageListener" ref="messageListener"/>
  <property name="transactionManager" ref="transactionManager"/>
</bean>


22.5 JCA 메시지 종점(Message Endpoints)에 대한 지원

스프링 2.5부터 JCA에 기반한 MessageListener 컨테이너도 지원하기 시작했다. JmsMessageEndpointManager는 프로바이더의 ResourceAdapter 클래스 이름으로 ActivationSpec 클래스 이름을 자동적으로 결정하려고 할 것이다. 그래서 다음 예제에서 보듯이 스프링의 지네릭 JmsActivationSpecConfig을 제공하는 것이 보통 가능하다.

<bean class="org.springframework.jms.listener.endpoint.JmsMessageEndpointManager">
  <property name="resourceAdapter" ref="resourceAdapter"/>
  <property name="activationSpecConfig">
    <bean class="org.springframework.jms.listener.endpoint.JmsActivationSpecConfig">
      <property name="destinationName" value="myQueue"/>
    </bean>
  </property>
  <property name="messageListener" ref="myMessageListener"/>
</bean>

또는 주어진 ActivationSpec 객체에 JmsMessageEndpointManager를 설정할 수 있다. ActivationSpec 객체도 JNDI 검색 (를 사용해서)에서 올 것이다.

<bean class="org.springframework.jms.listener.endpoint.JmsMessageEndpointManager">
  <property name="resourceAdapter" ref="resourceAdapter"/>
  <property name="activationSpec">
    <bean class="org.apache.activemq.ra.ActiveMQActivationSpec">
      <property name="destination" value="myQueue"/>
      <property name="destinationType" value="javax.jms.Queue"/>
    </bean>
  </property>
  <property name="messageListener" ref="myMessageListener"/>
</bean>

예제에서 설명한대로 스프링의 ResourceAdapterFactoryBean를 사용하면 로컬로 대상 ResourceAdapter를 설정한다.

<bean id="resourceAdapter" class="org.springframework.jca.support.ResourceAdapterFactoryBean">
  <property name="resourceAdapter">
    <bean class="org.apache.activemq.ra.ActiveMQResourceAdapter">
      <property name="serverUrl" value="tcp://localhost:61616"/>
    </bean>
  </property>
  <property name="workManager">
    <bean class="org.springframework.jca.work.SimpleTaskWorkManager"/>
  </property>
</bean>

지정한 WorkManager도 환경에 특화된 스레드 풀을 가리킬 것이다.(보통 SimpleTaskWorkManager's "asyncTaskExecutor" 프로퍼티를 통해서) 다중 아답터를 사용한다면 모든 ResourceAdapter 인스턴스에 공유 스레드 풀을 정의하는 것을 고려해 봐라.

일부 환경(WebLogic 9 이상 등)에서는 JNDI (를 사용해서)에서 전체 ResourceAdapter 객체를 가져올 것이다. 스프링 기반의 메시지 리스너는 서버가 제공하는 ResourceAdapter와 상호작용할 수 있고 서버에 내장된 WorkManager도 사용할 수 있다.

자세한 내용은 JmsMessageEndpointManager, JmsActivationSpecConfig, ResourceAdapterFactoryBean의 JavaDoc를 참고해라.

스프링은 JMS에 의존하지 않는 일반적인 JCA 메시지 엔트포인트 관리자 org.springframework.jca.endpoint.GenericMessageEndpointManager도 제공한다. 이 컴포넌트로 모든 종류의 메시지 리스너(CCI MessageListener등)와 프로바이더에 특화된 ActivationSpec 객체를 사용할 수 있다. 사용하는 커넥터의 실제 기능은 JCA 프로바이더의 문서를 참고하고 스프링에 특화된 자세한 설정은 GenericMessageEndpointManager JavaDoc을 참고해라.

Note
JCA 기반의 메시지 엔드포인트 관리는 EJB 2.1 메시지 주도 빈과 아주 비슷하다. JCA 기반의 메시지 엔드포인트 관리도 같은 의존 리소스 프로바이더 규약을 사용한다. EJB 2.1 MDB에서처럼 JCA 프로바이더가 지원하는 모든 메시지 리스너 인터페이스는 스프링 컨텍스트에서도 사용할 수 있다. 하지만 JMS가 JCA 엔드포인트 관리 규약에서 사용하는 가장 일반적인 엔드포인트 API이므로 스프링의 JMS 지원이 확실히 '편리하다'.


22.6 JMS 네임스페이스 지원

스프링 2.5에는 JMS 설정을 간략히 할 수 있는 XML 네임스페이스가 도입되었다. JMS 네임스페이스 요소를 사용하려면 JMS 스키마를 참조해야 한다.


<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:jms="http://www.springframework.org/schema/jms"
    xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/jms http://www.springframework.org/schema/jms/spring-jms-3.0.xsd">

  
</beans>

네임스페이스는 두개의 최상위 요소 <listener-container/>와 <jca-listener-container/>로 구성되어 있고 둘 다 하나 이상의 <listener/> 자식 요소를 가질 것이다. 다음은 두 리스터의 기본 설정에 대한 예제이다.

<jms:listener-container>
  <jms:listener destination="queue.orders" ref="orderService" method="placeOrder"/>

  <jms:listener destination="queue.confirmations" ref="confirmationLogger" method="log"/>
</jms:listener-container>

위의 예제는 Section 22.4.4, “MessageListenerAdapter”에서 보여준 두 리스너 컨테이너 빈 정의와 두 MessageListenerAdapter 빈 정의를 생성하는 예제와 같다. 위에서 보여준 속성을 조금 더 설명하자면 listener 요소는 여러가지 선택적인 값을 포함할 수 있다. 다음 표에 사용가능한 모든 속성이 나와 있다.

Table 22.1. JMS 요소의 속성

속성 설명
id 제공하는 리스너 컨테이너의 빈(bean) 이름. 지정하지 않을 경우 빈 이름을 자동으로 생성한다.
destination
(필수)
해당 리스너의 목적지 이름으로 DestinationResolver 전략으로 처리한다.
ref
(필수)
핸들러 객체의 빈 이름.
method 호출할 핸들러 메서드의 이름. ref가 MessageListener나 스프링 SessionAwareMessageListener를 가리킨다면 이 속성을 제외할 것이다.
response-destination 응답 메시지를 보낼 기본 응답 목적지의 이름. 이 이름은 요청 메시지에 "JMSReplyTo" 필드가 없는 경우에 적용될 것이다. 이 목적지의 타입은 리스너 컨테이너의 "destination-type" 속성으로 결정한다. Note: 이 값은 각 최종 객체가 응답 메시지로 잘 변환되도록 반환 값을 가진 리스너 메서드에만 적용한다.
subscription 존재한다면 지속가능한(durable) 구독의 이름.
selector 해당 리스너에 대한 선택적인 메시지 선택자.


<listener-container/> 요소도 여러가지 선택적인 속성을 받는다. 그래서 기본 JMS 설정과 리소스 참조, 다양한 전략(예를 들면 taskExecutor와 destinationResolver)을 커스터마이징할 수 있다. 이러한 속성을 사용해서 편리한 네임스페이스의 장점을 이용하면서 리스너 컨테이너를 커스터마이징해서 정의할 수 있다.

<jms:listener-container connection-factory="myConnectionFactory"
      task-executor="myTaskExecutor"
      destination-resolver="myDestinationResolver"
      transaction-manager="myTransactionManager"
      concurrency="10">

  <jms:listener destination="queue.orders" ref="orderService" method="placeOrder"/>

  <jms:listener destination="queue.confirmations" ref="confirmationLogger" method="log"/>
</jms:listener-container>

다음 표에 사용할 수 있는 모든 속성이 나와 있다. 각 프로퍼티의 자세한 내용은 AbstractMessageListenerContainer와 그 하위 클래스의 클래스 수준 JavaDoc을 참고해라. Javadoc에는 트랜잭션 선택과 메시지 반환 시나리오에 대한 논의도 나와 있다.

Table 22.2. JMS 요소의 속성

속성 설명
container-type 해당 리스너 컨테이터의 타입. 사용할 수 있는 옵션은 default, simple, default102, simple102가 있다.(기본값은 'default'이다.)
connection-factory JMS ConnectionFactory 빈에 대한 참조(기반 빈 이름은 'connectionFactory'이다.)
task-executor JMS 리스너 인보커에 대한 스프링 TaskExecutor 참조.
destination-resolver JMS Destinations 처리의 DestinationResolver 전략에 대한 참조.
message-converter JMS 메시지를 리스너 메시지 인자로 변환하는 MessageConverter 전략에 대한 참조. 기본값은 SimpleMessageConverter이다.
destination-type 해당 리스너에 대한 JMS 목적지 타입으로 queue, topic, durableTopic가 있다. 기본값은 queue이다.
client-id 해당 리스너 컨테이너에 대한 JMS 클라이언트 아이디. 지속가능한 구독을 사용하는 경우에는 지정해야 한다.
cache JMS 리소스에 대한 캐시 수준으로 none, connection, session, consumer, auto의 값을 사용할 수 있다. 기본값(auto)에서 외부 트랜잭션 관리자를 지정하지 않는 한 캐시 수준은 사실상 "consumer"가 될 것이다. 외부 트랜잭션 관리자를 지정한 경우 기본값이 none가 될 것이다.(주어진 ConnectionFactory의 Java EE 방식 트랜잭션 관리가 XA식 풀(pool)이라고 가정한다.)
acknowledge 네이티브 JMS 확인(acknowledge) 모드로 auto, client, dups-ok, transacted의 값을 사용할 수 있다. transacted의 값은 로컬 트랜잭션이 적용된 Session을 활성화한다. 아니면 아래에서 설명하는 transaction-manager 속성을 지정해라. 기본값은 auto이다.
transaction-manager 외부 PlatformTransactionManager(보통 XA기반 트랜잭션 코디네이터로 예를 들면 스프링의 JtaTransactionManager이다)에 대한 참조. 지정하지 않은 경우 네이티브 확인을 사용한다. ("acknowledge" 속성 참고)
concurrency 각 리스너를 시작할때 동시에 사용할 세션/컨슈머의 수. 최대 수를 숫자로 지정하거나(예: "5") 최대/최소 범위를 지정(예: "3-5")할 수 있다. 지정한 최소값은 힌트값으로만 사용하고 런타임에서는 무시한다. 기본값은 1이다. 토픽(topic) 리스너이거나 큐의 순서가 중요한 경우에는 concurrency를 1로 지정하고 일반적인 큐에는 1 이상으로 지정하는 것을 고려해봐라.
prefetch 단일 세션에 로드할 최대 메시지의 수. 동시성 컨슈머의 기아상태(starvation)를 유도하려면 이 값을 더 크게 지정해라!


"jms" 스키마를 지원하는 JCA 기반 리스너 컨테이너를 설정하는 것은 아주 비슷하다.

<jms:jca-listener-container resource-adapter="myResourceAdapter"
      destination-resolver="myDestinationResolver"
      transaction-manager="myTransactionManager"
      concurrency="10">

  <jms:listener destination="queue.orders" ref="myMessageListener"/>

</jms:jca-listener-container>

다양한 JCA에서 사용할 수 있는 설정 옵션은 다음 표에 나와 있다.

Table 22.3. JMS <jca-listener-container/> 요소의 속성

속성 설명
resource-adapter JCA ResourceAdapter 빈에 대한 참조(기본 빈 이름은 'resourceAdapter'다).
activation-spec-factory JmsActivationSpecFactory에 대한 참조. JMS 프로바이더와 프로바이더의 ActivationSpec 클래스를 자동으로 탐지하는 것이 기본값이다.(DefaultJmsActivationSpecFactory 참고)
destination-resolver JMS Destinations 처리의 DestinationResolver 전략에 대한 참조.
message-converter JMS 메시지를 리스너 메시드의 인자로 변환하는 MessageConverter 전략에 대한 참조. 기본값은 SimpleMessageConverter이다.
destination-type 해당 리스너의 JMS 목적지 종류로 queue, topic, durableTopic가 있다. 기본값은 queue다.
client-id 해당 리스너 컨테이너에 대한 JMS 클라이언트 아이디. 지속가능한 구독을 사용하는 경우 지정해야 한다.
acknowledge 네이티브 JMS 승인 모드로 auto, client, dups-ok, transacted가 있다. transacted의 값은 로컬로 트랜잭션이 적용되 Session를 활성화시킨다. 아니면 아래에서 설명한 transaction-manager를 지정해라. 기본값은 auto다.
transaction-manager 들어오는 각 메시지에 XA 트랜잭션을 시작하는 스프링 JtaTransactionManager나 javax.transaction.TransactionManager에 대한 참조. 지정하지 않으면 네이티브 승인을 사용한다.("acknowledge" 속성 참조)
concurrency 각 리스너에 동시에 시작할 세션/컨슈머의 수. 최대 수를 숫자로 지정하거나(예: "5") 최대/최소 범위를 지정(예: "3-5")할 수 있다. 지정한 최소값은 힌트값으로만 사용하고 JCA 리스너 컨테이너를 사용하는 경우 런타인에서는 보통 무시한다. 기본값은 1이다.
prefetch 단일 세션에 로드할 메시지의 최대 수. 동시성 컨슈머의 기아상태(starvation)를 유도하려면 이 값을 더 크게 지정해라!
2013/09/30 02:18 2013/09/30 02:18