BackEnd

์•ˆ์ „ํ•œ ์ด๋ฒคํŠธ ๋ฐœํ–‰ ๋ฐฉ๋ฒ•

ddonghyeo 2024. 11. 5. 20:41

0. ์‹œ์ž‘

์ด๋ฒคํŠธ๋Š” ๊ฐ•๊ฒฐํ•ฉ์„ ์ œ๊ฑฐํ•˜๊ฑฐ๋‚˜ ๋ฐ์ดํ„ฐ ๋™๊ธฐํ™” ๋“ฑ ์—ฌ๋Ÿฌ ์œ ์šฉํ•œ ์ƒํ™ฉ์—์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

 

ํ•˜์ง€๋งŒ, ์–ธ์ œ๋‚˜ ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ๋‹จ์ˆœํ•˜๊ฒŒ ์ด๋ฒคํŠธ๋ฅผ ๋ฐœํ–‰/๊ตฌ๋…์„ ๊ตฌํ˜„ํ•˜๋Š” ๊ฒƒ๋งŒ์œผ๋กœ ์•ˆ์ „ํ•˜์ง€ ์•Š๋‹ค.

 

์—ฌ๊ธฐ์„œ "์•ˆ์ „ํ•˜๋‹ค"๋Š” ์—ฌ๋Ÿฌ ์กฐ๊ฑด์ด ์žˆ์„ ์ˆ˜ ์žˆ๊ฒ ๋‹ค.

 

  • ๋ฉ”์„ธ์ง€ ๋ฐœํ–‰์ด ๋ณด์žฅ๋˜๋Š”์ง€
  • ๋ฐœํ–‰๋œ ๋ฉ”์„ธ์ง€๊ฐ€ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ „๋‹ฌ๋˜์—ˆ๋Š”์ง€
  • ์ „๋‹ฌ๋œ ๋ฉ”์„ธ์ง€๊ฐ€ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ฒ˜๋ฆฌ๋˜์—ˆ๋Š”์ง€
  • ์ค‘๋ณต ๋ฉ”์„ธ์ง€๊ฐ€ ์ค‘๋ณต ์ฒ˜๋ฆฌ๋˜์ง€ ์•Š์•˜๋Š”์ง€
  • (ํ•„์š”ํ•œ ๊ฒฝ์šฐ) ๋ฉ”์„ธ์ง€์˜ ์ˆœ์„œ๊ฐ€ ๋ณด์žฅ๋˜์—ˆ๋Š”์ง€

 

์ด๋ฒคํŠธ ๋ฐœํ–‰/๊ตฌ๋…์—์„œ ์‚ฌ์†Œํ•œ ์˜ค๋ฅ˜ ํ•˜๋‚˜๊ฐ€ ์‹ฌ๊ฐํ•œ ์˜ํ–ฅ์„ ๋ถˆ๋Ÿฌ์˜ฌ ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ์•ˆ์ „ํ•จ์€ ๋งค์šฐ ์ค‘์š”ํ•˜๋‹ค.

 

์˜ค๋Š˜์€ ์ด๋ฒคํŠธ ๋ฐœํ–‰ ๊ณผ์ •์—์„œ ๊ณ ๋ คํ•ด์•ผํ•  ์—ฌ๋Ÿฌ ์˜ˆ์™ธ ์ƒํ™ฉ์„ ์•Œ์•„๋ณด๊ณ , ๊ทธ์— ๋Œ€ํ•œ ๋Œ€์ฒ˜๋ฅผ ์•Œ์•„๋ณด๋„๋ก ํ•˜๊ฒ ๋‹ค.

 

1. ์˜ˆ์™ธ

๋จผ์ €, ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋Š” ์˜ˆ์™ธ๋“ค์„ ์‚ดํŽด๋ณด๊ฒ ๋‹ค.

 

๋„คํŠธ์›Œํฌ ๊ด€๋ จ ์˜ˆ์™ธ - ๋ธŒ๋กœ์ปค์™€์˜ ํ†ต์‹ ์—์„œ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•˜๋Š” ๊ฒฝ์šฐ

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

Kafka์—์„œ๋Š” ์•„๋ž˜์™€ ๊ฐ™์€ ์˜ˆ์™ธ๋“ค์ด ์žˆ๋‹ค.

  • TimeoutException: ๋ธŒ๋กœ์ปค ์‘๋‹ต ๋Œ€๊ธฐ ์‹œ๊ฐ„ ์ดˆ๊ณผ
  • SocketTimeoutException: ์†Œ์ผ“ ์—ฐ๊ฒฐ ์‹œ๊ฐ„ ์ดˆ๊ณผ
  • ConnectionException: ๋ธŒ๋กœ์ปค ์—ฐ๊ฒฐ ์‹คํŒจ
  • NetworkException: ์ผ๋ฐ˜์ ์ธ ๋„คํŠธ์›Œํฌ ์˜ค๋ฅ˜

ํ•œ ๋ฒˆ ๋ธŒ๋กœ์ปค์™€์˜ ํ†ต์‹ ์ด ์™„๋ฃŒ๋˜์—ˆ๋‹ค๊ณ  ์•ˆ์‹ฌํ•˜๋ฉด ์•ˆ ๋œ๋‹ค. ์–ธ์ œ๋“ ์ง€ ๋ธŒ๋กœ์ปค๋Š” ๋‹ค์šด๋  ์ˆ˜ ์žˆ๋‹ค๋Š” ์ƒ๊ฐ์„ ๊ฐ€์ง€๊ณ  ๊ตฌํ˜„ํ•ด์•ผ ํ•œ๋‹ค.

 

 

 

 

๋ฉ”์„ธ์ง€ ์ง๋ ฌํ™”/์—ญ์ง๋ ฌํ™” ์˜ˆ์™ธ

Kafka์—์„œ๋Š” ๋ฏธ๋ฆฌ ์ง๋ ฌํ™”/์—ญ์ง๋ ฌํ™”๋ฅผ ์œ„ํ•œ ํด๋ž˜์Šค๋ฅผ ์„ค์ •ํ•ด๋‘”๋‹ค.

