๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ
ํ”„๋กœ์ ํŠธ ํšŒ๊ณ /ํŠธ๋Ÿฌ๋ธ”์ŠˆํŒ…

๋‹จ์ผ ํ† ํ”ฝ ํ™˜๊ฒฝ์—์„œ์˜ ๋ฉ”์‹œ์ง€ ๋ถ„๊ธฐ ์ฒ˜๋ฆฌ: Envelope DTO ํŒจํ„ด vs ๋‹คํ˜•์„ฑ ๋ผ์šฐํŒ…(@KafkaHandler)

by ์ ผ์ ผ39 2026. 6. 2.
  • ๋ถ„์•ผ: Spring Kafka, Message Routing, Payload Design
  • ๊ด€๋ จ ํŒŒ์ผ: OrderEventConsumer.java

1. ๋ฐฐ๊ฒฝ: ํ† ํ”ฝ ํ†ตํ•ฉ์œผ๋กœ ์ธํ•œ ์‹๋ณ„ ๋ฌธ์ œ

์ง์ „์—, ์นดํ”„์นด ๋ฉ”์‹œ์ง€ ์ˆœ์„œ ์—ญ์ „(Out-of-Order) ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ์ธ๋ฒคํ† ๋ฆฌ ์„œ๋น„์Šค๋กœ ํ–ฅํ•˜๋Š” ๋‘ ๊ฐ€์ง€ ๋ช…๋ น(์žฌ๊ณ  ์ฐจ๊ฐ, ์žฌ๊ณ  ๋ณต๊ตฌ)์„ ๊ฐœ๋ณ„ ํ† ํ”ฝ์—์„œ ๋‹จ์ผ ํ† ํ”ฝ(inventory.command)์œผ๋กœ ํ†ตํ•ฉํ•จ

๋ฌธ์ œ๋Š” ํ† ํ”ฝ์ด ํ•˜๋‚˜๋กœ ํ•ฉ์ณ์ง€๋ฉด์„œ, ์ธ๋ฒคํ† ๋ฆฌ ์„œ๋น„์Šค์˜ ์ปจ์Šˆ๋จธ๊ฐ€ "์ง€๊ธˆ ๋“ค์–ด์˜จ ๋ฉ”์‹œ์ง€๊ฐ€ ์ฐจ๊ฐ ๋ช…๋ น์ธ์ง€ ๋ณต๊ตฌ ๋ช…๋ น์ธ์ง€ ์–ด๋–ป๊ฒŒ ๊ตฌ๋ถ„ํ•  ๊ฒƒ์ธ๊ฐ€?" ์— ๋Œ€ํ•œ ์•„์ด๋””์–ด๊ฐ€ ์ž˜ ๋– ์˜ค๋ฅด์ง€ ์•Š์•˜์Œ...

2. ๊ณ ๋ฏผ: ์ „ํ†ต์ ์ธ Envelope(๋ด‰ํˆฌ) ํŒจํ„ด์˜ ํ•œ๊ณ„

๊ฐ€์žฅ ๋จผ์ € ๋– ์˜ฌ๋ฆฐ ํ•ด๊ฒฐ์ฑ…์€ ๋‘ ๊ฐ์ฒด๋ฅผ ํ•˜๋‚˜๋กœ ๊ฐ์‹ธ๋Š” ๊ฑฐ๋Œ€ํ•œ '๋ด‰ํˆฌ(Envelope) DTO'๋ฅผ ๋งŒ๋“œ๋Š” ๊ฒƒ์ด์—ˆ์Œ

// ๊ตฌ์ƒํ–ˆ๋˜ Envelope DTO ๊ตฌ์กฐ
{
  "commandType": "RESERVE", // ๋˜๋Š” "RESTORE"
  "payload": {
     // ์‹ค์ œ ๋ฐ์ดํ„ฐ...
  }
}

