게시자 가이드

게시자 애플리케이션이 메시지를 만들어 주제로 전달합니다. 구독자 개요에서 설명하는 것처럼, Cloud Pub/Sub는 기존 구독자에 최소 1회의 메시지 전달과 최선의 순서 지정을 제공합니다. 게시자 애플리케이션의 일반적인 흐름은 다음과 같습니다.

  1. 사용자 데이터를 포함하는 메시지를 생성합니다.
  2. 메시지를 원하는 주제에 게시하라는 요청을 Cloud Pub/Sub 서버에 전송합니다.

주제 생성 및 관리에 관한 자세한 내용은 주제 및 구독 관리를 참조하세요.

설정

원하는 프로그래밍 언어의 환경 설정 방법은 클라이언트 라이브러리 시작 가이드를 참조하세요.

주제에 메시지 게시

REST를 통해 JSON을 사용하는 경우, 메시지 데이터는 base64로 인코딩되어야 합니다. 하나 이상의 메시지를 포함하는 전체 요청은 디코딩 후 10MB보다 작아야 합니다. 메시지 페이로드는 비어 있으면 안 됩니다. 비어 있지 않은 데이터 필드나 하나 이상의 속성을 포함해야 합니다.

선택한 프로그래밍 언어에 따라 클라이언트 라이브러리는 메시지를 동기적 또는 비동기적으로 게시합니다. 비동기 게시는 일괄 처리를 지원하고 애플리케이션 처리량을 높입니다.

모든 클라이언트 라이브러리는 메시지 게시를 비동기적으로 지원합니다. 비동기적 메시지 게시를 원한다면 선택한 프로그래밍 언어의 API 참조 문서를 참고해 해당 언어의 클라이언트 라이브러리가 비동기적 메시지 게시를 지원하는지 확인하세요. 서버에서 생성한 ID(주제별로 하나씩만 존재)는 메시지 게시 성공 시 반환됩니다.

프로토콜

요청:

POST https://pubsub.googleapis.com/v1/projects/myproject/topics/mytopic:publish
{
  "messages": [
    {
      "attributes": {
        "key": "iana.org/language_tag",
        "value": "en"
      },
      "data": "SGVsbG8gQ2xvdWQgUHViL1N1YiEgSGVyZSBpcyBteSBtZXNzYWdlIQ=="
    }
  ]
}

응답:

200 OK
{
  "messageIds": [
    "19916711285"
  ]
}

C#

이 샘플을 시도해 보기 전에 클라이언트 라이브러리를 사용하는 Cloud Pub/Sub 빠른 시작의 C# 설정 안내를 따르세요. 자세한 내용은 Cloud Pub/Sub C# API 참조 문서를 참조하세요.

PublisherServiceApiClient publisherClient = PublisherServiceApiClient.Create();
PublisherClient publisher = PublisherClient.Create(
    new TopicName(projectId, topicId), new[] { publisherClient });

Go

이 샘플을 시도해 보기 전에 클라이언트 라이브러리를 사용하는 Cloud Pub/Sub 빠른 시작의 Go 설정 안내를 따르세요. 자세한 내용은 Cloud Pub/Sub Go API 참조 문서를 참조하세요.

var wg sync.WaitGroup
var totalErrors uint64
t := client.Topic(topic)

for i := 0; i < n; i++ {
	result := t.Publish(ctx, &pubsub.Message{
		// data must be a ByteString
		Data: []byte("Message " + strconv.Itoa(i)),
	})

	wg.Add(1)
	go func(i int, res *pubsub.PublishResult) {
		defer wg.Done()
		// The Get method blocks until a server-generated ID or
		// an error is returned for the published message.
		id, err := res.Get(ctx)
		if err != nil {
			// Error handling code can be added here.
			log.Output(1, fmt.Sprintf("Failed to publish: %v", err))
			atomic.AddUint64(&totalErrors, 1)
			return
		}
		fmt.Printf("Published message %d; msg ID: %v\n", i, id)
	}(i, result)
}

wg.Wait()

if totalErrors > 0 {
	return errors.New(
		fmt.Sprintf("%d of %d messages did not publish successfully",
			totalErrors, n))
}
return nil

자바

