Clean Code that Works.

이번에는 스프링에서 JMS를 어떻게 지원하고, 사용하는 방법에 대해서 알아보자.


위 이미지에서 보듯이 JMS API는 JDBC API 와 상당히 유사하다. 스프링에서 JDBC template을 제공 하는 것 처럼 JMS template도 제공한다.

JMS template 과 message listener container 는 스프링에서 JMS 메시징을 사용하는 핵심 컴포넌트 이다. Spring JMS template(JmsTeplate)는 동기적으로 메시지를 받거나 보내는데 사용한다. message listener container(DefaultMessageListenerContainer)는 MDP를 사용하여 비동기 메시지를 받는데 사용한다. 대부분의 자바 EE 애플리케이션 서버들과는 다르게(JBoss, WebSphere) 스프링은 자기 자신이 JMS 제공자가 되지 않는다. 이것은 외부의 JMS 제공자(ActiveMQ, JBoss Messaging, IBM WebSphereMQ)가 스프링에서 메시징을 하기 위해 필요한 것이다. JMS template 과 message listener container 의 목적은 개발자가 JMS 제공자에 대한 자세한 JMS 연결, JMS 세션 그리고 JMS message producer 와 message consumer를 만드는것에 연결정보를 필요로 하는 것을 격리한다.

고 레벨 아키텍쳐


스프링 JMS 템플릿(JmsTemplate)은 메시지를 동기적으로 주고 받기 위한 primary interface다. JNDI를 사용할 때, JmsTemplate 여러 스프링 오프젝트들이 JMS 제공자에 연결하는데 이것을을 포함하여 사용한다. JndiTemplate, JndiObjectFactoryBean, JndiDestinationResolver 와 CachingConnectionFactory(아니면 SingleConnectionFactory)
JndiTempate bean은 제공자 URL, 보안 사용자와 JMS 제공자에 연결하기 위한 보안 자격 증명의 팩토리를 초기화 하는데 사용한다. JndiObjectFactoryBean 을 경유하여 JMS 커넥션 팩토리를 정의 하는데 사용되고, JMS destination은 JndiDestinationResolver를 통해 사용된다. 
아래 그림은 Spring JNDI object와 JMS provider와 애플리케이션 사이의 관계와 협력 관계를 표시한다.

 
메시지를 비동기적으로 받기 위해서, 스프링은 message listener container(DefaultMessageListener or SimpleMessageListenerContainer)를 제공 한다. 이것은 MDP를 만들기 위해서 사용된다. 첫 눈에 보이듯이 MDP는 Java EE 명세에서 보이는 message-driven bean과 유사함을 알 수 있다. 하지만 스프링 MDP는 MDB 보다 조금 더 유연성을 제공한다. non-message-aware POJO를 통해 Spring MDP를 생성 할 수 있다. 반면에 MDS는 EJB 3 명세를 준수해서 만들어야 한다. 즉 오브젝트는 반드시 javax.jms.MessageListener interface를 구현해야 하고 onMessage메서드를 override 해야 하고 
destination type(e.g., javax.jms.Queue or javax.jms.Topic)을 포함 하는 @MessagDrieven 어노테이션이나 XML을 제공 해야 합니다. 그리고 JNDI destiname도 포함하여야 합니다.

JmsTemplate 처럼 message listener container는 JndiTemplate, JndiObjectFactoryBean, JndiDestinationResolver와 CachingConnectionFactory 사이에 JMS provider와 연결을 위해 사용되고 비동기 listener를 시작하는데 사용합니다. message-driven bean과는 달리 스프링은 MDP를 만들기 위한 3가지 다른 방법을 제공 합니다. 아래 그림은 비동기 메시지를 받기위한 오브젝트들의 관계를 표현합니다.


JmsTemplate 개요

