프로듀서 사용하기

프로듀서 사용하기 (Producers)

클라이언트를 설정한 뒤에는 프로듀서를 만들어 메시지를 게시할 수 있어요. 이 문서는 프로듀서를 만들고 이름을 지정하며, 메시지를 동기·비동기로 게시하고 다양한 메시지 속성을 설정하는 방법을 정리했어요. 파티셔닝된 토픽에서의 라우팅, 청킹, 인터셉터, 접근 모드 같은 고급 기능도 함께 살펴볼게요.

출처: 문서

본문

클라이언트를 설정한 뒤에는 더 살펴보며 프로듀서로 작업을 시작할 수 있어요.

프로듀서 만들기 (Create the producer)

이 예시는 프로듀서를 만드는 방법을 보여줘요.

  • Java C++ Python

Java로 프로듀서를 동기적으로 만들기: Producer<String> producer = pulsarClient.newProducer(Schema.STRING).topic("my-topic").create(); Java로 프로듀서를 비동기적으로 만들기: pulsarClient.newProducer(Schema.STRING).topic("my-topic").createAsync().thenAccept(p -> { log.info("Producer created: {}", p.getProducerName()); });

Producer producer;
Result result = client.createProducer("my-topic", producer);
producer = client.create_producer('my-topic')

프로듀서 이름 지정 (Producer naming)

모든 프로듀서에는 모든 Pulsar 클러스터에서 고유해야 하는 이름이 있어요. 이름을 명시적으로 설정하지 않으면 Pulsar가 전역적으로 고유한 이름을 자동으로 생성해요. 이름을 지정하면 브로커가 그 이름을 가진 프로듀서 하나만 한 번에 토픽에 게시할 수 있도록 강제해요.

메시지 중복 제거(deduplication)를 사용할 때는 명시적인 프로듀서 이름을 설정해야 해요. 중복 제거가 필요하지 않더라도 의미 있는 프로듀서 이름을 설정하는 것이 권장돼요 — 브로커 로그, 관리자 통계, 메트릭에 이름이 나타나므로 디버깅이 훨씬 쉬워지고, 메시지를 만든 애플리케이션까지 빠르게 추적할 수 있거든요.

  • Java C++ Python
Producer<String> producer = pulsarClient.newProducer(Schema.STRING)
              .topic("my-topic")
              .producerName("my-unique-producer-name")
              .create();
ProducerConfiguration producerConfig;
producerConfig.setProducerName("my-unique-producer-name");
Producer producer;
Result result = client.createProducer("my-topic", producerConfig, producer);
producer = client.create_producer('my-topic', producer_name='my-unique-producer-name')

메시지 게시 (Publish messages)

Pulsar는 대부분의 클라이언트에서 메시지의 동기 및 비동기 게시를 모두 지원해요. Node.js, C# 같은 일부 언어별 클라이언트에서는 언어별 메커니즘(예: await)을 사용해 비동기 메서드를 기반으로 동기적으로 메시지를 게시할 수 있어요.

비동기 게시를 사용하면 프로듀서가 메시지를 블로킹 큐에 넣고 즉시 반환해요. 그러면 클라이언트 라이브러리가 브로커에 메시지를 백그라운드로 보내요. 큐가 가득 차면(최대 크기 구성 가능) 프로듀서에 전달된 인자에 따라 API 호출 시 프로듀서가 블록되거나 즉시 실패해요.

이 예시는 프로듀서를 사용해 메시지를 게시하는 방법을 보여줘요. 게시 연산은 브로커가 메시지가 성공적으로 게시됐다고 알려줄 때까지 수행돼요. 브로커는 메시지가 성공적으로 게시된 후 메시지 ID를 반환해요.

  • Java C++ Go Node.js C# Python

Java로 메시지를 동기적으로 게시하기: MessageId messageId = producer.newMessage().value("my-sync-message").send(); Java로 메시지를 비동기적으로 게시하기: producer.newMessage().value("my-sync-message").sendAsync().thenAccept(messageId -> { log.info("Message ID: {}", messageId); });

Message msg = MessageBuilder()
                    .setContent("my-sync-message")
                    .build();
