12장. 메시징 서브시스템

Name

Date

Reason For Changes

Version

오픈나루

2013/11

Initial Version

1.0

전준식, jjeon@opennaru.com

2018/02

Second Version

2.0

이번 장에서는 메시징 서브시스템에 대해 설명한다.

메시징 서브시스템은 Java EE 6의 중요한 컴포넌트 중 하나인 Java Message Service(JMS)를 제공하기 위한 서브시스템이다. 먼저 JMS의 개념 및 메시징 서브시스템을 간단하게 설명하고 JMS 서브시스템의 설정 방법을 설명한다.

12-1.메시징 개념

대부분 요청을 보내면 응답이 오기까지 기다리는 동기 처리에 익숙하여서, 요청을 보낸 후 응답을 기다리지 않고 곧바로 다른 일을 처리하는 비동기 처리에 대해 막연히 불안하게 생각될 수 있다.

비동기 처리에 대해 자주 예로 드는 것이 메일이다. 일상생활에서 종이로 보내는 메일이나 전자메일 모두 보내는 즉시 답변 메일이 오는 것이 아녀서 기다리지 않는다. 반면 전화는 실생활에서 경험할 수 있는 대표적인 동기 처리의 예이다. 통화 상대가 다른 일을 하고 있었더라도 연결되면 전화 통화를 시작하게 된다.

엔터프라이즈 시스템에서는 일반적으로 MQ(Message Queue) 혹은 MOM(Message Oriented Middleware)라고 부르는 비동기 메시징 시스템을 사용하여 여러 시스템 간에 메시지를 전송하여 처리하는 메시지 교환 시스템을 구현해 왔다. JMS는 Java 애플리케이션에서 이런 메시징 시스템을 구현하기 위한 표준이며, Java EE에도 초기부터 포함된 비교적 오랜 시간동안 발전해 온 표준이다. JMS를 사용하면 Java EE환경에서 간단하게 비동기 메시지 처리를 구현할 수 있다. 비동기 메시징을 사용하면 시스템 성능 향상, 자원 효율화, 처리 순서지정 등 여러 가지 장점이 있다.

시스템 간 메시지 전송을 비동기로 설계하면 하드웨어 리소스 사용률이 줄어들고 IO 작업을 최소화하며 네트워크 대역폭을 더 적게 사용할 수 있다.

동기 전송을 사용하는 RPC 방법은 요청에 대한 응답을 기다려야 하기 때문에 네트워크 지연 시간이 발생한다.

메시징 시스템은 메시지를 보내는 메시지 제공자와 메시지를 받는 메시지 소비자와 분리하여 처리한다. 메시지 제공자와 소비자는 완전히 독립적이며 유연하고 느슨한 결합 시스템을 만들 수 있다.

JMS는 메시지 지향 미들웨어(Message Oriented Middleware : MOM)라는 메시징 시스템에 접근하기 위한 표준 인터페이스이다. 즉 메시징 시스템 자체의 구현이 아닌 MOM 서비스를 이용하기 위한 Java 기반의 표준 인터페이스이다. JMS는 Java EE에 포함되어 있으며 JMS 표준은 벤더 중립적이다.

JMS 제공자는 트랜잭션 시스템을 사용하여 변경 사항을 원자적으로 커밋 또는 롤백한다. RPC를 기반으로 하는 시스템과 달리 메시징 시스템은 요청과 응답 사이에 비동기 메시지 전달 패턴을 주로 사용한다.

JMS를 이용하여 여러 애플리케이션이 서로 메시지를 전송하여 통신할 수 있다. 대부분의 메시징 형태는 피어 투 피어 형이고 JMS 애플리케이션은 대부분 ‘클라이언트’가 된다. JMS는 J2EE의 다른 서비스(JTA/JTS, JNDI, JDBC 등)와 연계하여 Servlet, JSP, EJB 애플리케이션에서 사용할 수 있다. 또 메시지 드리븐 빈을 통해 JMS메시지를 수신하여 동작하는 비동기 EJB를 만들 수 있다.

JMS의 특징을 정리하면 다음과 같다.

  • Java 애플리케이션에서 기존 MOM시스템과 메시지를 주고받을 수 있다.

  • 메시지를 작성하고 송수신하기 위한 표준 인터페이스가 제공되어 메시징 애플리케이션 개발이 쉬워진다.

  • 표준화된 메시징 API 를 사용하여 메시징 애플리케이션의 이식성을 높일 수 있다.

12-2.JMS 메시징 모델