props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, IntegerDeserializer.class);
props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
// ...
props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, IntegerSerializer.class);
props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class);

 

๋งŒ์•ฝ ์„ค์ •๊ณผ ๋งž์ง€ ์•Š๋Š” ์ด๋ฒคํŠธ๊ฐ€ ๋ฐœํ–‰๋˜๋ฉด ์˜ˆ์™ธ๋ฅผ ์ผ์œผํ‚ฌ ๊ฒƒ์ด๋‹ค.

 

์ด๋Š” Kafka๊ฐ€ ์•„๋‹Œ ๋‹ค๋ฅธ ํ”„๋ ˆ์ž„์›Œํฌ์—์„œ๋„ ์ผ์–ด๋‚  ์ˆ˜ ์žˆ๋‹ค.

 

 

 

๋ธŒ๋กœ์ปค ๊ด€๋ จ ์˜ˆ์™ธ

๋ธŒ๋กœ์ปค๊ฐ€ ํ•ญ์ƒ ๊ธฐ๋Œ€ํ•˜๋Š” ๋ฉ”์„ธ์ง€๋ฅผ ๋ฐ›๋Š”๋‹ค๊ณ  ์ƒ๊ฐํ•˜๋ฉด ์•ˆ ๋œ๋‹ค๊ณ  ์ƒ๊ฐํ•œ๋‹ค.

 

์•„๋ž˜๋Š” ๋ธŒ๋กœ์ปค์—์„œ ์ผ์–ด๋‚  ์ˆ˜ ์žˆ๋Š” ์—ฌ๋Ÿฌ ์˜ˆ์™ธ๋“ค์ด๋‹ค.

 

  • ํ† ํ”ฝ ์ด๋ฆ„์ด ์ž˜๋ชป๋จ
  • ํ† ํ”ฝ ์ ‘๊ทผ ๊ถŒํ•œ์ด ์—†์Œ
  • ๋ณต์ œ๋ณธ(Replica) ๋ถ€์กฑ
  • ๋ฆฌ๋” ๋…ธ๋“œ(Leader Node) ์‚ฌ์šฉ ๋ถˆ๊ฐ€

 

์œ„์— ๋Œ€ํ•œ ์˜ค๋ฅ˜๋“ค์„ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋‹จ์—์„œ ๊ฐ์ง€ํ•˜์—ฌ ์ฒ˜๋ฆฌํ•ด์•ผ ํ•  ๊ฒƒ์ด๋‹ค.

 

 

๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ์˜ˆ์™ธ

๋‹จ์ˆœํžˆ ์ด๋ฒคํŠธ ํ”„๋ ˆ์ž„์›Œํฌ์™€ ์ƒ๊ด€์—†์ด, ๋ฉ”์„ธ์ง€๋ฅผ ๋ฐ›๊ณ  ์ฒ˜๋ฆฌํ•˜๋Š” ๊ณผ์ •์—์„œ ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋‹ค.

 

์ด๋ฒคํŠธ ์ข…๋ฅ˜, ํ”„๋ ˆ์ž„์›Œํฌ ๋“ฑ์— ๋”ฐ๋ผ์„œ ์—ฌ๋Ÿฌ ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๊ณ , ๊ทธ์— ๋Œ€ํ•œ ๋Œ€์ฒ˜๋„ ๋‹ค๋ฅด๋‹ค.

 

  • ์œ ํšจ์„ฑ ๊ฒ€์ฆ ์‹คํŒจ
  • ์ด๋ฒคํŠธ ์ค‘๋ณต
  • ๋ฆฌ์†Œ์Šค ์—†์Œ

 

 

์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ข…๋ฃŒ

์‚ฌ์šฉ์ž์˜ ์š”์ฒญ์„ ์ฒ˜๋ฆฌํ•˜๋Š” ๊ณผ์ •์—์„œ ๊ฐ‘์ž‘์Šค๋Ÿฌ์šด ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ข…๋ฃŒ ๋˜ํ•œ ๊ณ ๋ คํ•ด์•ผํ•  ์‚ฌ์•ˆ์ด๋‹ค.

 

์ด๋ฒคํŠธ ๋ฐœํ–‰ ์ „/ ํ›„ ๋ชจ๋‘ ๊ณ ๋ คํ•ด์•ผ ํ•  ๊ฒƒ์ด๋‹ค.

 

 

์ด๋ฒคํŠธ ์ˆœ์„œ ๋ณด์žฅ

์„œ๋น„์Šค์— ๋”ฐ๋ผ์„œ ๋ฐœํ–‰๋˜๋Š” ์ด๋ฒคํŠธ์˜ ์ˆœ์„œ์— ๋”ฐ๋ผ ์ฒ˜๋ฆฌํ•ด์•ผํ•  ์ˆ˜๋„ ์žˆ๋‹ค. 

 

์ƒํ™ฉ์— ๋”ฐ๋ผ์„œ ์ด์ „ ์ด๋ฒคํŠธ๊ฐ€ ์™„๋ฃŒ๋œ ํ›„ ์‹คํ–‰ํ•ด์•ผ ํ•  ์ˆ˜๋„ ์žˆ๊ณ ,

 

ํ”„๋ ˆ์ž„์›Œํฌ์— ๋”ฐ๋ผ์„œ ์ด๋ฒคํŠธ ์ˆœ์„œ๊ฐ€ ๋ณด์žฅ๋˜์ง€ ์•Š๋Š” ๊ฒฝ์šฐ๋„ ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ์ด๋ฅผ ๊ณ ๋ คํ•ด์•ผ ํ•  ์ˆ˜๋„ ์žˆ๋‹ค.

 

 

์ด๋ฒคํŠธ ๋ฒ„์ „ ํ™•์ธ

์ƒํ™ฉ์— ๋”ฐ๋ผ ์ด๋ฒคํŠธ์˜ ๋ฒ„์ „์ด ๋ฐ”๋€Œ๋Š” ์‹œ์ ๋„ ๊ณ ๋ คํ•ด์•ผ ํ•œ๋‹ค.

@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
public interface Event {
    String getVersion();
    String getType();
}

 