Result res = producer.send(msg);
 msg := pulsar.ProducerMessage{
     Payload: []byte("my-sync-message"),
 }
 if _, err := producer.send(msg); err != nil {
   log.Fatalf("Could not publish message due to: %v", err)
 }

ProducerMessage 객체의 모든 메서드는 Go API 문서를 참고하세요.

const msg = {
  data: Buffer.from('my-sync-message'),
}
await producer.send(msg);

프로듀서 메시지 객체에 사용할 수 있는 키는 다음과 같아요.

파라미터 설명
data 메시지의 실제 데이터 페이로드.
properties 메시지에 첨부된 애플리케이션별 메타데이터용 Object.
eventTimestamp 메시지와 연관된 타임스탬프.
sequenceId 메시지의 시퀀스 ID.
partitionKey 메시지와 연관된 선택적 키 (토픽 컴팩션 같은 것에 특히 유용).
replicationClusters 이 메시지가 복제되는 클러스터. Pulsar 브로커가 메시지 복제를 자동으로 처리하므로, 브로커 기본값을 덮어쓰고 싶을 때만 이 설정을 바꿔야 해요.
deliverAt 메시지가 전달되는 절대 타임스탬프.
deliverAfter 메시지가 전달되는 상대 지연.

메시지 객체 연산 — Pulsar Node.js 클라이언트에서는 컨슈머(또는 리더)로 메시지 객체를 받을(또는 읽을) 수 있어요. 메시지 객체의 메서드:

메서드 설명 반환 타입
getTopicName() 토픽 이름의 getter 메서드. String
getProperties() 프로퍼티의 getter 메서드. Array<Object>
getData() 메시지 데이터의 getter 메서드. Buffer
getMessageId() 메시지 ID 객체의 getter 메서드. Object
getPublishTimestamp() 게시 타임스탬프의 getter 메서드. Number
getEventTimestamp() 이벤트 타임스탬프의 getter 메서드. Number
getRedeliveryCount() 재전달 횟수의 getter 메서드. Number
getPartitionKey() 파티션 키의 getter 메서드. String

메시지 ID 객체 연산 — Pulsar Node.js 클라이언트에서는 메시지 객체에서 메시지 ID 객체를 얻을 수 있어요. 메시지 ID 객체의 메서드:

메서드 설명 반환 타입
serialize() 메시지 ID를 저장용 Buffer로 직렬화. Buffer
toString() 메시지 ID를 String으로 가져오기. String

클라이언트는 메시지 ID 객체의 정적 메서드를 갖고 있어요. Pulsar.MessageId.someStaticMethod로 접근할 수 있어요. 정적 메서드:

메서드 설명 반환 타입
earliest() 가장 이른 메시지로 리더를 시작하기 위한 MessageId.
latest() 가장 최근 메시지로 리더를 시작하기 위한 MessageId.
var data = Encoding.UTF8.GetBytes("Hello World");
await producer.Send(data);
producer.send('Hello World'.encode('utf-8'))

메시지 구성 (Configure messages)

Pulsar 메시지의 다양한 속성을 설정할 수 있어요. 이 속성들의 값은 메시지의 메타데이터에 저장돼요.

  • Java C++ Go C# Python
producer.newMessage()
              .key("my-key") // Set the message key
              .eventTime(System.currentTimeMillis()) // Set the event time
              .sequenceId(1203) // Set the sequenceId for the deduplication purposes
              .deliverAfter(1, TimeUnit.HOURS) // Delay message delivery for 1 hour
              .property("my-key", "my-value") // Set the customized metadata
              .property("my-other-key", "my-other-value")
              .replicationClusters(
                      Lists.newArrayList("r1", "r2")) // Set the geo-replication clusters for this message.
              .value("content")
              .send();

Java 클라이언트에서는 loadConf를 사용해 메시지 메타데이터를 구성할 수도 있어요. 예시: Map<String, Object> conf = new HashMap<>(); conf.put("key", "my-key"); conf.put("eventTime", System.currentTimeMillis()); producer.newMessage().value("my-message").loadConf(conf).send();

