์ฝ˜ํ…์ธ  ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ - ๐Ÿš€ PHP์™€ Apache Kafka๋กœ ๋งŒ๋“œ๋Š” ๋Œ€๊ทœ๋ชจ ๋กœ๊ทธ ์ˆ˜์ง‘ ์‹œ์Šคํ…œ

๐Ÿš€ PHP์™€ Apache Kafka๋กœ ๋งŒ๋“œ๋Š” ๋Œ€๊ทœ๋ชจ ๋กœ๊ทธ ์ˆ˜์ง‘ ์‹œ์Šคํ…œ

์‹ค์‹œ๊ฐ„์œผ๋กœ ์Ÿ์•„์ง€๋Š” ๋ฐ์ดํ„ฐ๋ฅผ ์šฐ์•„ํ•˜๊ฒŒ ์ฒ˜๋ฆฌํ•˜๋Š” ๋ฐฉ๋ฒ•

์•ˆ๋…•! ์˜ค๋Š˜์€ ์ •๋ง ์žฌ๋ฏธ์žˆ๋Š” ์ฃผ์ œ๋ฅผ ๊ฐ€์ง€๊ณ  ์™”์–ด. ๋ฐ”๋กœ PHP์™€ Apache Kafka๋ฅผ ์ด์šฉํ•œ ๋Œ€๊ทœ๋ชจ ๋กœ๊ทธ ์ˆ˜์ง‘ ๋ฐ ์ฒ˜๋ฆฌ ์‹œ์Šคํ…œ์— ๋Œ€ํ•œ ์ด์•ผ๊ธฐ์•ผ. ๐Ÿ˜Š

"์–ด? PHP๋กœ ๋Œ€๊ทœ๋ชจ ์‹œ์Šคํ…œ์„ ๋งŒ๋“ ๋‹ค๊ณ ?" ํ•˜๊ณ  ์˜์•„ํ•ดํ•  ์ˆ˜๋„ ์žˆ๊ฒ ์ง€๋งŒ, ์‚ฌ์‹ค PHP๋Š” ์ƒ๊ฐ๋ณด๋‹ค ํ›จ์”ฌ ๊ฐ•๋ ฅํ•œ ์–ธ์–ด์•ผ. ํŠนํžˆ ์ตœ๊ทผ PHP 8.x ๋ฒ„์ „๋“ค์€ ์„ฑ๋Šฅ๋„ ์—„์ฒญ๋‚˜๊ฒŒ ๊ฐœ์„ ๋๊ณ , ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ๋„ ๊ฐ€๋Šฅํ•ด์กŒ๊ฑฐ๋“ . ์—ฌ๊ธฐ์— Apache Kafka๋ผ๋Š” ๊ฐ•๋ ฅํ•œ ๋ฉ”์‹œ์ง€ ๋ธŒ๋กœ์ปค๋ฅผ ๊ฒฐํ•ฉํ•˜๋ฉด? ์™€์šฐ, ์ •๋ง ๋ฉ‹์ง„ ์‹œ์Šคํ…œ์„ ๋งŒ๋“ค ์ˆ˜ ์žˆ์–ด! ๐ŸŽฏ

์ด ๊ธ€์—์„œ๋Š” ์‹ค์ œ๋กœ ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์—์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ๋กœ๊ทธ ์ˆ˜์ง‘ ์‹œ์Šคํ…œ์„ ์–ด๋–ป๊ฒŒ ๊ตฌ์ถ•ํ•˜๋Š”์ง€, ๊ทธ ๊ณผ์ •์—์„œ ๋งˆ์ฃผ์น˜๋Š” ๋ฌธ์ œ๋“ค๊ณผ ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•๋“ค์„ ์นœ๊ตฌ์—๊ฒŒ ์„ค๋ช…ํ•˜๋“ฏ ํŽธํ•˜๊ฒŒ ํ’€์–ด๋ณผ๊ฒŒ. ์žฌ๋Šฅ๋„ท ๊ฐ™์€ ํ”Œ๋žซํผ์—์„œ ๊ฐœ๋ฐœ ๊ด€๋ จ ์žฌ๋Šฅ์„ ๊ณต์œ ํ•˜๊ฑฐ๋‚˜ ๋ฐฐ์šฐ๊ณ  ์‹ถ์€ ์‚ฌ๋žŒ๋“ค์—๊ฒŒ๋„ ์ •๋ง ์œ ์šฉํ•œ ๋‚ด์šฉ์ด ๋  ๊ฑฐ์•ผ!