์œ„์™€ ๊ฐ™์€ ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ํ†ตํ•ด ์ด๋ฒคํŠธ ๋ฒ„์ €๋‹์„ ํ•˜๊ณ , ๊ฐ ๋ฒ„์ „์— ๋”ฐ๋ผ ์ด๋ฒคํŠธ ํ”„๋กœ์„ธ์„œ๋ฅผ ๋ฐ”๊ฟ”์•ผ ํ•  ์ˆ˜๋„ ์žˆ๋‹ค.

 

 

2. ํ•ด๊ฒฐ

๊ทธ๋ ‡๋‹ค๋ฉด, ์œ„์˜ ์˜ˆ์™ธ๋“ค์„ ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ๋Š” ๋ฐฉ๋ฒ•๋“ค์ด ๋ฌด์—‡์ด ์žˆ์„์ง€ ์•Œ์•„๋ณด๊ฒ ๋‹ค.

 

2-1. ํŠธ๋žœ์žญ์…˜ ๋™๊ธฐํ™”

์ด๋ฒคํŠธ ๋ฐœํ–‰์˜ ์ค‘์š”ํ•œ ์ ์€ ์ด๋ฒคํŠธ ๋ฐœํ–‰ ํ•จ์ˆ˜์™€ ์ด๋ฒคํŠธ ์‹คํ–‰ ํ•จ์ˆ˜์˜ ํŠธ๋žœ์žญ์…˜์ด ๋ถ„๋ฆฌ๋˜์–ด์•ผ ํ•œ๋‹ค๋Š” ๊ฒƒ์ด๋‹ค.

 

๋งŒ์•ฝ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ฃผ๋ฌธ์„ ํ•˜๋ฉด ์ฃผ๋ฌธ์ด ์™„๋ฃŒ๋˜์—ˆ๋‹ค๋Š” ๋ฉ”์„ธ์ง€๋ฅผ ๋ณด๋‚ด๋Š” ๊ณผ์ •์ด ์žˆ๋‹ค๊ณ  ํ•ด๋ณด์ž.

 

๋งŒ์•ฝ ์ด ๊ณผ์ •์ด ํ•œ ํŠธ๋žœ์žญ์…˜์— ๋ฌถ์—ฌ์žˆ๋‹ค๋ฉด, ์—ฌ๊ธฐ์„œ ์ฃผ๋ฌธ ์™„๋ฃŒ ๋ฉ”์„ธ์ง€๋ฅผ ๋ณด๋‚ด๋Š” ๊ณผ์ •์ด ์‹คํŒจํ•˜๋ฉด ์–ด๋–ป๊ฒŒ ๋ ๊นŒ?

 

 

์ฃผ๋ฌธ ์™„๋ฃŒ ๋ฉ”์„ธ์ง€์—์„œ ๋ฐœ์ƒํ•œ ์˜ˆ์™ธ๊ฐ€ ํ˜ธ์ถœ ์Šคํƒ์„ ๋”ฐ๋ผ๊ฐ€์„œ ์ฃผ๋ฌธ์—๋„ ์˜ํ–ฅ์„ ๋ผ์ณ์„œ, ์ฃผ๋ฌธ๋„ ์‹คํŒจํ•˜๋Š” ์ƒํ™ฉ์ด ๋ฒŒ์–ด์งˆ ๊ฒƒ์ด๋‹ค.

 

๋”ฐ๋ผ์„œ, ํŠธ๋žœ์žญ์…˜์ด ๋ชจ๋‘ ์™„๋ฃŒ๋œ ํ›„์— ์ด๋ฒคํŠธ ๋ฐœํ–‰์„ ๋™๊ธฐํ™” ํ•ด์•ผํ•œ๋‹ค.

 

 

์ฃผ๋ฌธ ์™„๋ฃŒ ๋ฉ”์„ธ์ง€ ์ „์†ก์ด ์‹คํŒจํ•˜๋”๋ผ๋„ ์ฃผ๋ฌธ์€ ์™„๋ฃŒ๋˜์–ด์•ผ ํ•œ๋‹ค.

 

 

๋ชจ๋†€๋ฆฌ์‹์—์„œ ์‚ฌ์šฉํ•˜๋Š” Spring Application Event์—์„œ๋Š” @TranscationalEventListener๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

public class OrderEventHandler {
    private final EmailService emailService;

    // ํŠธ๋žœ์žญ์…˜ ์ปค๋ฐ‹ ํ›„ ์‹คํ–‰
    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    public void handleOrderCreated(OrderCreatedEvent event) {
        log.info("Order created: {}", event.getOrderId());
        emailService.sendOrderConfirmation(event.getOrderId());
    }
    
    // ํŠธ๋žœ์žญ์…˜ ๋กค๋ฐฑ ์‹œ ์‹คํ–‰
    @TransactionalEventListener(phase = TransactionPhase.AFTER_ROLLBACK)
    public void handleOrderFailed(OrderCreatedEvent event) {
        log.error("Order creation failed: {}", event.getOrderId());
        // ์‹คํŒจ ์ฒ˜๋ฆฌ ๋กœ์ง
    }
}

 

๊ฐ phase์— ๋”ฐ๋ผ ํŠธ๋žœ์žญ์…˜ ์„ฑ๊ณต/ ์‹คํŒจ ํ›„ ๋™์ž‘์„ ๋‚˜๋ˆŒ ์ˆ˜ ์žˆ๋‹ค.

 

 

 

2-2. TransactionalOutbox Pattern

 

TransactionalOutbox ํŒจํ„ด์€ ๋ฐœํ–‰ํ•œ ๋ฉ”์„ธ์ง€๋ฅผ ๋ธŒ๋กœ์ปค์— ์ „์†กํ•˜๋ฉด์„œ DB์— ์ €์žฅํ•ด๋‘๋Š” ๋ฐฉ๋ฒ•์ด๋‹ค.

 

Outbox๋Š” "๋ณด๋‚ธ ํŽธ์ง€ํ•จ"์„ ์˜๋ฏธํ•œ๋‹ค. 

 

https://microservices.io/patterns/data/transactional-outbox.html

 

 

