Kafka์—์„œ ์ˆ˜์‹ ๋œ ์ด๋ฒคํŠธ ์žฌ์ฒ˜๋ฆฌ

Kafka์—์„œ ์ˆ˜์‹ ๋œ ์ด๋ฒคํŠธ ์žฌ์ฒ˜๋ฆฌ

ํ—ค์ด ํ•˜๋ธŒ๋ฅด.

์ตœ๊ทผ์— ๋‚˜๋Š” ์ž์‹ ์˜ ๊ฒฝํ—˜์„ ๊ณต์œ ํ–ˆ๋‹ค Kafka ์ƒ์‚ฐ์ž ๋ฐ ์†Œ๋น„์ž๊ฐ€ ๋ณด์žฅ๋œ ์ „๋‹ฌ์— ๋” ๊ฐ€๊นŒ์›Œ์ง€๊ธฐ ์œ„ํ•ด ํŒ€์œผ๋กœ์„œ ๊ฐ€์žฅ ์ž์ฃผ ์‚ฌ์šฉํ•˜๋Š” ๋งค๊ฐœ๋ณ€์ˆ˜์— ๋Œ€ํ•ด ์„ค๋ช…ํ•ฉ๋‹ˆ๋‹ค. ์ด ๊ธฐ์‚ฌ์—์„œ๋Š” ์™ธ๋ถ€ ์‹œ์Šคํ…œ์„ ์ผ์‹œ์ ์œผ๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†์–ด Kafka์—์„œ ์ˆ˜์‹ ํ•œ ์ด๋ฒคํŠธ์˜ ์žฌ์ฒ˜๋ฆฌ๋ฅผ ์–ด๋–ป๊ฒŒ ๊ตฌ์„ฑํ–ˆ๋Š”์ง€ ์„ค๋ช…ํ•˜๊ณ  ์‹ถ์Šต๋‹ˆ๋‹ค.

์ตœ์‹  ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์€ ๋งค์šฐ ๋ณต์žกํ•œ ํ™˜๊ฒฝ์—์„œ ์ž‘๋™ํ•ฉ๋‹ˆ๋‹ค. ํ˜„๋Œ€ ๊ธฐ์ˆ  ์Šคํƒ์œผ๋กœ ๊ฐ์‹ธ์ธ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์€ Kubernetes ๋˜๋Š” OpenShift์™€ ๊ฐ™์€ ์˜ค์ผ€์ŠคํŠธ๋ ˆ์ดํ„ฐ๊ฐ€ ๊ด€๋ฆฌํ•˜๋Š” Docker ์ด๋ฏธ์ง€์—์„œ ์‹คํ–‰๋˜๋ฉฐ ๋ฌผ๋ฆฌ์  ๋ฐ ๊ฐ€์ƒ ๋ผ์šฐํ„ฐ ์ฒด์ธ์„ ํ†ตํ•ด ๋‹ค๋ฅธ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋˜๋Š” ์—”ํ„ฐํ”„๋ผ์ด์ฆˆ ์†”๋ฃจ์…˜๊ณผ ํ†ต์‹ ํ•ฉ๋‹ˆ๋‹ค. ์ด๋Ÿฌํ•œ ํ™˜๊ฒฝ์—์„œ๋Š” ํ•ญ์ƒ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ ์™ธ๋ถ€ ์‹œ์Šคํ…œ ์ค‘ ํ•˜๋‚˜๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†๋Š” ๊ฒฝ์šฐ ์ด๋ฒคํŠธ๋ฅผ ์žฌ์ฒ˜๋ฆฌํ•˜๋Š” ๊ฒƒ์ด ๋น„์ฆˆ๋‹ˆ์Šค ํ”„๋กœ์„ธ์Šค์˜ ์ค‘์š”ํ•œ ๋ถ€๋ถ„์ž…๋‹ˆ๋‹ค.

์นดํ”„์นด ์ด์ „์—๋Š” ์–ด๋• ๋‚˜์š”?

ํ”„๋กœ์ ํŠธ ์ดˆ๊ธฐ์—๋Š” ๋น„๋™๊ธฐ์‹ ๋ฉ”์‹œ์ง€ ์ „๋‹ฌ์„ ์œ„ํ•ด IBM MQ๋ฅผ ์‚ฌ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค. ์„œ๋น„์Šค ์ž‘์—… ์ค‘์— ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด ์ˆ˜์‹ ๋œ ๋ฉ”์‹œ์ง€๋Š” ์ถ”๊ฐ€ ์ˆ˜๋™ ๊ตฌ๋ฌธ ๋ถ„์„์„ ์œ„ํ•ด DLQ(๋ฐฐ๋‹ฌ ๋ชปํ•œ ํŽธ์ง€ ๋Œ€๊ธฐ์—ด)์— ๋ฐฐ์น˜๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. DLQ๋Š” ์ˆ˜์‹  ํ ์˜†์— ์ƒ์„ฑ๋˜์—ˆ์œผ๋ฉฐ ๋ฉ”์‹œ์ง€๋Š” IBM MQ ๋‚ด๋ถ€๋กœ ์ „์†ก๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