PHP Kafka ๋กœ๊ทธ ์ €์žฅ์†Œ ์›น ์„œ๋ฒ„ ์‚ฌ์šฉ์ž ์•ก์…˜ ์—๋Ÿฌ ๋กœ๊ทธ API ํ˜ธ์ถœ ํŠธ๋žœ์žญ์…˜ Kafka ํด๋Ÿฌ์Šคํ„ฐ ํ† ํ”ฝ ๋ถ„์‚ฐ ํŒŒํ‹ฐ์…˜ ๊ด€๋ฆฌ ๋ฉ”์‹œ์ง€ ํ์ž‰ ๊ณ ๊ฐ€์šฉ์„ฑ ๋ณด์žฅ ์ปจ์Šˆ๋จธ ์‹ค์‹œ๊ฐ„ ๋ถ„์„ ๋ฐ์ดํ„ฐ ์ €์žฅ ์•Œ๋ฆผ ๋ฐœ์†ก ๋ชจ๋‹ˆํ„ฐ๋ง

๐Ÿค” ์™œ ๋Œ€๊ทœ๋ชจ ๋กœ๊ทธ ์ˆ˜์ง‘ ์‹œ์Šคํ…œ์ด ํ•„์š”ํ• ๊นŒ?

๋จผ์ € ์ด์•ผ๊ธฐ๋ฅผ ์‹œ์ž‘ํ•˜๊ธฐ ์ „์—, ์™œ ์ด๋Ÿฐ ์‹œ์Šคํ…œ์ด ํ•„์š”ํ•œ์ง€๋ถ€ํ„ฐ ์•Œ์•„๋ณด์ž. ์ƒ์ƒํ•ด๋ด. ๋„ˆ๊ฐ€ ์šด์˜ํ•˜๋Š” ์›น ์„œ๋น„์Šค์— ํ•˜๋ฃจ์— ์ˆ˜๋ฐฑ๋งŒ ๋ช…์˜ ์‚ฌ์šฉ์ž๊ฐ€ ์ ‘์†ํ•œ๋‹ค๊ณ  ๊ฐ€์ •ํ•ด๋ณด์ž. ๐Ÿ˜ฑ

๊ฐ ์‚ฌ์šฉ์ž๊ฐ€ ํด๋ฆญ ํ•œ ๋ฒˆ, ํŽ˜์ด์ง€ ์ด๋™ ํ•œ ๋ฒˆ, API ํ˜ธ์ถœ ํ•œ ๋ฒˆ ํ•  ๋•Œ๋งˆ๋‹ค ๋กœ๊ทธ๊ฐ€ ์ƒ์„ฑ๋ผ. ๊ทธ๋Ÿผ ํ•˜๋ฃจ์— ๋ช‡ ๊ฐœ์˜ ๋กœ๊ทธ๊ฐ€ ์ƒ์„ฑ๋ ๊นŒ? ์ˆ˜์–ต ๊ฐœ? ์ˆ˜์‹ญ์–ต ๊ฐœ? ๋งž์•„, ์ •๋ง ์–ด๋งˆ์–ด๋งˆํ•œ ์–‘์ด์•ผ!

์ด๋Ÿฐ ๋กœ๊ทธ๋“ค์„ ๊ทธ๋ƒฅ ํŒŒ์ผ์— ์“ฐ๊ฑฐ๋‚˜ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ๋ฐ”๋กœ ์ €์žฅํ•˜๋ฉด ์–ด๋–ป๊ฒŒ ๋ ๊นŒ? ๐Ÿ”ฅ

โš ๏ธ ์ „ํ†ต์ ์ธ ๋ฐฉ์‹์˜ ๋ฌธ์ œ์ 

1. ์„ฑ๋Šฅ ์ €ํ•˜: ๋””์Šคํฌ I/O๊ฐ€ ๋ณ‘๋ชฉ์ด ๋˜์–ด ์„œ๋น„์Šค ์ „์ฒด๊ฐ€ ๋А๋ ค์ ธ
2. ๋ฐ์ดํ„ฐ ์†์‹ค: ์„œ๋ฒ„๊ฐ€ ๋‹ค์šด๋˜๋ฉด ๋ฉ”๋ชจ๋ฆฌ์— ์žˆ๋˜ ๋กœ๊ทธ๊ฐ€ ๋‚ ์•„๊ฐ€
3. ํ™•์žฅ์„ฑ ๋ถ€์กฑ: ํŠธ๋ž˜ํ”ฝ์ด ์ฆ๊ฐ€ํ•˜๋ฉด ์‹œ์Šคํ…œ์„ ํ™•์žฅํ•˜๊ธฐ ์–ด๋ ค์›Œ
4. ์‹ค์‹œ๊ฐ„ ์ฒ˜๋ฆฌ ๋ถˆ๊ฐ€: ๋กœ๊ทธ๋ฅผ ์‹ค์‹œ๊ฐ„์œผ๋กœ ๋ถ„์„ํ•˜๊ณ  ๋Œ€์‘ํ•˜๊ธฐ ํž˜๋“ค์–ด