이 샘플을 시도해 보기 전에 클라이언트 라이브러리를 사용하는 Cloud Pub/Sub 빠른 시작의 자바 설정 안내를 따르세요. 자세한 내용은 Cloud Pub/Sub 자바 API 참조 문서를 참조하세요.

ProjectTopicName topicName = ProjectTopicName.of("my-project-id", "my-topic-id");
Publisher publisher = null;

try {
  // Create a publisher instance with default settings bound to the topic
  publisher = Publisher.newBuilder(topicName).build();

  List<String> messages = Arrays.asList("first message", "second message");

  for (final String message : messages) {
    ByteString data = ByteString.copyFromUtf8(message);
    PubsubMessage pubsubMessage = PubsubMessage.newBuilder().setData(data).build();

    // Once published, returns a server-assigned message id (unique within the topic)
    ApiFuture<String> future = publisher.publish(pubsubMessage);

    // Add an asynchronous callback to handle success / failure
    ApiFutures.addCallback(future, new ApiFutureCallback<String>() {

      @Override
      public void onFailure(Throwable throwable) {
        if (throwable instanceof ApiException) {
          ApiException apiException = ((ApiException) throwable);
          // details on the API exception
          System.out.println(apiException.getStatusCode().getCode());
          System.out.println(apiException.isRetryable());
        }
        System.out.println("Error publishing message : " + message);
      }

      @Override
      public void onSuccess(String messageId) {
        // Once published, returns server-assigned message ids (unique within the topic)
        System.out.println(messageId);
      }
    });
  }
} finally {
  if (publisher != null) {
    // When finished with the publisher, shutdown to free up resources.
    publisher.shutdown();
    publisher.awaitTermination(1, TimeUnit.MINUTES);
  }
}

Node.js

이 샘플을 시도해 보기 전에 클라이언트 라이브러리를 사용하는 Cloud Pub/Sub 빠른 시작의 Node.js 설정 안내를 따르세요. 자세한 내용은 Cloud Pub/Sub Node.js API 참조 문서를 참조하세요.

// Imports the Google Cloud client library
const {PubSub} = require('@google-cloud/pubsub');

// Creates a client
const pubsub = new PubSub();

/**
 * TODO(developer): Uncomment the following lines to run the sample.
 */
// const topicName = 'my-topic';
// const data = JSON.stringify({ foo: 'bar' });

// Publishes the message as a string, e.g. "Hello, world!" or JSON.stringify(someObject)
const dataBuffer = Buffer.from(data);

pubsub
  .topic(topicName)
  .publisher()
  .publish(dataBuffer)
  .then(messageId => {
    console.log(`Message ${messageId} published.`);
  })
  .catch(err => {
    console.error('ERROR:', err);
  });

PHP

이 샘플을 시도해 보기 전에 클라이언트 라이브러리를 사용하는 Cloud Pub/Sub 빠른 시작의 PHP 설정 안내를 따르세요. 자세한 내용은 Cloud Pub/Sub PHP API 참조 문서를 참조하세요.

use Google\Cloud\PubSub\PubSubClient;

/**
 * Publishes a message for a Pub/Sub topic.
 *
 * @param string $projectId  The Google project ID.
 * @param string $topicName  The Pub/Sub topic name.
 * @param string $message  The message to publish.
 */
function publish_message($projectId, $topicName, $message)
{
    $pubsub = new PubSubClient([
        'projectId' => $projectId,
    ]);
    $topic = $pubsub->topic($topicName);
    $topic->publish(['data' => $message]);
    print('Message published' . PHP_EOL);
}

Python

이 샘플을 시도해 보기 전에 클라이언트 라이브러리를 사용하는 Cloud Pub/Sub 빠른 시작의 Python 설정 안내를 따르세요. 자세한 내용은 Cloud Pub/Sub Python API 참조 문서를 참조하세요.

import time

from google.cloud import pubsub_v1

# TODO project_id = "Your Google Cloud Project ID"
# TODO topic_name = "Your Pub/Sub topic name"

publisher = pubsub_v1.PublisherClient()
topic_path = publisher.topic_path(project_id, topic_name)

def callback(message_future):
    # When timeout is unspecified, the exception method waits indefinitely.
    if message_future.exception(timeout=30):
        print('Publishing message on {} threw an Exception {}.'.format(
            topic_name, message_future.exception()))
    else:
        print(message_future.result())