์˜ค๋ฅ˜๊ฐ€ ์ผ์‹œ์ ์ด๊ณ  ์ด๋ฅผ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋Š” ๊ฒฝ์šฐ(์˜ˆ: HTTP ํ˜ธ์ถœ์˜ ResourceAccessException ๋˜๋Š” MongoDb ์š”์ฒญ์˜ MongoTimeoutException) ์žฌ์‹œ๋„ ์ „๋žต์ด ์ ์šฉ๋ฉ๋‹ˆ๋‹ค. ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ๋ถ„๊ธฐ ๋…ผ๋ฆฌ์— ๊ด€๊ณ„์—†์ด ์›๋ž˜ ๋ฉ”์‹œ์ง€๋Š” ์ง€์—ฐ ์ „์†ก์„ ์œ„ํ•ด ์‹œ์Šคํ…œ ํ๋กœ ์ด๋™๋˜๊ฑฐ๋‚˜ ๋ฉ”์‹œ์ง€๋ฅผ ์žฌ์ „์†กํ•˜๊ธฐ ์œ„ํ•ด ์˜ค๋ž˜ ์ „์— ๋งŒ๋“ค์–ด์ง„ ๋ณ„๋„์˜ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์œผ๋กœ ์ด๋™๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์—ฌ๊ธฐ์—๋Š” ์ง€์—ฐ ๊ฐ„๊ฒฉ ๋˜๋Š” ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ˆ˜์ค€ ์ „๋žต์˜ ๋๊ณผ ์—ฐ๊ฒฐ๋œ ๋ฉ”์‹œ์ง€ ํ—ค๋”์˜ ์žฌ์ „์†ก ๋ฒˆํ˜ธ๊ฐ€ ํฌํ•จ๋ฉ๋‹ˆ๋‹ค. ์ „๋žต์ด ๋๋‚ฌ์ง€๋งŒ ์™ธ๋ถ€ ์‹œ์Šคํ…œ์„ ์—ฌ์ „ํžˆ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†๋Š” ๊ฒฝ์šฐ ์ˆ˜๋™ ๊ตฌ๋ฌธ ๋ถ„์„์„ ์œ„ํ•ด ๋ฉ”์‹œ์ง€๊ฐ€ DLQ์— ๋ฐฐ์น˜๋ฉ๋‹ˆ๋‹ค.

์†”๋ฃจ์…˜ ๊ฒ€์ƒ‰

์ธํ„ฐ๋„ท์—์„œ ๊ฒ€์ƒ‰ํ•˜๊ธฐ, ๋‹ค์Œ์„ ์ฐพ์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค ๊ฒฐ์ •. ์ฆ‰, ๊ฐ ์ง€์—ฐ ๊ฐ„๊ฒฉ์— ๋Œ€ํ•œ ์ฃผ์ œ๋ฅผ ์ƒ์„ฑํ•˜๊ณ  ํ•„์š”ํ•œ ์ง€์—ฐ ์‹œ๊ฐ„ ๋™์•ˆ ๋ฉ”์‹œ์ง€๋ฅผ ์ฝ๋Š” ์†Œ๋น„์ž ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ์ธก๋ฉด์—์„œ ๊ตฌํ˜„ํ•˜๋Š” ๊ฒƒ์ด ์ œ์•ˆ๋ฉ๋‹ˆ๋‹ค.

Kafka์—์„œ ์ˆ˜์‹ ๋œ ์ด๋ฒคํŠธ ์žฌ์ฒ˜๋ฆฌ

๋งŽ์€ ๊ธ์ •์ ์ธ ๋ฆฌ๋ทฐ์—๋„ ๋ถˆ๊ตฌํ•˜๊ณ  ์™„์ „ํžˆ ์„ฑ๊ณตํ•˜์ง€๋Š” ๋ชปํ•œ ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค. ์šฐ์„ , ๊ฐœ๋ฐœ์ž๋Š” ๋น„์ฆˆ๋‹ˆ์Šค ์š”๊ตฌ ์‚ฌํ•ญ์„ ๊ตฌํ˜„ํ•˜๋Š” ๊ฒƒ ์™ธ์—๋„ ์„ค๋ช…๋œ ๋ฉ”์ปค๋‹ˆ์ฆ˜์„ ๊ตฌํ˜„ํ•˜๋Š” ๋ฐ ๋งŽ์€ ์‹œ๊ฐ„์„ ์†Œ๋น„ํ•ด์•ผ ํ•˜๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.