๋ฐ”๋กœ ์ด๋Ÿฐ ๋ฌธ์ œ๋“ค์„ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด Apache Kafka์™€ ๊ฐ™์€ ๋ฉ”์‹œ์ง€ ๋ธŒ๋กœ์ปค๊ฐ€ ๋“ฑ์žฅํ•œ ๊ฑฐ์•ผ. Kafka๋Š” LinkedIn์—์„œ ๊ฐœ๋ฐœํ•œ ๋ถ„์‚ฐ ์ŠคํŠธ๋ฆฌ๋ฐ ํ”Œ๋žซํผ์œผ๋กœ, ์ดˆ๋‹น ์ˆ˜๋ฐฑ๋งŒ ๊ฑด์˜ ๋ฉ”์‹œ์ง€๋ฅผ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋Š” ์—„์ฒญ๋‚œ ์„ฑ๋Šฅ์„ ์ž๋ž‘ํ•ด! ๐Ÿ’ช

๐ŸŽฏ Apache Kafka, ๋„ˆ๋Š” ๋ˆ„๊ตฌ๋ƒ?

Kafka๋ฅผ ์ฒ˜์Œ ์ ‘ํ•˜๋Š” ์นœ๊ตฌ๋“ค์„ ์œ„ํ•ด ๊ฐ„๋‹จํ•˜๊ฒŒ ์„ค๋ช…ํ•ด๋ณผ๊ฒŒ. Kafka๋Š” ๋ฐœํ–‰-๊ตฌ๋…(Publish-Subscribe) ๋ชจ๋ธ์„ ๊ธฐ๋ฐ˜์œผ๋กœ ํ•˜๋Š” ๋ฉ”์‹œ์ง€ ํ ์‹œ์Šคํ…œ์ด์•ผ.

์‰ฝ๊ฒŒ ๋งํ•˜๋ฉด, ์šฐ์ฒด๊ตญ ๊ฐ™์€ ๊ฑฐ๋ผ๊ณ  ์ƒ๊ฐํ•˜๋ฉด ๋ผ. ๐Ÿ“ฎ

๐Ÿข Kafka์˜ ํ•ต์‹ฌ ๊ฐœ๋… โ€ข Producer (์ƒ์‚ฐ์ž): ๋ฉ”์‹œ์ง€๋ฅผ ๋ณด๋‚ด๋Š” ์ฃผ์ฒด. ์šฐ๋ฆฌ์˜ ๊ฒฝ์šฐ PHP ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์ด ๋˜๊ฒ ์ง€?

โ€ข Topic (ํ† ํ”ฝ): ๋ฉ”์‹œ์ง€๊ฐ€ ์ €์žฅ๋˜๋Š” ์นดํ…Œ๊ณ ๋ฆฌ. ์˜ˆ๋ฅผ ๋“ค์–ด "user-logs", "error-logs", "api-logs" ๊ฐ™์€ ๊ฑฐ์•ผ.

โ€ข Partition (ํŒŒํ‹ฐ์…˜): ํ† ํ”ฝ์„ ์—ฌ๋Ÿฌ ๊ฐœ๋กœ ๋‚˜๋ˆˆ ๊ฒƒ. ๋ณ‘๋ ฌ ์ฒ˜๋ฆฌ๋ฅผ ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ•ด์ค˜.

โ€ข Consumer (์†Œ๋น„์ž): ๋ฉ”์‹œ์ง€๋ฅผ ์ฝ์–ด๊ฐ€๋Š” ์ฃผ์ฒด. ๋กœ๊ทธ๋ฅผ ์ฒ˜๋ฆฌํ•˜๋Š” ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์ด์ง€.