Transactional Outbox Pattern์€ ๋ณดํ†ต ํฌ๊ฒŒ Polling & Publisher ๋ฐฉ๋ฒ•๊ณผ Transaction Log Tailing ๋ฐฉ๋ฒ•์œผ๋กœ ๋‚˜๋‰œ๋‹ค.

 

2-2-1. Poliing & Publisher

Polling์€ ์ผ์ •ํ•œ ์ฃผ๊ธฐ๋ฅผ ๊ฐ€์ง€๊ณ  ์„œ๋ฒ„์™€ ์‘๋‹ต์„ ์ฃผ๊ณ ๋ฐ›๋Š” ๋ฐฉ๋ฒ•์„ ๋งํ•œ๋‹ค.

 

๋ฐœํ–‰ํ•  ๋ฉ”์„ธ์ง€๋ฅผ Outbox DB์— ์ €์žฅํ•ด๋‘๊ณ , ์ฃผ๊ธฐ์ ์œผ๋กœ pollingํ•˜์—ฌ ์ด๋ฒคํŠธ๋ฅผ ๋ฐœํ–‰ํ•˜๋Š” ๋ฐฉ๋ฒ•์ด๋‹ค.

 

ํ•ด๋‹น ๋ฐฉ๋ฒ•์œผ๋กœ ์ด๋ฒคํŠธ ๋ฐœํ–‰์„ ๋ณด์žฅํ•  ์ˆ˜ ์žˆ๋‹ค.

 

์•„๋ž˜ ์˜ˆ์‹œ ๊ตฌํ˜„์œผ๋กœ ์‚ดํŽด๋ณด๊ฒ ๋‹ค.

 

@Entity
@Table(name = "outbox_events")
public class OutboxEvent {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private String aggregateType;    // "ORDER", "PAYMENT" ๋“ฑ
    private String aggregateId;      // ํ•ด๋‹น ๋„๋ฉ”์ธ ๊ฐ์ฒด์˜ ID
    private String eventType;        // "ORDER_CREATED", "PAYMENT_COMPLETED" ๋“ฑ
    private String payload;          // ์ด๋ฒคํŠธ ๋ฐ์ดํ„ฐ (JSON)
    
    @Enumerated(EnumType.STRING)
    private EventStatus status = EventStatus.PENDING;
    
    private int retryCount = 0;
    private LocalDateTime createdAt;
    private LocalDateTime processedAt;
    
    @Version
    private Long version;            // ๋‚™๊ด€์  ๋ฝ์„ ์œ„ํ•œ ๋ฒ„์ „
}

 

์œ„์™€ ๊ฐ™์ด Outbox DB์— ์ €์žฅํ•  ์ด๋ฒคํŠธ ๊ฐ์ฒด๋ฅผ ์ •์˜ํ•œ๋‹ค.

 

public void createOrder(OrderRequest request) {
    //์ฃผ๋ฌธ ์ƒ์„ฑ
    Order order = orderRepository.save(Order.from(request));

    //Outbox ์ด๋ฒคํŠธ ์ €์žฅ
    OutboxEvent event = OutboxEvent.builder()
    	//...
        .build();

    outboxRepository.save(event);
}

 

์ฃผ๋ฌธ์„ ์ƒ์„ฑํ•˜๋ฉด Outbox DB์— ์ด๋ฒคํŠธ๋ฅผ ์ €์žฅํ•œ๋‹ค.

 

@Component
@EnableScheduling
@Slf4j
public class OutboxEventPublisher {
    private final OutboxEventRepository outboxRepository;
    private final KafkaTemplate<String, String> kafkaTemplate;
    private final ObjectMapper objectMapper;
    
    @Scheduled(fixedRate = 1000) // 1์ดˆ๋งˆ๋‹ค ์‹คํ–‰
    @Transactional
    public void publishEvents() {
        List<OutboxEvent> events = outboxRepository
            .findByStatusAndRetryCountLessThan(
                EventStatus.PENDING, 
                3,
                PageRequest.of(0, 100)
            );
            
        for (OutboxEvent event : events) {
            try {
                // Kafka๋กœ ์ด๋ฒคํŠธ ๋ฐœํ–‰
                kafkaTemplate.send(
                    event.getAggregateType().toLowerCase(), 
                    event.getAggregateId(),
                    event.getPayload()
                ).get(10, TimeUnit.SECONDS); // ํƒ€์ž„์•„์›ƒ ์„ค์ •
                
                // ๋ฐœํ–‰ ์„ฑ๊ณต ์‹œ ์ƒํƒœ ์—…๋ฐ์ดํŠธ
                event.setStatus(EventStatus.PUBLISHED);
                event.setProcessedAt(LocalDateTime.now());
                outboxRepository.save(event);
                
            } catch (Exception e) {
                log.error("Failed to publish event: {}", event.getId(), e);
                
                // ์žฌ์‹œ๋„ ํšŸ์ˆ˜ ์ฆ๊ฐ€
                event.incrementRetryCount();
                
                // ์ตœ๋Œ€ ์žฌ์‹œ๋„ ํšŸ์ˆ˜ ์ดˆ๊ณผ ์‹œ
                if (event.getRetryCount() >= 3) {
                    event.setStatus(EventStatus.FAILED);
                }
                
                outboxRepository.save(event);
            }
        }
    }
}

 

์ด์ œ ์ผ์ • ์ฃผ๊ธฐ๋งˆ๋‹ค OutboxDB๋กœ๋ถ€ํ„ฐ ๋ฉ”์„ธ์ง€๋ฅผ Pollingํ•ด์„œ ์ด๋ฒคํŠธ๋ฅผ ๋ฐœํ–‰ํ•œ๋‹ค.

 

์ด๋ฒคํŠธ์˜ ๋ฐœํ–‰์ด ์™„๋ฃŒ๋˜๋ฉด status๋ฅผ ๋ฐ”๊พธ๋Š” ๋ฐฉ์‹์œผ๋กœ ๊ตฌํ˜„ํ•˜๊ฑฐ๋‚˜, ๋Œ€๊ธฐ ๋ฉ”์„ธ์ง€/์™„๋ฃŒ ๋ฉ”์„ธ์ง€ ํ…Œ์ด๋ธ”๋กœ ๋‚˜๋ˆ„์–ด ์ฟผ๋ฆฌ ๊ณผ์ •์—์„œ Join์œผ๋กœ ์‚ฌ์šฉํ•ด๋„ ๊ดœ์ฐฎ์„ ๊ฒƒ ๊ฐ™๋‹ค.

 