for n in range(1, 10):
    data = u'Message number {}'.format(n)
    # Data must be a bytestring
    data = data.encode('utf-8')
    # When you publish a message, the client returns a Future.
    message_future = publisher.publish(topic_path, data=data)
    message_future.add_done_callback(callback)

print('Published message IDs:')

# We must keep the main thread from exiting to allow it to process
# messages in the background.
while True:
    time.sleep(60)

Ruby

이 샘플을 시도해 보기 전에 클라이언트 라이브러리를 사용하는 Cloud Pub/Sub 빠른 시작의 Ruby 설정 안내를 따르세요. 자세한 내용은 Cloud Pub/Sub Ruby API 참조 문서를 참조하세요.

# project_id = "Your Google Cloud Project ID"
# topic_name = "Your Pubsub topic name"
require "google/cloud/pubsub"

pubsub = Google::Cloud::Pubsub.new project: project_id

topic = pubsub.topic topic_name
topic.publish_async "This is a test message." do |result|
  if result.succeeded?
    puts "Message published asynchronously."
  else
    raise "Failed to publish the message."
  end
end

# Stop the async_publisher to send all queued messages immediately.
topic.async_publisher.stop.wait!

맞춤 속성

Pub/Sub 메시지에 맞춤 속성을 메타데이터로 삽입할 수 있습니다. 속성은 텍스트 문자열이나 바이트 문자열이 지원됩니다.

Node.js

이 샘플을 시도해 보기 전에 클라이언트 라이브러리를 사용하는 Cloud Pub/Sub 빠른 시작의 Node.js 설정 안내를 따르세요. 자세한 내용은 Cloud Pub/Sub Node.js API 참조 문서를 참조하세요.

// Imports the Google Cloud client library
const {PubSub} = require('@google-cloud/pubsub');

// Creates a client
const pubsub = new PubSub();

/**
 * TODO(developer): Uncomment the following lines to run the sample.
 */
// const topicName = 'my-topic';
// const data = JSON.stringify({ foo: 'bar' });

// Publishes the message as a string, e.g. "Hello, world!" or JSON.stringify(someObject)
const dataBuffer = Buffer.from(data);
// Add two custom attributes, origin and username, to the message
const customAttributes = {
  origin: 'nodejs-sample',
  username: 'gcp',
};

pubsub
  .topic(topicName)
  .publisher()
  .publish(dataBuffer, customAttributes)
  .then(messageId => {
    console.log(`Message ${messageId} published.`);
  })
  .catch(err => {
    console.error('ERROR:', err);
  });

Python

이 샘플을 시도해 보기 전에 클라이언트 라이브러리를 사용하는 Cloud Pub/Sub 빠른 시작의 Python 설정 안내를 따르세요. 자세한 내용은 Cloud Pub/Sub Python API 참조 문서를 참조하세요.

from google.cloud import pubsub_v1

# TODO project_id = "Your Google Cloud Project ID"
# TODO topic_name = "Your Pub/Sub topic name"

publisher = pubsub_v1.PublisherClient()
topic_path = publisher.topic_path(project_id, topic_name)

for n in range(1, 10):
    data = u'Message number {}'.format(n)
    # Data must be a bytestring
    data = data.encode('utf-8')
    # Add two attributes, origin and username, to the message
    publisher.publish(
        topic_path, data, origin='python-sample', username='gcp')

print('Published messages with custom attributes.')

Ruby

이 샘플을 시도해 보기 전에 클라이언트 라이브러리를 사용하는 Cloud Pub/Sub 빠른 시작의 Ruby 설정 안내를 따르세요. 자세한 내용은 Cloud Pub/Sub Ruby API 참조 문서를 참조하세요.

# project_id = "Your Google Cloud Project ID"
# topic_name = "Your Pubsub topic name"
require "google/cloud/pubsub"

pubsub = Google::Cloud::Pubsub.new project: project_id

topic = pubsub.topic topic_name
# Add two attributes, origin and username, to the message
topic.publish_async "This is a test message.",
                    origin: "ruby-sample",
                    username: "gcp" do |result|
  if result.succeeded?
    puts "Message with custom attributes published asynchronously."
  else
    raise "Failed to publish the message."
  end
end

# Stop the async_publisher to send all queued messages immediately.
topic.async_publisher.stop.wait!

지연 시간 및 처리량의 균형을 유지하기 위한 일괄 처리