๋˜ํ•œ Kafka ํด๋Ÿฌ์Šคํ„ฐ์—์„œ ์•ก์„ธ์Šค ์ œ์–ด๊ฐ€ ํ™œ์„ฑํ™”๋œ ๊ฒฝ์šฐ ์ฃผ์ œ๋ฅผ ์ƒ์„ฑํ•˜๊ณ  ํ•„์š”ํ•œ ์•ก์„ธ์Šค ๊ถŒํ•œ์„ ์ œ๊ณตํ•˜๋Š” ๋ฐ ์‹œ๊ฐ„์„ ํˆฌ์žํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๋˜ํ•œ ๋ฉ”์‹œ์ง€๊ฐ€ ์žฌ์ „์†ก๋  ์‹œ๊ฐ„์„ ๊ฐ–๊ณ  ๋ฉ”์‹œ์ง€์—์„œ ์‚ฌ๋ผ์ง€์ง€ ์•Š๋„๋ก ๊ฐ ์žฌ์‹œ๋„ ํ•ญ๋ชฉ์— ๋Œ€ํ•ด ์˜ฌ๋ฐ”๋ฅธtention.ms ๋งค๊ฐœ ๋ณ€์ˆ˜๋ฅผ ์„ ํƒํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๊ธฐ์กด ์„œ๋น„์Šค๋‚˜ ์‹ ๊ทœ ์„œ๋น„์Šค ๊ฐ๊ฐ์— ๋Œ€ํ•ด ์•ก์„ธ์Šค ๊ตฌํ˜„ ๋ฐ ์š”์ฒญ์„ ๋ฐ˜๋ณตํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

์ด์ œ ๋ฉ”์‹œ์ง€ ์žฌ์ฒ˜๋ฆฌ๋ฅผ ์œ„ํ•ด ์ผ๋ฐ˜์ ์œผ๋กœ spring-kafka๊ฐ€ ์ œ๊ณตํ•˜๋Š” ๋ฉ”์ปค๋‹ˆ์ฆ˜์ด ๋ฌด์—‡์ธ์ง€ ์‚ดํŽด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค. Spring-kafka๋Š” ๋‹ค์–‘ํ•œ BackOffPolicy๋ฅผ ๊ด€๋ฆฌํ•˜๊ธฐ ์œ„ํ•œ ์ถ”์ƒํ™”๋ฅผ ์ œ๊ณตํ•˜๋Š” spring-retry์— ๋Œ€ํ•œ ์ „์ด์  ์ข…์†์„ฑ์„ ๊ฐ€์ง€๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋Š” ์ƒ๋‹นํžˆ ์œ ์—ฐํ•œ ๋„๊ตฌ์ด์ง€๋งŒ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋ฉ”๋ชจ๋ฆฌ์— ์žฌ์ „์†กํ•˜๊ธฐ ์œ„ํ•ด ๋ฉ”์‹œ์ง€๋ฅผ ์ €์žฅํ•œ๋‹ค๋Š” ๊ฒƒ์ด ์ค‘์š”ํ•œ ๋‹จ์ ์ž…๋‹ˆ๋‹ค. ์ฆ‰, ์—…๋ฐ์ดํŠธ๋‚˜ ์ž‘์—… ์˜ค๋ฅ˜๋กœ ์ธํ•ด ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ๋‹ค์‹œ ์‹œ์ž‘ํ•˜๋ฉด ์žฌ์ฒ˜๋ฆฌ๋ฅผ ๋ณด๋ฅ˜ ์ค‘์ธ ๋ชจ๋“  ๋ฉ”์‹œ์ง€๊ฐ€ ์†์‹ค๋ฉ๋‹ˆ๋‹ค. ์ด ์ ์€ ์šฐ๋ฆฌ ์‹œ์Šคํ…œ์— ๋งค์šฐ ์ค‘์š”ํ•˜๋ฏ€๋กœ ๋” ์ด์ƒ ๊ณ ๋ คํ•˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.

spring-kafka ์ž์ฒด๋Š” ContainerAwareErrorHandler์˜ ์—ฌ๋Ÿฌ ๊ตฌํ˜„์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด SeekToCurrentErrorHandler, ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•œ ๊ฒฝ์šฐ ์˜คํ”„์…‹์„ ์ด๋™ํ•˜์ง€ ์•Š๊ณ  ๋‚˜์ค‘์— ๋ฉ”์‹œ์ง€๋ฅผ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. spring-kafka 2.3 ๋ฒ„์ „๋ถ€ํ„ฐ BackOffPolicy ์„ค์ •์ด ๊ฐ€๋Šฅํ•ด์กŒ๋‹ค.

์ด ์ ‘๊ทผ ๋ฐฉ์‹์„ ์‚ฌ์šฉํ•˜๋ฉด ์žฌ์ฒ˜๋ฆฌ๋œ ๋ฉ”์‹œ์ง€๊ฐ€ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ๋‹ค์‹œ ์‹œ์ž‘ํ•ด๋„ ์‚ด์•„๋‚จ์„ ์ˆ˜ ์žˆ์ง€๋งŒ ์•„์ง DLQ ๋ฉ”์ปค๋‹ˆ์ฆ˜์€ ์—†์Šต๋‹ˆ๋‹ค. ์šฐ๋ฆฌ๋Š” 2019๋…„ ์ดˆ์— DLQ๊ฐ€ ํ•„์š”ํ•˜์ง€ ์•Š์„ ๊ฒƒ์ด๋ผ๊ณ  ๋‚™๊ด€์ ์œผ๋กœ ๋ฏฟ๊ณ  ์ด ์˜ต์…˜์„ ์„ ํƒํ–ˆ์Šต๋‹ˆ๋‹ค(์šด์ด ์ข‹์•˜๊ณ  ์ด๋Ÿฌํ•œ ์žฌ์ฒ˜๋ฆฌ ์‹œ์Šคํ…œ์œผ๋กœ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ๋ช‡ ๋‹ฌ ๋™์•ˆ ์šด์˜ํ•œ ํ›„์—๋„ ์‹ค์ œ๋กœ๋Š” ํ•„์š”ํ•˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค). ์ผ์‹œ์ ์ธ ์˜ค๋ฅ˜๋กœ ์ธํ•ด SeekToCurrentErrorHandler๊ฐ€ ์‹คํ–‰๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ๋‚˜๋จธ์ง€ ์˜ค๋ฅ˜๋Š” ๋กœ๊ทธ์— ์ธ์‡„๋˜์–ด ์˜คํ”„์…‹์ด ๋ฐœ์ƒํ•˜๊ณ  ๋‹ค์Œ ๋ฉ”์‹œ์ง€์—์„œ ์ฒ˜๋ฆฌ๊ฐ€ ๊ณ„์†๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