โ€ข Broker (๋ธŒ๋กœ์ปค): Kafka ์„œ๋ฒ„ ์ž์ฒด. ์—ฌ๋Ÿฌ ๋Œ€๋ฅผ ํด๋Ÿฌ์Šคํ„ฐ๋กœ ๊ตฌ์„ฑํ•  ์ˆ˜ ์žˆ์–ด.

โ€ข Consumer Group (์ปจ์Šˆ๋จธ ๊ทธ๋ฃน): ์—ฌ๋Ÿฌ ์ปจ์Šˆ๋จธ๋ฅผ ๋ฌถ์€ ๊ทธ๋ฃน. ๋ถ€ํ•˜ ๋ถ„์‚ฐ์ด ๊ฐ€๋Šฅํ•ด์ ธ.

Kafka์˜ ๊ฐ€์žฅ ํฐ ์žฅ์ ์€ ๋†’์€ ์ฒ˜๋ฆฌ๋Ÿ‰(High Throughput)๊ณผ ๋‚ด๊ตฌ์„ฑ(Durability)์ด์•ผ. ๋ฉ”์‹œ์ง€๋ฅผ ๋””์Šคํฌ์— ์ˆœ์ฐจ์ ์œผ๋กœ ์ €์žฅํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์—„์ฒญ๋‚˜๊ฒŒ ๋น ๋ฅด๊ณ , ๋ณต์ œ(Replication) ๊ธฐ๋Šฅ์œผ๋กœ ๋ฐ์ดํ„ฐ ์†์‹ค์„ ๋ฐฉ์ง€ํ•  ์ˆ˜ ์žˆ์–ด. ๐Ÿ›ก๏ธ

๐Ÿ”ง ์‹œ์Šคํ…œ ์•„ํ‚คํ…์ฒ˜ ์„ค๊ณ„ํ•˜๊ธฐ

์ž, ์ด์ œ ๋ณธ๊ฒฉ์ ์œผ๋กœ ์‹œ์Šคํ…œ์„ ์„ค๊ณ„ํ•ด๋ณด์ž! ์šฐ๋ฆฌ๊ฐ€ ๋งŒ๋“ค ์‹œ์Šคํ…œ์˜ ์ „์ฒด์ ์ธ ํ๋ฆ„์€ ์ด๋ž˜:

1 ๋กœ๊ทธ ์ƒ์„ฑ: PHP ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์—์„œ ์‚ฌ์šฉ์ž ํ™œ๋™, ์—๋Ÿฌ, API ํ˜ธ์ถœ ๋“ฑ์˜ ๋กœ๊ทธ๊ฐ€ ๋ฐœ์ƒํ•ด.
2 ๋กœ๊ทธ ์ „์†ก: PHP Producer๊ฐ€ ๋กœ๊ทธ๋ฅผ Kafka ํ† ํ”ฝ์œผ๋กœ ์ „์†กํ•ด. ์ด๋•Œ ๋น„๋™๊ธฐ๋กœ ์ฒ˜๋ฆฌํ•ด์„œ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์„ฑ๋Šฅ์— ์˜ํ–ฅ์„ ์ฃผ์ง€ ์•Š์•„.
3 ๋ฉ”์‹œ์ง€ ์ €์žฅ: Kafka ๋ธŒ๋กœ์ปค๊ฐ€ ๋ฉ”์‹œ์ง€๋ฅผ ํŒŒํ‹ฐ์…˜์— ๋ถ„์‚ฐ ์ €์žฅํ•ด. ๋ณต์ œ๋ณธ๋„ ๋งŒ๋“ค์–ด์„œ ์•ˆ์ „ํ•˜๊ฒŒ ๋ณด๊ด€ํ•˜์ง€.
4 ๋กœ๊ทธ ์†Œ๋น„: PHP Consumer๊ฐ€ Kafka์—์„œ ๋กœ๊ทธ๋ฅผ ์ฝ์–ด์™€์„œ ์ฒ˜๋ฆฌํ•ด. ์—ฌ๋Ÿฌ ์ปจ์Šˆ๋จธ๊ฐ€ ๋™์‹œ์— ์ž‘๋™ํ•  ์ˆ˜ ์žˆ์–ด.
5 ๋ฐ์ดํ„ฐ ์ €์žฅ ๋ฐ ๋ถ„์„: ์ฒ˜๋ฆฌ๋œ ๋กœ๊ทธ๋ฅผ Elasticsearch, MySQL, MongoDB ๋“ฑ์— ์ €์žฅํ•˜๊ณ  ๋ถ„์„ํ•ด.