Cloud Pub/Sub 클라이언트 라이브러리는 여러 메시지를 단일 호출로 일괄 처리해 서비스에 전달합니다. 배치 크기가 커지면 메시지 처리량(CPU별 전송 메시지 비율)이 상승합니다. 일괄 처리의 단점은 개별 메시지의 지연 시간 증가인데, 이러한 메시지는 해당 배치가 채워져 네트워크로 전송될 준비가 될 때까지 메모리에서 대기합니다. 지연 시간을 최소화하려면 일괄 처리를 꺼야 합니다. 이는 요청-응답 시퀀스의 일부로 단일 메시지를 게시하는 애플리케이션에서 특히 더 중요합니다. 이러한 패턴의 대표적인 예는 Cloud Functions 또는 App Engine을 사용하는.서버리스 이벤트 기반 애플리케이션에서 볼 수 있습니다.

메시지는 요청 크기(단위: 바이트), 메시지 수, 시간을 기준으로 일괄 처리할 수 있습니다. 이 샘플에서처럼 기본 설정을 재정의할 수도 있습니다.

C#

이 샘플을 시도해 보기 전에 클라이언트 라이브러리를 사용하는 Cloud Pub/Sub 빠른 시작의 C# 설정 안내를 따르세요. 자세한 내용은 Cloud Pub/Sub C# API 참조 문서를 참조하세요.

PublisherServiceApiClient publisherClient = PublisherServiceApiClient.Create();
PublisherClient publisher = PublisherClient.Create(
    new TopicName(projectId, topicId), new[] { publisherClient },
    new PublisherClient.Settings
    {
        BatchingSettings = new Google.Api.Gax.BatchingSettings(
            elementCountThreshold: 100,
            byteCountThreshold: 10240,
            delayThreshold: TimeSpan.FromSeconds(3))
    });
var publishTasks = new List<Task<string>>();
// PublisherClient collects messages into appropriately sized
// batches.
foreach (string text in messageTexts)
{
    // Record all publishing Tasks. When each Task completes
    // without error, the message has been successfully published.
    // In real use don't simply store all publish Tasks forever;
    // it is usually appropriate to keep a reference to the Task
    // only until it has completed.
    publishTasks.Add(publisher.PublishAsync(text));
}
foreach (var task in publishTasks)
{
    Console.WriteLine("Published message {0}", task.Result);
}

Go

이 샘플을 시도해 보기 전에 클라이언트 라이브러리를 사용하는 Cloud Pub/Sub 빠른 시작의 Go 설정 안내를 따르세요. 자세한 내용은 Cloud Pub/Sub Go API 참조 문서를 참조하세요.

t := client.Topic(topic)
t.PublishSettings = pubsub.PublishSettings{
	ByteThreshold:  5000,
	CountThreshold: 10,
	DelayThreshold: 100 * time.Millisecond,
}
result := t.Publish(ctx, &pubsub.Message{Data: msg})
// Block until the result is returned and a server-generated
// ID is returned for the published message.
id, err := result.Get(ctx)
if err != nil {
	return err
}
fmt.Printf("Published a message; msg ID: %v\n", id)

자바

이 샘플을 시도해 보기 전에 클라이언트 라이브러리를 사용하는 Cloud Pub/Sub 빠른 시작의 자바 설정 안내를 따르세요. 자세한 내용은 Cloud Pub/Sub 자바 API 참조 문서를 참조하세요.

// Batch settings control how the publisher batches messages
long requestBytesThreshold = 5000L; // default : 1kb
long messageCountBatchSize = 10L; // default : 100

Duration publishDelayThreshold = Duration.ofMillis(100); // default : 1 ms

// Publish request get triggered based on request size, messages count & time since last publish
BatchingSettings batchingSettings = BatchingSettings.newBuilder()
    .setElementCountThreshold(messageCountBatchSize)
    .setRequestByteThreshold(requestBytesThreshold)
    .setDelayThreshold(publishDelayThreshold)
    .build();

Publisher publisher = Publisher.newBuilder(topicName)
    .setBatchingSettings(batchingSettings).build();

Node.js

이 샘플을 시도해 보기 전에 클라이언트 라이브러리를 사용하는 Cloud Pub/Sub 빠른 시작의 Node.js 설정 안내를 따르세요. 자세한 내용은 Cloud Pub/Sub Node.js API 참조 문서를 참조하세요.

