Spring Boot와 ActiveMQ를 활용한 메시지 큐 구현

ActiveMQ 원격 메시지 큐 시스템

Spring Boot 프로젝트 구성

ActiveMQ 개요

메시지 브로커는 데이터를 저장하고 전달하는 미들웨어 소프트웨어로, 실제 우체국의 택배 보관함과 유사한 개념입니다. 응용 프로그램은 데이터를 브로커에 전송하고, 해당 서비스가 이를 수신합니다. 이러한 시스템은 메시지 전송 과정에서 정보를 안전하게 보관하는 역할을 합니다.

메시지 브로커 활용 사례

  1. 대용량 큐로 사용하여 FIFO(선입선출) 방식으로 고부하 쓰기 작업 처리
  2. 직렬 실행을 병렬 처리로 변경하여 시스템 성능 최적화

동기 및 비동기 통신

동기 방식: Dubbo와 같은 기술로 실시간 처리가 필요한 경우 사용되며, 호출자는 결과를 받을 때까지 대기합니다.

비동기 방식: JMS 기반 MQ는 발신자가 메시지를 서버에 전달하면 즉시 처리되지 않을 수 있으며, 서버 상태에 따라 처리 시점이 결정됩니다. 실시간성이 낮지만 확장성이 우수합니다.

JMS (Java Message Service)

JMS 소개

JMS는 자바 플랫폼에서 메시징 서비스를 표준화한 API로, 다양한 벤더들이 이를 구현하고 있습니다.

주요 메시지 브로커 비교

  • ActiveMQ: 아파치의 오래된 솔루션으로 균형 잡힌 성능 제공
  • RabbitMQ: 금융 분야에 적합하며 데이터 무결성 보장
  • Kafka: 높은 처리량(초당 10만 건)을 지원하는 대용량 처리용

지원되는 메시지 형식

  • TextMessage: 문자열 데이터
  • MapMessage: 키-값 쌍
  • ObjectMessage: 직렬화된 자바 객체
  • BytesMessage: 바이트 스트림
  • StreamMessage: 자바 기본 타입 스트림

전송 모드

  1. 포인트투포인트: 일대일 통신 패턴
  2. 퍼블리시-서브스크라이브: 일대다 통신 패턴

ActiveMQ 설치

공식 사이트: http://activemq.apache.org/

프로젝트 설정

Maven 의존성

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-activemq</artifactId>
</dependency>

<dependency>
    <groupId>org.apache.activemq</groupId>
    <artifactId>activemq-pool</artifactId>
    <version>5.15.0</version>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

설정 파일(application.yml)

server:
  port: 8080
spring:
  activemq:
    broker-url: tcp://www.yangbuyi.top:61616
    user: admin
    password: admin
    close-timeout: 15s
    in-memory: true
    non-blocking-redelivery: false
    send-timeout: 0
    queue-name: active.queue
    topic-name: active.topic.name.model
    packages:
      trust-all: true

pool:
  enabled: true
  max-connections: 10
  idle-timeout: 30000

메시지 설정 클래스

package com.example.config;

import org.apache.activemq.ActiveMQConnectionFactory;
import org.apache.activemq.command.ActiveMQQueue;
import org.apache.activemq.command.ActiveMQTopic;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jms.config.JmsListenerContainerFactory;
import org.springframework.jms.config.SimpleJmsListenerContainerFactory;
import org.springframework.jms.core.JmsMessagingTemplate;

import javax.jms.ConnectionFactory;
import javax.jms.Queue;
import javax.jms.Topic;

@Configuration
public class MessageConfig {

    @Value("${spring.activemq.broker-url}")
    private String brokerUrl;

    @Value("${spring.activemq.user}")
    private String username;

    @Value("${spring.activemq.password}")
    private String password;

    @Value("${spring.activemq.queue-name}")
    private String queueName;

    @Value("${spring.activemq.topic-name}")
    private String topicName;

    @Bean(name = "queueDestination")
    public Queue queue() {
        return new ActiveMQQueue(queueName);
    }

    @Bean(name = "topicDestination")
    public Topic topic() {
        return new ActiveMQTopic(topicName);
    }

