[스프링 인 액션] 8장 JMS : 비동기 메시지 전송하기

    8장 비동기 메시지 전송하기 : JMS

    💻 실습 : https://github.com/cusbert/spring-in-action-5th

    🎯 이 장에서 배우는 내용

    • 비동기 메시지 전송
    • JMS, RabbitMQ, 카프카 Kafka를 사용해서 메시지 전송하기
    • 브로커에서 메시지 가져오기
    • 메시지 리스닝하기

    비동기(Asynchronous) 메시징은 애플리케이션 간에 응답을 기다리지 않고 간접적으로 메시지를 전송하는 반복이다. 따라서 통신하는 애플리케이션 간의 결합도를 낮추고 확장성을 높여준다.

    스프링은 JMS(Java Message Service), RabbitMQ, AMQP(Advanced Message Queueing Protocol), 아파치 카프카(Apahche Kafka) 가 등의 비동기 메시징을 지원한다.

    • Producer : 큐와 토픽에 메시지 전송
    • Consumer : 메시지 수신
    • Broker : 프로듀서와 컨슈머 사이의 메시지를 정의된 형식으로 메시지를 전달해 주는 중간 다리 역할
      ActiveMQ, RabbitMQ, Kafka 등등

    8.1 JMS로 메시지 전송하기

    JMS 는 두 개 이상의 클라이언트 간에 메시지 통신을 위한 공통 API를 정의하는 자바 표준이다.

    스프링은 JmsTemplate 이라는 템플릿 기반의 클래스를 통해 JMS 를 지원한다.

    8.1.1 JMS 설정하기

    • ActiveMQ pom.xml 에 설정
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-activemq</artifactId>
    </dependency>
    • ActiveMQ Artemis pom.xml 에 설정
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-artemis</artifactId>
    </dependency>

    Artemis는 ActiveMQ 를 새롭게 다시 구현한 차세대 브로커다.

    Artemis 브로커의 위치와 인증 정보를 구성하는 속성

      설명
    spring.artemis.host 브로커 호스트
    spring.artemis.port 브로커 포트
    spring.artemis.user 브로커 사용자 (선택 속성)
    spring.artemis.password 브로커 사용자 암호 (선택 속성)
    • application.yml 설정
    spring
        artemis:
            host: localhost
            port: 61616
            user: admin
            password: admin

    기본적으로 스프링은 Artemis 브로커가 localhost의 61616 포트를 리스닝 하는 것으로 간주한다. 이는 설정 변경 가능하다.

    자세한 내용은 아래 문서 참고

    8.1.2 JmsTemplate을 사용해서 메시지 전송하기

    JmsTemplate 은 아래와 같은 메소드를 가지고 있다

    // Send raw messages 원시 메시지 전송
    void send(MessageCreator messageCreator) 
                    throws JmsException;
    
    void send(Destination destination, MessageCreator messageCreator) 
                    throws JmsException;
    
    void send(String destinationName, MessageCreator messageCreator) 
                    throws JmsException;
    // Send messages converted from objects 
    // 객체로 부터 변환된 메시지 전송
    void convertAndSend(Object message) throws JmsException;
    
    void convertAndSend(Destination destination, Object message) 
                            throws JmsException;
    
    void convertAndSend(String destinationName, Object message)
                             throws JmsException;
    // Send messages converted from objects with post-processing
    // 객체로부터 변환되고 전송에 앞서 후처리 메시지를 전송
    void convertAndSend(Object message, MessagePostProcessor postProcessor) throws JmsException;
    
    void convertAndSend(Destination destination, Object message, MessagePostProcessor postProcessor) 
                            throws JmsException;
    
    void convertAndSend(String destinationName, Object message, MessagePostProcessor postProcessor) 
                            throws JmsException;

    실제로는 send()convertAndSend() 만 있으면 된다.

    • send() 메서드는 Message 객체를 생성하기 위해 MessageCreator 가 필요하다.
    • convertAndSend() 는 Object 타입 객체를 인자로 받아 내부적으로 Message 타입으로 변환한다.
    • convertAndSend() 는 Object 타입 객체를 Message 타입으로 변환한다. 그러나 메세지가 전송되기 전에 Message의 커스터마이징을 할 수 있도록 MessagePostProcessor도 인자로 받는다.

    1. Send()를 사용해서 주문 데이터를 Message 전송

    1. 기본 도착지를 application.yml 에 따로 지정
    spring:
      jms:
        template:
          default-destination: tacocloud.order.queue
    @Service
    public class JmsOrderMessagingService implements OrderMessagingService {
    
        private JmsTemplate jmsTemplate;
    
        @Autowired
        public JmsOrderMessagingService(JmsTemplate jmsTemplate) {
            this.jmsTemplate = jmsTemplate;
        }
    
    
        @Override
        public void sendOrder(Order order) {
            // Order로 부터 새로운 메시지 생성
            jmsTemplate.send(
                session -> session.createObjectMessage(order));
        }
    
    }
    1. 기본 도착지를 send() 의 파라미터로 전달할 때
    @Service
    public class JmsOrderMessagingService implements OrderMessagingService {
    
        private JmsTemplate jmsTemplate;
    
        @Autowired
        public JmsOrderMessagingService(JmsTemplate jmsTemplate) {
            this.jmsTemplate = jmsTemplate;
        }
    
        public void sendOrder(Order order) {
            // Order로 부터 새로운 메시지 생성
            jmsTemplate.send(
                    "tacocloud.order.queue",
                    session -> session.createObjectMessage(order));
        }
    
    }

    send() 메서드 사용하면 Message 객체를 생성하는 MessageCreator 를 두번째 인자로 전달 하므로 조금 복잡해진다.
    이 때 convertAndSend() 를 사용 가능하다.

    2. 메시지 변환하고 전송하기

    JmsTemplates의 convertAndSend() 메서드는 전송될 객체를 인자로 직접 전달한다. 즉 해당 객체가 Message 객체로 변환되어 전송된다.

    @Override
    public void sendOrder(Order order) {
        jmsTemplate.convertAndSend("tacocloud.order.queue", order);
    }

    이 때 객체를 Message 객체로 변환하는 번거로은 일은 MessageConverter 를 구현하여 처리할 수 있다.

    MessageConverter 구현하기

    MessageConverter 는 스프링에 정의된 인터페이스 이며 두개만 정의되어있다.

    public interface MessageConverter { 
        Message toMessage(Object object, Session session) 
                    throws JMSException, MessageConversionException;
    
        Object fromMessage(Message message)
    }

    Spring message converters 종류

    • MappingJackson2MessageConverter
    • MarshallingMessageConverter
    • MessagingMessageConverter
    • SimpleMessageConverter

    기본적으로는 SimpleMessageConverter가 사용되며 이 경우 전송될 객체가 Serializable 인터페이스를 구현하는 것이어야 한다.

    다른 message converter를 적용할 때는 해당 변환기의 인스턴스를 빈으로 선언만 하면 된다.

    아래 처럼 message converter의 setTypeIdMappings()를 호출하여 Order 클래스를 order 라는 타입 id로 매핑하도록 할 수 있다.
    _typeId 속성에 전송되는 클래스 이름 대신 order 값이 전송된다.

    @Configuration
    public class MessagingConfig {
    
      @Bean
      public MappingJackson2MessageConverter messageConverter() {
        MappingJackson2MessageConverter messageConverter =
                                new MappingJackson2MessageConverter();
        messageConverter.setTypeIdPropertyName("_typeId");
    
        Map<String, Class<?>> typeIdMappings = new HashMap<String, Class<?>>();
        typeIdMappings.put("order", Order.class);
        messageConverter.setTypeIdMappings(typeIdMappings);
    
        return messageConverter;
      }
    }

    3. 후처리 메시지

    후처리로 메시지가 전송되기 전에 커스텀 헤더에 메시지를 추가해보자.

    @Override
    public void sendOrder(Order order) {
        jmsTemplate.convertAndSend("tacocloud.order.queue", order,
                this::addOrderSource);
    }
    
    private Message addOrderSource(Message message) throws JMSException {
        message.setStringProperty("X_ORDER_SOURCE", "WEB");
        return message;
    }

    8.1.3 JMS 메시지 수신하기

    메시지를 수신하는 방식에는 두 가지가 있다.

    • Pull model : 우리 코드에서 메시지를 요청하고 도착할 때 까지 기다린다.
    • Push model : 메시지가 수신 가능 하게 되면 우리 코드로 자동 전달 한다.

    두가지 방식 모두 용도에 맞게 사용할 수 있다.

    1. JmsTemplate (Pull model) 로 메시지 수신하기

    JmsTemplate 을 사용해서 메시지 수신하기

    // 원시 메시지 수신
    Message receive() throws JmsException;
    Message receive(Destination destination) throws JmsException;
    Message receive(String destinationName) throws JmsException;
    
    // Message 를 도메인 타입으로 변환하여 수신
    Object receiveAndConvert() throws JmsException;
    Object receiveAndConvert(Destination destination) throws JmsException;
    Object receiveAndConvert(String destinationName) throws JmsException;
    @Service
    public class JmsOrderReceiver implements OrderReceiver {
    
      private JmsTemplate jms;
    
      public JmsOrderReceiver(JmsTemplate jms) {
        this.jms = jms;
      }
    
      @Override
      public Order receiveOrder() {
        return (Order) jms.receiveAndConvert("tacocloud.order.queue");
      }
    
    }

    2. 메시지 리스너 (Push model) 수신하기

    receive()receiveAndConvert() 를 호출해야 하는 pull 모델과 달리
    메시지 리스너는 메시지가 도착할 때 까지 대기하는 수동적 컴포넌트이다.

    @JmsListener 를 지정하여 JMS 메시지를 수동적으로 리스닝하는 메시지 리스너를 생성할 수 있다.

    @Component
    public class OrderListener {
    
      private KitchenUI ui;
    
      @Autowired
      public OrderListener(KitchenUI ui) {
        this.ui = ui;
      }
    
      @JmsListener(destination = "tacocloud.order.queue")
      public void receiveOrder(Order order) {
        ui.displayOrder(order);
      } 
    }

    @JmsListener 는 "tacocloud.order.queue" 도착지의 메시지를 리스닝할 수 있도록 한다. 이 메서드는 JmsTemplate 를 사용하지 않으며 애플리케이션 코드에서도 호출하지 않는다. 대신에 스프링 프레임워크 코드가 특정 도착지에 메시지가 도착하는 것을 기다리다가 도착하면 해당 메시지에 적재된 Order 객체가 인자로 전달 되면서 receiveOrder() 메서드가 자동으로 호출된다.

    여러면에서 @JmsListener 어노테이션은 @RequestMapping 과 유사하다. 단 @JmsListener 가 지정된 메서드들은 지정된 도착지에 들어오는 메시지에 반응한다.

    일반적으로는 스레드 실행을 막지 않으므로 Push 모델이 좋은 선택이다. 메시지 리스너는 중단 없이 다수의 메시지를 빠르게 처리할 수 있어 좋은 선택이 될 때가 있다.

    단 많은 메시지가 한번에 도착하여 리스너에 과부하가 걸리는 경우가 생길 수 있다. 즉 메시지를 수신 후 빠르게 처리할 수 없다면 심각한 병목현상이 생길 수 있다.

    • 용도에 따른 선택
      메시지 리스너는 메시지가 빠르게 처리될 수 있을 때 사용하면 매우 적합하다.
      JmsTemplate 은 메시지 처리기가 자신의 시간에 맞춰 더 많은 메시지를 요청할 수 있어야 한다면 JmsTemplate 이 제공하는 pull 모델이 적합하다.

    JMS 는 표준 자바 명세에 정의되어 있고 여러 브로커에 지원되므로 자바 메시징에 많이 사용된다.

    그러나 몇가지 단점이 있고 특히 java 애플리케이션에만 사용할 수 있다.

    따라서 RabbitMQ, 카프카 같이 다른 언어와 JVM 이외의 다른 플랫폼에서 사용 가능한 메시징 시스템이 등장 하였다.

    📌 요약

    • 애플리케이션 간 비동기 메시지 큐를 이용한 통신 방식은 간접 계층을 제공하므로 애플리케이션 간의 결합도는 낮추면서 확장성은 높인다
    • 스프링은 JMS, RabbitMQ 또는 아파치 카프카를 사용해서 비동기 메시지을 지원한다.
    • template-based clients (JmsTemplate, RabbitTemplate, or KafkaTemplate)을 사용해서 메시지 브로커를 통한 메시지 전송을 할 수 있다.
    • 메시지 수신 애플리케이션은 같은 템플릿 기반의 클라이언트들을 사용해서 Pull 모델 형태의 메시지 consume 할 수 있다.
    • message listener 어노테이션 (@JmsListener, @RabbitListener, or @KafkaListener) 를 Bean 메서드에 지정하면 푸쉬모델 형태로 컨슈머에게 메시지가 전송될 수 있다.

    참고

    반응형

    댓글

    Designed by JB FACTORY