// Imports the Google Cloud client library
const {PubSub} = require('@google-cloud/pubsub');

// Creates a client
const pubsub = new PubSub();

/**
 * TODO(developer): Uncomment the following lines to run the sample.
 */
// const topicName = 'my-topic';
// const data = JSON.stringify({ foo: 'bar' });
// const maxMessages = 10;
// const maxWaitTime = 10000;

// Publishes the message as a string, e.g. "Hello, world!" or JSON.stringify(someObject)
const dataBuffer = Buffer.from(data);

pubsub
  .topic(topicName)
  .publisher({
    batching: {
      maxMessages: maxMessages,
      maxMilliseconds: maxWaitTime,
    },
  })
  .publish(dataBuffer)
  .then(results => {
    const messageId = results[0];
    console.log(`Message ${messageId} published.`);
  })
  .catch(err => {
    console.error('ERROR:', err);
  });

Python

이 샘플을 시도해 보기 전에 클라이언트 라이브러리를 사용하는 Cloud Pub/Sub 빠른 시작의 Python 설정 안내를 따르세요. 자세한 내용은 Cloud Pub/Sub Python API 참조 문서를 참조하세요.

from google.cloud import pubsub_v1

# TODO project_id = "Your Google Cloud Project ID"
# TODO topic_name = "Your Pub/Sub topic name"

# Configure the batch to publish as soon as there is one kilobyte
# of data or one second has passed.
batch_settings = pubsub_v1.types.BatchSettings(
    max_bytes=1024,  # One kilobyte
    max_latency=1,  # One second
)
publisher = pubsub_v1.PublisherClient(batch_settings)
topic_path = publisher.topic_path(project_id, topic_name)

for n in range(1, 10):
    data = u'Message number {}'.format(n)
    # Data must be a bytestring
    data = data.encode('utf-8')
    publisher.publish(topic_path, data=data)

print('Published messages.')

Ruby

이 샘플을 시도해 보기 전에 클라이언트 라이브러리를 사용하는 Cloud Pub/Sub 빠른 시작의 Ruby 설정 안내를 따르세요. 자세한 내용은 Cloud Pub/Sub Ruby API 참조 문서를 참조하세요.

# project_id = "Your Google Cloud Project ID"
# topic_name = "Your Pubsub topic name"
require "google/cloud/pubsub"

pubsub = Google::Cloud::Pubsub.new project: project_id

topic = pubsub.topic topic_name
topic.publish do |batch|
  10.times do |i|
    batch.publish "This is message \##{i}."
  end
end

puts "Messages published in batch."
# project_id = "Your Google Cloud Project ID"
# topic_name = "Your Pubsub topic name"
require "google/cloud/pubsub"

pubsub = Google::Cloud::Pubsub.new project: project_id

# Start sending messages in one request once the size of all queued messages
# reaches 1 MB or the number of queued messages reaches 20
topic = pubsub.topic topic_name, async: {
  :max_bytes => 1000000,
  :max_messages => 20
}
10.times do |i|
  topic.publish_async "This is message \##{i}."
end

# Stop the async_publisher to send all queued messages immediately.
topic.async_publisher.stop.wait!
puts "Messages published asynchronously in batch."

재시도 요청

재시도를 할 수 없는 오류를 제외하고, 실패한 게시는 자동으로 다시 시도됩니다. 본 샘플 코드는 맞춤 재시도 설정을 적용한 게시자 생성 방법을 보여줍니다(맞춤 재시도 설정을 지원하지 않는 클라이언트 라이브러리도 있습니다. 자세한 내용은 선택한 언어의 API 참조 문서에서 확인하세요).

자바

이 샘플을 시도해 보기 전에 클라이언트 라이브러리를 사용하는 Cloud Pub/Sub 빠른 시작의 자바 설정 안내를 따르세요. 자세한 내용은 Cloud Pub/Sub 자바 API 참조 문서를 참조하세요.

// Retry settings control how the publisher handles retryable failures
Duration retryDelay = Duration.ofMillis(100); // default : 1 ms
double retryDelayMultiplier = 2.0; // back off for repeated failures
Duration maxRetryDelay = Duration.ofSeconds(5); // default : 10 seconds
Duration totalTimeout = Duration.ofSeconds(1); // default: 0
Duration initialRpcTimeout = Duration.ofSeconds(1); // default: 0
Duration maxRpcTimeout = Duration.ofSeconds(10); // default: 0