JmsTemplate은 동기적으로 메세지를 주고 받는데 기본오브젝트이다(메시지를 받기위해 대기하는 동안 블록킹함). JMS 1.1(JmsTemplate) 과 JMS 1.0.2(JmsTemplate102) 버전이 있다. 대부분의 JMS 프로바이더와 JMS EE 서버들은 JMS 1.1을 지원 하므로 JMS1.1을 사용할 것이다. JmsTemplate 를 사용하면 개발에 들어가는 노력과 시간을 상당부분 줄여준다. JmsTemplate를 사용할 때 JMS 프로바이더와 연결할때, JMS session(Queue Session)을 만들때나 message producer(Queue Sender)를 만들거나 또는 JMS message를 만드는데 대한 걱정을 할 필요가 없다. JmsTemplate는 자동적으로 String 오브젝트, Byte[] Object, Java Object 나 java.util.Map 에 해당하는 JMS 메시지 오브젝트로 변환해준다. 또한 자신의 메시지 컨버터를 제공하여 복잡한 메시지나 기본 메시지 컨버터가 지원 하지 않는 것을 해결 할 수 있다. 
아래 코드는 스프링을 사용하여 간단한 텍스트 메시지를 전송하는 예제이다.

public class SimpleJMSSender {

public static void main(String[] args) {

try {

ApplicationContext ctx =

new ClassPathXmlApplicationContext("app-context.xml");

JmsTemplate jmsTemplate =

(JmsTemplate)ctx.getBean("jmsTemplate");

jmsTemplate.convertAndSend("This is easy!");

}

...

}

}




대충 생략

메시지 드리븐 POJOs(Message-Driven POJOs)
메시지를 비 동기 적으로 받는 다는 것은 특정 큐나 토픽에 대해 응답 개기중에 논 블록킹 프로세스를 가지고 있다는 것이다. 이 기술은 이벤트 드리븐 형태의 처리 방식이다. 메세지 리스너에서 메시지가 존재하는것을 알려준다. Message-driven beans는 비동기 리스너를 위한 Java EE 기술이다. 스프링 프레인 워크는 MDP를 사용한 비동기 리스너 역시 지원 한다.
스프링에서 비동기 메시지 리스너를 구성하기위한 3가지 방법이 있다. javax.jms.MessageListener interface, implementing Spring’s SessionAwareMessageListener를 구현 하거나 POJO 클래스를 Spring MessageListenerAdapter 클래스로 랩핑 하면 된다. 이 3가지 방법은 메시지 리스너 클래스가 어떻게 구성되어 있느야에 따라 다양하다. 아래 설명에서는 메시지 리스너 컨테이너에 대해 상세한 설명과 함께 3가지 message driven bean 기술을 배우게 된다.

The Spring Message Listener Container
Message-driven POJOs 는 메시지 리스너 컨테이너의 context에서 생성 된다. 메시지 리스너 컨테이너는 connection factory, JMS destination, JNDI destination resolver 와 message listener bean을 바인딩 한다. 스프링은 두가지 타입의 메시지 리스너컨테이너를 제공한다(DefaultMessageListenerContainer 와 SimpleMessageListenerContainer). 이러한 메시지 리스너 컨테이너 들은 비동기 리스너 스레드를 명시해 주어야 한다. 
DefaultMessageListenerContainer 만이 실행시에 동적으로 리스너 숫자를 조절 할 수 있다. 추가로 DefaultMessageListenerContainer 는 XA 트랜잭션과의 통합을 지원 하는 반면에 SimpleMessageListenerContainer는 지원하지 않는다.  로컬 트랜잭션 메니저를 사용 하고 쓰레드 및 세션, 부하조건 변화에 따라 연결 조정을 사용 하는 간단한 애플리케이션의 경우 SimpleMessageListenerContainer를 사용하면 된다. 외부 트랜잭션 매니저나 XA 트랜잭션을 사용하고, 튜닝이 필요한 메시지 애플리케이션의 경우는 DefaultMessageListenerContainer를 사용하면 된다. 

MDP Option1 : MessageListener 인터페이스 사용하기
message-driven POJO의 가장 간편한 형태는 javax.jms.MessageListener 인터페이스를 구현한 비동기 receiver를 사용하는 것이다. 이것은 EJB3 의 message0driven bean 과 비슷하다. DefaultMessageListenerContainer는 CachingConnectionFactory 와 JNDIDestinationResolver에 주입 된다.



MessageListener 인터페이스를 구현할 때, message listener 클래스에 onMessage를 반드시 오버라이드 하여야 한다. 이 방법을 사용할때에 XML 설정에서 바꿔야할 것은 없다. 아래 코드 예제를 보면, javax.jms.MessageListener를 구현하고 onMessage 메서드를 오버라이드하고 TextMessage를 받는 SimpleJMSReceiver 메시지 리스너이다.