์ตœ์ข… ๊ฒฐ์ •

SeekToCurrentErrorHandler๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ํ•œ ๊ตฌํ˜„์„ ํ†ตํ•ด ๋ฉ”์‹œ์ง€ ์žฌ์ „์†ก์„ ์œ„ํ•œ ์ž์ฒด ๋ฉ”์ปค๋‹ˆ์ฆ˜์„ ๊ฐœ๋ฐœํ•˜๊ฒŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

์šฐ์„  ๊ธฐ์กด ๊ฒฝํ—˜์„ ํ™œ์šฉํ•˜๊ณ  ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋กœ์ง์— ๋”ฐ๋ผ ํ™•์žฅํ•˜๊ณ  ์‹ถ์—ˆ์Šต๋‹ˆ๋‹ค. ์„ ํ˜• ๋…ผ๋ฆฌ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ๊ฒฝ์šฐ ์žฌ์‹œ๋„ ์ „๋žต์— ์ง€์ •๋œ ์งง์€ ๊ธฐ๊ฐ„ ๋™์•ˆ ์ƒˆ ๋ฉ”์‹œ์ง€ ์ฝ๊ธฐ๋ฅผ ์ค‘์ง€ํ•˜๋Š” ๊ฒƒ์ด ๊ฐ€์žฅ ์ข‹์Šต๋‹ˆ๋‹ค. ๋‹ค๋ฅธ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ๊ฒฝ์šฐ ์žฌ์‹œ๋„ ์ „๋žต์„ ์‹œํ–‰ํ•˜๋Š” ๋‹จ์ผ ์ง€์ ์„ ๊ฐ–๊ณ  ์‹ถ์—ˆ์Šต๋‹ˆ๋‹ค. ๋˜ํ•œ ์ด ๋‹จ์ผ ์ง€์ ์—๋Š” ๋‘ ์ ‘๊ทผ ๋ฐฉ์‹ ๋ชจ๋‘์— ๋Œ€ํ•œ DLQ ๊ธฐ๋Šฅ์ด ์žˆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

์žฌ์‹œ๋„ ์ „๋žต ์ž์ฒด๋Š” ์ผ์‹œ์ ์ธ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•  ๋•Œ ๋‹ค์Œ ๊ฐ„๊ฒฉ์„ ๊ฒ€์ƒ‰ํ•˜๋Š” ์—ญํ• ์„ ํ•˜๋Š” ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์— ์ €์žฅ๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

์„ ํ˜• ๋…ผ๋ฆฌ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์— ๋Œ€ํ•œ ์†Œ๋น„์ž ์ค‘์ง€

spring-kafka๋กœ ์ž‘์—…ํ•  ๋•Œ ์†Œ๋น„์ž๋ฅผ ์ค‘์ง€ํ•˜๋Š” ์ฝ”๋“œ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

public void pauseListenerContainer(MessageListenerContainer listenerContainer, 
                                   Instant retryAt) {
        if (nonNull(retryAt) && listenerContainer.isRunning()) {
            listenerContainer.stop();
            taskScheduler.schedule(() -> listenerContainer.start(), retryAt);
            return;
        }
        // to DLQ
    }

์˜ˆ์ œ์—์„œ retryAt๋Š” MessageListenerContainer๊ฐ€ ์•„์ง ์‹คํ–‰ ์ค‘์ธ ๊ฒฝ์šฐ ์ด๋ฅผ ๋‹ค์‹œ ์‹œ์ž‘ํ•˜๋Š” ์‹œ๊ฐ„์ž…๋‹ˆ๋‹ค. ๋‹ค์‹œ ์‹œ์ž‘์€ TaskScheduler์—์„œ ์‹œ์ž‘๋œ ๋ณ„๋„์˜ ์Šค๋ ˆ๋“œ์—์„œ ๋ฐœ์ƒํ•˜๋ฉฐ ๊ทธ ๊ตฌํ˜„๋„ Spring์—์„œ ์ œ๊ณต๋ฉ๋‹ˆ๋‹ค.