์ด๋Ÿฐ ๊ตฌ์กฐ์˜ ๊ฐ€์žฅ ํฐ ์žฅ์ ์€ ๊ฐ ๋‹จ๊ณ„๊ฐ€ ๋…๋ฆฝ์ ์ด๋ผ๋Š” ๊ฑฐ์•ผ. 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 ๋ฐ”์ธ๋”ฉ์ด์•ผ. ์„ฑ๋Šฅ์ด ์ •๋ง ์ข‹์•„์„œ ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์—์„œ๋„ ์ถฉ๋ถ„ํžˆ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์–ด! ๐Ÿš€

Ubuntu/Debian ํ™˜๊ฒฝ์—์„œ ์„ค์น˜:
# 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();
    }
}

์™€! ์ฝ”๋“œ๊ฐ€ ์ข€ ๊ธธ์ง€? ํ•˜์ง€๋งŒ ํ•˜๋‚˜ํ•˜๋‚˜ ๋œฏ์–ด๋ณด๋ฉด ๋ณ„๊ฑฐ ์—†์–ด. ํ•ต์‹ฌ์€ ๋น„๋™๊ธฐ ์ „์†ก๊ณผ ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ์•ผ. ๐ŸŽฏ

๐Ÿ’ก ์„ฑ๋Šฅ ์ตœ์ ํ™” ํŒ 1. ์••์ถ• ์‚ฌ์šฉ: compression.codec๋ฅผ 'snappy'๋‚˜ 'lz4'๋กœ ์„ค์ •ํ•˜๋ฉด ๋„คํŠธ์›Œํฌ ๋Œ€์—ญํญ์„ ์ ˆ์•ฝํ•  ์ˆ˜ ์žˆ์–ด.

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 ์ตœ์ ํ™”

1. ๋ฐฐ์น˜ ์ „์†ก ํ™œ์šฉ

๋ฉ”์‹œ์ง€๋ฅผ ํ•˜๋‚˜์”ฉ ๋ณด๋‚ด๋Š” ๊ฒƒ๋ณด๋‹ค ์—ฌ๋Ÿฌ ๊ฐœ๋ฅผ ๋ชจ์•„์„œ ํ•œ ๋ฒˆ์— ๋ณด๋‚ด๋Š” ๊ฒŒ ํ›จ์”ฌ ํšจ์œจ์ ์ด์•ผ. 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 ์ตœ์ ํ™”

1. ๋ณ‘๋ ฌ ์ฒ˜๋ฆฌ

์—ฌ๋Ÿฌ 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 ํด๋Ÿฌ์Šคํ„ฐ ์ตœ์ ํ™”

๐Ÿ’ก Kafka ์„ค์ • ์ตœ์ ํ™” 1. ํŒŒํ‹ฐ์…˜ ์ˆ˜ ์กฐ์ •
ํ† ํ”ฝ์˜ ํŒŒํ‹ฐ์…˜ ์ˆ˜๋Š” ๋ณ‘๋ ฌ ์ฒ˜๋ฆฌ ๋Šฅ๋ ฅ์„ ๊ฒฐ์ •ํ•ด. ์ผ๋ฐ˜์ ์œผ๋กœ 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๊นŒ์ง€ ๋ชจ๋‘ ์ค€๋น„๋ผ. ์ •๋ง ํŽธํ•˜์ง€? ๐Ÿ˜Ž

๐ŸŽ“ ๋ฒ ์ŠคํŠธ ํ”„๋ž™ํ‹ฐ์Šค์™€ ํŒ

๋งˆ์ง€๋ง‰์œผ๋กœ ์‹ค์ „์—์„œ ์œ ์šฉํ•œ ๋ฒ ์ŠคํŠธ ํ”„๋ž™ํ‹ฐ์Šค๋“ค์„ ์ •๋ฆฌํ•ด๋ณผ๊ฒŒ. ์ด๊ฑด ๋‚ด๊ฐ€ ์‹ค์ œ๋กœ ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์—์„œ ๊ฒช์œผ๋ฉด์„œ ๋ฐฐ์šด ๊ฒƒ๋“ค์ด์•ผ! ๐Ÿ’ก

๐ŸŽฏ Producer ๋ฒ ์ŠคํŠธ ํ”„๋ž™ํ‹ฐ์Šค 1. ๋น„๋™๊ธฐ ์ „์†ก ์‚ฌ์šฉ: ๋™๊ธฐ ์ „์†ก์€ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์„ฑ๋Šฅ์— ์˜ํ–ฅ์„ ์ค˜. ํ•ญ์ƒ ๋น„๋™๊ธฐ๋กœ!