public class SimpleJMSReceiver implements MessageListener {
public void onMessage(Message message) {
try {
if (message instanceof TextMessage) {
System.out.println(((TextMessage)message).getText());
} else {
throw new IllegalStateException("Message Type Not Supported");
}
} catch (JMSException e) {
e.printStackTrace();
}
}
}

MDP Option 2: SessionAwareMessageListener 인터페이스 사용하기

스프링 프레임 워크는 javax.jms.MessageListener를 확장한 SessionAwareMessageListener를 제공한다. javax.jms.MessageListener 처럼,
SessionAwareMessageListener는 listener클래스에서 onMessage  메서드를 오버라이드 해야 한다. 그러나 javax.jms.MessageListener는 다르게 Message 오브젝트에 추가되는게 있다. SessionAwareMessageListener는 JMS Session에 접근할수 있도록 해준다.

void onMessage(Message message, Session session) throws JMSException

아래 그림은
SessionAwareMessageListener의 사용방법이다. 설정방식의 관점에서 보면 이것은 javax.jms.Mes
sageListener를 사용할 때와 같음을 알 수 있다.


만약 비동기 메시지 리스너에서 JMS Session 오브젝트에 접근하여야 할 경우 SessionAwareMessageListener는 유용하게 사용된다. 이 방식의 일방적인 사용 방법중에 하나는 응답 메시지를 보내는 쪽에 전달해야 할 경우 이다. 다른 사용 방식은 Session에 트랜잭션을 적용할 경우 이다. 아래의 간단한 예제에 대하여 생각해보자. SimpleJMSReceiver 클래스가 보낸쪽에 메시지들 돌려주고 JMSReplyTo 헤더 속성에서 이 메시지를 처리 했을을 나타 낸다.

public class SimpleJMSReceiver implements SessionAwareMessageListener {
public void onMessage(Message message, Session session) throws JMSException {
if (message instanceof TextMessage) {
String text = ((TextMessage)message).getText();
System.out.println(text);
//send the response
MessageProducer sender =
session.createProducer(message.getJMSReplyTo());
TextMessage msg = session.createTextMessage();
msg.setJMSCorrelationID(message.getJMSMessageID());
msg.setText("Message " + message.getJMSMessageID() + " received");
sender.send(msg);
} else {
throw new IllegalStateException("Message type not supported");
}
}
}

메시지를 보낸 후에 Session 오브젝트를 스스로 clean up 할 필요가 없음을 기억하자. 스프링이 JmsTemplate에서 다 처리해 준다. 또한 SessionAwareMessageListener의 onMessage 리스너는 JMSException을 던진다.(javax.jms.MessageListener에서 하지 않는)

MDP Option 3 : MessageListenerAdapter 사용하기.
비동기 메시지 리스너를 만드는 3번째 방법은 스프링 MessageListenerAdapter 오브젝트로 POJO 오브젝트를 덮어 씌우는 방법이다. 이 방법이 다른 두가지 방법과 다른 이유는 POJO receiver 클래스가 어떤 message listener인터페이스도 구현하지 않아도 되고, javax.jms.Message 오브젝트의 어떤것도 포함하지 않아도 되서 이다. 아래 그림을 보면 POJO receiver 클랫는 스프링 MessageListenerAdapter에 주입 된다.
MessageListenerAdapter에를 사용해서 POJO receiver의 메서드를 구성하는 방법이 몇가지 있다. 기본 메시지를 핸들링 하는 메서드를 MessageListenerAdapter를 통해 사용하거나 별도의 메서드를 리스너 클래스에서 지정해서 리스너 메서드로 사용할 수 있다. 후자를 사용하는 경우, 자바 객체 타입의 메시지를 변환하는 컨버터를 사용하던지 직접 JMS message 객체를 사용하는 방식을 사용할 수 있다. 이 두가지 예제는 아래 섹션에서 살펴보도록 하자.


