Ver3.0 ๐ PHP์ Apache Kafka๋ก ๋ง๋๋ ๋๊ท๋ชจ ๋ก๊ทธ ์์ง ์์คํ

๐ PHP์ Apache Kafka๋ก ๋ง๋๋ ๋๊ท๋ชจ ๋ก๊ทธ ์์ง ์์คํ
์๋
! ์ค๋์ ์ ๋ง ์ฌ๋ฏธ์๋ ์ฃผ์ ๋ฅผ ๊ฐ์ง๊ณ ์์ด. ๋ฐ๋ก PHP์ Apache Kafka๋ฅผ ์ด์ฉํ ๋๊ท๋ชจ ๋ก๊ทธ ์์ง ๋ฐ ์ฒ๋ฆฌ ์์คํ
์ ๋ํ ์ด์ผ๊ธฐ์ผ. ๐
"์ด? PHP๋ก ๋๊ท๋ชจ ์์คํ
์ ๋ง๋ ๋ค๊ณ ?" ํ๊ณ ์์ํดํ ์๋ ์๊ฒ ์ง๋ง, ์ฌ์ค PHP๋ ์๊ฐ๋ณด๋ค ํจ์ฌ ๊ฐ๋ ฅํ ์ธ์ด์ผ. ํนํ ์ต๊ทผ PHP 8.x ๋ฒ์ ๋ค์ ์ฑ๋ฅ๋ ์์ฒญ๋๊ฒ ๊ฐ์ ๋๊ณ , ๋น๋๊ธฐ ์ฒ๋ฆฌ๋ ๊ฐ๋ฅํด์ก๊ฑฐ๋ . ์ฌ๊ธฐ์ Apache Kafka๋ผ๋ ๊ฐ๋ ฅํ ๋ฉ์์ง ๋ธ๋ก์ปค๋ฅผ ๊ฒฐํฉํ๋ฉด? ์์ฐ, ์ ๋ง ๋ฉ์ง ์์คํ
์ ๋ง๋ค ์ ์์ด! ๐ฏ
์ด ๊ธ์์๋ ์ค์ ๋ก ํ๋ก๋์
ํ๊ฒฝ์์ ์ฌ์ฉํ ์ ์๋ ๋ก๊ทธ ์์ง ์์คํ
์ ์ด๋ป๊ฒ ๊ตฌ์ถํ๋์ง, ๊ทธ ๊ณผ์ ์์ ๋ง์ฃผ์น๋ ๋ฌธ์ ๋ค๊ณผ ํด๊ฒฐ ๋ฐฉ๋ฒ๋ค์ ์น๊ตฌ์๊ฒ ์ค๋ช
ํ๋ฏ ํธํ๊ฒ ํ์ด๋ณผ๊ฒ. ์ฌ๋ฅ๋ท ๊ฐ์ ํ๋ซํผ์์ ๊ฐ๋ฐ ๊ด๋ จ ์ฌ๋ฅ์ ๊ณต์ ํ๊ฑฐ๋ ๋ฐฐ์ฐ๊ณ ์ถ์ ์ฌ๋๋ค์๊ฒ๋ ์ ๋ง ์ ์ฉํ ๋ด์ฉ์ด ๋ ๊ฑฐ์ผ!
๐ค ์ ๋๊ท๋ชจ ๋ก๊ทธ ์์ง ์์คํ ์ด ํ์ํ ๊น?
๋จผ์ ์ด์ผ๊ธฐ๋ฅผ ์์ํ๊ธฐ ์ ์, ์ ์ด๋ฐ ์์คํ
์ด ํ์ํ์ง๋ถํฐ ์์๋ณด์. ์์ํด๋ด. ๋๊ฐ ์ด์ํ๋ ์น ์๋น์ค์ ํ๋ฃจ์ ์๋ฐฑ๋ง ๋ช
์ ์ฌ์ฉ์๊ฐ ์ ์ํ๋ค๊ณ ๊ฐ์ ํด๋ณด์. ๐ฑ
๊ฐ ์ฌ์ฉ์๊ฐ ํด๋ฆญ ํ ๋ฒ, ํ์ด์ง ์ด๋ ํ ๋ฒ, API ํธ์ถ ํ ๋ฒ ํ ๋๋ง๋ค ๋ก๊ทธ๊ฐ ์์ฑ๋ผ. ๊ทธ๋ผ ํ๋ฃจ์ ๋ช ๊ฐ์ ๋ก๊ทธ๊ฐ ์์ฑ๋ ๊น? ์์ต ๊ฐ? ์์ญ์ต ๊ฐ? ๋ง์, ์ ๋ง ์ด๋ง์ด๋งํ ์์ด์ผ!
์ด๋ฐ ๋ก๊ทธ๋ค์ ๊ทธ๋ฅ ํ์ผ์ ์ฐ๊ฑฐ๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ๋ฐ๋ก ์ ์ฅํ๋ฉด ์ด๋ป๊ฒ ๋ ๊น? ๐ฅ
1. ์ฑ๋ฅ ์ ํ: ๋์คํฌ I/O๊ฐ ๋ณ๋ชฉ์ด ๋์ด ์๋น์ค ์ ์ฒด๊ฐ ๋๋ ค์ ธ
2. ๋ฐ์ดํฐ ์์ค: ์๋ฒ๊ฐ ๋ค์ด๋๋ฉด ๋ฉ๋ชจ๋ฆฌ์ ์๋ ๋ก๊ทธ๊ฐ ๋ ์๊ฐ
3. ํ์ฅ์ฑ ๋ถ์กฑ: ํธ๋ํฝ์ด ์ฆ๊ฐํ๋ฉด ์์คํ ์ ํ์ฅํ๊ธฐ ์ด๋ ค์
4. ์ค์๊ฐ ์ฒ๋ฆฌ ๋ถ๊ฐ: ๋ก๊ทธ๋ฅผ ์ค์๊ฐ์ผ๋ก ๋ถ์ํ๊ณ ๋์ํ๊ธฐ ํ๋ค์ด
๋ฐ๋ก ์ด๋ฐ ๋ฌธ์ ๋ค์ ํด๊ฒฐํ๊ธฐ ์ํด Apache Kafka์ ๊ฐ์ ๋ฉ์์ง ๋ธ๋ก์ปค๊ฐ ๋ฑ์ฅํ ๊ฑฐ์ผ. Kafka๋ LinkedIn์์ ๊ฐ๋ฐํ ๋ถ์ฐ ์คํธ๋ฆฌ๋ฐ ํ๋ซํผ์ผ๋ก, ์ด๋น ์๋ฐฑ๋ง ๊ฑด์ ๋ฉ์์ง๋ฅผ ์ฒ๋ฆฌํ ์ ์๋ ์์ฒญ๋ ์ฑ๋ฅ์ ์๋ํด! ๐ช
๐ฏ Apache Kafka, ๋๋ ๋๊ตฌ๋?
Kafka๋ฅผ ์ฒ์ ์ ํ๋ ์น๊ตฌ๋ค์ ์ํด ๊ฐ๋จํ๊ฒ ์ค๋ช
ํด๋ณผ๊ฒ. Kafka๋ ๋ฐํ-๊ตฌ๋
(Publish-Subscribe) ๋ชจ๋ธ์ ๊ธฐ๋ฐ์ผ๋ก ํ๋ ๋ฉ์์ง ํ ์์คํ
์ด์ผ.
์ฝ๊ฒ ๋งํ๋ฉด, ์ฐ์ฒด๊ตญ ๊ฐ์ ๊ฑฐ๋ผ๊ณ ์๊ฐํ๋ฉด ๋ผ. ๐ฎ
โข Topic (ํ ํฝ): ๋ฉ์์ง๊ฐ ์ ์ฅ๋๋ ์นดํ ๊ณ ๋ฆฌ. ์๋ฅผ ๋ค์ด "user-logs", "error-logs", "api-logs" ๊ฐ์ ๊ฑฐ์ผ.
โข Partition (ํํฐ์ ): ํ ํฝ์ ์ฌ๋ฌ ๊ฐ๋ก ๋๋ ๊ฒ. ๋ณ๋ ฌ ์ฒ๋ฆฌ๋ฅผ ๊ฐ๋ฅํ๊ฒ ํด์ค.
โข Consumer (์๋น์): ๋ฉ์์ง๋ฅผ ์ฝ์ด๊ฐ๋ ์ฃผ์ฒด. ๋ก๊ทธ๋ฅผ ์ฒ๋ฆฌํ๋ ์ ํ๋ฆฌ์ผ์ด์ ์ด์ง.
โข Broker (๋ธ๋ก์ปค): Kafka ์๋ฒ ์์ฒด. ์ฌ๋ฌ ๋๋ฅผ ํด๋ฌ์คํฐ๋ก ๊ตฌ์ฑํ ์ ์์ด.
โข Consumer Group (์ปจ์๋จธ ๊ทธ๋ฃน): ์ฌ๋ฌ ์ปจ์๋จธ๋ฅผ ๋ฌถ์ ๊ทธ๋ฃน. ๋ถํ ๋ถ์ฐ์ด ๊ฐ๋ฅํด์ ธ.
Kafka์ ๊ฐ์ฅ ํฐ ์ฅ์ ์ ๋์ ์ฒ๋ฆฌ๋(High Throughput)๊ณผ ๋ด๊ตฌ์ฑ(Durability)์ด์ผ. ๋ฉ์์ง๋ฅผ ๋์คํฌ์ ์์ฐจ์ ์ผ๋ก ์ ์ฅํ๊ธฐ ๋๋ฌธ์ ์์ฒญ๋๊ฒ ๋น ๋ฅด๊ณ , ๋ณต์ (Replication) ๊ธฐ๋ฅ์ผ๋ก ๋ฐ์ดํฐ ์์ค์ ๋ฐฉ์งํ ์ ์์ด. ๐ก๏ธ
๐ง ์์คํ ์ํคํ ์ฒ ์ค๊ณํ๊ธฐ
์, ์ด์ ๋ณธ๊ฒฉ์ ์ผ๋ก ์์คํ ์ ์ค๊ณํด๋ณด์! ์ฐ๋ฆฌ๊ฐ ๋ง๋ค ์์คํ ์ ์ ์ฒด์ ์ธ ํ๋ฆ์ ์ด๋:
์ด๋ฐ ๊ตฌ์กฐ์ ๊ฐ์ฅ ํฐ ์ฅ์ ์ ๊ฐ ๋จ๊ณ๊ฐ ๋ ๋ฆฝ์ ์ด๋ผ๋ ๊ฑฐ์ผ. Producer๊ฐ ๋ง์ ๋ก๊ทธ๋ฅผ ๋ณด๋ด๋ Consumer๊ฐ ์ฒ๋ฆฌํ ์ค๋น๊ฐ ๋ ๋๊น์ง Kafka๊ฐ ์์ ํ๊ฒ ๋ณด๊ดํด์ฃผ๊ฑฐ๋ . ๋ง์น ๋ฒํผ ์ญํ ์ ํ๋ ๊ฑฐ์ง! ๐ช
โ๏ธ ํ๊ฒฝ ์ค์ ๋ฐ ์ค๋น ์์
์ค์ ๋ก ์์คํ ์ ๊ตฌ์ถํ๊ธฐ ์ ์ ํ์ํ ๊ฒ๋ค์ ์ค๋นํด๋ณด์. ๋จผ์ ๊ฐ๋ฐ ํ๊ฒฝ๋ถํฐ ์ธํ ํด์ผ ํด.
๐ฆ ํ์ํ ์ํํธ์จ์ด
| ์ํํธ์จ์ด | ๋ฒ์ | ์ฉ๋ |
|---|---|---|
| PHP | 8.0 ์ด์ | ์ ํ๋ฆฌ์ผ์ด์ ๊ฐ๋ฐ |
| Apache Kafka | 3.0 ์ด์ | ๋ฉ์์ง ๋ธ๋ก์ปค |
| Zookeeper | 3.7 ์ด์ | Kafka ํด๋ฌ์คํฐ ๊ด๋ฆฌ |
| Composer | ์ต์ ๋ฒ์ | PHP ํจํค์ง ๊ด๋ฆฌ |
| rdkafka ํ์ฅ | 6.0 ์ด์ | PHP-Kafka ์ฐ๋ |
๐ PHP rdkafka ํ์ฅ ์ค์น
PHP์์ Kafka๋ฅผ ์ฌ์ฉํ๋ ค๋ฉด rdkafka ํ์ฅ์ ์ค์นํด์ผ ํด. ์ด๊ฑด librdkafka C ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ PHP ๋ฐ์ธ๋ฉ์ด์ผ. ์ฑ๋ฅ์ด ์ ๋ง ์ข์์ ํ๋ก๋์ ํ๊ฒฝ์์๋ ์ถฉ๋ถํ ์ฌ์ฉํ ์ ์์ด! ๐
# librdkafka ์ค์น
sudo apt-get install librdkafka-dev
# PECL๋ก PHP ํ์ฅ ์ค์น
sudo pecl install rdkafka
# php.ini์ ํ์ฅ ์ถ๊ฐ
echo "extension=rdkafka.so" | sudo tee -a /etc/php/8.1/cli/php.ini
echo "extension=rdkafka.so" | sudo tee -a /etc/php/8.1/fpm/php.ini
# ์ค์น ํ์ธ
php -m | grep rdkafka
๐ต Kafka ์ค์น ๋ฐ ์คํ
๊ฐ๋ฐ ํ๊ฒฝ์์๋ Docker๋ฅผ ์ฌ์ฉํ๋ฉด ์ ๋ง ํธํด. Docker Compose๋ก Kafka์ Zookeeper๋ฅผ ํ ๋ฒ์ ๋์ธ ์ ์๊ฑฐ๋ !
# docker-compose.yml
version: '3'
services:
zookeeper:
image: confluentinc/cp-zookeeper:latest
environment:
ZOOKEEPER_CLIENT_PORT: 2181
ZOOKEEPER_TICK_TIME: 2000
ports:
- "2181:2181"
kafka:
image: confluentinc/cp-kafka:latest
depends_on:
- zookeeper
ports:
- "9092:9092"
environment:
KAFKA_BROKER_ID: 1
KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
KAFKA_AUTO_CREATE_TOPICS_ENABLE: "true"
์ด์ docker-compose up -d ๋ช ๋ น์ด๋ก ์คํํ๋ฉด ๋! ์ ๋ง ๊ฐ๋จํ์ง? ๐
๐ป PHP Producer ๊ตฌํํ๊ธฐ
์, ์ด์ ๋ณธ๊ฒฉ์ ์ผ๋ก ์ฝ๋๋ฅผ ์์ฑํด๋ณด์! ๋จผ์ ๋ก๊ทธ๋ฅผ Kafka๋ก ์ ์กํ๋ Producer๋ถํฐ ๋ง๋ค์ด๋ณผ๊ฒ.
๐จ ๊ธฐ๋ณธ Producer ํด๋์ค
์ฌ์ฌ์ฉ ๊ฐ๋ฅํ Producer ํด๋์ค๋ฅผ ๋ง๋ค๋ฉด ์ฌ๋ฌ ๊ณณ์์ ํธํ๊ฒ ์ฌ์ฉํ ์ ์์ด. ๊ฐ์ฒด์งํฅ์ ์ผ๋ก ์ค๊ณํด๋ณด์!
<?php
// src/KafkaProducer.php
namespace App\Kafka;
use RdKafka\Conf;
use RdKafka\Producer;
use RdKafka\ProducerTopic;
class KafkaProducer
{
private Producer $producer;
private array $topics = [];
private array $config;
public function __construct(array $config = [])
{
$this->config = array_merge([
'metadata.broker.list' => 'localhost:9092',
'compression.codec' => 'snappy',
'queue.buffering.max.messages' => 100000,
'queue.buffering.max.ms' => 100,
'batch.num.messages' => 10000,
], $config);
$this->initProducer();
}
private function initProducer(): void
{
$conf = new Conf();
foreach ($this->config as $key => $value) {
$conf->set($key, $value);
}
// ์๋ฌ ์ฝ๋ฐฑ ์ค์
$conf->setErrorCb(function ($kafka, $err, $reason) {
error_log(sprintf(
"Kafka error: %s (reason: %s)",
rd_kafka_err2str($err),
$reason
));
});
// ์ ์ก ์๋ฃ ์ฝ๋ฐฑ ์ค์
$conf->setDrMsgCb(function ($kafka, $message) {
if ($message->err) {
error_log(sprintf(
"Message delivery failed: %s",
$message->errstr()
));
}
});
$this->producer = new Producer($conf);
}
public function getTopic(string $topicName): ProducerTopic
{
if (!isset($this->topics[$topicName])) {
$this->topics[$topicName] = $this->producer->newTopic($topicName);
}
return $this->topics[$topicName];
}
public function send(
string $topicName,
string $message,
?string $key = null,
?int $partition = RD_KAFKA_PARTITION_UA
): bool {
try {
$topic = $this->getTopic($topicName);
$topic->produce(
$partition,
0,
$message,
$key
);
// ๋น๋๊ธฐ ์ ์ก์ ์ํด poll ํธ์ถ
$this->producer->poll(0);
return true;
} catch (\Exception $e) {
error_log("Failed to send message: " . $e->getMessage());
return false;
}
}
public function sendBatch(string $topicName, array $messages): int
{
$successCount = 0;
$topic = $this->getTopic($topicName);
foreach ($messages as $message) {
try {
$key = $message['key'] ?? null;
$payload = $message['payload'] ?? $message;
if (is_array($payload)) {
$payload = json_encode($payload);
}
$topic->produce(
RD_KAFKA_PARTITION_UA,
0,
$payload,
$key
);
$successCount++;
// ์ฃผ๊ธฐ์ ์ผ๋ก poll ํธ์ถ
if ($successCount % 100 === 0) {
$this->producer->poll(0);
}
} catch (\Exception $e) {
error_log("Failed to send batch message: " . $e->getMessage());
}
}
// ๋จ์ ๋ฉ์์ง ์ ์ก ์๋ฃ ๋๊ธฐ
$this->flush();
return $successCount;
}
public function flush(int $timeoutMs = 10000): void
{
$result = $this->producer->flush($timeoutMs);
if (RD_KAFKA_RESP_ERR_NO_ERROR !== $result) {
error_log("Flush failed with error: " . rd_kafka_err2str($result));
}
}
public function __destruct()
{
$this->flush();
}
}
์! ์ฝ๋๊ฐ ์ข ๊ธธ์ง? ํ์ง๋ง ํ๋ํ๋ ๋ฏ์ด๋ณด๋ฉด ๋ณ๊ฑฐ ์์ด. ํต์ฌ์ ๋น๋๊ธฐ ์ ์ก๊ณผ ๋ฐฐ์น ์ฒ๋ฆฌ์ผ. ๐ฏ
2. ๋ฐฐ์น ์ ์ก: batch.num.messages๋ฅผ ์ค์ ํด์ ์ฌ๋ฌ ๋ฉ์์ง๋ฅผ ํ ๋ฒ์ ๋ณด๋ด๋ฉด ์ฒ๋ฆฌ๋์ด ํฌ๊ฒ ํฅ์๋ผ.
3. ๋ฒํผ๋ง: queue.buffering.max.ms๋ก ๋ฉ์์ง๋ฅผ ๋ชจ์๋ค๊ฐ ํ ๋ฒ์ ๋ณด๋ผ ์ ์์ด.
4. ๋น๋๊ธฐ ์ฒ๋ฆฌ: poll(0)์ ํธ์ถํด์ ๋ธ๋กํน ์์ด ๋ฉ์์ง๋ฅผ ์ ์กํด.
๐ ๋ก๊ทธ ์์ง ๋ํผ ํด๋์ค
์ด์ ์ค์ ๋ก ์ ํ๋ฆฌ์ผ์ด์ ์์ ์ฌ์ฉํ ๋ก๊ทธ ์์ง ํด๋์ค๋ฅผ ๋ง๋ค์ด๋ณด์. ์ด ํด๋์ค๋ ๋ค์ํ ํ์ ์ ๋ก๊ทธ๋ฅผ ์ฝ๊ฒ ์ ์กํ ์ ์๊ฒ ํด์ค ๊ฑฐ์ผ.
<?php
// src/LogCollector.php
namespace App\Kafka;
class LogCollector
{
private KafkaProducer $producer;
private string $applicationName;
private string $environment;
public function __construct(
KafkaProducer $producer,
string $applicationName = 'my-app',
string $environment = 'production'
) {
$this->producer = $producer;
$this->applicationName = $applicationName;
$this->environment = $environment;
}
public function info(string $message, array $context = []): void
{
$this->log('info', $message, $context);
}
public function warning(string $message, array $context = []): void
{
$this->log('warning', $message, $context);
}
public function error(string $message, array $context = []): void
{
$this->log('error', $message, $context);
}
public function critical(string $message, array $context = []): void
{
$this->log('critical', $message, $context);
}
public function userAction(string $userId, string $action, array $data = []): void
{
$logData = [
'type' => 'user_action',
'user_id' => $userId,
'action' => $action,
'data' => $data,
'timestamp' => microtime(true),
'application' => $this->applicationName,
'environment' => $this->environment,
'server' => gethostname(),
'ip' => $_SERVER['REMOTE_ADDR'] ?? 'unknown',
'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? 'unknown',
];
$this->producer->send(
'user-actions',
json_encode($logData),
$userId
);
}
public function apiCall(
string $endpoint,
string $method,
int $statusCode,
float $duration,
array $context = []
): void {
$logData = [
'type' => 'api_call',
'endpoint' => $endpoint,
'method' => $method,
'status_code' => $statusCode,
'duration' => $duration,
'context' => $context,
'timestamp' => microtime(true),
'application' => $this->applicationName,
'environment' => $this->environment,
];
$this->producer->send(
'api-logs',
json_encode($logData)
);
}
public function performance(string $operation, float $duration, array $metrics = []): void
{
$logData = [
'type' => 'performance',
'operation' => $operation,
'duration' => $duration,
'metrics' => array_merge($metrics, [
'memory_usage' => memory_get_usage(true),
'memory_peak' => memory_get_peak_usage(true),
]),
'timestamp' => microtime(true),
'application' => $this->applicationName,
'environment' => $this->environment,
];
$this->producer->send(
'performance-logs',
json_encode($logData)
);
}
private function log(string $level, string $message, array $context = []): void
{
$logData = [
'level' => $level,
'message' => $message,
'context' => $context,
'timestamp' => microtime(true),
'application' => $this->applicationName,
'environment' => $this->environment,
'file' => $context['file'] ?? null,
'line' => $context['line'] ?? null,
'trace' => $context['trace'] ?? null,
];
$topicMap = [
'info' => 'info-logs',
'warning' => 'warning-logs',
'error' => 'error-logs',
'critical' => 'critical-logs',
];
$topic = $topicMap[$level] ?? 'general-logs';
$this->producer->send(
$topic,
json_encode($logData)
);
}
}
์ด์ ์ ํ๋ฆฌ์ผ์ด์ ์์ ์ ๋ง ์ฝ๊ฒ ๋ก๊ทธ๋ฅผ ์์งํ ์ ์์ด! ์ฌ์ฉ ์์๋ฅผ ๋ณผ๊น? ๐
<?php
// ์ฌ์ฉ ์์
$producer = new KafkaProducer([
'metadata.broker.list' => 'localhost:9092',
]);
$logger = new LogCollector($producer, 'my-awesome-app', 'production');
// ์ผ๋ฐ ๋ก๊ทธ
$logger->info('User logged in successfully', [
'user_id' => 12345,
'login_method' => 'oauth',
]);
// ์ฌ์ฉ์ ์ก์
๋ก๊ทธ
$logger->userAction('user-12345', 'purchase', [
'product_id' => 'prod-789',
'amount' => 29.99,
'currency' => 'USD',
]);
// API ํธ์ถ ๋ก๊ทธ
$startTime = microtime(true);
// ... API ์ฒ๋ฆฌ ...
$duration = microtime(true) - $startTime;
$logger->apiCall(
'/api/v1/users',
'GET',
200,
$duration,
['query_params' => $_GET]
);
// ์ฑ๋ฅ ๋ก๊ทธ
$logger->performance('database_query', 0.125, [
'query' => 'SELECT * FROM users WHERE id = ?',
'rows_affected' => 1,
]);
์ ๋ง ๊ฐ๋จํ์ง? ์ด๋ ๊ฒ ํ๋ฉด ์ ํ๋ฆฌ์ผ์ด์ ์ ๋ชจ๋ ์ค์ํ ์ด๋ฒคํธ๋ฅผ Kafka๋ก ์ ์กํ ์ ์์ด. ๊ทธ๋ฆฌ๊ณ ๋์ค์ ์ด ๋ฐ์ดํฐ๋ฅผ ๋ถ์ํด์ ์ฌ์ฉ์ ํ๋ ํจํด์ ํ์ ํ๊ฑฐ๋, ์ฑ๋ฅ ๋ณ๋ชฉ์ ์ฐพ๊ฑฐ๋, ์๋ฌ๋ฅผ ๋ชจ๋ํฐ๋งํ ์ ์์ง! ๐
๐ PHP Consumer ๊ตฌํํ๊ธฐ
Producer๋ฅผ ๋ง๋ค์์ผ๋ ์ด์ Consumer๋ฅผ ๋ง๋ค ์ฐจ๋ก์ผ! Consumer๋ Kafka์์ ๋ฉ์์ง๋ฅผ ์ฝ์ด์์ ์ค์ ๋ก ์ฒ๋ฆฌํ๋ ์ญํ ์ ํด. ์ฌ๊ธฐ์ ์ค์ํ ๊ฑด ์์ ์ฑ๊ณผ ํ์ฅ์ฑ์ด์ผ. ๐ช
๐ฏ ๊ธฐ๋ณธ Consumer ํด๋์ค
<?php
// src/KafkaConsumer.php
namespace App\Kafka;
use RdKafka\Conf;
use RdKafka\KafkaConsumer;
use RdKafka\Message;
use RdKafka\TopicPartition;
class Consumer
{
private KafkaConsumer $consumer;
private array $config;
private bool $running = true;
private array $handlers = [];
public function __construct(array $config = [])
{
$this->config = array_merge([
'metadata.broker.list' => 'localhost:9092',
'group.id' => 'default-consumer-group',
'auto.offset.reset' => 'earliest',
'enable.auto.commit' => 'false',
'compression.codec' => 'snappy',
], $config);
$this->initConsumer();
}
private function initConsumer(): void
{
$conf = new Conf();
foreach ($this->config as $key => $value) {
$conf->set($key, $value);
}
// ๋ฆฌ๋ฐธ๋ฐ์ฑ ์ฝ๋ฐฑ
$conf->setRebalanceCb(function (
KafkaConsumer $kafka,
$err,
array $partitions = null
) {
switch ($err) {
case RD_KAFKA_RESP_ERR__ASSIGN_PARTITIONS:
echo "Partitions assigned: " . count($partitions) . "\n";
$kafka->assign($partitions);
break;
case RD_KAFKA_RESP_ERR__REVOKE_PARTITIONS:
echo "Partitions revoked\n";
$kafka->assign(null);
break;
default:
error_log("Rebalance error: " . rd_kafka_err2str($err));
break;
}
});
// ์๋ฌ ์ฝ๋ฐฑ
$conf->setErrorCb(function ($kafka, $err, $reason) {
error_log(sprintf(
"Consumer error: %s (reason: %s)",
rd_kafka_err2str($err),
$reason
));
});
$this->consumer = new KafkaConsumer($conf);
}
public function subscribe(array $topics): void
{
$this->consumer->subscribe($topics);
echo "Subscribed to topics: " . implode(', ', $topics) . "\n";
}
public function registerHandler(string $topic, callable $handler): void
{
$this->handlers[$topic] = $handler;
}
public function consume(int $timeoutMs = 1000): void
{
echo "Starting consumer...\n";
// ์๊ทธ๋ ํธ๋ค๋ฌ ๋ฑ๋ก
pcntl_signal(SIGTERM, [$this, 'shutdown']);
pcntl_signal(SIGINT, [$this, 'shutdown']);
while ($this->running) {
pcntl_signal_dispatch();
$message = $this->consumer->consume($timeoutMs);
switch ($message->err) {
case RD_KAFKA_RESP_ERR_NO_ERROR:
$this->handleMessage($message);
break;
case RD_KAFKA_RESP_ERR__PARTITION_EOF:
// ํํฐ์
๋์ ๋๋ฌ (์ ์)
break;
case RD_KAFKA_RESP_ERR__TIMED_OUT:
// ํ์์์ (์ ์)
break;
default:
error_log("Consumer error: " . $message->errstr());
break;
}
}
echo "Consumer stopped\n";
}
private function handleMessage(Message $message): void
{
$topic = $message->topic_name;
try {
$payload = json_decode($message->payload, true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new \Exception("Invalid JSON: " . json_last_error_msg());
}
// ํ ํฝ๋ณ ํธ๋ค๋ฌ ์คํ
if (isset($this->handlers[$topic])) {
$handler = $this->handlers[$topic];
$handler($payload, $message);
} else {
// ๊ธฐ๋ณธ ํธ๋ค๋ฌ
$this->defaultHandler($payload, $message);
}
// ์๋ ์ปค๋ฐ
$this->consumer->commit($message);
} catch (\Exception $e) {
error_log(sprintf(
"Failed to handle message from topic %s: %s",
$topic,
$e->getMessage()
));
// ์๋ฌ ์ฒ๋ฆฌ ๋ก์ง (์ฌ์๋, DLQ ์ ์ก ๋ฑ)
$this->handleError($message, $e);
}
}
private function defaultHandler(array $payload, Message $message): void
{
echo sprintf(
"[%s] Received message: %s\n",
$message->topic_name,
json_encode($payload, JSON_PRETTY_PRINT)
);
}
private function handleError(Message $message, \Exception $e): void
{
// Dead Letter Queue๋ก ์ ์กํ๊ฑฐ๋ ๋ณ๋ ์๋ฌ ๋ก๊ทธ ์ ์ฅ
error_log(sprintf(
"Error handling message: %s\nPayload: %s\nError: %s",
$message->topic_name,
$message->payload,
$e->getMessage()
));
}
public function shutdown(): void
{
echo "Shutting down consumer...\n";
$this->running = false;
}
public function __destruct()
{
$this->consumer->close();
}
}
Consumer ์ฝ๋๋ ๊ฝค ๊ธธ์ง? ํ์ง๋ง ์ฌ๊ธฐ์๋ ํ๋ก๋์
ํ๊ฒฝ์์ ํ์ํ ๋ง์ ๊ธฐ๋ฅ๋ค์ด ๋ค์ด์์ด:
โข ์๋ ๋ฆฌ๋ฐธ๋ฐ์ฑ: ์ปจ์๋จธ๊ฐ ์ถ๊ฐ๋๊ฑฐ๋ ์ ๊ฑฐ๋ ๋ ํํฐ์
์ ์๋์ผ๋ก ์ฌ๋ถ๋ฐฐํด
โข ์๋ ์ปค๋ฐ: ๋ฉ์์ง ์ฒ๋ฆฌ๊ฐ ์๋ฃ๋ ํ์๋ง ์คํ์
์ ์ปค๋ฐํด์ ๋ฐ์ดํฐ ์์ค์ ๋ฐฉ์งํด
โข ์๋ฌ ์ฒ๋ฆฌ: ๋ฉ์์ง ์ฒ๋ฆฌ ์ค ์๋ฌ๊ฐ ๋ฐ์ํด๋ ์์ ํ๊ฒ ์ฒ๋ฆฌํด
โข ๊ทธ๋ ์ด์คํ ์
ง๋ค์ด: SIGTERM, SIGINT ์๊ทธ๋์ ๋ฐ์ผ๋ฉด ์์ ํ๊ฒ ์ข
๋ฃํด
๐ ๋ก๊ทธ ์ฒ๋ฆฌ ํธ๋ค๋ฌ ๊ตฌํ
์ด์ ์ค์ ๋ก ๋ก๊ทธ๋ฅผ ์ฒ๋ฆฌํ๋ ํธ๋ค๋ฌ๋ค์ ๋ง๋ค์ด๋ณด์. ๊ฐ ํ ํฝ๋ณ๋ก ๋ค๋ฅธ ์ฒ๋ฆฌ ๋ก์ง์ ์ ์ฉํ ์ ์์ด!
<?php
// src/LogProcessor.php
namespace App\Kafka;
use PDO;
class LogProcessor
{
private PDO $db;
private ElasticsearchClient $elasticsearch;
public function __construct(PDO $db, ElasticsearchClient $elasticsearch)
{
$this->db = $db;
$this->elasticsearch = $elasticsearch;
}
public function processUserAction(array $payload, $message): void
{
echo "Processing user action: {$payload['action']}\n";
// ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ ์ฅ
$stmt = $this->db->prepare("
INSERT INTO user_actions
(user_id, action, data, timestamp, ip, user_agent)
VALUES (?, ?, ?, ?, ?, ?)
");
$stmt->execute([
$payload['user_id'],
$payload['action'],
json_encode($payload['data']),
date('Y-m-d H:i:s', $payload['timestamp']),
$payload['ip'],
$payload['user_agent'],
]);
// Elasticsearch์๋ ์ธ๋ฑ์ฑ (์ค์๊ฐ ๊ฒ์์ฉ)
$this->elasticsearch->index('user-actions', [
'user_id' => $payload['user_id'],
'action' => $payload['action'],
'data' => $payload['data'],
'timestamp' => date('c', $payload['timestamp']),
'application' => $payload['application'],
]);
// ํน์ ์ก์
์ ๋ํ ์ค์๊ฐ ์๋ฆผ
if ($payload['action'] === 'purchase') {
$this->sendPurchaseNotification($payload);
}
}
public function processApiLog(array $payload, $message): void
{
echo "Processing API log: {$payload['endpoint']}\n";
// ๋๋ฆฐ API ํธ์ถ ๊ฐ์ง
if ($payload['duration'] > 1.0) {
$this->alertSlowApi($payload);
}
// ์๋ฌ ์ํ ์ฝ๋ ๊ฐ์ง
if ($payload['status_code'] >= 500) {
$this->alertServerError($payload);
}
// ์๊ณ์ด ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ ์ฅ (InfluxDB, TimescaleDB ๋ฑ)
$this->saveToTimeSeriesDB($payload);
// ํต๊ณ ์
๋ฐ์ดํธ
$this->updateApiStats($payload);
}
public function processErrorLog(array $payload, $message): void
{
echo "Processing error log: {$payload['message']}\n";
// ์ฌ๊ฐํ ์๋ฌ๋ ์ฆ์ ์๋ฆผ
if ($payload['level'] === 'critical') {
$this->sendCriticalAlert($payload);
}
// ์๋ฌ ๋ก๊ทธ ์ ์ฅ
$stmt = $this->db->prepare("
INSERT INTO error_logs
(level, message, context, file, line, trace, timestamp, application)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
");
$stmt->execute([
$payload['level'],
$payload['message'],
json_encode($payload['context']),
$payload['file'],
$payload['line'],
$payload['trace'],
date('Y-m-d H:i:s', $payload['timestamp']),
$payload['application'],
]);
// ์๋ฌ ํจํด ๋ถ์
$this->analyzeErrorPattern($payload);
}
public function processPerformanceLog(array $payload, $message): void
{
echo "Processing performance log: {$payload['operation']}\n";
// ์ฑ๋ฅ ๋ฉํธ๋ฆญ ์ ์ฅ
$this->savePerformanceMetrics($payload);
// ์ฑ๋ฅ ์ ํ ๊ฐ์ง
if ($payload['duration'] > $this->getThreshold($payload['operation'])) {
$this->alertPerformanceDegradation($payload);
}
}
private function sendPurchaseNotification(array $payload): void
{
// ๊ตฌ๋งค ์๋ฆผ ์ ์ก (์ด๋ฉ์ผ, ์ฌ๋, SMS ๋ฑ)
echo "Sending purchase notification for user {$payload['user_id']}\n";
}
private function alertSlowApi(array $payload): void
{
// ๋๋ฆฐ API ์๋ฆผ
echo "ALERT: Slow API detected - {$payload['endpoint']} took {$payload['duration']}s\n";
}
private function alertServerError(array $payload): void
{
// ์๋ฒ ์๋ฌ ์๋ฆผ
echo "ALERT: Server error - {$payload['endpoint']} returned {$payload['status_code']}\n";
}
private function sendCriticalAlert(array $payload): void
{
// ํฌ๋ฆฌํฐ์ปฌ ์๋ฌ ์๋ฆผ
echo "CRITICAL ALERT: {$payload['message']}\n";
}
private function saveToTimeSeriesDB(array $payload): void
{
// ์๊ณ์ด DB์ ์ ์ฅ
echo "Saving to time series DB\n";
}
private function updateApiStats(array $payload): void
{
// API ํต๊ณ ์
๋ฐ์ดํธ (Redis ๋ฑ ์ฌ์ฉ)
echo "Updating API stats\n";
}
private function analyzeErrorPattern(array $payload): void
{
// ์๋ฌ ํจํด ๋ถ์
echo "Analyzing error pattern\n";
}
private function savePerformanceMetrics(array $payload): void
{
// ์ฑ๋ฅ ๋ฉํธ๋ฆญ ์ ์ฅ
echo "Saving performance metrics\n";
}
private function getThreshold(string $operation): float
{
// ์์
๋ณ ์๊ณ๊ฐ ๋ฐํ
$thresholds = [
'database_query' => 0.5,
'api_call' => 2.0,
'file_upload' => 5.0,
];
return $thresholds[$operation] ?? 1.0;
}
private function alertPerformanceDegradation(array $payload): void
{
// ์ฑ๋ฅ ์ ํ ์๋ฆผ
echo "ALERT: Performance degradation - {$payload['operation']} took {$payload['duration']}s\n";
}
}
๐ Consumer ์คํ ์คํฌ๋ฆฝํธ
์ด์ ๋ชจ๋ ๊ฑธ ์กฐํฉํด์ ์ค์ ๋ก ์คํํ ์ ์๋ ์คํฌ๋ฆฝํธ๋ฅผ ๋ง๋ค์ด๋ณด์!
<?php
// bin/consume-logs.php
require_once __DIR__ . '/../vendor/autoload.php';
use App\Kafka\Consumer;
use App\Kafka\LogProcessor;
// ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฐ๊ฒฐ
$db = new PDO(
'mysql:host=localhost;dbname=logs',
'username',
'password',
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
);
// Elasticsearch ํด๋ผ์ด์ธํธ (๊ฐ์)
$elasticsearch = new ElasticsearchClient([
'hosts' => ['localhost:9200']
]);
// ๋ก๊ทธ ํ๋ก์ธ์ ์์ฑ
$processor = new LogProcessor($db, $elasticsearch);
// Consumer ์์ฑ
$consumer = new Consumer([
'metadata.broker.list' => 'localhost:9092',
'group.id' => 'log-processor-group',
'auto.offset.reset' => 'earliest',
]);
// ํ ํฝ ๊ตฌ๋
$consumer->subscribe([
'user-actions',
'api-logs',
'error-logs',
'critical-logs',
'performance-logs',
]);
// ํธ๋ค๋ฌ ๋ฑ๋ก
$consumer->registerHandler('user-actions', [$processor, 'processUserAction']);
$consumer->registerHandler('api-logs', [$processor, 'processApiLog']);
$consumer->registerHandler('error-logs', [$processor, 'processErrorLog']);
$consumer->registerHandler('critical-logs', [$processor, 'processErrorLog']);
$consumer->registerHandler('performance-logs', [$processor, 'processPerformanceLog']);
// ์๋น ์์!
echo "Starting log consumer...\n";
$consumer->consume();
์ด์ php bin/consume-logs.php ๋ช ๋ น์ด๋ก ์คํํ๋ฉด Consumer๊ฐ ๊ณ์ ์คํ๋๋ฉด์ Kafka์์ ๋ฉ์์ง๋ฅผ ๊ฐ์ ธ์ ์ฒ๋ฆฌํด! ๐
โก ์ฑ๋ฅ ์ต์ ํ ์ ๋ต
์์คํ ์ ๋ง๋ค์์ผ๋ ์ด์ ์ฑ๋ฅ์ ์ต์ ํํด์ผ ํด. ๋๊ท๋ชจ ๋ก๊ทธ ์ฒ๋ฆฌ ์์คํ ์์ ์ฑ๋ฅ์ ์ ๋ง ์ค์ํ๊ฑฐ๋ ! ์ด๋น ์๋ง ๊ฑด์ ๋ก๊ทธ๋ฅผ ์ฒ๋ฆฌํด์ผ ํ๋๋ฐ ๋๋ฆฌ๋ฉด ์ ๋์์? ๐
๐ฏ Producer ์ต์ ํ
๋ฉ์์ง๋ฅผ ํ๋์ฉ ๋ณด๋ด๋ ๊ฒ๋ณด๋ค ์ฌ๋ฌ ๊ฐ๋ฅผ ๋ชจ์์ ํ ๋ฒ์ ๋ณด๋ด๋ ๊ฒ ํจ์ฌ ํจ์จ์ ์ด์ผ. Kafka๋ ๋ฐฐ์น ์ ์ก์ ์ต์ ํ๋์ด ์๊ฑฐ๋ !
$conf = new Conf();
$conf->set('batch.num.messages', 10000); // ๋ฐฐ์น ํฌ๊ธฐ
$conf->set('queue.buffering.max.ms', 100); // ์ต๋ ๋๊ธฐ ์๊ฐ
$conf->set('linger.ms', 10); // ์ถ๊ฐ ๋ฉ์์ง ๋๊ธฐ ์๊ฐ
2. ์์ถ ์ฌ์ฉ๋คํธ์ํฌ ๋์ญํญ์ ์ ์ฝํ๊ณ ์ ์ก ์๋๋ฅผ ๋์ด๋ ค๋ฉด ์์ถ์ ์ฌ์ฉํด์ผ ํด. Snappy๋ LZ4๊ฐ ์ข์ ์ ํ์ด์ผ.
$conf->set('compression.codec', 'snappy'); // ๋๋ 'lz4', 'gzip'
3. ๋น๋๊ธฐ ์ ์ก๋๊ธฐ ์ ์ก์ ๋๋ ค! ๋น๋๊ธฐ๋ก ๋ณด๋ด๊ณ ๋์ค์ ํ์ธํ๋ ๊ฒ ํจ์ฌ ๋นจ๋ผ.
// ๋น๋๊ธฐ ์ ์ก
$topic->produce(RD_KAFKA_PARTITION_UA, 0, $message);
$producer->poll(0); // ๋
ผ๋ธ๋กํน
// ์ฃผ๊ธฐ์ ์ผ๋ก flush
if ($count % 1000 === 0) {
$producer->flush(100);
}
๐ Consumer ์ต์ ํ
์ฌ๋ฌ Consumer๋ฅผ ๋์์ ์คํํ๋ฉด ์ฒ๋ฆฌ๋์ด ํฌ๊ฒ ์ฆ๊ฐํด. Consumer Group์ ํ์ฉํ์!
// ๊ฐ์ ๊ทธ๋ฃน ID๋ฅผ ์ฌ์ฉํ๋ ์ฌ๋ฌ Consumer ์คํ
// Consumer 1
$consumer1 = new Consumer(['group.id' => 'log-processor-group']);
// Consumer 2
$consumer2 = new Consumer(['group.id' => 'log-processor-group']);
// Consumer 3
$consumer3 = new Consumer(['group.id' => 'log-processor-group']);
๊ฐ Consumer๋ ์๋ก ๋ค๋ฅธ ํํฐ์
์ ์ฒ๋ฆฌํ๊ฒ ๋ผ. ํํฐ์
์๋งํผ Consumer๋ฅผ ์คํํ ์ ์์ด!2. ๋ฐฐ์น ์ฒ๋ฆฌ
๋ฉ์์ง๋ฅผ ํ๋์ฉ ์ฒ๋ฆฌํ๋ ๊ฒ๋ณด๋ค ์ฌ๋ฌ ๊ฐ๋ฅผ ๋ชจ์์ ์ฒ๋ฆฌํ๋ฉด DB ์ฐ๊ธฐ ์ฑ๋ฅ์ด ํฅ์๋ผ.
$batch = [];
$batchSize = 100;
while ($message = $consumer->consume(1000)) {
if ($message->err === RD_KAFKA_RESP_ERR_NO_ERROR) {
$batch[] = json_decode($message->payload, true);
if (count($batch) >= $batchSize) {
$this->processBatch($batch);
$consumer->commit($message);
$batch = [];
}
}
}
3. ๋ฉ๋ชจ๋ฆฌ ๊ด๋ฆฌPHP๋ ๋ฉ๋ชจ๋ฆฌ ๊ด๋ฆฌ๊ฐ ์ค์ํด. ํนํ ์ฅ์๊ฐ ์คํ๋๋ Consumer๋ ๋ฉ๋ชจ๋ฆฌ ๋์๋ฅผ ์กฐ์ฌํด์ผ ํด.
// ์ฃผ๊ธฐ์ ์ผ๋ก ๊ฐ๋น์ง ์ปฌ๋ ์
์คํ
if ($count % 1000 === 0) {
gc_collect_cycles();
$memoryUsage = memory_get_usage(true) / 1024 / 1024;
echo "Memory usage: {$memoryUsage} MB\n";
// ๋ฉ๋ชจ๋ฆฌ ์ฌ์ฉ๋์ด ๋๋ฌด ๋์ผ๋ฉด ์ฌ์์
if ($memoryUsage > 500) {
echo "Memory limit reached, restarting...\n";
exit(0); // Supervisor ๋ฑ์ด ์๋์ผ๋ก ์ฌ์์
}
}
๐ Kafka ํด๋ฌ์คํฐ ์ต์ ํ
ํ ํฝ์ ํํฐ์ ์๋ ๋ณ๋ ฌ ์ฒ๋ฆฌ ๋ฅ๋ ฅ์ ๊ฒฐ์ ํด. ์ผ๋ฐ์ ์ผ๋ก Consumer ์์ 2~3๋ฐฐ ์ ๋๊ฐ ์ ๋นํด.
2. ๋ณต์ ํฉํฐ ์ค์
๋ฐ์ดํฐ ์์ ์ฑ์ ์ํด ๋ณต์ ํฉํฐ๋ฅผ 2 ์ด์์ผ๋ก ์ค์ ํ์. ํ๋ก๋์ ์์๋ 3์ด ๊ถ์ฅ๋ผ.
3. ๋ก๊ทธ ๋ณด๊ด ์ ์ฑ
๋์คํฌ ๊ณต๊ฐ์ ๊ณ ๋ คํด์ ๋ก๊ทธ ๋ณด๊ด ๊ธฐ๊ฐ์ ์ค์ ํด์ผ ํด.
4. ์์ถ ํ์ฑํ
Kafka ๋ธ๋ก์ปค ๋ ๋ฒจ์์๋ ์์ถ์ ํ์ฑํํ๋ฉด ๋์คํฌ ์ฌ์ฉ๋์ ์ค์ผ ์ ์์ด.
# Kafka ํ ํฝ ์์ฑ ์์
kafka-topics.sh --create \
--bootstrap-server localhost:9092 \
--topic user-actions \
--partitions 12 \
--replication-factor 3 \
--config compression.type=snappy \
--config retention.ms=604800000 \ # 7์ผ
--config segment.ms=3600000 # 1์๊ฐ
๐ก๏ธ ์์ ์ฑ๊ณผ ๋ชจ๋ํฐ๋ง
๋๊ท๋ชจ ์์คํ ์์ ๊ฐ์ฅ ์ค์ํ ๊ฑด ์์ ์ฑ์ด์ผ. ์์คํ ์ด ๋ค์ด๋๋ฉด ๋ก๊ทธ๊ฐ ์์ค๋ ์ ์๊ณ , ์ด๋ ๊ณง ์ค์ํ ์ ๋ณด๋ฅผ ๋์น๋ ๊ฑฐ๋๊น! ๐ฐ
๐ ์ฌ์๋ ๋ฐ ์๋ฌ ์ฒ๋ฆฌ
<?php
// src/RetryHandler.php
namespace App\Kafka;
class RetryHandler
{
private int $maxRetries;
private int $retryDelay;
private KafkaProducer $dlqProducer;
public function __construct(
int $maxRetries = 3,
int $retryDelay = 1000,
?KafkaProducer $dlqProducer = null
) {
$this->maxRetries = $maxRetries;
$this->retryDelay = $retryDelay;
$this->dlqProducer = $dlqProducer;
}
public function handle(callable $callback, array $payload, Message $message): bool
{
$attempts = 0;
$lastException = null;
while ($attempts < $this->maxRetries) {
try {
$callback($payload, $message);
return true;
} catch (\Exception $e) {
$attempts++;
$lastException = $e;
echo sprintf(
"Retry %d/%d failed: %s\n",
$attempts,
$this->maxRetries,
$e->getMessage()
);
if ($attempts < $this->maxRetries) {
usleep($this->retryDelay * 1000 * $attempts); // ์ง์ ๋ฐฑ์คํ
}
}
}
// ๋ชจ๋ ์ฌ์๋ ์คํจ ์ DLQ๋ก ์ ์ก
if ($this->dlqProducer) {
$this->sendToDeadLetterQueue($payload, $message, $lastException);
}
return false;
}
private function sendToDeadLetterQueue(
array $payload,
Message $message,
\Exception $exception
): void {
$dlqPayload = [
'original_topic' => $message->topic_name,
'original_partition' => $message->partition,
'original_offset' => $message->offset,
'payload' => $payload,
'error' => [
'message' => $exception->getMessage(),
'trace' => $exception->getTraceAsString(),
],
'timestamp' => time(),
];
$this->dlqProducer->send(
'dead-letter-queue',
json_encode($dlqPayload)
);
echo "Message sent to Dead Letter Queue\n";
}
}
๐ ๋ชจ๋ํฐ๋ง ์์คํ ๊ตฌ์ถ
์์คํ ์ด ์ ๋์๊ฐ๋์ง ํ์ธํ๋ ค๋ฉด ๋ชจ๋ํฐ๋ง์ด ํ์์ผ! Prometheus์ Grafana๋ฅผ ์ฌ์ฉํ๋ฉด ๋ฉ์ง ๋์๋ณด๋๋ฅผ ๋ง๋ค ์ ์์ด. ๐
<?php
// src/Metrics.php
namespace App\Kafka;
class Metrics
{
private array $counters = [];
private array $gauges = [];
private array $histograms = [];
public function incrementCounter(string $name, int $value = 1, array $labels = []): void
{
$key = $this->makeKey($name, $labels);
if (!isset($this->counters[$key])) {
$this->counters[$key] = 0;
}
$this->counters[$key] += $value;
}
public function setGauge(string $name, float $value, array $labels = []): void
{
$key = $this->makeKey($name, $labels);
$this->gauges[$key] = $value;
}
public function observeHistogram(string $name, float $value, array $labels = []): void
{
$key = $this->makeKey($name, $labels);
if (!isset($this->histograms[$key])) {
$this->histograms[$key] = [];
}
$this->histograms[$key][] = $value;
}
public function export(): string
{
$output = [];
// Counters
foreach ($this->counters as $key => $value) {
$output[] = "{$key} {$value}";
}
// Gauges
foreach ($this->gauges as $key => $value) {
$output[] = "{$key} {$value}";
}
// Histograms
foreach ($this->histograms as $key => $values) {
$count = count($values);
$sum = array_sum($values);
$output[] = "{$key}_count {$count}";
$output[] = "{$key}_sum {$sum}";
}
return implode("\n", $output);
}
private function makeKey(string $name, array $labels): string
{
if (empty($labels)) {
return $name;
}
$labelStr = [];
foreach ($labels as $key => $value) {
$labelStr[] = "{$key}=\"{$value}\"";
}
return $name . '{' . implode(',', $labelStr) . '}';
}
}
// ์ฌ์ฉ ์์
$metrics = new Metrics();
// ๋ฉ์์ง ์ฒ๋ฆฌ ์นด์ดํฐ
$metrics->incrementCounter('kafka_messages_processed_total', 1, [
'topic' => 'user-actions',
'status' => 'success',
]);
// Consumer lag ๊ฒ์ด์ง
$metrics->setGauge('kafka_consumer_lag', 1234, [
'topic' => 'user-actions',
'partition' => '0',
]);
// ์ฒ๋ฆฌ ์๊ฐ ํ์คํ ๊ทธ๋จ
$metrics->observeHistogram('kafka_message_processing_duration_seconds', 0.125, [
'topic' => 'user-actions',
]);
๐จ ์๋ฆผ ์์คํ
๋ฌธ์ ๊ฐ ๋ฐ์ํ๋ฉด ์ฆ์ ์์์ผ ํด! ์ฌ๋, ์ด๋ฉ์ผ, SMS ๋ฑ์ผ๋ก ์๋ฆผ์ ๋ณด๋ด๋ ์์คํ ์ ๋ง๋ค์ด๋ณด์.
<?php
// src/AlertManager.php
namespace App\Kafka;
class AlertManager
{
private array $channels = [];
private array $alertHistory = [];
private int $cooldownPeriod = 300; // 5๋ถ
public function addChannel(string $name, callable $sender): void
{
$this->channels[$name] = $sender;
}
public function sendAlert(
string $level,
string $message,
array $context = [],
array $channels = []
): void {
// ์ค๋ณต ์๋ฆผ ๋ฐฉ์ง
$alertKey = md5($level . $message);
$now = time();
if (isset($this->alertHistory[$alertKey])) {
$lastSent = $this->alertHistory[$alertKey];
if ($now - $lastSent < $this->cooldownPeriod) {
return; // ์ฟจ๋ค์ด ๊ธฐ๊ฐ ๋ด์๋ ๊ฐ์ ์๋ฆผ ๋ณด๋ด์ง ์์
}
}
$this->alertHistory[$alertKey] = $now;
// ์๋ฆผ ์ ์ก
$alertData = [
'level' => $level,
'message' => $message,
'context' => $context,
'timestamp' => date('Y-m-d H:i:s'),
'hostname' => gethostname(),
];
$channelsToUse = empty($channels) ? array_keys($this->channels) : $channels;
foreach ($channelsToUse as $channelName) {
if (isset($this->channels[$channelName])) {
try {
$this->channels[$channelName]($alertData);
} catch (\Exception $e) {
error_log("Failed to send alert via {$channelName}: " . $e->getMessage());
}
}
}
}
public function critical(string $message, array $context = []): void
{
$this->sendAlert('CRITICAL', $message, $context);
}
public function warning(string $message, array $context = []): void
{
$this->sendAlert('WARNING', $message, $context);
}
public function info(string $message, array $context = []): void
{
$this->sendAlert('INFO', $message, $context);
}
}
// ์ฌ์ฉ ์์
$alertManager = new AlertManager();
// ์ฌ๋ ์ฑ๋ ์ถ๊ฐ
$alertManager->addChannel('slack', function($alert) {
$webhook = 'https://hooks.slack.com/services/YOUR/WEBHOOK/URL';
$payload = [
'text' => "[{$alert['level']}] {$alert['message']}",
'attachments' => [
[
'color' => $alert['level'] === 'CRITICAL' ? 'danger' : 'warning',
'fields' => [
[
'title' => 'Hostname',
'value' => $alert['hostname'],
'short' => true,
],
[
'title' => 'Timestamp',
'value' => $alert['timestamp'],
'short' => true,
],
],
],
],
];
$ch = curl_init($webhook);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_exec($ch);
curl_close($ch);
});
// ์ด๋ฉ์ผ ์ฑ๋ ์ถ๊ฐ
$alertManager->addChannel('email', function($alert) {
mail(
'admin@example.com',
"[{$alert['level']}] Kafka Alert",
print_r($alert, true)
);
});
// ์๋ฆผ ์ ์ก
$alertManager->critical('Consumer lag exceeded threshold', [
'topic' => 'user-actions',
'lag' => 10000,
'threshold' => 5000,
]);
๐ ๋ณด์ ๊ณ ๋ ค์ฌํญ
๋ก๊ทธ์๋ ๋ฏผ๊ฐํ ์ ๋ณด๊ฐ ํฌํจ๋ ์ ์์ด. ์ฌ์ฉ์ ์ ๋ณด, API ํค, ๋น๋ฐ๋ฒํธ ๋ฑ์ด ์ค์๋ก ๋ก๊ทธ์ ๋จ์ ์ ์๊ฑฐ๋ . ๋ณด์์ ์ ๋ง ์ค์ํด! ๐
๐ญ ๋ฐ์ดํฐ ๋ง์คํน
<?php
// src/DataMasker.php
namespace App\Kafka;
class DataMasker
{
private array $sensitiveFields = [
'password',
'credit_card',
'ssn',
'api_key',
'secret',
'token',
'authorization',
];
private array $patterns = [
'email' => '/([a-zA-Z0-9._%+-]+)@([a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/',
'credit_card' => '/\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b/',
'phone' => '/\b\d{3}[-.]?\d{3}[-.]?\d{4}\b/',
'ip' => '/\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/',
];
public function mask(array $data): array
{
return $this->maskRecursive($data);
}
private function maskRecursive(array $data): array
{
$masked = [];
foreach ($data as $key => $value) {
if (is_array($value)) {
$masked[$key] = $this->maskRecursive($value);
} elseif (is_string($value)) {
if ($this->isSensitiveField($key)) {
$masked[$key] = $this->maskValue($value);
} else {
$masked[$key] = $this->maskPatterns($value);
}
} else {
$masked[$key] = $value;
}
}
return $masked;
}
private function isSensitiveField(string $key): bool
{
$key = strtolower($key);
foreach ($this->sensitiveFields as $field) {
if (strpos($key, $field) !== false) {
return true;
}
}
return false;
}
private function maskValue(string $value): string
{
if (strlen($value) <= 4) {
return '****';
}
return substr($value, 0, 2) . str_repeat('*', strlen($value) - 4) . substr($value, -2);
}
private function maskPatterns(string $value): string
{
foreach ($this->patterns as $type => $pattern) {
$value = preg_replace_callback($pattern, function($matches) use ($type) {
switch ($type) {
case 'email':
return $this->maskEmail($matches[0]);
case 'credit_card':
return 'XXXX-XXXX-XXXX-' . substr($matches[0], -4);
case 'phone':
return 'XXX-XXX-' . substr($matches[0], -4);
case 'ip':
$parts = explode('.', $matches[0]);
return 'XXX.XXX.XXX.' . $parts[3];
default:
return str_repeat('X', strlen($matches[0]));
}
}, $value);
}
return $value;
}
private function maskEmail(string $email): string
{
list($local, $domain) = explode('@', $email);
if (strlen($local) <= 2) {
$maskedLocal = str_repeat('*', strlen($local));
} else {
$maskedLocal = substr($local, 0, 1) .
str_repeat('*', strlen($local) - 2) .
substr($local, -1);
}
return $maskedLocal . '@' . $domain;
}
}
// ์ฌ์ฉ ์์
$masker = new DataMasker();
$data = [
'user_id' => 12345,
'email' => 'user@example.com',
'password' => 'secret123',
'credit_card' => '4532-1234-5678-9010',
'phone' => '555-123-4567',
'ip' => '192.168.1.100',
'message' => 'User logged in from 10.0.0.50',
];
$masked = $masker->mask($data);
// ๊ฒฐ๊ณผ:
// [
// 'user_id' => 12345,
// 'email' => 'u***r@example.com',
// 'password' => 'se******23',
// 'credit_card' => 'XXXX-XXXX-XXXX-9010',
// 'phone' => 'XXX-XXX-4567',
// 'ip' => 'XXX.XXX.XXX.100',
// 'message' => 'User logged in from XXX.XXX.XXX.50',
// ]
๐ Kafka ๋ณด์ ์ค์
1. SSL/TLS ์ํธํ: ๋คํธ์ํฌ ํต์ ์ ์ํธํํด์ ์ค๊ฐ์ ๊ณต๊ฒฉ์ ๋ฐฉ์งํด
2. SASL ์ธ์ฆ: ํด๋ผ์ด์ธํธ ์ธ์ฆ์ ํตํด ๋ฌด๋จ ์ ๊ทผ์ ์ฐจ๋จํด
3. ACL ์ค์ : ํ ํฝ๋ณ๋ก ์ ๊ทผ ๊ถํ์ ์ธ๋ฐํ๊ฒ ๊ด๋ฆฌํด
4. ๋คํธ์ํฌ ๊ฒฉ๋ฆฌ: Kafka ํด๋ฌ์คํฐ๋ฅผ private ๋คํธ์ํฌ์ ๋ฐฐ์นํด
5. ๋ก๊ทธ ์ํธํ: ๋ฏผ๊ฐํ ๋ฐ์ดํฐ๋ ์ํธํํด์ ์ ์ฅํด
# Kafka SSL ์ค์ ์์
# server.properties
listeners=SSL://localhost:9093
security.inter.broker.protocol=SSL
ssl.keystore.location=/var/private/ssl/kafka.server.keystore.jks
ssl.keystore.password=your-keystore-password
ssl.key.password=your-key-password
ssl.truststore.location=/var/private/ssl/kafka.server.truststore.jks
ssl.truststore.password=your-truststore-password
# SASL ์ค์
sasl.enabled.mechanisms=SCRAM-SHA-512
sasl.mechanism.inter.broker.protocol=SCRAM-SHA-512
<?php
// PHP์์ SSL/SASL ์ฌ์ฉ
$conf = new Conf();
$conf->set('metadata.broker.list', 'localhost:9093');
$conf->set('security.protocol', 'SASL_SSL');
$conf->set('sasl.mechanism', 'SCRAM-SHA-512');
$conf->set('sasl.username', 'your-username');
$conf->set('sasl.password', 'your-password');
// SSL ์ธ์ฆ์ ์ค์
$conf->set('ssl.ca.location', '/path/to/ca-cert');
$conf->set('ssl.certificate.location', '/path/to/client-cert');
$conf->set('ssl.key.location', '/path/to/client-key');
๐ ์ค์ ์์ : ์ ์ฒด ์์คํ ๊ตฌ์ฑ
์ด์ ์ง๊ธ๊น์ง ๋ฐฐ์ด ๋ชจ๋ ๊ฑธ ์กฐํฉํด์ ์ค์ ๋ก ๋์ํ๋ ์์ ํ ์์คํ ์ ๋ง๋ค์ด๋ณด์! ์ฌ๋ฅ๋ท ๊ฐ์ ํ๋ซํผ์์ ์ด๋ฐ ์์คํ ์ ๊ตฌ์ถํ๋ค๋ฉด ์ด๋ป๊ฒ ํ ์ง ์์ํด๋ณด๋ฉด์ ๋ฐ๋ผ์๋ด. ๐
๐๏ธ ํ๋ก์ ํธ ๊ตฌ์กฐ
kafka-log-system/
โโโ bin/
โ โโโ produce-logs.php
โ โโโ consume-logs.php
โ โโโ monitor.php
โโโ config/
โ โโโ kafka.php
โ โโโ database.php
โ โโโ alerts.php
โโโ src/
โ โโโ Kafka/
โ โ โโโ KafkaProducer.php
โ โ โโโ Consumer.php
โ โ โโโ LogCollector.php
โ โ โโโ LogProcessor.php
โ โโโ Security/
โ โ โโโ DataMasker.php
โ โโโ Monitoring/
โ โ โโโ Metrics.php
โ โ โโโ AlertManager.php
โ โโโ Utils/
โ โโโ RetryHandler.php
โโโ tests/
โ โโโ ProducerTest.php
โ โโโ ConsumerTest.php
โโโ docker/
โ โโโ docker-compose.yml
โโโ composer.json
โโโ README.md
โ๏ธ ์ค์ ํ์ผ
<?php
// config/kafka.php
return [
'brokers' => env('KAFKA_BROKERS', 'localhost:9092'),
'producer' => [
'compression.codec' => 'snappy',
'queue.buffering.max.messages' => 100000,
'queue.buffering.max.ms' => 100,
'batch.num.messages' => 10000,
'linger.ms' => 10,
'acks' => 1, // 0: no ack, 1: leader ack, -1: all replicas ack
],
'consumer' => [
'group.id' => env('KAFKA_CONSUMER_GROUP', 'log-processor-group'),
'auto.offset.reset' => 'earliest',
'enable.auto.commit' => 'false',
'max.poll.records' => 500,
'session.timeout.ms' => 30000,
'heartbeat.interval.ms' => 3000,
],
'topics' => [
'user-actions' => [
'partitions' => 12,
'replication_factor' => 3,
'retention_ms' => 604800000, // 7 days
],
'api-logs' => [
'partitions' => 8,
'replication_factor' => 3,
'retention_ms' => 259200000, // 3 days
],
'error-logs' => [
'partitions' => 6,
'replication_factor' => 3,
'retention_ms' => 2592000000, // 30 days
],
'performance-logs' => [
'partitions' => 6,
'replication_factor' => 2,
'retention_ms' => 86400000, // 1 day
],
],
'security' => [
'enabled' => env('KAFKA_SECURITY_ENABLED', false),
'protocol' => 'SASL_SSL',
'mechanism' => 'SCRAM-SHA-512',
'username' => env('KAFKA_USERNAME'),
'password' => env('KAFKA_PASSWORD'),
'ssl_ca_location' => env('KAFKA_SSL_CA'),
],
];
๐ฌ ์คํ ์คํฌ๋ฆฝํธ
<?php
// bin/produce-logs.php
// ์น ์ ํ๋ฆฌ์ผ์ด์
์์ ๋ก๊ทธ๋ฅผ ์์ฑํ๋ ์์
require_once __DIR__ . '/../vendor/autoload.php';
use App\Kafka\KafkaProducer;
use App\Kafka\LogCollector;
use App\Security\DataMasker;
// ์ค์ ๋ก๋
$config = require __DIR__ . '/../config/kafka.php';
// Producer ์์ฑ
$producer = new KafkaProducer(array_merge(
['metadata.broker.list' => $config['brokers']],
$config['producer']
));
// ๋ฐ์ดํฐ ๋ง์ค์ปค ์์ฑ
$masker = new DataMasker();
// ๋ก๊ทธ ์ปฌ๋ ํฐ ์์ฑ
$logger = new LogCollector($producer, 'jaenung-platform', 'production');
// ์น ์์ฒญ ์ฒ๋ฆฌ ์๋ฎฌ๋ ์ด์
$startTime = microtime(true);
try {
// ์ฌ์ฉ์ ๋ก๊ทธ์ธ
$userId = 'user-' . rand(1000, 9999);
$logger->userAction($userId, 'login', [
'method' => 'oauth',
'provider' => 'google',
]);
// API ํธ์ถ
$apiStartTime = microtime(true);
// ... API ์ฒ๋ฆฌ ...
$apiDuration = microtime(true) - $apiStartTime;
$logger->apiCall(
'/api/v1/talents/search',
'GET',
200,
$apiDuration,
['query' => 'php development']
);
// ์ฑ๋ฅ ๋ฉํธ๋ฆญ
$logger->performance('page_render', microtime(true) - $startTime, [
'page' => 'talent_search',
'db_queries' => 5,
'cache_hits' => 3,
]);
echo "Logs sent successfully!\n";
} catch (\Exception $e) {
// ์๋ฌ ๋ก๊ทธ
$logger->error('Failed to process request', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
echo "Error: " . $e->getMessage() . "\n";
}
<?php
// bin/consume-logs.php
// ๋ก๊ทธ๋ฅผ ์๋นํ๊ณ ์ฒ๋ฆฌํ๋ ์คํฌ๋ฆฝํธ
require_once __DIR__ . '/../vendor/autoload.php';
use App\Kafka\Consumer;
use App\Kafka\LogProcessor;
use App\Monitoring\Metrics;
use App\Monitoring\AlertManager;
use App\Utils\RetryHandler;
// ์ค์ ๋ก๋
$kafkaConfig = require __DIR__ . '/../config/kafka.php';
$dbConfig = require __DIR__ . '/../config/database.php';
$alertConfig = require __DIR__ . '/../config/alerts.php';
// ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฐ๊ฒฐ
$db = new PDO(
"mysql:host={$dbConfig['host']};dbname={$dbConfig['database']}",
$dbConfig['username'],
$dbConfig['password'],
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
);
// Elasticsearch ํด๋ผ์ด์ธํธ (์ค์ ๊ตฌํ ํ์)
$elasticsearch = new ElasticsearchClient($dbConfig['elasticsearch']);
// ๋ฉํธ๋ฆญ ์์ง๊ธฐ
$metrics = new Metrics();
// ์๋ฆผ ๊ด๋ฆฌ์
$alertManager = new AlertManager();
$alertManager->addChannel('slack', $alertConfig['slack_webhook']);
// ์ฌ์๋ ํธ๋ค๋ฌ
$retryHandler = new RetryHandler(3, 1000);
// ๋ก๊ทธ ํ๋ก์ธ์
$processor = new LogProcessor($db, $elasticsearch, $metrics, $alertManager);
// Consumer ์์ฑ
$consumer = new Consumer(array_merge(
['metadata.broker.list' => $kafkaConfig['brokers']],
$kafkaConfig['consumer']
));
// ํ ํฝ ๊ตฌ๋
$consumer->subscribe(array_keys($kafkaConfig['topics']));
// ํธ๋ค๋ฌ ๋ฑ๋ก (์ฌ์๋ ๋ก์ง ํฌํจ)
$consumer->registerHandler('user-actions', function($payload, $message) use ($processor, $retryHandler, $metrics) {
$startTime = microtime(true);
$success = $retryHandler->handle(
[$processor, 'processUserAction'],
$payload,
$message
);
$duration = microtime(true) - $startTime;
$metrics->incrementCounter('messages_processed_total', 1, [
'topic' => 'user-actions',
'status' => $success ? 'success' : 'failed',
]);
$metrics->observeHistogram('message_processing_duration_seconds', $duration, [
'topic' => 'user-actions',
]);
});
$consumer->registerHandler('api-logs', function($payload, $message) use ($processor, $retryHandler, $metrics) {
$startTime = microtime(true);
$success = $retryHandler->handle(
[$processor, 'processApiLog'],
$payload,
$message
);
$duration = microtime(true) - $startTime;
$metrics->incrementCounter('messages_processed_total', 1, [
'topic' => 'api-logs',
'status' => $success ? 'success' : 'failed',
]);
$metrics->observeHistogram('message_processing_duration_seconds', $duration, [
'topic' => 'api-logs',
]);
});
// ๋๋จธ์ง ํธ๋ค๋ฌ๋ค๋ ๋น์ทํ๊ฒ ๋ฑ๋ก...
// ๋ฉํธ๋ฆญ ๋ด๋ณด๋ด๊ธฐ (์ฃผ๊ธฐ์ ์ผ๋ก)
register_shutdown_function(function() use ($metrics) {
file_put_contents('/tmp/kafka_metrics.prom', $metrics->export());
});
// Consumer ์์!
echo "Starting Kafka consumer...\n";
echo "Subscribed topics: " . implode(', ', array_keys($kafkaConfig['topics'])) . "\n";
echo "Consumer group: {$kafkaConfig['consumer']['group.id']}\n";
echo "Press Ctrl+C to stop\n\n";
$consumer->consume();
๐ณ Docker Compose ์ค์
# docker/docker-compose.yml
version: '3.8'
services:
zookeeper:
image: confluentinc/cp-zookeeper:7.4.0
hostname: zookeeper
container_name: zookeeper
ports:
- "2181:2181"
environment:
ZOOKEEPER_CLIENT_PORT: 2181
ZOOKEEPER_TICK_TIME: 2000
volumes:
- zookeeper-data:/var/lib/zookeeper/data
- zookeeper-logs:/var/lib/zookeeper/log
kafka-1:
image: confluentinc/cp-kafka:7.4.0
hostname: kafka-1
container_name: kafka-1
depends_on:
- zookeeper
ports:
- "9092:9092"
environment:
KAFKA_BROKER_ID: 1
KAFKA_ZOOKEEPER_CONNECT: 'zookeeper:2181'
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka-1:29092,PLAINTEXT_HOST://localhost:9092
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 3
KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 2
KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 3
KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0
KAFKA_AUTO_CREATE_TOPICS_ENABLE: 'true'
KAFKA_COMPRESSION_TYPE: 'snappy'
volumes:
- kafka-1-data:/var/lib/kafka/data
kafka-2:
image: confluentinc/cp-kafka:7.4.0
hostname: kafka-2
container_name: kafka-2
depends_on:
- zookeeper
ports:
- "9093:9093"
environment:
KAFKA_BROKER_ID: 2
KAFKA_ZOOKEEPER_CONNECT: 'zookeeper:2181'
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka-2:29093,PLAINTEXT_HOST://localhost:9093
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 3
KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 2
KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 3
KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0
KAFKA_COMPRESSION_TYPE: 'snappy'
volumes:
- kafka-2-data:/var/lib/kafka/data
kafka-3:
image: confluentinc/cp-kafka:7.4.0
hostname: kafka-3
container_name: kafka-3
depends_on:
- zookeeper
ports:
- "9094:9094"
environment:
KAFKA_BROKER_ID: 3
KAFKA_ZOOKEEPER_CONNECT: 'zookeeper:2181'
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka-3:29094,PLAINTEXT_HOST://localhost:9094
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 3
KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 2
KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 3
KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0
KAFKA_COMPRESSION_TYPE: 'snappy'
volumes:
- kafka-3-data:/var/lib/kafka/data
kafka-ui:
image: provectuslabs/kafka-ui:latest
container_name: kafka-ui
depends_on:
- kafka-1
- kafka-2
- kafka-3
ports:
- "8080:8080"
environment:
KAFKA_CLUSTERS_0_NAME: local
KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: kafka-1:29092,kafka-2:29093,kafka-3:29094
KAFKA_CLUSTERS_0_ZOOKEEPER: zookeeper:2181
mysql:
image: mysql:8.0
container_name: mysql
ports:
- "3306:3306"
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: logs
MYSQL_USER: logger
MYSQL_PASSWORD: logger123
volumes:
- mysql-data:/var/lib/mysql
- ./init.sql:/docker-entrypoint-initdb.d/init.sql
elasticsearch:
image: docker.elastic.co/elasticsearch/elasticsearch:8.8.0
container_name: elasticsearch
ports:
- "9200:9200"
environment:
- discovery.type=single-node
- xpack.security.enabled=false
- "ES_JAVA_OPTS=-Xms512m -Xmx512m"
volumes:
- elasticsearch-data:/usr/share/elasticsearch/data
kibana:
image: docker.elastic.co/kibana/kibana:8.8.0
container_name: kibana
depends_on:
- elasticsearch
ports:
- "5601:5601"
environment:
ELASTICSEARCH_HOSTS: http://elasticsearch:9200
volumes:
zookeeper-data:
zookeeper-logs:
kafka-1-data:
kafka-2-data:
kafka-3-data:
mysql-data:
elasticsearch-data:
์ด์ docker-compose up -d ๋ช ๋ น์ด๋ก ์ ์ฒด ์ธํ๋ผ๋ฅผ ํ ๋ฒ์ ๋์ธ ์ ์์ด! Kafka ํด๋ฌ์คํฐ 3๋, MySQL, Elasticsearch, Kibana, ๊ทธ๋ฆฌ๊ณ Kafka UI๊น์ง ๋ชจ๋ ์ค๋น๋ผ. ์ ๋ง ํธํ์ง? ๐
๐ ๋ฒ ์คํธ ํ๋ํฐ์ค์ ํ
๋ง์ง๋ง์ผ๋ก ์ค์ ์์ ์ ์ฉํ ๋ฒ ์คํธ ํ๋ํฐ์ค๋ค์ ์ ๋ฆฌํด๋ณผ๊ฒ. ์ด๊ฑด ๋ด๊ฐ ์ค์ ๋ก ํ๋ก๋์ ํ๊ฒฝ์์ ๊ฒช์ผ๋ฉด์ ๋ฐฐ์ด ๊ฒ๋ค์ด์ผ! ๐ก
2. ๋ฐฐ์น ํฌ๊ธฐ ์ต์ ํ: ๋๋ฌด ์์ผ๋ฉด ์ค๋ฒํค๋๊ฐ ํฌ๊ณ , ๋๋ฌด ํฌ๋ฉด ์ง์ฐ์ด ๋ฐ์ํด. ํ ์คํธํด์ ์ต์ ๊ฐ์ ์ฐพ์.
3. ์์ถ ํ์ฉ: ๋คํธ์ํฌ ๋์ญํญ์ ์ ์ฝํ๊ณ ์ฒ๋ฆฌ๋์ ๋์ผ ์ ์์ด.
4. ์๋ฌ ์ฒ๋ฆฌ: ์ ์ก ์คํจ ์ ์ฌ์๋ ๋ก์ง์ ๊ตฌํํ๊ณ , ์ต์ข ์คํจ ์ ๋ก์ปฌ์ ์ ์ฅํ์.
5. ๋ฉ์์ง ํค ํ์ฉ: ๊ฐ์ ํค๋ฅผ ๊ฐ์ง ๋ฉ์์ง๋ ๊ฐ์ ํํฐ์ ์ผ๋ก ๊ฐ์ ์์๊ฐ ๋ณด์ฅ๋ผ.
6. ํ์์์ ์ค์ : ์ ์ ํ ํ์์์์ ์ค์ ํด์ ๋ฌดํ ๋๊ธฐ๋ฅผ ๋ฐฉ์งํ์.
2. ์๋ ์ปค๋ฐ: ๋ฉ์์ง ์ฒ๋ฆฌ๊ฐ ์์ ํ ๋๋ ํ์๋ง ์ปค๋ฐํด์ ๋ฐ์ดํฐ ์์ค์ ๋ฐฉ์งํ์.
3. ๋ฐฐ์น ์ฒ๋ฆฌ: ์ฌ๋ฌ ๋ฉ์์ง๋ฅผ ๋ชจ์์ ์ฒ๋ฆฌํ๋ฉด DB ์ฐ๊ธฐ ์ฑ๋ฅ์ด ํฅ์๋ผ.
4. ์๋ฌ ์ฒ๋ฆฌ: ์ฒ๋ฆฌ ์คํจ ์ ์ฌ์๋ํ๊ณ , ์ต์ข ์คํจ ์ DLQ๋ก ๋ณด๋ด์.
5. ๊ทธ๋ ์ด์คํ ์ ง๋ค์ด: ์๊ทธ๋์ ๋ฐ์ผ๋ฉด ํ์ฌ ์ฒ๋ฆฌ ์ค์ธ ๋ฉ์์ง๋ฅผ ์๋ฃํ๊ณ ์ข ๋ฃํ์.
6. Consumer Lag ๋ชจ๋ํฐ๋ง: Lag์ด ์ฆ๊ฐํ๋ฉด Consumer๋ฅผ ์ถ๊ฐํ๊ฑฐ๋ ์ฒ๋ฆฌ ์๋๋ฅผ ๊ฐ์ ํ์.
โข ๋ฉ์์ง ์ฒ๋ฆฌ๋ (messages/sec)
โข Consumer Lag
โข ์ฒ๋ฆฌ ์๊ฐ (latency)
โข ์๋ฌ์จ
โข ๋์คํฌ ์ฌ์ฉ๋
2. ์๋ฆผ ์ค์ :
โข Consumer Lag > ์๊ณ๊ฐ
โข ์๋ฌ์จ > 5%
โข ๋์คํฌ ์ฌ์ฉ๋ > 80%
โข ์ฒ๋ฆฌ ์๊ฐ > ์๊ณ๊ฐ
3. ๋์๋ณด๋ ๊ตฌ์ฑ: Grafana๋ก ์ค์๊ฐ ๋ชจ๋ํฐ๋ง ๋์๋ณด๋๋ฅผ ๋ง๋ค์.
4. ๋ก๊ทธ ๋ ๋ฒจ ๊ด๋ฆฌ: ํ๋ก๋์ ์์๋ INFO ์ด์๋ง ๋ก๊น ํ๊ณ , ๋๋ฒ๊น ์์๋ง DEBUG ์ฌ์ฉ.
5. ์ ๊ธฐ ์ ๊ฒ: ์ฃผ๊ธฐ์ ์ผ๋ก ์์คํ ์ํ๋ฅผ ์ ๊ฒํ๊ณ ์ต์ ํํ์.
ํํฐ์ ์ = (๋ชฉํ ์ฒ๋ฆฌ๋ / Consumer ์ฒ๋ฆฌ๋) ร 2
์: ์ด๋น 10,000 ๋ฉ์์ง๋ฅผ ์ฒ๋ฆฌํ๊ณ , Consumer ํ๋๊ฐ ์ด๋น 1,000 ๋ฉ์์ง๋ฅผ ์ฒ๋ฆฌํ๋ค๋ฉด
ํํฐ์ ์ = (10,000 / 1,000) ร 2 = 20๊ฐ
Consumer ์ ๊ฒฐ์ :
Consumer ์๋ ํํฐ์ ์๋ฅผ ์ด๊ณผํ ์ ์์ด. ํํฐ์ ์์ ๊ฐ๊ฑฐ๋ ์ ๊ฒ ์ค์ ํ์.
๋ฉ๋ชจ๋ฆฌ ์ค์ :
PHP Consumer๋ ๋ฉ๋ชจ๋ฆฌ ๋์์ ์ฃผ์! ์ฃผ๊ธฐ์ ์ผ๋ก ์ฌ์์ํ๊ฑฐ๋ ๋ฉ๋ชจ๋ฆฌ ์ ํ์ ์ค์ ํ์.
๋คํธ์ํฌ ์ต์ ํ:
Producer์ Consumer๋ฅผ Kafka ๋ธ๋ก์ปค์ ๊ฐ์ ๋ฐ์ดํฐ์ผํฐ์ ๋ฐฐ์นํ๋ฉด ์ง์ฐ์ด ์ค์ด๋ค์ด.
๐ ํ์ฅ ๋ฐ ๊ณ ๊ธ ๊ธฐ๋ฅ
๊ธฐ๋ณธ ์์คํ ์ ๋ง๋ค์์ผ๋ ์ด์ ๋ ๊ณ ๊ธ ๊ธฐ๋ฅ๋ค์ ์ถ๊ฐํด๋ณด์! ์ค์ ํ๋ก๋์ ํ๊ฒฝ์์๋ ์ด๋ฐ ๊ธฐ๋ฅ๋ค์ด ์ ๋ง ์ ์ฉํด. ๐ช
๐ ์คํธ๋ฆผ ์ฒ๋ฆฌ (Kafka Streams)
Kafka Streams๋ฅผ ์ฌ์ฉํ๋ฉด ์ค์๊ฐ์ผ๋ก ๋ฐ์ดํฐ๋ฅผ ๋ณํํ๊ณ ์ง๊ณํ ์ ์์ด. PHP์์๋ ์ง์ Kafka Streams API๋ฅผ ์ฌ์ฉํ ์ ์์ง๋ง, Consumer์์ ๋น์ทํ ๊ธฐ๋ฅ์ ๊ตฌํํ ์ ์์ด!
<?php
// src/StreamProcessor.php
namespace App\Kafka;
class StreamProcessor
{
private array $windows = [];
private int $windowSize = 60; // 60์ด ์๋์ฐ
public function processWithWindow(array $payload): void
{
$timestamp = $payload['timestamp'];
$windowKey = floor($timestamp / $this->windowSize) * $this->windowSize;
if (!isset($this->windows[$windowKey])) {
$this->windows[$windowKey] = [
'count' => 0,
'sum' => 0,
'min' => PHP_FLOAT_MAX,
'max' => PHP_FLOAT_MIN,
'events' => [],
];
}
$window = &$this->windows[$windowKey];
$window['count']++;
if (isset($payload['value'])) {
$value = $payload['value'];
$window['sum'] += $value;
$window['min'] = min($window['min'], $value);
$window['max'] = max($window['max'], $value);
}
$window['events'][] = $payload;
// ์ค๋๋ ์๋์ฐ ์ ๋ฆฌ
$this->cleanOldWindows($timestamp);
// ์๋์ฐ๊ฐ ์๋ฃ๋๋ฉด ์ง๊ณ ๊ฒฐ๊ณผ ์ ์ฅ
if ($this->isWindowComplete($windowKey, $timestamp)) {
$this->finalizeWindow($windowKey);
}
}
private function cleanOldWindows(float $currentTimestamp): void
{
$cutoff = floor(($currentTimestamp - $this->windowSize * 10) / $this->windowSize) * $this->windowSize;
foreach ($this->windows as $key => $window) {
if ($key < $cutoff) {
unset($this->windows[$key]);
}
}
}
private function isWindowComplete(int $windowKey, float $currentTimestamp): bool
{
return $currentTimestamp > ($windowKey + $this->windowSize);
}
private function finalizeWindow(int $windowKey): void
{
$window = $this->windows[$windowKey];
$aggregated = [
'window_start' => date('Y-m-d H:i:s', $windowKey),
'window_end' => date('Y-m-d H:i:s', $windowKey + $this->windowSize),
'count' => $window['count'],
'average' => $window['count'] > 0 ? $window['sum'] / $window['count'] : 0,
'min' => $window['min'] !== PHP_FLOAT_MAX ? $window['min'] : null,
'max' => $window['max'] !== PHP_FLOAT_MIN ? $window['max'] : null,
];
// ์ง๊ณ ๊ฒฐ๊ณผ ์ ์ฅ
$this->saveAggregatedData($aggregated);
echo "Window finalized: " . json_encode($aggregated) . "\n";
}
private function saveAggregatedData(array $data): void
{
// ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ ๋ค๋ฅธ Kafka ํ ํฝ์ผ๋ก ์ ์ฅ
echo "Saving aggregated data\n";
}
}
๐ญ ๋ฐ์ดํฐ ๋ณํ ํ์ดํ๋ผ์ธ
๋ก๊ทธ ๋ฐ์ดํฐ๋ฅผ ๋ค์ํ ํํ๋ก ๋ณํํด์ ์ฌ๋ฌ ๋ชฉ์ ์ผ๋ก ์ฌ์ฉํ ์ ์์ด. ETL ํ์ดํ๋ผ์ธ์ ๋ง๋ค์ด๋ณด์!
<?php
// src/Pipeline/TransformPipeline.php
namespace App\Pipeline;
class TransformPipeline
{
private array $transformers = [];
public function addTransformer(callable $transformer): self
{
$this->transformers[] = $transformer;
return $this;
}
public function process(array $data): array
{
$result = $data;
foreach ($this->transformers as $transformer) {
$result = $transformer($result);
if ($result === null) {
return null; // ํํฐ๋ง๋จ
}
}
return $result;
}
}
// ์ฌ์ฉ ์์
$pipeline = new TransformPipeline();
// 1. ๋ฐ์ดํฐ ์ ์
$pipeline->addTransformer(function($data) {
// null ๊ฐ ์ ๊ฑฐ
return array_filter($data, function($value) {
return $value !== null;
});
});
// 2. ํ์
๋ณํ
$pipeline->addTransformer(function($data) {
if (isset($data['timestamp']) && is_string($data['timestamp'])) {
$data['timestamp'] = strtotime($data['timestamp']);
}
return $data;
});
// 3. ํ๋ ์ถ๊ฐ
$pipeline->addTransformer(function($data) {
$data['processed_at'] = time();
$data['version'] = '1.0';
return $data;
});
// 4. ๋ฏผ๊ฐ ์ ๋ณด ๋ง์คํน
$pipeline->addTransformer(function($data) use ($masker) {
return $masker->mask($data);
});
// 5. ํํฐ๋ง (ํน์ ์กฐ๊ฑด์ ๋ง๋ ๊ฒ๋ง)
$pipeline->addTransformer(function($data) {
if (isset($data['level']) && $data['level'] === 'debug') {
return null; // debug ๋ก๊ทธ๋ ์ ์ธ
}
return $data;
});
// ํ์ดํ๋ผ์ธ ์คํ
$transformed = $pipeline->process($rawData);
๐ ์ค์๊ฐ ๋ถ์ ๋ฐ ์๋ฆผ
๋ก๊ทธ๋ฅผ ์ค์๊ฐ์ผ๋ก ๋ถ์ํด์ ์ด์ ์งํ๋ฅผ ๊ฐ์งํ๊ณ ์ฆ์ ์๋ฆผ์ ๋ณด๋ผ ์ ์์ด!
<?php
// src/Analytics/AnomalyDetector.php
namespace App\Analytics;
class AnomalyDetector
{
private array $baselines = [];
private float $threshold = 2.0; // ํ์คํธ์ฐจ์ 2๋ฐฐ
public function detect(string $metric, float $value): bool
{
if (!isset($this->baselines[$metric])) {
$this->baselines[$metric] = [
'values' => [],
'mean' => 0,
'stddev' => 0,
];
}
$baseline = &$this->baselines[$metric];
$baseline['values'][] = $value;
// ์ต๊ทผ 100๊ฐ ๊ฐ๋ง ์ ์ง
if (count($baseline['values']) > 100) {
array_shift($baseline['values']);
}
// ํ๊ท ๊ณผ ํ์คํธ์ฐจ ๊ณ์ฐ
$baseline['mean'] = array_sum($baseline['values']) / count($baseline['values']);
$baseline['stddev'] = $this->calculateStdDev($baseline['values'], $baseline['mean']);
// ์ด์ ์งํ ๊ฐ์ง
$deviation = abs($value - $baseline['mean']);
$isAnomaly = $deviation > ($baseline['stddev'] * $this->threshold);
if ($isAnomaly) {
echo "ANOMALY DETECTED: {$metric} = {$value} (mean: {$baseline['mean']}, stddev: {$baseline['stddev']})\n";
}
return $isAnomaly;
}
private function calculateStdDev(array $values, float $mean): float
{
$variance = 0;
foreach ($values as $value) {
$variance += pow($value - $mean, 2);
}
$variance /= count($values);
return sqrt($variance);
}
}
// ์ฌ์ฉ ์์
$detector = new AnomalyDetector();
// API ์๋ต ์๊ฐ ๋ชจ๋ํฐ๋ง
foreach ($apiLogs as $log) {
$isAnomaly = $detector->detect('api_response_time', $log['duration']);
if ($isAnomaly) {
$alertManager->warning('Abnormal API response time detected', [
'endpoint' => $log['endpoint'],
'duration' => $log['duration'],
]);
}
}
// ์๋ฌ์จ ๋ชจ๋ํฐ๋ง
$errorRate = $errorCount / $totalRequests;
$isAnomaly = $detector->detect('error_rate', $errorRate);
if ($isAnomaly) {
$alertManager->critical('Error rate spike detected', [
'error_rate' => $errorRate,
'error_count' => $errorCount,
'total_requests' => $totalRequests,
]);
}
๐ฌ ๋ง๋ฌด๋ฆฌํ๋ฉฐ
์! ์ ๋ง ๊ธด ์ฌ์ ์ด์์ง? ๐
์ฐ๋ฆฌ๋ PHP์ Apache Kafka๋ฅผ ์ด์ฉํด์ ๋๊ท๋ชจ ๋ก๊ทธ ์์ง ๋ฐ ์ฒ๋ฆฌ ์์คํ
์ ์ฒ์๋ถํฐ ๋๊น์ง ๋ง๋ค์ด๋ดค์ด.
์ด ์์คํ
์ ์ด๋น ์๋ง ๊ฑด์ ๋ก๊ทธ๋ฅผ ์์ ์ ์ผ๋ก ์ฒ๋ฆฌํ ์ ์๊ณ , ์ค์๊ฐ์ผ๋ก ๋ถ์ํ๊ณ ์๋ฆผ์ ๋ณด๋ผ ์ ์์ด. ์ฌ๋ฅ๋ท ๊ฐ์ ํ๋ซํผ์์๋ ์ถฉ๋ถํ ์ฌ์ฉํ ์ ์๋ ์์ค์ด์ผ! ๐
ํต์ฌ ํฌ์ธํธ๋ฅผ ๋ค์ ์ ๋ฆฌํด๋ณผ๊น?
โข Producer: ๋ก๊ทธ๋ฅผ ๋น๋๊ธฐ๋ก Kafka์ ์ ์ก
โข Kafka: ๋ฉ์์ง๋ฅผ ์์ ํ๊ฒ ์ ์ฅํ๊ณ ๋ถ์ฐ ์ฒ๋ฆฌ
โข Consumer: ๋ก๊ทธ๋ฅผ ์ฝ์ด์ ์ฒ๋ฆฌํ๊ณ ์ ์ฅ
2. ์ฑ๋ฅ ์ต์ ํ:
โข ๋ฐฐ์น ์ ์ก๊ณผ ์์ถ ์ฌ์ฉ
โข ๋ณ๋ ฌ ์ฒ๋ฆฌ์ ํํฐ์ ๋
โข ๋น๋๊ธฐ ์ฒ๋ฆฌ์ ๋ฒํผ๋ง
3. ์์ ์ฑ:
โข ์ฌ์๋ ๋ก์ง๊ณผ DLQ
โข ์๋ ์ปค๋ฐ๊ณผ ๋ฉฑ๋ฑ์ฑ
โข ๊ทธ๋ ์ด์คํ ์ ง๋ค์ด
4. ๋ณด์:
โข ๋ฐ์ดํฐ ๋ง์คํน
โข SSL/TLS ์ํธํ
โข SASL ์ธ์ฆ
5. ๋ชจ๋ํฐ๋ง:
โข ๋ฉํธ๋ฆญ ์์ง
โข ์ค์๊ฐ ์๋ฆผ
โข ์ด์ ์งํ ๊ฐ์ง
์ด์ ๋๋ ๋๊ท๋ชจ ๋ก๊ทธ ์ฒ๋ฆฌ ์์คํ
์ ๋ง๋ค ์ ์์ด! ๋ฌผ๋ก ์ฒ์์๋ ์ด๋ ค์ธ ์ ์์ง๋ง, ํ๋์ฉ ์ฐจ๊ทผ์ฐจ๊ทผ ๊ตฌํํ๋ค ๋ณด๋ฉด ๊ธ๋ฐฉ ์ต์ํด์ง ๊ฑฐ์ผ. ๐ช
ํน์ ์ด๋ฐ ์์คํ
์ ๊ตฌ์ถํ๋ ๋ฐ ๋์์ด ํ์ํ๋ค๋ฉด, ์ฌ๋ฅ๋ท์์ ๊ด๋ จ ์ ๋ฌธ๊ฐ๋ฅผ ์ฐพ์๋ณผ ์๋ ์์ด. ์ค์ ๋ก ํ๋ก๋์
ํ๊ฒฝ์์ ์ด์ํด๋ณธ ๊ฒฝํ์ด ์๋ ๊ฐ๋ฐ์๋ค์ ์กฐ์ธ์ ์ ๋ง ๊ฐ์ง ๊ฑฐ๋๊น! ๐
๋ง์ง๋ง์ผ๋ก ํ ๊ฐ์ง ํ์ ๋ ์ฃผ์๋ฉด, ํญ์ ์๊ฒ ์์ํด์ ์ ์ง์ ์ผ๋ก ํ์ฅํ๋ ๊ฒ ์ข์. ์ฒ์๋ถํฐ ์๋ฒฝํ ์์คํ
์ ๋ง๋ค๋ ค๊ณ ํ์ง ๋ง๊ณ , ๊ธฐ๋ณธ์ ์ธ ๊ธฐ๋ฅ๋ถํฐ ๊ตฌํํ๊ณ ํ์์ ๋ฐ๋ผ ๊ฐ์ ํด๋๊ฐ์!
๊ทธ๋ผ ์ฆ๊ฑฐ์ด ์ฝ๋ฉ ๋๊ธธ ๋ฐ๋ผ! ํ์ดํ
! ๐
๊ด๋ จ ํค์๋
๋๊ธ ์์ฑ
์ด ๊ธ์ ๋ํ ์ฌ๋ฌ๋ถ์ ์๊ฐ์ ๋ค๋ ค์ฃผ์ธ์
๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค
๋๊ธ์ ์์ฑํ๋ ค๋ฉด ๋จผ์ ๋ก๊ทธ์ธํด์ฃผ์ธ์.
๋๊ธ 0
์ง์์ธ์ ์ฒ - ์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
- ์ ์๊ถ ๋ฐ ์์ ๊ถ: ๋ณธ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ๋ ์ AI ๊ธฐ์ ๋ก ์์ฑ๋์์ผ๋ฉฐ, ๋ํ๋ฏผ๊ตญ ์ ์๊ถ๋ฒ ๋ฐ ๊ตญ์ ์ ์๊ถ ํ์ฝ์ ์ํด ๋ณดํธ๋ฉ๋๋ค.
- AI ์์ฑ ์ปจํ ์ธ ์ ๋ฒ์ ์ง์: ๋ณธ AI ์์ฑ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ์ง์ ์ฐฝ์๋ฌผ๋ก ์ธ์ ๋๋ฉฐ, ๊ด๋ จ ๋ฒ๊ท์ ๋ฐ๋ผ ์ ์๊ถ ๋ณดํธ๋ฅผ ๋ฐ์ต๋๋ค.
- ์ฌ์ฉ ์ ํ: ์ฌ๋ฅ๋ท์ ๋ช ์์ ์๋ฉด ๋์ ์์ด ๋ณธ ์ปจํ ์ธ ๋ฅผ ๋ณต์ , ์์ , ๋ฐฐํฌ, ๋๋ ์์ ์ ์ผ๋ก ํ์ฉํ๋ ํ์๋ ์๊ฒฉํ ๊ธ์ง๋ฉ๋๋ค.
- ๋ฐ์ดํฐ ์์ง ๊ธ์ง: ๋ณธ ์ปจํ ์ธ ์ ๋ํ ๋ฌด๋จ ์คํฌ๋ํ, ํฌ๋กค๋ง, ๋ฐ ์๋ํ๋ ๋ฐ์ดํฐ ์์ง์ ๋ฒ์ ์ ์ฌ์ ๋์์ด ๋ฉ๋๋ค.
- AI ํ์ต ์ ํ: ์ฌ๋ฅ๋ท์ AI ์์ฑ ์ปจํ ์ธ ๋ฅผ ํ AI ๋ชจ๋ธ ํ์ต์ ๋ฌด๋จ ์ฌ์ฉํ๋ ํ์๋ ๊ธ์ง๋๋ฉฐ, ์ด๋ ์ง์ ์ฌ์ฐ๊ถ ์นจํด๋ก ๊ฐ์ฃผ๋ฉ๋๋ค.
์์ง ๋๊ธ์ด ์์ต๋๋ค.