๋‹ค์Œ๊ณผ ๊ฐ™์€ ๋ฐฉ๋ฒ•์œผ๋กœ retryAt ๊ฐ’์„ ์ฐพ์Šต๋‹ˆ๋‹ค.

  1. ๋ฆฌ์ฝœ ์นด์šดํ„ฐ์˜ ๊ฐ’์„ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค.
  2. ์นด์šดํ„ฐ ๊ฐ’์„ ๊ธฐ์ค€์œผ๋กœ ์žฌ์‹œ๋„ ์ „๋žต์˜ ํ˜„์žฌ ์ง€์—ฐ ๊ฐ„๊ฒฉ์„ ๊ฒ€์ƒ‰ํ•ฉ๋‹ˆ๋‹ค. ์ „๋žต์€ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ž์ฒด์—์„œ ์„ ์–ธ๋˜๋ฉฐ ์ด๋ฅผ ์ €์žฅํ•˜๊ธฐ ์œ„ํ•ด JSON ํ˜•์‹์„ ์„ ํƒํ–ˆ์Šต๋‹ˆ๋‹ค.
  3. JSON ๋ฐฐ์—ด์— ์žˆ๋Š” ๊ฐ„๊ฒฉ์—๋Š” ์ฒ˜๋ฆฌ๋ฅผ ๋ฐ˜๋ณตํ•ด์•ผ ํ•˜๋Š” ์‹œ๊ฐ„(์ดˆ)์ด ํฌํ•จ๋ฉ๋‹ˆ๋‹ค. ์ด ์ดˆ ์ˆ˜๋Š” ํ˜„์žฌ ์‹œ๊ฐ„์— ์ถ”๊ฐ€๋˜์–ด retryAt ๊ฐ’์„ ํ˜•์„ฑํ•ฉ๋‹ˆ๋‹ค.
  4. ๊ฐ„๊ฒฉ์„ ์ฐพ์„ ์ˆ˜ ์—†์œผ๋ฉด retryAt ๊ฐ’์€ null์ด๊ณ  ๋ฉ”์‹œ์ง€๋Š” ์ˆ˜๋™ ๊ตฌ๋ฌธ ๋ถ„์„์„ ์œ„ํ•ด DLQ๋กœ ์ „์†ก๋ฉ๋‹ˆ๋‹ค.

์ด ์ ‘๊ทผ ๋ฐฉ์‹์„ ์‚ฌ์šฉํ•˜๋ฉด ํ˜„์žฌ ์ฒ˜๋ฆฌ ์ค‘์ธ ๊ฐ ๋ฉ”์‹œ์ง€์— ๋Œ€ํ•œ ๋ฐ˜๋ณต ํ˜ธ์ถœ ํšŸ์ˆ˜(์˜ˆ: ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋ฉ”๋ชจ๋ฆฌ)๋ฅผ ์ €์žฅํ•˜๋Š” ๊ฒƒ๋งŒ ๋‚จ์Šต๋‹ˆ๋‹ค. ์„ ํ˜• ๋…ผ๋ฆฌ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์€ ์ „์ฒด ์ฒ˜๋ฆฌ๋ฅผ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์—†๊ธฐ ๋•Œ๋ฌธ์— ์ด ์ ‘๊ทผ ๋ฐฉ์‹์—์„œ๋Š” ์žฌ์‹œ๋„ ํšŸ์ˆ˜๋ฅผ ๋ฉ”๋ชจ๋ฆฌ์— ์œ ์ง€ํ•˜๋Š” ๊ฒƒ์ด ์ค‘์š”ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. spring-retry์™€ ๋‹ฌ๋ฆฌ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ๋‹ค์‹œ ์‹œ์ž‘ํ•˜๋ฉด ๋ชจ๋“  ๋ฉ”์‹œ์ง€๊ฐ€ ์†์‹ค๋˜์–ด ์žฌ์ฒ˜๋ฆฌ๋˜์ง€ ์•Š๊ณ  ๋‹จ์ˆœํžˆ ์ „๋žต์ด ๋‹ค์‹œ ์‹œ์ž‘๋ฉ๋‹ˆ๋‹ค.

์ด ์ ‘๊ทผ ๋ฐฉ์‹์€ ๋งค์šฐ ๋†’์€ ๋กœ๋“œ๋กœ ์ธํ•ด ์‚ฌ์šฉํ•  ์ˆ˜ ์—†๋Š” ์™ธ๋ถ€ ์‹œ์Šคํ…œ์˜ ๋กœ๋“œ๋ฅผ ์ค„์ด๋Š” ๋ฐ ๋„์›€์ด ๋ฉ๋‹ˆ๋‹ค. ์ฆ‰, ์žฌ์ฒ˜๋ฆฌ ์™ธ์—๋„ ํŒจํ„ด ๊ตฌํ˜„์„ ๋‹ฌ์„ฑํ–ˆ์Šต๋‹ˆ๋‹ค. ํšŒ๋กœ ์ฐจ๋‹จ๊ธฐ.