Default message handler method
기본적으로, MessageListenerAdaptor는 handlmessage 메서드를 JMS 메시지가 수신되고 이에 상응하는 
POJO에서 찾습니다.
아래의 목록은 자동 메시지 변환을 사용하는  handleMessage 메서드 목록이다.

//receive a converted TextMessage
public void handleMessage(String message) {...}
//receive a converted BytesMessage
public void handleMessage(byte[ ] message) {...}
//receive a converted MapMessage
public void handleMessage(Map message) {...}
//receive a converted ObjectMessage
public void handleMessage(Object message) {...}

기본 message listner handler method를 사용하기 위해 message-driven POJO(eg, SimpleJMSReceiver)를 MessageListenerAdapter 빈에다가 생성자 속성을 통해 주입해 주어야 한다.(아니면 변수의 프로퍼티를 통해)
<bean id="messageListener" class="org.springframework.jms.listener.adapter.MessageListenerAdapter">
<constructor-arg>
<bean class="SimpleJMSReceiver"/>
</constructor-arg>
</bean>
<bean id="jmsContainer" class="org.springframework.jms.listener.DefaultMessageListenerContainer">
<property name="connectionFactory" ref="queueConnectionFactory"/>
<property name="destinationResolver" ref="destinationResolver"/>
<property name="concurrentConsumers" value="3" />
<property name="destinationName" value="queue1"/>
<property name="messageListener" ref="messageListener" />
</bean>

message-driven POJO를 정의 할 때 간단하게 사용하고자 하는 JMS message 타입에 따라 handlerMessage 메서드를 정의 할 수 있습니다. 예를 들어 아래 코드는 JMS TextMessage를 받는다.

public class SimpleJMSReceiver {
public void handleMessage(String message) {
System.out.println(message);
}
}

SimpleJMSReceiver 클래스는 JMS API에 대한 어떠한 참조도 가지고 있지 않다는 것을 확인하자. 실제로, 이 예제 안에서 message-driven POJO는 심지어 컨텍스트 안에서 메시징이 사용되는지에 대해서 알고 있는것이 아무것도 없다. 모든 메시징 인프라 스트럭쳐는 MessageListenerAdapter 나 DefaultMessageListenerContainer를 통해 전달하도록 되어 있다. 당신이 할 일은 JMS 메시지가 수신되고 이 유형에 따라 필요한 POJO handleMessage 메서드를 작성 해야 한다.

만약 수신되는 JMS 메시지 타입이 확실하지 않거나 TextMessage 나 MapMessage 중에 하나를 받을 가능성이 있다면 어떻게 해야 할까? 이전 예제에서 처럼 JMS Message Object의 instance를 확인 하거나
Message Type에 따라 즉시 처리할수 있다.

public class SimpleJMSReceiver implements MessageListener {
public void onMessage(Message message) {
try {
if (message instanceof TextMessage) {
//process message text...
} else if (message instanceof MapMessage) {
//process map message...
} else {
throw new IllegalStateException("Message Type Not Supported");
}
} catch (JMSException e) {
e.printStackTrace();
}
}
}


그러나 default handleMessage 메서드에서의 인수가 이미 받을 타입에 대해 "캐스팅" 되어 있을 경우가 있다. 스프링은 몇가지 방법으로 이것을 처리 한다. 특정 메시지 유형에 대한 default handleMessage 메서드가 정의 되지 않는 경우, MessageListenerAdapter는 NoSuchMethodException을 표시하고 JMS 메시징 타입을 찾을수 없게 된다.
따라서 handleMessage 메서드에서 각각 타입에 대해서 받기 원하는 파라미터를 명시 해야 한다. 예를 들어 TextMessage 나 MapMessage 메시지 타입을 인수로 받을 려면 HandleMessage aptjemrk String 이나 Map 파라미터를 받을 수 있도록 하면 된다.

public class SimpleJMSReceiver {
public void handleMessage(String message) {
//process String message body
}
public void handleMessage(Map message) {
//process Map message body
}
}