일반적으로 메시징 시스템은 두 가지 비동기 메시징 패턴을 사용한다. 하나는 메시지 큐 방식의 PTP(point-to-point) 패턴과 다른 하나는 게시/가입 패턴(Publish/Subscribe) 메시지 방식이다. PTP는 메시지 큐를 사용하고 있어 하나의 대상(destination)에 대해 하나의 소스에서 메시지가 전송된다. Pub/Sub는 하나의 소스에서 여러 클라이언트에 같은 메시지를 동시에 전달한다. JMS는 이 두 가지 메시지 모델과 애플리케이션 개발에 필요한 클라이언트 인터페이스를 제공한다.

12-3.PTP패턴

PTP 메시징은 먼저 메시지를 큐에 보낸다. 메시지는 일반적으로 전송을 보장하기 위해 디스크에 저장된다. 다음 메시징 시스템이 메시지를 소비자에게 전달한다. 소비자는 메시지를 처리하고 처리가 완료되면 승인 메시지를 보낸다. 승인 메시지를 받으면 메시징 큐에서 메시지는 삭제되고 다시 배포할 수 없다. 메시징 서버가 소비자로부터 승인을 받기 전에 오류가 발생한 경우에는 복구하여 다시 메시지를 소비자에게 보낼 수 있다.

P2P 메시징에서 같은 큐를 바라보는 여러 소비자가 있을 수 있지만, 특정 메시지는 하나의 소비자에게서만 처리된다. 큐에 메시지를 보낸 제공자는 큐에서 메시지를 받는 소비자와 완전히 분리된다. 즉, 보낸 사람과 받는 사람이 서로의 존재를 인지할 필요가 없다.

기업의 도서 주문 시스템의 주문 큐가 P2P 메시징의 전형적인 예제이다. 각 주문은 주문 큐에 전송되는 메시지가 된다. 주문 큐로 메시지를 보내는 주문 시스템을 생각해보자. 메시지가 큐에 도착하면 일단 저장한다. 이렇게 하면 오류가 발생했을 때 주문 정보가 유실되지 않는다. 또한, 주문 큐에 많은 소비자가 있다고 생각하자. 각 소비자는 주문을 처리하는 인스턴스라고 생각할 수 있다. 메시징 시스템은 각 메시지를 하나의 주문 처리 컴포넌트로 전달한다. 다양한 메시지를 각종 주문 프로세서에서 처리 할 수 있지만 하나의 주문은 1개의 주문 프로세서에서만 처리된다. 이렇게 하면 주문이 두 번 처리되지 않는다.

주문처리 프로세서가 메시지를 받으면 도서 주문이 완료되고, 주문된 정보가 물류 시스템으로 전송되어 데이터베이스에 주문 정보가 업데이트된다. 주문 프로세서가 데이터베이스를 업데이트하면 주문이 처리되며 완료됐다는 메시지를 서버에게 알린다. 창고의 재고관리 시스템에 전송할 수도 있고, 데이터베이스의 업데이트는 단일 트랜잭션으로 완료한다.

image

그림 1. P2P 메시징 시스템

12-4.게시-가입 패턴

게시-가입 메시징은 게시자(Publisher)가 어떤 가입자(Subscriber)가 있는지 모르는 상태에서 메시지를 보내게 되고 이렇게 전송된 메시지가 여러 가입자에게 모두 전달되는 구조이다. 즉, 1:N 형식으로 여러 가입자에게 동시에 메시지를 전달하고 싶을 때 사용한다.

JMS에서는 이런 형식의 메시징을 ‘토픽(Topic)’이라고 부른다.

게시-가입 메시징에서도 영속성을 유지할 수 있다. PTP 메시징과 마찬가지로 가입자가 메시지를 완전히 소비할 때까지 메시지의 복사본을 유지하고 있다가 처리가 완료되면 삭제하는 방식이다.

게시-가입 메시징의 대표적인 예제는 뉴스 피드이다. 전세계에서 발생하는 다양한 뉴스가 작성되면 뉴스 피드에 전송된다. 뉴스 기사를 받아보고 싶은 많은 구독자가 세계 곳곳에 걸쳐 있다. 뉴스 피드를 관리하는 시스템은 구독자들에게 뉴스 메시지의 복사본을 전달하면, 구독자들은 각자에게 전달된 최신 뉴스를 읽어 볼 수 있게 된다.

image

그림 2. 게시-구독 메시징 시스템

12-5.HornetQ

JBoss EAP 6의 JMS 구현은 JBoss.org 커뮤니티에서 개발된 HornetQ 라는 고성능, 고신뢰성, 유연성을 특징으로 하는 비동기 메시징 시스템이 사용되었다.

JBoss Messaging 2.0이라는 이름으로 프로젝트를 진행하다 HornetQ로 이름을 변경하여 독립 프로젝트로 진행되었다.