์šฐ๋ฆฌ์˜ ๊ฒฝ์šฐ ์˜ค๋ฅ˜ ์ž„๊ณ„๊ฐ’์€ 1์— ๋ถˆ๊ณผํ•˜๋ฉฐ ์ผ์‹œ์ ์ธ ๋„คํŠธ์›Œํฌ ์ค‘๋‹จ์œผ๋กœ ์ธํ•œ ์‹œ์Šคํ…œ ๊ฐ€๋™ ์ค‘์ง€ ์‹œ๊ฐ„์„ ์ตœ์†Œํ™”ํ•˜๊ธฐ ์œ„ํ•ด ์งง์€ ๋Œ€๊ธฐ ์‹œ๊ฐ„ ๊ฐ„๊ฒฉ์œผ๋กœ ๋งค์šฐ ์„ธ๋ถ„ํ™”๋œ ์žฌ์‹œ๋„ ์ „๋žต์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. ์ด๋Š” ๋ชจ๋“  ๊ทธ๋ฃน ์ ์šฉ์— ์ ํ•ฉํ•˜์ง€ ์•Š์„ ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ ์‹œ์Šคํ…œ ํŠน์„ฑ์— ๋”ฐ๋ผ ์˜ค๋ฅ˜ ์ž„๊ณ„๊ฐ’๊ณผ ๊ฐ„๊ฒฉ ๊ฐ’ ๊ฐ„์˜ ๊ด€๊ณ„๋ฅผ ์„ ํƒํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

๋น„๊ฒฐ์ •์  ๋…ผ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ๋ฉ”์‹œ์ง€๋ฅผ ์ฒ˜๋ฆฌํ•˜๊ธฐ ์œ„ํ•œ ๋ณ„๋„์˜ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜

๋‹ค์Œ์€ RETRY_AT ์‹œ๊ฐ„์— ๋„๋‹ฌํ•˜๋ฉด DESTINATION ์ฃผ์ œ๋กœ ๋‹ค์‹œ ๋ณด๋‚ด๋Š” ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜(Retryer)์— ๋ฉ”์‹œ์ง€๋ฅผ ๋ณด๋‚ด๋Š” ์ฝ”๋“œ์˜ ์˜ˆ์ž…๋‹ˆ๋‹ค.


public <K, V> void retry(ConsumerRecord<K, V> record, String retryToTopic, 
                         Instant retryAt, String counter, String groupId, Exception e) {
        Headers headers = ofNullable(record.headers()).orElse(new RecordHeaders());
        List<Header> arrayOfHeaders = 
            new ArrayList<>(Arrays.asList(headers.toArray()));
        updateHeader(arrayOfHeaders, GROUP_ID, groupId::getBytes);
        updateHeader(arrayOfHeaders, DESTINATION, retryToTopic::getBytes);
        updateHeader(arrayOfHeaders, ORIGINAL_PARTITION, 
                     () -> Integer.toString(record.partition()).getBytes());
        if (nonNull(retryAt)) {
            updateHeader(arrayOfHeaders, COUNTER, counter::getBytes);
            updateHeader(arrayOfHeaders, SEND_TO, "retry"::getBytes);
            updateHeader(arrayOfHeaders, RETRY_AT, retryAt.toString()::getBytes);
        } else {
            updateHeader(arrayOfHeaders, REASON, 
                         ExceptionUtils.getStackTrace(e)::getBytes);
            updateHeader(arrayOfHeaders, SEND_TO, "backout"::getBytes);
        }
        ProducerRecord<K, V> messageToSend =
            new ProducerRecord<>(retryTopic, null, null, record.key(), record.value(), arrayOfHeaders);
        kafkaTemplate.send(messageToSend);
    }

์ด ์˜ˆ์—์„œ๋Š” ํ—ค๋”๋ฅผ ํ†ตํ•ด ๋งŽ์€ ์ •๋ณด๊ฐ€ ์ „์†ก๋˜๋Š” ๊ฒƒ์„ ๋ณด์—ฌ์ค๋‹ˆ๋‹ค. RETRY_AT ๊ฐ’์€ ์†Œ๋น„์ž ์ค‘์ง€๋ฅผ ํ†ตํ•œ ์žฌ์‹œ๋„ ๋ฉ”์ปค๋‹ˆ์ฆ˜๊ณผ ๋™์ผํ•œ ๋ฐฉ์‹์œผ๋กœ ๊ฒ€์ƒ‰๋ฉ๋‹ˆ๋‹ค. DESTINATION ๋ฐ RETRY_AT ์™ธ์—๋„ ๋‹ค์Œ์„ ์ „๋‹ฌํ•ฉ๋‹ˆ๋‹ค.

  • GROUP_ID - ์ˆ˜๋™ ๋ถ„์„ ๋ฐ ๋‹จ์ˆœํ™”๋œ ๊ฒ€์ƒ‰์„ ์œ„ํ•ด ๋ฉ”์‹œ์ง€๋ฅผ ๊ทธ๋ฃนํ™”ํ•ฉ๋‹ˆ๋‹ค.
  • ORIGINAL_PARTITION์€ ์žฌ์ฒ˜๋ฆฌ๋ฅผ ์œ„ํ•ด ๋™์ผํ•œ ์†Œ๋น„์ž๋ฅผ ์œ ์ง€ํ•˜๋ ค๊ณ  ํ•ฉ๋‹ˆ๋‹ค. ์ด ๋งค๊ฐœ๋ณ€์ˆ˜๋Š” null์ผ ์ˆ˜ ์žˆ์œผ๋ฉฐ, ์ด ๊ฒฝ์šฐ ์›๋ณธ ๋ฉ”์‹œ์ง€์˜ Record.key() ํ‚ค๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ƒˆ ํŒŒํ‹ฐ์…˜์„ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค.
  • ์žฌ์‹œ๋„ ์ „๋žต์„ ๋”ฐ๋ฅด๋„๋ก COUNTER ๊ฐ’์„ ์—…๋ฐ์ดํŠธํ–ˆ์Šต๋‹ˆ๋‹ค.
  • SEND_TO๋Š” ๋ฉ”์‹œ์ง€๊ฐ€ RETRY_AT์— ๋„๋‹ฌํ•œ ํ›„ ์žฌ์ฒ˜๋ฆฌ๋ฅผ ์œ„ํ•ด ์ „์†ก๋˜๋Š”์ง€ ์•„๋‹ˆ๋ฉด DLQ์— ๋ฐฐ์น˜๋˜๋Š”์ง€๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ์ƒ์ˆ˜์ž…๋‹ˆ๋‹ค.
  • REASON - ๋ฉ”์‹œ์ง€ ์ฒ˜๋ฆฌ๊ฐ€ ์ค‘๋‹จ๋œ ์ด์œ ์ž…๋‹ˆ๋‹ค.