ํ•˜์ง€๋งŒ ์ด ๋ฐฉ์‹์„ ๋„์ž…ํ•  ๊ฒฝ์šฐ ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์น˜๋ช…์ ์ธ ๋‹จ์ ์ด ์กด์žฌํ•จ:

  1. ๋ณด์ผ๋Ÿฌํ”Œ๋ ˆ์ดํŠธ ์ฝ”๋“œ ์ฆ๊ฐ€: ์˜ค๋” ์„œ๋น„์Šค์™€ ์ธ๋ฒคํ† ๋ฆฌ ์„œ๋น„์Šค ์–‘์ชฝ์— ๊ณตํ†ต Envelope DTO๋ฅผ ๋งŒ๋“ค์–ด์•ผ ํ•จ.
  2. ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ์˜ค์—ผ: ์ปจ์Šˆ๋จธ ๋‚ด๋ถ€์—์„œ if-else ๋˜๋Š” switch-case ๋ฌธ์„ ํ†ตํ•ด commandType์„ ๊ฒ€์‚ฌํ•˜๊ณ  ๋กœ์ง์„ ๋ถ„๊ธฐํ•˜๋Š” ์ง€์ €๋ถ„ํ•œ ์ฝ”๋“œ๊ฐ€ ์–‘์‚ฐ๋จ.

3. ํ•ด๊ฒฐ: Spring Kafka์˜ __TypeId__ ํ—ค๋”์™€ ๋‹คํ˜•์„ฑ ๋ผ์šฐํŒ…(@KafkaHandler)

๋ช…์‹œ์ ์ธ Envelope DTO๋ฅผ ๋งŒ๋“œ๋Š” ๋Œ€์‹ , ํ”„๋ ˆ์ž„์›Œํฌ๊ฐ€ ์ œ๊ณตํ•˜๋Š” ๋ฐฉ์‹์ธ '๋‹คํ˜•์„ฑ(Polymorphic) ๋ผ์šฐํŒ…'์„ ํ™œ์šฉํ•˜์—ฌ ์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•ด๋ณด๋ ค๊ณ  ์‹œ๋„ํ•จ

[์›๋ฆฌ ๋ฐ ์ ์šฉ ๋ฐฉ์‹]

  1. ์ž๋™ ํ—ค๋” ์‚ฝ์ž…: Spring Kafka๋Š” JsonSerializer๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ฉ”์‹œ์ง€๋ฅผ ๋ฐœํ–‰ํ•  ๋•Œ, JSON ๋ณธ๋ฌธ(Body)์„ ๊ฑด๋“œ๋ฆฌ์ง€ ์•Š๊ณ  ์นดํ”„์นด ๋ฉ”์‹œ์ง€ ํ—ค๋”์— __TypeId__: OrderCreatedEventPayload ๋ผ๋Š” ํƒ€์ž… ์ •๋ณด๋ฅผ ํ•จ๊ป˜ ์‚ฝ์ž…ํ•จ.
  2. Type Mapping: ์ปจ์Šˆ๋จธ ์„ค์ •(application.yml)์—์„œ spring.json.type.mapping์„ ํ†ตํ•ด ํ”„๋กœ๋“€์„œ์˜ ํŒจํ‚ค์ง€๋ช…๊ณผ ์ปจ์Šˆ๋จธ์˜ ํŒจํ‚ค์ง€๋ช…์„ 1:1๋กœ ๋งคํ•‘ํ•จ.
  3. ํด๋ž˜์Šค ๋ ˆ๋ฒจ ๊ตฌ๋… ๋ฐ ๋ฉ”์„œ๋“œ ๋ผ์šฐํŒ…: @KafkaListener๋ฅผ ๋ฉ”์„œ๋“œ๊ฐ€ ์•„๋‹Œ ํด๋ž˜์Šค ๋ ˆ๋ฒจ๋กœ ๋Œ์–ด์˜ฌ๋ฆฌ๊ณ , ๊ฐ ๋ฉ”์„œ๋“œ์—๋Š” @KafkaHandler๋ฅผ ๋ถ€์ฐฉํ•จ.
@Component
@KafkaListener(topics = "inventory.command") // ๐Ÿ’ก ๋‹จ์ผ ํ† ํ”ฝ ๊ตฌ๋…
public class OrderEventConsumer {