@Component
@EnableScheduling
public class FailedEventProcessor {
    @Scheduled(cron = "0 0 * * * *") // ๋งค์‹œ๊ฐ„ ์‹คํ–‰
    public void processFailedEvents() {
        List<OutboxEvent> failedEvents = outboxRepository
            .findByStatus(EventStatus.FAILED);
            
        // ์‹คํŒจํ•œ ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ (์•Œ๋ฆผ, ๋กœ๊น… ๋“ฑ)
    }
}

 

์ƒํƒœ์— ๋”ฐ๋ผ ์‹คํŒจํ•œ ์ด๋ฒคํŠธ๋ฅผ ๋”ฐ๋กœ ์ฒ˜๋ฆฌํ•  ์ˆ˜๋„ ์žˆ๋‹ค.

 

์—ฌ๊ธฐ์„œ ํ•ด๊ฒฐํ•ด์•ผ ํ•  ๋ฌธ์ œ๋Š” ์—ฌ๋Ÿฌ๊ฐœ ๋” ์žˆ๋‹ค.

  • ์ธ์Šคํ„ด์Šค๋‹น ์žฅ์•  ๋ฐœ์ƒ ์‹œ ์‹คํ–‰ํ•  ์—ฌ๋Ÿฌ ๋…ธ๋“œ๋ฅผ ๋ฐฐ์น˜
    • ๋ณต์ˆ˜ ๋…ธ๋“œ๋กœ ์‚ฌ์šฉํ•  ์‹œ ๋™์‹œ์„ฑ ์ด์Šˆ -> ๋ฝํ‚น์„ ํ†ตํ•ด ํ•ด๊ฒฐ
  • ๋ฒ„์ €๋‹ -> ๋ฝํ‚น ์‚ฌ์šฉ

์œ„ ๋ฐฉ๋ฒ•๋“ค์€ ๋‹ค์Œ์— ๋” ์ž์„ธํ•˜๊ฒŒ ๋‹ค๋ฃจ๋Š” ๊ธฐํšŒ์—์„œ ์•Œ์•„๋ณด๋„๋ก ํ•˜๊ฒ ๋‹ค.

 

์ด๋กœ์จ ์ฃผ๋ฌธ ํŠธ๋žœ์žญ์…˜ ํ›„์— ๋ฉ”์„ธ์ง€ publish๋ฅผ ๋ณด์žฅํ•  ์ˆ˜ ์žˆ๋‹ค.

 

ํ•ด๋‹น ๋ฐฉ๋ฒ•์€ ๋น„๊ต์  ๊ตฌํ˜„์ด ๊ฐ„๋‹จํ•˜๊ณ  ํŽธ๋ฆฌํ•˜์ง€๋งŒ, DB๋กœ๋ถ€ํ„ฐ ์ฃผ๊ธฐ์ ์œผ๋กœ Pollingํ•˜๊ธฐ ๋•Œ๋ฌธ์— DB์— ๋ถ€ํ•˜๊ฐ€ ๊ฐˆ ์ˆ˜ ์žˆ๋‹ค๋Š” ์ ์ด ๋‹จ์ ์ด๋‹ค.

 

 

 

2-2-2. Transaction Log Tailing ๋ฐฉ๋ฒ•

 

Transaction Log Tailing, https://microservices.io/patterns/data/transaction-log-tailing.html

 

 

Transaction Log Trailing์€ DB ํŠธ๋žœ์žญ์…˜์˜ ๋กœ๊ทธ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•์ด๋‹ค.

 

DB์— ํŠธ๋žœ์žญ์…˜์ด ์ฒ˜๋ฆฌ๋˜๋ฉด ์ƒ์„ฑ๋˜๋Š” ๋กœ๊ทธ์— ๋Œ€ํ•œ CDC๋ฅผ ๊ตฌํ˜„ํ•˜๋Š” ๋ฐฉ๋ฒ•์ด๋‹ค.

 

CDC(Change Data Capture)
๋ฐ์ดํ„ฐ ๋ณ€๊ฒฝ์„ ์ถ”์ ํ•˜์—ฌ ๋ณ€๊ฒฝ ์‚ฌํ•ญ์„ ์‹ค์‹œ๊ฐ„์œผ๋กœ ์ „๋‹ฌํ•˜๋Š” ๋ฐ์ดํ„ฐ ํ†ตํ•ฉ ๊ธฐ์ˆ 

 

๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ๋”ฐ๋ผ ์—ฌ๋Ÿฌ ๋กœ๊ทธ๋ฅผ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

  • MySQL binlog
  • Postgres WAL
  • AWS DynamoDB table streams

์œ„ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋กœ๊ทธ๋ฅผ ํ™œ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•์€ ๊ตฌํ˜„ ๋‚œ์ด๋„๊ฐ€ ๋†’์•„ ๋ณดํ†ต ํˆด์„ ์‚ฌ์šฉํ•˜๋Š”๋ฐ, ๋Œ€ํ‘œ์ ์ธ ํˆด์€ ์•„๋ž˜์™€ ๊ฐ™๋‹ค.

 

  • ๋””๋น„์ง€์›€(Debezium)
  • ๋งํฌ๋“œ์ธ ๋ฐ์ดํ„ฐ ๋ฒ„์Šค(LinkdIn Databus)
  • DynamoDB ์ŠคํŠธ๋ฆผ์ฆˆ
  • ์ด๋ฒค์ถ”์—์ดํŠธ ํŠธ๋žจ

 

@Component
public class MySQLBinLogTailer {
    private final BinaryLogClient client;
    