    @Bean
    public ConnectionFactory connectionFactory() {
        ActiveMQConnectionFactory factory = new ActiveMQConnectionFactory(username, password, brokerUrl);
        factory.setTrustAllPackages(true);
        return factory;
    }

    @Bean
    public JmsMessagingTemplate messagingTemplate() {
        return new JmsMessagingTemplate(connectionFactory());
    }

    @Bean("queueListenerFactory")
    public JmsListenerContainerFactory<?> queueListenerFactory(ConnectionFactory connectionFactory) {
        SimpleJmsListenerContainerFactory factory = new SimpleJmsListenerContainerFactory();
        factory.setConnectionFactory(connectionFactory);
        factory.setPubSubDomain(false);
        return factory;
    }

    @Bean("topicListenerFactory")
    public JmsListenerContainerFactory<?> topicListenerFactory(ConnectionFactory connectionFactory) {
        SimpleJmsListenerContainerFactory factory = new SimpleJmsListenerContainerFactory();
        factory.setConnectionFactory(connectionFactory);
        factory.setPubSubDomain(true);
        return factory;
    }
}

메시지 리스너 구현

큐 기반 리스너

package com.example.listener;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.jms.annotation.JmsListener;
import org.springframework.stereotype.Component;

@Component
public class QueueMessageListener {
    
    @Value("${spring.activemq.queue-name}")
    private String queueName;

    @JmsListener(destination = "${spring.activemq.queue-name}", containerFactory = "queueListenerFactory")
    public void handleQueueMessage(String message) {
        System.out.println("큐 메시지 수신: " + message);
        System.out.println("큐 이름: " + queueName);
    }
}

토픽 기반 리스너

package com.example.listener;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.jms.annotation.JmsListener;
import org.springframework.stereotype.Component;

import javax.jms.Message;
import javax.jms.ObjectMessage;
import javax.jms.JMSException;

@Component
public class TopicMessageListener {
    
    @Value("${spring.activemq.topic-name}")
    private String topicName;

    @JmsListener(destination = "${spring.activemq.topic-name}", containerFactory = "topicListenerFactory")
    public void handleTopicMessage(Message message) {
        try {
            if (message instanceof ObjectMessage) {
                ObjectMessage objMsg = (ObjectMessage) message;
                UserData userData = (UserData) objMsg.getObject();
                System.out.println("사용자: " + userData.getName() + ", 나이: " + userData.getAge());
            }
        } catch (JMSException e) {
            e.printStackTrace();
        }
        System.out.println("토픽 메시지 수신: " + message);
        System.out.println("토픽 이름: " + topicName);
    }
}

테스트용 데이터 객체

package com.example.model;

import java.io.Serializable;

public class UserData implements Serializable {
    private String name;
    private Integer age;

    public UserData(String name, Integer age) {
        this.name = name;
        this.age = age;
    }

    // getter 및 setter 생략
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public Integer getAge() { return age; }
    public void setAge(Integer age) { this.age = age; }
}

컨트롤러 구현

package com.example.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jms.core.JmsMessagingTemplate;
import org.springframework.web.bind.annotation.*;

import javax.jms.Destination;
import javax.jms.Queue;
import javax.jms.Topic;

@RestController
public class MessageController {
    
    @Autowired
    private JmsMessagingTemplate template;

    @Autowired
    private Queue queueDestination;

    @Autowired
    private Topic topicDestination;

    @PostMapping("/queue/send")
    public String sendToQueue(@RequestBody String message) {
        template.convertAndSend(queueDestination, message);
        return "큐 메시지 전송 완료";
    }

    @PostMapping("/topic/send")
    public String sendToTopic(@RequestBody String userName) {
        UserData data = new UserData(userName, 25);
        template.convertAndSend(topicDestination, data);
        return "토픽 메시지 전송 완료";
    }
}

테스트 실행

Postman 등을 사용하여 REST API 엔드포인트를 통해 메시지를 전송하고, 리스너가 정상적으로 메시지를 수신하는지 확인할 수 있습니다.

태그: Spring Boot ActiveMQ JMS Message Queue microservices

7월 23일 11:22에 게시됨