Retryer๋Š” PostgreSQL์—์„œ ์žฌ์ „์†ก ๋ฐ ์ˆ˜๋™ ๊ตฌ๋ฌธ ๋ถ„์„์„ ์œ„ํ•ด ๋ฉ”์‹œ์ง€๋ฅผ ์ €์žฅํ•ฉ๋‹ˆ๋‹ค. ํƒ€์ด๋จธ๋Š” RETRY_AT๊ฐ€ ์žˆ๋Š” ๋ฉ”์‹œ์ง€๋ฅผ ์ฐพ๋Š” ์ž‘์—…์„ ์‹œ์ž‘ํ•˜๊ณ  ์ด๋ฅผ Record.key() ํ‚ค๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ DESTINATION ์ฃผ์ œ์˜ ORIGINAL_PARTITION ํŒŒํ‹ฐ์…˜์œผ๋กœ ๋‹ค์‹œ ๋ณด๋ƒ…๋‹ˆ๋‹ค.

๋ฉ”์‹œ์ง€๊ฐ€ ์ „์†ก๋˜๋ฉด PostgreSQL์—์„œ ์‚ญ์ œ๋ฉ๋‹ˆ๋‹ค. ๋ฉ”์‹œ์ง€์˜ ์ˆ˜๋™ ๊ตฌ๋ฌธ ๋ถ„์„์€ REST API๋ฅผ ํ†ตํ•ด Retryer์™€ ์ƒํ˜ธ ์ž‘์šฉํ•˜๋Š” ๊ฐ„๋‹จํ•œ UI์—์„œ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค. ์ฃผ์š” ๊ธฐ๋Šฅ์€ DLQ์—์„œ ๋ฉ”์‹œ์ง€ ์žฌ์ „์†ก ๋˜๋Š” ์‚ญ์ œ, ์˜ค๋ฅ˜ ์ •๋ณด ๋ณด๊ธฐ, ์˜ค๋ฅ˜ ์ด๋ฆ„ ๋“ฑ์œผ๋กœ ๋ฉ”์‹œ์ง€ ๊ฒ€์ƒ‰ ๋“ฑ์ž…๋‹ˆ๋‹ค.

์šฐ๋ฆฌ ํด๋Ÿฌ์Šคํ„ฐ์—๋Š” ์ ‘๊ทผ ์ œ์–ด๊ฐ€ ํ™œ์„ฑํ™”๋˜์–ด ์žˆ์œผ๋ฏ€๋กœ Retryer๊ฐ€ ์ฒญ์ทจ ์ค‘์ธ ์ฃผ์ œ์— ๋Œ€ํ•œ ์ ‘๊ทผ์„ ์ถ”๊ฐ€๋กœ ์š”์ฒญํ•˜๊ณ , Retryer๊ฐ€ DESTINATION ์ฃผ์ œ์— ์“ธ ์ˆ˜ ์žˆ๋„๋ก ํ—ˆ์šฉํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์ด๊ฒƒ์ด ๋ถˆํŽธํ•˜์ง€๋งŒ ๊ฐ„๊ฒฉ์ฃผ์ œ ์ ‘๊ทผ๋ฐฉ์‹๊ณผ ๋‹ฌ๋ฆฌ ์ด๋ฅผ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ๋Š” ๋ณธ๊ฒฉ์ ์ธ DLQ์™€ UI๋ฅผ ๊ฐ–์ถ”๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์ด ์„œ๋กœ ๋‹ค๋ฅธ ๋กœ์ง์„ ๊ตฌํ˜„ํ•˜๋Š” ์—ฌ๋Ÿฌ ๋‹ค๋ฅธ ์†Œ๋น„์ž ๊ทธ๋ฃน์ด ์ˆ˜์‹  ์ฃผ์ œ๋ฅผ ์ฝ๋Š” ๊ฒฝ์šฐ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋Ÿฌํ•œ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ค‘ ํ•˜๋‚˜์— ๋Œ€ํ•ด Retryer๋ฅผ ํ†ตํ•ด ๋ฉ”์‹œ์ง€๋ฅผ ์žฌ์ฒ˜๋ฆฌํ•˜๋ฉด ๋‹ค๋ฅธ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์—๋„ ์ค‘๋ณต์ด ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค. ์ด๋ฅผ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•ด ์žฌ์ฒ˜๋ฆฌ๋ฅผ ์œ„ํ•œ ๋ณ„๋„์˜ ์ฃผ์ œ๋ฅผ ๋งŒ๋“ญ๋‹ˆ๋‹ค. ์ˆ˜์‹  ๋ฐ ์žฌ์‹œ๋„ ์ฃผ์ œ๋Š” ์•„๋ฌด๋Ÿฐ ์ œํ•œ ์—†์ด ๋™์ผํ•œ ์†Œ๋น„์ž๊ฐ€ ์ฝ์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Kafka์—์„œ ์ˆ˜์‹ ๋œ ์ด๋ฒคํŠธ ์žฌ์ฒ˜๋ฆฌ