    public MySQLBinLogTailer() {
        client = new BinaryLogClient(
            "localhost", 
            3306, 
            "username", 
            "password"
        );
        
        client.registerEventListener(event -> {
            if (event.getData() instanceof WriteRowsEventData) {
                WriteRowsEventData writeData = (WriteRowsEventData) event.getData();
                // ์ƒˆ๋กœ์šด Row ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ
                processNewRows(writeData);
            }
        });
    }
    
    @PostConstruct
    public void start() {
        new Thread(() -> {
            try {
                client.connect();
            } catch (IOException e) {
                log.error("Failed to connect to MySQL binlog", e);
            }
        }).start();
    }
}

 

์œ„์™€ ๊ฐ™์ด Mysql binlog๋ฅผ ํ™œ์šฉํ•˜๊ฑฐ๋‚˜,

 

{
    "name": "mysql-outbox-connector",
    "config": {
        "connector.class": "io.debezium.connector.mysql.MySqlConnector",
        "database.hostname": "localhost",
        "database.port": "3306",
        "database.user": "debezium",
        "database.password": "dbz",
        "database.server.name": "mysql",
        "database.server.id": "1",
        "database.include.list": "mydb",
        "table.include.list": "mydb.outbox_events",
        "database.history.kafka.bootstrap.servers": "kafka:9092",
        "database.history.kafka.topic": "schema-changes.outbox",
        
        "transforms": "outbox",
        "transforms.outbox.type": "io.debezium.transforms.outbox.EventRouter",
        "transforms.outbox.table.field.event.id": "id",
        "transforms.outbox.table.field.event.key": "aggregate_id",
        "transforms.outbox.table.field.event.type": "event_type",
        "transforms.outbox.table.field.event.payload": "payload",
        "transforms.outbox.table.fields.additional.placement": "event_type:header:eventType,aggregate_type:header:aggregateType",
        "transforms.outbox.route.by.field": "aggregate_type",
        
        "key.converter": "org.apache.kafka.connect.storage.StringConverter",
        "value.converter": "org.apache.kafka.connect.json.JsonConverter",
        "value.converter.schemas.enable": false
    }
}

 

์œ„์™€ ๊ฐ™์ด Debezium์„ ์„ค์ •ํ•ด๋‘๊ณ  ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•์ด ์žˆ๊ฒ ๋‹ค.

 

 

Transaction Log Tailing ๋ฐฉ๋ฒ•์€ Polling & Publisher ๋ฐฉ๋ฒ•์— ๋น„ํ•ด Polling ์ž‘์—…์ด ์—†๋‹ค๋Š” ์ ์—์„œ ์žฅ์ ์ด ์žˆ์ง€๋งŒ,

Kafka ๋ฉ”์„ธ์ง€ ํ˜•์‹์— ๋งž์ถ”์–ด ๋‹ค์‹œ ๋ฐ์ดํ„ฐ dto๋ฅผ ๋นŒ๋“œํ•ด์•ผ ํ•œ๋‹ค.

 

 

2-3. Saga ํŒจํ„ด : ํŠธ๋žœ์žญ์…˜ ๊ด€๋ฆฌ

 

Saga ํŒจํ„ด์€ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋‚ด ํŠธ๋žœ์žญ์…˜์„ ์ˆœ์ฐจ์ ์œผ๋กœ ์ฒ˜๋ฆฌํ•˜๋Š” ํŒจํ„ด์ด๋‹ค.

์—ฌ๊ธฐ์„œ ํŠธ๋žœ์žญ์…˜์€ DB๊ฐ€ ์•„๋‹Œ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋‹จ์˜ ํŠธ๋žœ์žญ์…˜ ๋ฒ”์œ„๋ฅผ ๋งํ•œ๋‹ค.

https://learn.microsoft.com/ko-kr/azure/architecture/reference-architectures/saga/saga

 

ํ•ต์‹ฌ์€ ๋ณด์ƒ ํŠธ๋žœ์žญ์…˜์ด ์žˆ๋‹ค๋Š” ๊ฒƒ์ด๋‹ค.

 

๋ณด์ƒ ํŠธ๋žœ์žญ์…˜
๋ฐ˜๋Œ€ ํšจ๊ณผ๋ฅผ ๊ฐ€์ง„ ํŠธ๋žœ์žญ์…˜์„ ์ฒ˜๋ฆฌํ•˜์—ฌ ์ž ์žฌ์ ์œผ๋กœ ๋˜๋Œ๋ฆด ์ˆ˜ ์žˆ๋Š” ํŠธ๋žœ์žญ์…˜
์ฆ‰, ์ด์ „์— ์™„๋ฃŒ๋œ ํŠธ๋žœ์žญ์…˜์„ ๋กค๋ฐฑํ•˜๊ธฐ ์œ„ํ•ด ์—ญ์ˆœ์œผ๋กœ ์‹คํ–‰๋˜๋Š” ํŠธ๋žœ์žญ์…˜

 

SagaํŒจํ„ด์€ ๋ณดํ†ต MSA ํ™˜๊ฒฝ์—์„œ ๋งŽ์ด ๋“ฑ์žฅํ•˜๋Š”๋ฐ,

๋ชจ๋†€๋ฆฌ์‹๊ณผ ๋‹ฌ๋ฆฌ DB๊ฐ€ ๋ถ„์‚ฐ๋˜์–ด ์žˆ์–ด ACID๊ฐ€ ์ง€์ผœ์ง€๋Š” ํŠธ๋žœ์žญ์…˜์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

 

์ด๋ฅผ ์˜ˆ์ „์—๋Š” 2PC(2-Phase-Commit)์œผ๋กœ ํ•ด๊ฒฐํ–ˆ์ง€๋งŒ, ์ด๋Š” ์ƒ๋‹นํžˆ ๋น„ํšจ์œจ์ ์ด๊ธฐ ๋•Œ๋ฌธ์— Saga ํŒจํ„ด์ด ํ•ด๊ฒฐ์ ์œผ๋กœ ๋“ฑ์žฅํ–ˆ๋‹ค.

 

 

 

Saga ํŒจํ„ด์€ ํฌ๊ฒŒ ์ฝ”๋ ˆ์˜ค๊ทธ๋ž˜ํ”ผ(Choreography)์™€ ์˜ค์ผ€์ŠคํŠธ๋ ˆ์ด์…˜(Orchestration)์ด ์žˆ๋‹ค.

 