RetrySettings retrySettings = RetrySettings.newBuilder()
    .setInitialRetryDelay(retryDelay)
    .setRetryDelayMultiplier(retryDelayMultiplier)
    .setMaxRetryDelay(maxRetryDelay)
    .setTotalTimeout(totalTimeout)
    .setInitialRpcTimeout(initialRpcTimeout)
    .setMaxRpcTimeout(maxRpcTimeout)
    .build();

Publisher publisher = Publisher.newBuilder(topicName)
    .setRetrySettings(retrySettings).build();

동시 실행 제어

동시 실행 지원 여부는 프로그래밍 언어에 따라 다릅니다. 자세한 내용은 API 참조 문서를 참조하세요.

다음 샘플은 게시자의 동시 실행을 제어하는 방법을 보여줍니다.

Go

이 샘플을 시도해 보기 전에 클라이언트 라이브러리를 사용하는 Cloud Pub/Sub 빠른 시작의 Go 설정 안내를 따르세요. 자세한 내용은 Cloud Pub/Sub Go API 참조 문서를 참조하세요.

t := client.Topic(topic)
t.PublishSettings = pubsub.PublishSettings{
	NumGoroutines: 1,
}
result := t.Publish(ctx, &pubsub.Message{Data: msg})
// Block until the result is returned and a server-generated
// ID is returned for the published message.
id, err := result.Get(ctx)
if err != nil {
	return err
}
fmt.Printf("Published a message; msg ID: %v\n", id)

자바

이 샘플을 시도해 보기 전에 클라이언트 라이브러리를 사용하는 Cloud Pub/Sub 빠른 시작의 자바 설정 안내를 따르세요. 자세한 내용은 Cloud Pub/Sub 자바 API 참조 문서를 참조하세요.

// create a publisher with a single threaded executor
ExecutorProvider executorProvider = InstantiatingExecutorProvider.newBuilder()
    .setExecutorThreadCount(1).build();
Publisher publisher = Publisher.newBuilder(topicName)
    .setExecutorProvider(executorProvider).build();

Python

이 샘플을 시도해 보기 전에 클라이언트 라이브러리를 사용하는 Cloud Pub/Sub 빠른 시작의 Python 설정 안내를 따르세요. 자세한 내용은 Cloud Pub/Sub Python API 참조 문서를 참조하세요.

from google.cloud import pubsub_v1

# TODO project_id = "Your Google Cloud Project ID"
# TODO topic_name = "Your Pub/Sub topic name"

publisher = pubsub_v1.PublisherClient()
topic_path = publisher.topic_path(project_id, topic_name)

# When you publish a message, the client returns a Future. This Future
# can be used to track when the message is published.
futures = []

for n in range(1, 10):
    data = u'Message number {}'.format(n)
    # Data must be a bytestring
    data = data.encode('utf-8')
    message_future = publisher.publish(topic_path, data=data)
    futures.append(message_future)

print('Published message IDs:')
for future in futures:
    # result() blocks until the message is published.
    print(future.result())

Ruby

이 샘플을 시도해 보기 전에 클라이언트 라이브러리를 사용하는 Cloud Pub/Sub 빠른 시작의 Ruby 설정 안내를 따르세요. 자세한 내용은 Cloud Pub/Sub Ruby API 참조 문서를 참조하세요.

# project_id = "Your Google Cloud Project ID"
# topic_name = "Your Pubsub topic name"
require "google/cloud/pubsub"

pubsub = Google::Cloud::Pubsub.new project: project_id

topic = pubsub.topic topic_name, async: {
  :threads => {
    # Use exactly one thread for publishing message and exactly one thread
    # for executing callbacks
    :publish => 1,
    :callback => 1
  }
}
topic.publish_async "This is a test message." do |result|
  if result.succeeded?
    puts "Message published asynchronously."
  else
    raise "Failed to publish the message."
  end
end

# Stop the async_publisher to send all queued messages immediately.
topic.async_publisher.stop.wait!
이 페이지가 도움이 되었나요? 평가를 부탁드립니다.

다음에 대한 의견 보내기...