HornetQ의 내부 통신모듈은 Netty 라는 고성능 네트워크 애플리케이션 프레임워크를 사용하고 있다. 또, 메시지를 저장하는데 최적화된 저널이라는 파일기반의 영속화 시스템을 갖추고 있다. Linux 환경에서는 저널을 더 빠르게 처리할 수 있도록 AIO(Asynchronous I/O) 플러그인도 제공하고 있다.

HornetQ의 유연한 클러스터링 기능을 제공하여 메시지를 분산하여 처리할 수 있다. 또, 고가용성이 요구되는 엔터프라이즈에서 사용하는데 문제가 되지 않도록 클러스터링 구성을 라이브 백업 구성으로 구현할 수 있다.

12-6.JMS의 이용

Java EE 6에서는 JMS는 Full 프로파일에 포함되어 있고 웹 프로파일에는 포함되어 있지 않다. 스탠드얼론 모드에서 파일명을 지정하지 않고 JBoss EAP 6를 시작하면 웹 프로파일을 사용하게 되기 때문에 Messaging 서브시스템을 이용하려면 풀 프로파일의 설정 파일인 standalone-full.xml을 지정하고 시작해야 한다.

$./standalone.sh --server-config=standalone-full.xml

대상 설정

대상(destination)은 Queue나 Topic의 이름과 클라이언트에서 룩업할 때 사용하는 JNDI이름을 지정하여 작성한다. JNDI 이름은 다음 형식으로 지정한다.

java:jboss/exported/<JNDI 이름>

클라이언트에서는 <JNDI 이름>을 사용하여 대상(destination)을 룩업한다.

다음에서 CLI를 사용하여 Queue와 Topic을 생성하고 삭제하는 방법을 소개한다. CLI에서 오퍼레이션을 사용하여 Queue나 Topic의 생성, 삭제도 가능하지만, 더 간단하게 설정할 수 있도록 jms-queue, jms-topic이라는 별도의 명령어를 제공한다.

Queue 관리 CLI

Queue나 Topic을 생성하거나, 삭제하는 방법에 대해서 설명한다. 설정 방법이나 값의 확인 방법도 함께 설명한다.

명령 설명

큐 생성

  • jms-queue add --queue-address=<큐 이름> --entries=<JNDI 이름>

    [standalone@localhost:9999 /] jms-queue add --queue-address=myQueue --entries=queue/myQueue
    
    [standalone@localhost:9999 /] /subsystem=messaging/hornetq-server=default/jms-queue=myQueue:read-resource
    {
      "outcome" => "success",
      "result" => {
        "durable" => true,
        "entries" => ["queue/myQueue"],
        "selector" => undefined
      }
    }

큐 조회

  • jms-queue read-resource --queue-address=<큐 이름>

    [standalone@localhost:9999 /] jms-queue read-resource --queue-address=myQueue
    
    durable=true
    
    entries
    
    queue/myQueue
    
    selector=n/a

큐 삭제

  • jms-queue remove --queue-address=<큐 이름>

    [standalone@localhost:9999 /] jms-queue remove --queue-address=myQueue

토픽 관리 CLI

토픽도 큐와 마찬가지로 jms-topic 명령을 사용하여 간단하게 생성, 삭제할 수 있다.

명령 설명

토픽 생성

  • jms-topic add --topic-address=<토픽 이름> --entries=<JNDI 이름>

    [standalone@localhost:9999 /] jms-topic add --topic-address=myTopic --entries=topic/myTopic

토픽 조회

  • jms-topic read-resource --topic-address=<토픽 이름>

    [standalone@localhost:9999 /] jms-topic read-resource --topic-address=myTopic
    
    entries
    
    topic/myTopic

토픽 삭제

  • jms-topic remove --topic-address=<토픽 이름>

    [standalone@localhost:9999 /] jms-topic remove --topic -address=myTopic

12-7.DLQ와 ExpiryQueue

DLQ와 ExpiryQueue라는 특수한 큐가 있다. DLQ는 Dead Letter Queue의 약어이며 메시지 전송시 오류가 발생하여 전달되지 못한 메시지가 저장되는 큐이다. ExpiryQueue는 메시지에 설정한 메시지 유효기간이 지난 경우에 해당 메시지가 저장되는 큐이다. 기본적으로 메시지에 대해 DLQ와 ExpiryQueue로 전송하도록 설정되어 있긴 하지만, 애플리케이션에서 사용하기 위한 큐는 정의되어 있지 않다. DLQ와 ExpiryQueue를 이용하려면 먼저 사용하려는 큐를 생성해야 한다. 기본적으로 DLQ 와 ExpiryQueue의 JNDI 이름을 아래와 같이 설정하면 된다.

큐 이름 항목 설정값

DLQ

jms-queue

DLQ

entries

java:jboss/exported/DLQ