2. ๋ฐฐ์น˜ ํฌ๊ธฐ ์ตœ์ ํ™”: ๋„ˆ๋ฌด ์ž‘์œผ๋ฉด ์˜ค๋ฒ„ํ—ค๋“œ๊ฐ€ ํฌ๊ณ , ๋„ˆ๋ฌด ํฌ๋ฉด ์ง€์—ฐ์ด ๋ฐœ์ƒํ•ด. ํ…Œ์ŠคํŠธํ•ด์„œ ์ตœ์ ๊ฐ’์„ ์ฐพ์ž.

3. ์••์ถ• ํ™œ์šฉ: ๋„คํŠธ์›Œํฌ ๋Œ€์—ญํญ์„ ์ ˆ์•ฝํ•˜๊ณ  ์ฒ˜๋ฆฌ๋Ÿ‰์„ ๋†’์ผ ์ˆ˜ ์žˆ์–ด.

4. ์—๋Ÿฌ ์ฒ˜๋ฆฌ: ์ „์†ก ์‹คํŒจ ์‹œ ์žฌ์‹œ๋„ ๋กœ์ง์„ ๊ตฌํ˜„ํ•˜๊ณ , ์ตœ์ข… ์‹คํŒจ ์‹œ ๋กœ์ปฌ์— ์ €์žฅํ•˜์ž.

5. ๋ฉ”์‹œ์ง€ ํ‚ค ํ™œ์šฉ: ๊ฐ™์€ ํ‚ค๋ฅผ ๊ฐ€์ง„ ๋ฉ”์‹œ์ง€๋Š” ๊ฐ™์€ ํŒŒํ‹ฐ์…˜์œผ๋กœ ๊ฐ€์„œ ์ˆœ์„œ๊ฐ€ ๋ณด์žฅ๋ผ.

6. ํƒ€์ž„์•„์›ƒ ์„ค์ •: ์ ์ ˆํ•œ ํƒ€์ž„์•„์›ƒ์„ ์„ค์ •ํ•ด์„œ ๋ฌดํ•œ ๋Œ€๊ธฐ๋ฅผ ๋ฐฉ์ง€ํ•˜์ž.
๐Ÿ” Consumer ๋ฒ ์ŠคํŠธ ํ”„๋ž™ํ‹ฐ์Šค 1. ๋ฉฑ๋“ฑ์„ฑ ๋ณด์žฅ: ๊ฐ™์€ ๋ฉ”์‹œ์ง€๋ฅผ ์—ฌ๋Ÿฌ ๋ฒˆ ์ฒ˜๋ฆฌํ•ด๋„ ๊ฒฐ๊ณผ๊ฐ€ ๊ฐ™๋„๋ก ์„ค๊ณ„ํ•˜์ž.

2. ์ˆ˜๋™ ์ปค๋ฐ‹: ๋ฉ”์‹œ์ง€ ์ฒ˜๋ฆฌ๊ฐ€ ์™„์ „ํžˆ ๋๋‚œ ํ›„์—๋งŒ ์ปค๋ฐ‹ํ•ด์„œ ๋ฐ์ดํ„ฐ ์†์‹ค์„ ๋ฐฉ์ง€ํ•˜์ž.

3. ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ: ์—ฌ๋Ÿฌ ๋ฉ”์‹œ์ง€๋ฅผ ๋ชจ์•„์„œ ์ฒ˜๋ฆฌํ•˜๋ฉด DB ์“ฐ๊ธฐ ์„ฑ๋Šฅ์ด ํ–ฅ์ƒ๋ผ.

4. ์—๋Ÿฌ ์ฒ˜๋ฆฌ: ์ฒ˜๋ฆฌ ์‹คํŒจ ์‹œ ์žฌ์‹œ๋„ํ•˜๊ณ , ์ตœ์ข… ์‹คํŒจ ์‹œ DLQ๋กœ ๋ณด๋‚ด์ž.

5. ๊ทธ๋ ˆ์ด์Šคํ’€ ์…ง๋‹ค์šด: ์‹œ๊ทธ๋„์„ ๋ฐ›์œผ๋ฉด ํ˜„์žฌ ์ฒ˜๋ฆฌ ์ค‘์ธ ๋ฉ”์‹œ์ง€๋ฅผ ์™„๋ฃŒํ•˜๊ณ  ์ข…๋ฃŒํ•˜์ž.