방금 설명된 메시지 변환 방식의 한가지 이슈는 message handler 메서드를 통해서만 메시지가 전달 되어야 한다는 점이다. 따라서 메시지 헤더 프로퍼티나 메시지 애플리케이션 프로퍼티에는 접근하거나 수정 할 수 없다. 예를 들어 발신자가 애플리케이션 프로퍼티 섹션에 추가적인 메타 정보를 기입한 후 전송하려고 하거나 메시지 헤더 프로퍼티의 JMSReplyTo, JMSMessageID 속성에 접근해야 할 경우 사용 할 수 없다. 이 예제들에서 메시지들을 자동으로 변환하는 것들에 대한 것을 DefaultMessagelistenerContainer에 알려줄 수 있다. MessageListenerAdapter 빈에  messageConverter 프로퍼티에다가 값을 null로 세팅함으로써 쉽게 사용할 수 있다. JMS 메시지 개체를 인수로 받는 방법보다 해당 자바 객체 유형을 받아서 처리 할 수 있다.

<bean id="messageListener"
class="org.springframework.jms.listener.adapter.MessageListenerAdapter">
<constructor-arg>
<bean class="SimpleJMSReceiver"/>
</constructor-arg>
<property name="messageConverter"><null/></property>
</bean>

message conversion 기능을 중지 시키면 MessageListenerAdaptor는 기본적으로 아래의 handleMessage 메서드들 중에 하나를 찾는다.

//receive a JMS TextMessage
public void handleMessage(TextMessage message) {...}

//receive a JMS BytesMessage
public void handleMessage(BytesMessage message) {...}

//receive a JMS MapMessage
public void handleMessage(MapMessage message) {...}

//receive a JMS ObjectMessage
public void handleMessage(ObjectMessage message) {...}

//receive a JMS StreamMessage
public void handleMessage(StreamMessage message) {...}

이 방법은 JMS Message 오브젝트에 접근할 수 있는 방법을 제공해준다. 이 방법을 통해 헤더나 애플리케이션 정보를 뽑아내거나 수정 할 수 있다.

public class SimpleJMSReceiver {
public void handleMessage(TextMessage message) {
String text = message.getText();
String username = message.getStringProperty("username");
String msgId = message.getJMSMessageID();
//process text message
}
}

Custom message handler method

당연히 default handlerMessage 메소드들에  POJO 메시지 리서너를 제한할 필요가 없다. 사실 POJO 메시지 리스너들 중에  JMS Message Type이나 메시지 컨버젼 오브젝트(String, byte[], Map, or Object)둘중에 한개의 파라미터를 포함한
아무 메서드나 listener handler 메서드가 될수 있다. 자신만의 메서드를 message handler 처럼 사용할려면 반드시 MessageListenerAdapter의 defaultListenerMethod 프로퍼티에 메시지 핸들러로 사용하기 위한 메서드명을 적어 줘야 한다.
또한 MessageLIstenerAdapter는 메시지 본문을 변환하거나 message handelr message에 JMS Message type을 처리 하기 위한 것을 명시해 줘야 한다. 예를 들어,
TradeOrderManager 클래스에서 XML 거래 주문을 포함한 String 오브젝트를 처리하기 위한 createTradeOrder 메서드를MessageListenerAdapter에 구성해야 할 경우, defaultListenerMothod 프로퍼티에 createTradeOrder를 명시해 주고 SimpleMessageConverter를 사용하면 된다.

<bean id="messageListener"
class="org.springframework.jms.listener.adapter.MessageListenerAdapter">
<constructor-arg>
<bean class="TradeOrderManager"/>
</constructor-arg>
<property name="defaultListenerMethod" value="createTradeOrder"/>
</bean>

POJO 메시지 리스너에서 String Object 파라미터를 받는 createTradeOrder메서드는 아래와 같다.

public class TradeOrderManager {
public void createTradeOrder(String xml) {
//process trade order xml message
...
}
...
}

그냥 표시된 코드를 공부할 경우, 이 POJO가 메시징과 관련된 것이 하나도 없다는 것을 볼 수 있다. 이것은 message-driven POJOs 의 예제이다. 메시징과 커뮤니케이션 로직을 POJO로 부터 분리함으로써 코드를 추상화 할 수 있고 POJO가 메시징 인프라 구조 로직 보다 비즈니스 로직에 촛점을 맞출 수 있다. 이 클래스는 메시징 컨텍스트의 안이나 밖에서 사용될 수 있고, 메시징 프레임워크 바깥에서 테스트할 수 있다.