ExpiryQueue

jms-queue

ExpiryQueue

entries

java:jboss/exported/ExpiryQueue

DLQ 와 ExpiryQueue 를 생성하는 CLI는 다음과 같다.

항목 방법

DLQ/ ExpiryQueue 생성

[standalone@localhost:9999 /] jms-queue add --queue-address=DLQ --entries=jboss/exported/DLQ

[standalone@localhost:9999 /] jms-queue add --queue-address=ExpiryQueue --entries=jboss/exported/ExpiryQueue

DLQ/ ExpiryQueue 조회

[standalone@localhost:9999 /] *cd subsystem=messaging/hornetq-server=default*

[standalone@localhost:9999 hornetq-server=default] *:read-children-resources(child-type=jms-queue)*
{
  "outcome" => "success",
  "result" => {
    "DLQ" => {
      "durable" => true,
      "entries" => ["jboss/exported/DLQ"],
      "selector" => undefined
    },
    "ExpiryQueue" => {
      "durable" => true,
      "entries" => ["jboss/exported/ExpiryQueue"],
      "selector" => undefined
    },
  }
}

메시지 유효기간은 JMS의 API에서 설정할 수 있다. 메시지 제공자에서 메시지의 유효기간을 설정할 수 있다. 시간 단위는 밀리 초 단위로 설정한다.

javax.jms.MessageProducer.setTimeToLive(long timeToLive)

또, 메시지를 전송할 때 메시지마다 유효기간을 다르게 설정할 수도 있다.

java.jms.MessageProducer.send(Destination destination, Message message, int deliveryMode, int priority, long timeToLive)

지정한 timeToLive 시간 내에 전달되지 못한 메시지들이 ExpiryQueue에 보관된다.

12-8.Linux AIO 사용

HornetQ에서 메시지를 저널에 저장하고 읽을 때 기본적으로 Java의 NIO(Nonblocking I/O)를 사용한다. 리눅스에서는 더 빠른 I/O 처리를 위해 AIO(Asynchronous I/O) 모듈을 제공한다. HornetQ에서도 리눅스에서 사용하면 Java NIO보다 더 빠른 IO처리를 위해 리눅스의 AIO를 사용하는 모듈을 제공하고 있다. AIO를 사용하려면 리눅스 커널 2.6 이상에 libaio 패키지가 설치되어 있어야 한다. 또 AIO를 사용하려면 ext2, ext3, ext4, jfs, xfs와 같은 파일시스템이 필요하다. NFS를 사용하면 속도가 많이 떨어지게 되니, NFS에서는 사용하지 않는 것을 권장한다.

libaio 설치

RHEL에서는 다음 명령으로 libaio를 설치한다.

$ sudo yum install libaio

AIO 모듈 설치

AIO 모듈은 JBoss EAP 6의 네이티브 컴포넌트에 포함되어 있다. 네이티브 컴포넌트는 플랫폼별로 배포되기 때문에 설치하고자 하는 플랫폼의 파일을 다운로드 받아 사용한다.

설치 방법은 다음과 같이 다운로드 받은 파일의 압축을 풀어 JBoss 가 설치된 modules 디렉터리에 복사하면 된다.

$ unzip jboss-eap-native-6.2.0-RHEL6-x86_64.zip

$ cp -R jboss-eap-6.2/modules $JBOSS_HOME/.

저널타입 변경

리눅스에 libaio 패키지를 설치하고, JBoss EAP 6에 네이티브 컴포넌트를 설치한 후 JBoss EAP 6의 HornetQ에서 AIO를 사용하려면 저널타입(journal-type)을 변경하여야 한다. JBoss EAP 6의 full, full-ha 프로파일에서만 메시징 서브시스템을 사용할 수 있다.

다음 CLI 명령으로 journal-type을 AIO로 변경할 수 있다.

[standalone@127.0.0.1:9999 hornetq-server=default] :write-attribute(name=journal-type, value=ASYNCIO)
{
  "outcome" => "success",
  "response-headers" => {
    "operation-requires-reload" => true,
    "process-state" => "reload-required"
  }
}

위와 같이 CLI 명령으로 journal-type을 변경하면 사용하는 설정파일인 domain.xml이나 standalone-full.xml, standalone-full-ha.xml 파일의 내용이 다음과 같이 변경된다.

<hornetq-server>
  <persistence-enabled>true</persistence-enabled>
  <journal-type>ASYNCIO</journal-type>
  <journal-min-files>2</journal-min-files>

서버를 재 시작하면, server.log에 다음과 같은 메시지가 출력되면 AIO를 사용하도록 변경된 것이다.

16:56:14,165 INFO [org.hornetq.core.server] (MSC service thread 1-2) HQ221012: Using AIO Journal