Choreography

 

์ฝ”๋ ˆ์˜ค๊ทธ๋ž˜ํ”ผ๋Š” ๋ฉ”์„ธ์ง€ ๋ธŒ๋กœ์ปค๋ฅผ ์ด์šฉํ•ด์„œ ์ค‘์•™ ์ง‘์ค‘์‹ ์ œ์–ด ์ง€์  ์—†์ด ์ด๋ฒคํŠธ๋ฅผ ์ „์†กํ•˜๋Š” ๋ฐฉ๋ฒ•์ด๋‹ค.

 

 

๊ฐ ์„œ๋น„์Šค์˜ ๋กœ์ปฌ ํŠธ๋žœ์žญ์…˜์ด ์™„๋ฃŒ๋˜๋ฉด ์ด๋ฒคํŠธ๋ฅผ ๋ฐœํ–‰ํ•˜๊ณ , ๋‹ค์Œ ์ˆ˜ํ–‰ํ•ด์•ผํ•  ์•ฑ์ด ๋ฐ›์•„์„œ ๋‹ค์Œ ๋กœ์ปฌ ํŠธ๋žœ์žญ์…˜์„ ์‹คํ–‰ํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ์‚ฌ์šฉ๋œ๋‹ค.

 

 

๋งŒ์•ฝ ์ค‘๊ฐ„์— ํŠธ๋žœ์žญ์…˜์ด ์‹คํŒจํ•˜๋ฉด ์‹คํŒจํ•œ ์•ฑ์—์„œ ๋ณด์ƒ ์ด๋ฒคํŠธ๋ฅผ ๋ฐœํ–‰ํ•˜์—ฌ ๋กค๋ฐฑ์„ ์œ„ํ•œ ๋ณด์ƒ ํŠธ๋žœ์žญ์…˜์„ ๊ฐ ์•ฑ์— ์‹ค์‹œํ•˜๋„๋ก ๊ตฌ์„ฑํ•œ๋‹ค.

 

 

ํ•ด๋‹น ๋ฐฉ๋ฒ•์€ ์œ ์ง€/๊ด€๋ฆฌ๊ฐ€ ํ•„์š”ํ•˜์ง€ ์•Š๊ณ  ๊ตฌํ˜„์ด ๊ฐ„๋‹จํ•˜์ง€๋งŒ Saga ์ฐธ๊ฐ€์ž๊ฐ€ ์–ด๋–ค ๋ช…๋ น์„ ์ˆ˜์‹  ๋Œ€๊ธฐํ•˜๋Š”์ง€ ์ถ”์ ํ•˜๊ธฐ ์–ด๋ ต๊ณ , ํŠธ๋žœ์žญ์…˜์˜ ์ƒํƒœ๋ฅผ ํ™•์ธํ•˜๊ธฐ ์–ด๋ ต๋‹ค๋Š” ๋‹จ์ ์ด ์žˆ๋‹ค.

 

Orchestration

 

 

์˜ค์ผ€์ŠคํŠธ๋ ˆ์ด์…˜์€ ์ค‘์•™ ์ง‘์ค‘์‹ ์ปจํŠธ๋กค๋Ÿฌ(Orchestrator)๊ฐ€ ์‹คํ–‰ํ•  ๋กœ์ปฌ ํŠธ๋žœ์žญ์…˜์„ ์•Œ๋ ค์ฃผ๋Š” ๋ฐฉ๋ฒ•์ด๋‹ค.

 

์˜ค์ผ€์ŠคํŠธ๋ ˆ์ดํ„ฐ๋Š” ๋ชจ๋“  ํŠธ๋žœ์žญ์…˜์„ ์ฒ˜๋ฆฌํ•˜๊ณ , ์ด๋ฒคํŠธ์— ๋”ฐ๋ผ ์ˆ˜ํ–‰ํ•  ์ž‘์—…์„ ๊ฐ ์ฐธ๊ฐ€์žํ•œํ…Œ ์•Œ๋ ค์ค€๋‹ค.

 

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

 

 

 

2-4. ์žฌ์ „์†ก : ์ผ์‹œ์  ๋ฌธ์ œ ํ•ด๊ฒฐ

 

์ด๋ฒคํŠธ๋Š” ์–ด๋–ค ์ด์œ ์—์„œ๋ผ๋„ ์ „์†ก์„ ์‹คํŒจํ•  ์ˆ˜ ์žˆ๋‹ค.

 

์ „์†ก์— ์‹คํŒจํ•œ ์ด๋ฒคํŠธ๋ฅผ ์žฌ์‹œ๋„ ํ•˜๋Š”๊ฒƒ์€ ์ž์—ฐ์Šค๋Ÿฌ์šด ์ˆ˜์ˆœ์ธ๋ฐ,

 

์ด๋Š” ์–ด๋Š์ •๋„ ์ผ์‹œ์ ์ธ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•  ์ˆœ ์žˆ๊ฒ ์ง€๋งŒ, ์˜๊ตฌ์ ์ธ ๋ฌธ์ œ๋Š” ํ•ด๊ฒฐํ•  ์ˆ˜ ์—†๋‹ค.

 

์žฌ์‹œ๋„ ์ •์ฑ…์€ ๊ธฐ๋ณธ์ ์œผ๋กœ Spring Retry๋ฅผ ์ด์šฉํ•ด์„œ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ๋‹ค.

 

Spring Retry๋ฅผ ์‚ฌ์šฉํ•˜๋ ค๋ฉด @EnableRetry๋ฅผ ์„ค์ •ํ•ด์•ผ ํ•œ๋‹ค.

 

@Retryable(
    value = {KafkaException.class, TimeoutException.class},
    maxAttempts = 3,
    backoff = @Backoff(delay = 1000, multiplier = 2)
)
public void publishEvent(String topic, String key, Object event) {
    // ๋ฉ”์„ธ์ง€ ์ „์†ก ๋กœ์ง
}

@Recover
public void handlePublishFailure(Exception e, String topic, String key, Object event) {
    // ์‹คํŒจ ์ฒ˜๋ฆฌ ๋กœ์ง
}

 