๊ธฐ๋ณธ์ ์œผ๋กœ ์ด ์ ‘๊ทผ ๋ฐฉ์‹์€ ํšŒ๋กœ ์ฐจ๋‹จ๊ธฐ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•˜์ง€ ์•Š์ง€๋งŒ ๋‹ค์Œ์„ ์‚ฌ์šฉํ•˜์—ฌ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์— ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋ด„-ํด๋ผ์šฐ๋“œ-๋„ทํ”Œ๋ฆญ์Šค ๋˜๋Š” ์ƒˆ๋กœ์šด ์Šคํ”„๋ง ํด๋ผ์šฐ๋“œ ํšŒ๋กœ ์ฐจ๋‹จ๊ธฐ, ์™ธ๋ถ€ ์„œ๋น„์Šค๊ฐ€ ํ˜ธ์ถœ๋˜๋Š” ์žฅ์†Œ๋ฅผ ์ ์ ˆํ•œ ์ถ”์ƒํ™”๋กœ ๋ž˜ํ•‘ํ•ฉ๋‹ˆ๋‹ค. ๋˜ํ•œ, ์ „๋žต์„ ์„ ํƒํ•˜๋Š” ๊ฒƒ์ด ๊ฐ€๋Šฅํ•ด์ง‘๋‹ˆ๋‹ค. ์นธ๋ง‰์ด ํŒจํ„ด๋„ ์œ ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด spring-cloud-netflix์—์„œ๋Š” ์Šค๋ ˆ๋“œ ํ’€์ด๋‚˜ ์„ธ๋งˆํฌ์–ด๊ฐ€ ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ถœ๋ ฅ

๊ฒฐ๊ณผ์ ์œผ๋กœ, ์™ธ๋ถ€ ์‹œ์Šคํ…œ์„ ์ผ์‹œ์ ์œผ๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†๋Š” ๊ฒฝ์šฐ ๋ฉ”์‹œ์ง€ ์ฒ˜๋ฆฌ๋ฅผ ๋ฐ˜๋ณตํ•  ์ˆ˜ ์žˆ๋Š” ๋ณ„๋„์˜ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์ด ์žˆ์Šต๋‹ˆ๋‹ค.

์ด ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ์ฃผ์š” ์žฅ์  ์ค‘ ํ•˜๋‚˜๋Š” ๋™์ผํ•œ Kafka ํด๋Ÿฌ์Šคํ„ฐ์—์„œ ์‹คํ–‰๋˜๋Š” ์™ธ๋ถ€ ์‹œ์Šคํ…œ์—์„œ ์ƒ๋‹นํ•œ ์ˆ˜์ • ์—†์ด ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค! ์ด๋Ÿฌํ•œ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์€ ์žฌ์‹œ๋„ ์ฃผ์ œ์— ์•ก์„ธ์Šคํ•˜๊ณ  Kafka ํ—ค๋” ๋ช‡ ๊ฐœ๋ฅผ ์ž…๋ ฅํ•œ ํ›„ ์žฌ์‹œ๋„์ž์—๊ฒŒ ๋ฉ”์‹œ์ง€๋ฅผ ๋ณด๋‚ด๊ธฐ๋งŒ ํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค. ์ถ”๊ฐ€์ ์ธ ์ธํ”„๋ผ๋ฅผ ๊ตฌ์ถ•ํ•  ํ•„์š”๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์—์„œ Retryer๋กœ ์ „์†ก๋˜๋Š” ๋ฉ”์‹œ์ง€ ์ˆ˜๋ฅผ ์ค„์ด๊ธฐ ์œ„ํ•ด ์„ ํ˜• ๋กœ์ง์œผ๋กœ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ์‹๋ณ„ํ•˜๊ณ  Consumer stop์„ ํ†ตํ•ด ์žฌ์ฒ˜๋ฆฌํ–ˆ์Šต๋‹ˆ๋‹ค.

์ถœ์ฒ˜ : habr.com

์ฝ”๋ฉ˜ํŠธ๋ฅผ ์ถ”๊ฐ€