Message msg = MessageBuilder()
                  .setContent("content")
                  .setProperty("my-key", "my-value")
                  .setProperty("my-other-key", "my-other-value")
                  .setDeliverAfter(std::chrono::minutes(3)) // Delay message delivery for 3 minutes
                  .build();
Result res = producer.send(msg);
ID, err := producer.Send(context.Background(), &pulsar.ProducerMessage{
     Payload:      []byte(fmt.Sprintf("content")),
     DeliverAfter: 3 * time.Second, // Delay message delivery for 3 seconds
 })
 if err != nil {
     log.Fatal(err)
 }
var messageId = await producer.NewMessage()
                            .Property("SomeKey", "SomeValue")
                            .Send(data);
producer.send('content'.encode('utf-8'),
              properties={'my-key': 'my-value', 'my-other-key': 'my-other-value'},
              event_timestamp=int(time.time() * 1000))

파티셔닝된 토픽에 메시지 게시 (Publish messages to partitioned topics)

기본적으로 Pulsar 토픽은 단일 브로커가 서비스하므로 토픽의 최대 처리량이 제한돼요. 파티셔닝된 토픽은 여러 브로커에 걸쳐 있을 수 있어 더 높은 처리량을 허용해요.

Pulsar 클라이언트 라이브러리를 사용해 파티셔닝된 토픽에 메시지를 게시할 수 있어요. 파티셔닝된 토픽에 메시지를 게시할 때는 라우팅 모드를 지정해야 해요. 새 프로듀서를 만들 때 라우팅 모드를 지정하지 않으면 라운드 로빈 라우팅 모드가 사용돼요.

내장 메시지 라우터 사용 (Use built-in message router)

라우팅 모드는 각 메시지가 게시될 파티션(내부 토픽)을 결정해요.

다음은 예시예요.

  • Java C++ Go Python
Producer<byte[]> producer = pulsarClient.newProducer()
   .topic("my-topic")
   .messageRoutingMode(MessageRoutingMode.SinglePartition)
   .create();
#include "lib/RoundRobinMessageRouter.h" // Make sure include this header file
Producer producer;
Result result = client.createProducer(
   "my-topic",
   ProducerConfiguration().setMessageRouter(std::make_shared<RoundRobinMessageRouter>(
       ProducerConfiguration::BoostHash, true, 1000, 100000, boost::posix_time::seconds(1))),
   producer);
 producer, err := client.CreateProducer(pulsar.ProducerOptions{
     Topic: "my-topic",
     MessageRouter: func(msg *pulsar.ProducerMessage, tm pulsar.TopicMetadata) int {
         fmt.Println("Topic has", tm.NumPartitions(), "partitions. Routing message ", msg, " to partition 2.")
         // always push msg to partition 2
         return 2
     },
 })
 producer = client.create_producer(
     'my-topic',
     message_routing_mode=PartitionsRoutingMode.SinglePartition
 )

커스텀 메시지 라우터 (Customize message router)

  • Java C++ Go Python

커스텀 메시지 라우터를 사용하려면 MessageRouter 인터페이스 구현을 제공해야 해요. choosePartition(Message<?>, TopicMetadata) 메서드를 구현하세요(단일 인자 오버로드는 1.22.0부터 deprecated):

public interface MessageRouter extends Serializable {
    int choosePartition(Message<?> msg, TopicMetadata metadata);
}

다음 라우터는 모든 메시지를 파티션 10으로 라우팅해요:

public class AlwaysTenRouter implements MessageRouter {
    @Override
    public int choosePartition(Message<?> msg, TopicMetadata metadata) {
        return 10;
    }
}

그 구현으로 파티셔닝된 토픽에 다음과 같이 메시지를 보낼 수 있어요.

Producer<byte[]> producer = pulsarClient.newProducer()
    .topic("my-topic")
    .messageRouter(new AlwaysTenRouter())
    .create();
producer.send("Partitioned topic message".getBytes());

커스텀 메시지 라우터를 사용하려면 getPartition 메서드 하나를 가진 MessageRoutingPolicy 인터페이스 구현을 제공해야 해요:

class MessageRouter : public MessageRoutingPolicy {
  public:
    MessageRouter() : { }
    int getPartition(const Message& msg, const TopicMetadata& topicMetadata) {
      // The implementation of getPartition
    }
};