์ตœ๋Œ€ ์ „์†ก ํšŸ์ˆ˜๋Š” maxAttempts, ์žฌ์‹œ๋„ ๋ฉ”์„ธ์ง€๊ฐ„ ๊ฐ„๊ฒฉ์€ backoff๋กœ ์„ค์ •ํ•  ์ˆ˜ ์žˆ๋‹ค.

 

์‹คํŒจํ•œ ๋ฉ”์„ธ์ง€์— ๋Œ€ํ•œ ์ฒ˜๋ฆฌ๋Š” @Recover๋ฅผ ํ†ตํ•ด์„œ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋‹ค.

 

 

 

 

 

2-5. DLT (Dead Letter Topic) : ์‹คํŒจํ•œ ๋ฉ”์„ธ์ง€ ๊ด€๋ฆฌํ•˜๊ธฐ

DLQ๋Š” AWS SQS์—์„œ ์‚ฌ์šฉํ•˜๋Š” ์†Œ๋น„๋˜์ง€ ๋ชปํ•œ ๋ฉ”์„ธ์ง€(Dead Letter)๋ฅผ ๋‹ด์•„๋‘๋Š” ๋Œ€๊ธฐ์—ด(ํ)์ด๋‹ค.

 

https://aws.amazon.com/ko/blogs/compute/using-amazon-sqs-dead-letter-queues-to-replay-messages/

 

์‹คํŒจ๋œ ๋ฉ”์„ธ์ง€๋ฅผ Lambda๋ฅผ ํ†ตํ•ด ์–ด๋–ค ์ฒ˜๋ฆฌ๋ฅผ ํ• ์ง€ ์ •ํ•ด๋‘˜ ์ˆ˜ ์žˆ๋‹ค.

 

Kafka์—์„œ๋Š” DLQ๊ฐ€ ์•„๋‹Œ DLT(Dead Letter Topic)์„ ์‚ฌ์šฉํ•œ๋‹ค.

 

factory.getContainerProperties().setAckMode(ContainerProperties.AckMode.MANUAL);

 

Kafka ๋ฉ”์„ธ์ง€ ์ „์†ก์ด ์‹คํŒจํ•˜๋Š” ๊ฒฝ์šฐ๋ฅผ ํ™•์ธํ•˜๋ ค๋ฉด ์ˆ˜๋™ ACK๋ชจ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ์„ค์ •์ด ๊ถŒ์žฅ๋œ๋‹ค.

 

@RetryableTopic
@KafkaListener(topics = "my-annotated-topic")
public void processMessage(MyPojo message) {
    // ... message processing
}

@DltHandler
public void processMessage(MyPojo message) {
    // ... message processing, persistence, etc
}

 

๋ฉ”์„œ๋“œ๊ฐ€ ๊ฐ™์€ ํ˜•ํƒœ์˜ @RetryableTopic๊ณผ @DltHandler๋ฅผ ๋ถ™์ธ ๋ฉ”์„œ๋“œ๋กœ DLT๋ฅผ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

 

๋˜๋Š”, RetryTopicConfiguration์„ ํ†ตํ•ด ์„ค์ •ํ•ด๋‘˜ ์ˆ˜ ์žˆ๋‹ค.

@Bean
public RetryTopicConfiguration myRetryTopic(KafkaTemplate<Integer, MyPojo> template) {
    return RetryTopicConfigurationBuilder
            .newInstance()
            .dltHandlerMethod("myCustomDltProcessor", "processDltMessage")
            .create(template);
}

@Component
public class MyCustomDltProcessor {

    private final MyDependency myDependency;

    public MyCustomDltProcessor(MyDependency myDependency) {
        this.myDependency = myDependency;
    }

    public void processDltMessage(MyPojo message) {
        // ... message processing, persistence, etc
    }
}

 

@RetryableTopic์— ์—ฌ๋Ÿฌ ์• ํŠธ๋ฆฌ๋ทฐํŠธ๋ฅผ ๋‹ฌ์•„๋†“์œผ๋ฉด ์ฝ”๋“œ๊ฐ€ ์ค‘๋ณต๋˜๊ณ  ๋ณต์žกํ•ด์งˆ ์ˆ˜ ์žˆ๋‹ค.

 

RetryTopicConfiguration์„ ์‚ฌ์šฉํ•˜๋ฉด ์ „์—ญ์ ์œผ๋กœ ์žฌ์‹œ๋„ ํ† ํ”ฝ๊ณผ DLT ํ† ํ”ฝ์ด ์ ์šฉ๋˜๋ฏ€๋กœ ๋” ๊น”๋”ํ•œ ์ ์šฉ์ธ ๊ฒƒ ๊ฐ™๋‹ค.

 

์—ฌ๊ธฐ์„œ ์ค‘์š”ํ•œ ์ ์€, @KafkaListener์˜ Factory ์„ค์ •๊ณผ ๋™์ผํ•ด์•ผ MessageConversionException์„ ํ”ผํ•  ์ˆ˜ ์žˆ๋‹ค.

 

 

 

 

์ฐธ๊ณ 
https://ridicorp.com/story/transactional-outbox-pattern-ridi/
https://giron.tistory.com/156
https://docs.spring.io/spring-kafka/reference/index.html
https://docs.spring.io/spring-kafka/reference/kafka/micrometer.html
https://docs.spring.io/spring-kafka/reference/kafka/annotation-error-handling.html
https://hvho.github.io/2021-12-05/spring-retry
https://junuuu.tistory.com/795
https://www.kai-waehner.de/blog/2022/05/30/error-handling-via-dead-letter-queue-in-apache-kafka/
https://docs.spring.io/spring-kafka/reference/retrytopic/dlt-strategies.html
https://velog.io/@wwlee94/Kafka-%EC%9E%AC%EC%8B%9C%EB%8F%84-DLT-%EB%B9%8C%EB%8D%94-%EC%A0%91%EA%B7%BC-%EB%B0%A9%EC%8B%9D%EC%9C%BC%EB%A1%9C-%EB%A6%AC%ED%8C%A9%ED%86%A0%EB%A7%81