    // ๐Ÿ’ก TypeId๊ฐ€ 'OrderCreatedMessage'๋กœ ๋งคํ•‘๋˜๋ฉด ์ด ๋ฉ”์„œ๋“œ ์‹คํ–‰
    @KafkaHandler
    public void consumeOrderCreated(OrderCreatedMessage payload) {
        stockLockFacade.reserveOrderStocksWithLock(payload);
    }

    // ๐Ÿ’ก TypeId๊ฐ€ 'StockRestoreMessage'๋กœ ๋งคํ•‘๋˜๋ฉด ์ด ๋ฉ”์„œ๋“œ ์‹คํ–‰
    @KafkaHandler
    public void consumeStockRestoreRequest(StockRestoreMessage payload) {
        stockLockFacade.restoreStockWithLock(request);
    }
}

4. ์„ฑ๊ณผ ๋ฐ ํšŒ๊ณ 

์ด ๋ฐฉ์‹์„ ์ฑ„ํƒํ•จ์œผ๋กœ์จ ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์ด์ ์„ ์–ป์„ ์ˆ˜ ์žˆ์—ˆ์Œ:

  • ๊ด€์‹ฌ์‚ฌ์˜ ๋ถ„๋ฆฌ(Separation of Concerns): ๊ฐœ๋ฐœ์ž๋Š” ๋ฉ”์‹œ์ง€๋ฅผ ์–ด๋–ป๊ฒŒ ๋ผ์šฐํŒ…ํ• ์ง€ ๊ณ ๋ฏผํ•  ํ•„์š” ์—†์ด, ๊ฐ ๋น„์ฆˆ๋‹ˆ์Šค ๊ฐ์ฒด(OrderCreatedMessage, StockRestoreMessage)์˜ ์ฒ˜๋ฆฌ์—๋งŒ ์˜จ์ „ํžˆ ์ง‘์ค‘ํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋˜์—ˆ์Œ.
  • ๊ฐ์ฒด ์ง€ํ–ฅ์  ์„ค๊ณ„: switch-case ๋ฌธ์„ ๋ฐฐ์ œํ•˜๊ณ , ํ”„๋ ˆ์ž„์›Œํฌ๊ฐ€ ๋Ÿฐํƒ€์ž„์— ๋‹คํ˜•์„ฑ์„ ์ด์šฉํ•ด ์˜ฌ๋ฐ”๋ฅธ ๋ฉ”์„œ๋“œ(Handler)๋ฅผ ์ฐพ์•„์ฃผ๋Š” ๊ตฌ์กฐ๋ฅผ ์™„์„ฑํ•จ.
  • ์˜ค๋ฒ„ ์—”์ง€๋‹ˆ์–ด๋ง ๋ฐฉ์ง€: ๋ถˆํ•„์š”ํ•œ Envelope DTO๋ฅผ ์ƒ์„ฑํ•˜์ง€ ์•Š๊ณ , ์นดํ”„์นด ํ”„๋กœํ† ์ฝœ์˜ Native ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ(Header)๋ฅผ ์ ๊ทน ํ™œ์šฉํ•˜์—ฌ ๋„คํŠธ์›Œํฌ ํŽ˜์ด๋กœ๋“œ ํฌ๊ธฐ๊นŒ์ง€ ์ตœ์ ํ™”ํ–ˆ์Œ.

๊ฒฐ๊ณผ์ ์œผ๋กœ, ๋‹จ์ผ ํ† ํ”ฝ ํ†ตํ•ฉ์œผ๋กœ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์—ˆ๋˜ ๋ผ์šฐํŒ…์˜ ๋ณต์žก์„ฑ์„ Spring Kafka์˜ ํ•ต์‹ฌ ๊ธฐ๋Šฅ์„ ํ™œ์šฉํ•จ์œผ๋กœ์จ ํ•ด๊ฒฐํ•ด๋‚ผ ์ˆ˜ ์žˆ์—ˆ์Œ