다음 라우터는 모든 메시지를 파티션 10으로 라우팅해요:

class MessageRouter : public MessageRoutingPolicy {
  public:
    MessageRouter() { }
    int getPartition(const Message& msg, const TopicMetadata& topicMetadata) {
      return 10;
    }
};

그 구현으로 파티셔닝된 토픽에 아래와 같이 메시지를 보낼 수 있어요.

Producer producer;
Result result = client.createProducer(
    "my-topic",
    ProducerConfiguration().setMessageRouter(std::make_shared<MessageRouter>()),
    producer);
Message msg = MessageBuilder()
    .setContent("content")
    .build();
result = producer.send(msg);

Go 클라이언트에서는 함수를 전달해 커스텀 메시지 라우터를 구성할 수 있어요.

 producer, err := client.CreateProducer(pulsar.ProducerOptions{
     Topic: "my-topic",
     MessageRouter: func(msg *pulsar.ProducerMessage, tm pulsar.TopicMetadata) int {
         fmt.Println("Topic has", tm.NumPartitions(), "partitions. Routing message ", msg, " to partition 10.")
         // always push msg to partition 10
         return 10
     },
 })

Python 클라이언트에서는 함수를 전달해 커스텀 메시지 라우터를 구성할 수 있어요.

 def custom_message_router(msg: pulsar.Message, num_partitions: int):
     # always push msg to partition 10
     return 10

 producer = client.create_producer(
     'my-topic',
     message_router=custom_message_router
 )
 producer.send(b'content')

키를 사용할 때 파티션 선택 (Choose partitions when using a key)

메시지에 키가 있으면 라운드 로빈 라우팅 정책을 대체해요. 다음 Java 예시 코드는 키를 사용할 때 파티션을 선택하는 방법을 설명해요.

// If the message has a key, it supersedes the round robin routing policy
if (msg.hasKey()) {
    return signSafeMod(hash.makeHash(msg.getKey()), topicMetadata.numPartitions());
}

if (isBatchingEnabled) { // if batching is enabled, choose partition on `partitionSwitchMs` boundary.
    long currentMs = clock.millis();
    return signSafeMod(currentMs / partitionSwitchMs + startPtnIdx, topicMetadata.numPartitions());
} else {
    return signSafeMod(PARTITION_INDEX_UPDATER.getAndIncrement(this), topicMetadata.numPartitions());
}

청킹 활성화 (Enable chunking)

메시지 청킹을 사용하면 Pulsar가 대용량 페이로드 메시지를 처리할 수 있는데, 프로듀서 쪽에서 메시지를 청크(chunk)로 분할하고 컨슈머 쪽에서 청크된 메시지를 다시 모아요.

메시지 청킹 기능은 기본적으로 꺼져 있어요. 다음은 프로듀서를 만들 때 메시지 청킹을 활성화하는 예시예요.

  • Java C++ Go Python
  Producer<byte[]> producer = client.newProducer()
     .topic(topic)
     .enableChunking(true)
     .enableBatching(false)
     .create();
ProducerConfiguration conf;
conf.setBatchingEnabled(false);
conf.setChunkingEnabled(true);
Producer producer;
client.createProducer("my-topic", conf, producer);
// The message chunking feature is OFF by default.
// By default, a producer chunks the large message based on the max message size (`maxMessageSize`) configured at the broker side (for example, 5MB).
// Client can also configure the max chunked size using the producer configuration `ChunkMaxMessageSize`.
// Note: to enable chunking, you need to disable batching (`DisableBatching=true`) concurrently.
producer, err := client.CreateProducer(pulsar.ProducerOptions{
  Topic:               "my-topic",
  DisableBatching:     true,
  EnableChunking:      true,
})
if err != nil {
	log.Fatal(err)
}
defer producer.Close()
producer = client.create_producer(
        topic,
        chunking_enabled=True
    )

기본적으로 프로듀서는 브로커에 구성된 최대 메시지 크기(maxMessageSize, 예: 5MB)에 따라 대용량 메시지를 청크로 나눠요. 하지만 클라이언트는 프로듀서 구성 chunkMaxMessageSize를 사용해 최대 청크 크기를 구성할 수도 있어요.