6. Consumer Lag ๋ชจ๋‹ˆํ„ฐ๋ง: Lag์ด ์ฆ๊ฐ€ํ•˜๋ฉด Consumer๋ฅผ ์ถ”๊ฐ€ํ•˜๊ฑฐ๋‚˜ ์ฒ˜๋ฆฌ ์†๋„๋ฅผ ๊ฐœ์„ ํ•˜์ž.
๐Ÿ“Š ๋ชจ๋‹ˆํ„ฐ๋ง ๋ฒ ์ŠคํŠธ ํ”„๋ž™ํ‹ฐ์Šค 1. ํ•ต์‹ฌ ๋ฉ”ํŠธ๋ฆญ ์ถ”์ :
โ€ข ๋ฉ”์‹œ์ง€ ์ฒ˜๋ฆฌ๋Ÿ‰ (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๋ฅผ ์ด์šฉํ•ด์„œ ๋Œ€๊ทœ๋ชจ ๋กœ๊ทธ ์ˆ˜์ง‘ ๋ฐ ์ฒ˜๋ฆฌ ์‹œ์Šคํ…œ์„ ์ฒ˜์Œ๋ถ€ํ„ฐ ๋๊นŒ์ง€ ๋งŒ๋“ค์–ด๋ดค์–ด.

์ด ์‹œ์Šคํ…œ์€ ์ดˆ๋‹น ์ˆ˜๋งŒ ๊ฑด์˜ ๋กœ๊ทธ๋ฅผ ์•ˆ์ •์ ์œผ๋กœ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๊ณ , ์‹ค์‹œ๊ฐ„์œผ๋กœ ๋ถ„์„ํ•˜๊ณ  ์•Œ๋ฆผ์„ ๋ณด๋‚ผ ์ˆ˜ ์žˆ์–ด. ์žฌ๋Šฅ๋„ท ๊ฐ™์€ ํ”Œ๋žซํผ์—์„œ๋„ ์ถฉ๋ถ„ํžˆ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ์ˆ˜์ค€์ด์•ผ! ๐Ÿš€

ํ•ต์‹ฌ ํฌ์ธํŠธ๋ฅผ ๋‹ค์‹œ ์ •๋ฆฌํ•ด๋ณผ๊นŒ?

โœจ ํ•ต์‹ฌ ์š”์•ฝ 1. ์•„ํ‚คํ…์ฒ˜:
โ€ข Producer: ๋กœ๊ทธ๋ฅผ ๋น„๋™๊ธฐ๋กœ Kafka์— ์ „์†ก
โ€ข Kafka: ๋ฉ”์‹œ์ง€๋ฅผ ์•ˆ์ „ํ•˜๊ฒŒ ์ €์žฅํ•˜๊ณ  ๋ถ„์‚ฐ ์ฒ˜๋ฆฌ
โ€ข Consumer: ๋กœ๊ทธ๋ฅผ ์ฝ์–ด์„œ ์ฒ˜๋ฆฌํ•˜๊ณ  ์ €์žฅ

2. ์„ฑ๋Šฅ ์ตœ์ ํ™”:
โ€ข ๋ฐฐ์น˜ ์ „์†ก๊ณผ ์••์ถ• ์‚ฌ์šฉ
โ€ข ๋ณ‘๋ ฌ ์ฒ˜๋ฆฌ์™€ ํŒŒํ‹ฐ์…”๋‹
โ€ข ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ์™€ ๋ฒ„ํผ๋ง

3. ์•ˆ์ •์„ฑ:
โ€ข ์žฌ์‹œ๋„ ๋กœ์ง๊ณผ DLQ
โ€ข ์ˆ˜๋™ ์ปค๋ฐ‹๊ณผ ๋ฉฑ๋“ฑ์„ฑ
โ€ข ๊ทธ๋ ˆ์ด์Šคํ’€ ์…ง๋‹ค์šด

4. ๋ณด์•ˆ:
โ€ข ๋ฐ์ดํ„ฐ ๋งˆ์Šคํ‚น
โ€ข SSL/TLS ์•”ํ˜ธํ™”
โ€ข SASL ์ธ์ฆ

5. ๋ชจ๋‹ˆํ„ฐ๋ง:
โ€ข ๋ฉ”ํŠธ๋ฆญ ์ˆ˜์ง‘
โ€ข ์‹ค์‹œ๊ฐ„ ์•Œ๋ฆผ
โ€ข ์ด์ƒ ์ง•ํ›„ ๊ฐ์ง€

์ด์ œ ๋„ˆ๋„ ๋Œ€๊ทœ๋ชจ ๋กœ๊ทธ ์ฒ˜๋ฆฌ ์‹œ์Šคํ…œ์„ ๋งŒ๋“ค ์ˆ˜ ์žˆ์–ด! ๋ฌผ๋ก  ์ฒ˜์Œ์—๋Š” ์–ด๋ ค์šธ ์ˆ˜ ์žˆ์ง€๋งŒ, ํ•˜๋‚˜์”ฉ ์ฐจ๊ทผ์ฐจ๊ทผ ๊ตฌํ˜„ํ•˜๋‹ค ๋ณด๋ฉด ๊ธˆ๋ฐฉ ์ต์ˆ™ํ•ด์งˆ ๊ฑฐ์•ผ. ๐Ÿ’ช

ํ˜น์‹œ ์ด๋Ÿฐ ์‹œ์Šคํ…œ์„ ๊ตฌ์ถ•ํ•˜๋Š” ๋ฐ ๋„์›€์ด ํ•„์š”ํ•˜๋‹ค๋ฉด, ์žฌ๋Šฅ๋„ท์—์„œ ๊ด€๋ จ ์ „๋ฌธ๊ฐ€๋ฅผ ์ฐพ์•„๋ณผ ์ˆ˜๋„ ์žˆ์–ด. ์‹ค์ œ๋กœ ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์—์„œ ์šด์˜ํ•ด๋ณธ ๊ฒฝํ—˜์ด ์žˆ๋Š” ๊ฐœ๋ฐœ์ž๋“ค์˜ ์กฐ์–ธ์€ ์ •๋ง ๊ฐ’์ง„ ๊ฑฐ๋‹ˆ๊นŒ! ๐Ÿ˜Š

๋งˆ์ง€๋ง‰์œผ๋กœ ํ•œ ๊ฐ€์ง€ ํŒ์„ ๋” ์ฃผ์ž๋ฉด, ํ•ญ์ƒ ์ž‘๊ฒŒ ์‹œ์ž‘ํ•ด์„œ ์ ์ง„์ ์œผ๋กœ ํ™•์žฅํ•˜๋Š” ๊ฒŒ ์ข‹์•„. ์ฒ˜์Œ๋ถ€ํ„ฐ ์™„๋ฒฝํ•œ ์‹œ์Šคํ…œ์„ ๋งŒ๋“ค๋ ค๊ณ  ํ•˜์ง€ ๋ง๊ณ , ๊ธฐ๋ณธ์ ์ธ ๊ธฐ๋Šฅ๋ถ€ํ„ฐ ๊ตฌํ˜„ํ•˜๊ณ  ํ•„์š”์— ๋”ฐ๋ผ ๊ฐœ์„ ํ•ด๋‚˜๊ฐ€์ž!

๊ทธ๋Ÿผ ์ฆ๊ฑฐ์šด ์ฝ”๋”ฉ ๋˜๊ธธ ๋ฐ”๋ผ! ํ™”์ดํŒ…! ๐ŸŽ‰

์„ฑ๊ณต! ๋Œ€๊ทœ๋ชจ ๋กœ๊ทธ ์‹œ์Šคํ…œ ๊ตฌ์ถ• ์™„๋ฃŒ Producer ๋กœ๊ทธ ์ „์†ก Kafka ๋ฉ”์‹œ์ง€ ํ Consumer ๋กœ๊ทธ ์ฒ˜๋ฆฌ ๐ŸŽ‰ ์ถ•ํ•˜ํ•ฉ๋‹ˆ๋‹ค! ๐ŸŽ‰ ์ด์ œ ์—ฌ๋Ÿฌ๋ถ„๋„ ๋Œ€๊ทœ๋ชจ ์‹œ์Šคํ…œ์„ ๊ตฌ์ถ•ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค
๋Œ“๊ธ€ ์ž‘์„ฑ

์ด ๊ธ€์— ๋Œ€ํ•œ ์—ฌ๋Ÿฌ๋ถ„์˜ ์ƒ๊ฐ์„ ๋“ค๋ ค์ฃผ์„ธ์š”

๋Œ“๊ธ€ 0
๋Œ“๊ธ€์„ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘์ž…๋‹ˆ๋‹ค.

์•„์ง ๋Œ“๊ธ€์ด ์—†์Šต๋‹ˆ๋‹ค.