청킹을 활성화하려면 배칭(enableBatching = false)을 동시에 비활성화해야 해요.

메시지 인터셉트 (Intercept messages)

ProducerInterceptor는 프로듀서가 받은 메시지가 브로커에 게시되기 전에 가로채서 변형할 수 있어요.

인터페이스에는 세 가지 주요 이벤트가 있어요.

  • eligible — 인터셉터를 메시지에 적용할 수 있는지 확인해요.
  • beforeSend — 프로듀서가 메시지를 브로커에 보내기 전에 트리거돼요. 이 이벤트에서 메시지를 수정할 수 있어요.
  • onSendAcknowledgement — 메시지가 브로커에 ack되거나 전송이 실패했을 때 트리거돼요.

메시지를 인터셉트하려면 Producer를 만들 때 ProducerInterceptor를 하나 또는 여러 개 추가할 수 있어요.

  • Java C++
Producer<byte[]> producer = client.newProducer()
     .topic(topic)
     .intercept(new ProducerInterceptor() {
         @Override
         public void close() {
             // release any resources held by the interceptor
         }
         @Override
         public boolean eligible(Message<?> message) {
             return true;  // process all messages
         }
         @Override
         public Message<?> beforeSend(Producer<?> producer, Message<?> message) {
             // user-defined processing logic; return the (possibly modified) message
             return message;
         }
         @Override
         public void onSendAcknowledgement(Producer<?> producer, Message<?> message,
                                           MessageId msgId, Throwable exception) {
             // user-defined processing logic
         }
     })
     .create();

커스텀 인터셉터 구현:

class MyInterceptor : public ProducerInterceptor {
  public:
    MyInterceptor() { }

    Message beforeSend(const Producer& producer, const Message& message) override {
      // Your implementation code
      return message;
    }
    void onSendAcknowledgement(const Producer& producer, Result result, const Message& message, const MessageId& messageID) override {
      // Your implementation code
    }
    void close() override {
      // Your implementation code
    }
};

프로듀서 구성:

ProducerConfiguration conf;
conf.intercept({ std::make_shared<MyInterceptor>(), std::make_shared<MyInterceptor>() });  // You can add multiple interceptors to the same producer
Producer producer;
client.createProducer(topic, conf, producer);

여러 인터셉터는 intercept 메서드에 전달된 순서대로 적용돼요.

암호화 정책 구성 (Configure encryption policies)

Pulsar C# 클라이언트는 네 가지 종류의 암호화 정책을 지원해요.

  • EnforceUnencrypted — 항상 암호화되지 않은 연결을 사용해요.
  • EnforceEncrypted — 항상 암호화된 연결을 사용해요.
  • PreferUnencrypted — 가능하면 암호화되지 않은 연결을 사용해요.
  • PreferEncrypted — 가능하면 암호화된 연결을 사용해요.

이 예시는 EnforceUnencrypted 암호화 정책을 설정하는 방법을 보여줘요.

  • C#
using DotPulsar;

var client = PulsarClient.Builder()
                      .ConnectionSecurity(EncryptionPolicy.EnforceEncrypted)
                      .Build();

접근 모드 구성 (Configure access mode)

접근 모드를 사용하면 애플리케이션이 토픽에 대한 배타적 프로듀서 접근을 요구해 "단일 작성자(single-writer)" 상황을 만들 수 있어요.

이 예시는 프로듀서 접근 모드를 설정하는 방법을 보여줘요.

  • Java C++

이 기능은 Java 클라이언트 2.8.0 이상 버전에서 지원돼요.

Producer<byte[]> producer = client.newProducer()
     .topic(topic)
     .accessMode(ProducerAccessMode.Exclusive)
     .create();

이 기능은 C++ 클라이언트 3.1.0 이상 버전에서 지원돼요.

 Producer producer;
 ProducerConfiguration producerConfiguration;
 producerConfiguration.setAccessMode(ProducerConfiguration::Exclusive);
 client.createProducer(topicName, producerConfiguration, producer);

더 알아보기 (Learn more)