Transactional Outbox Pattern

λΆ„μ‚° ν™˜κ²½μ—μ„œμ˜ λ¬Έμ œλŠ” β€˜μ΄μ€‘ 쓰기’ λ¬Έμ œμž…λ‹ˆλ‹€.

  • DBμ—λŠ” μ €μž₯이 λλŠ”λ°, μΉ΄ν”„μΉ΄ λ©”μ‹œμ§€ λ°œν–‰μ΄ μ‹€νŒ¨ν•œλ‹€λ©΄?

  • λ°˜λŒ€λ‘œ λ©”μ‹œμ§€λŠ” λ³΄λƒˆλŠ”λ° DB μ €μž₯에 μ‹€νŒ¨ν•΄ λ‘€λ°±λœλ‹€λ©΄?

μ΄λŸ¬ν•œ 데이터 뢈일치 μƒν™©μ—μ„œ 자주 ν™œμš©λ˜λŠ” κ°•λ ₯ν•œ νŒ¨ν„΄μΈ νŠΈλžœμž­μ…”λ„ μ•„μ›ƒλ°•μŠ€ νŒ¨ν„΄μ΄ μžˆμŠ΅λ‹ˆλ‹€.

Dual Write Problem

μ£Όλ¬Έ μ„œλΉ„μŠ€μ—μ„œ μ£Όλ¬Έ 생성과 재고 κ°μ†Œ 이벀트 λ°œν–‰μ„ λ™μ‹œμ— μ§„ν–‰ν•˜λŠ” 경우λ₯Ό λ³΄κ² μŠ΅λ‹ˆλ‹€.

[상황]

  1. DB νŠΈλžœμž­μ…˜:

    • μ£Όλ¬Έ 데이터λ₯Ό DB에 μ €μž₯ (INSERT)

  2. μ™ΈλΆ€ 호좜:

    • Kafka둜 재고 κ°μ†Œ 이벀트 λ°œν–‰ (SEND)

[문제]

  1. DB μ €μž₯은 μ„±κ³΅ν•˜κ³ , Kafka 이벀트 λ°œν–‰ 직전에 μ„œλ²„κ°€ λ‹€μš΄ β†’ 주문은 μƒμ„±λμ§€λ§Œ μž¬κ³ λŠ” 쀄어듀지 μ•ŠμŒ

  2. Kafka에 μ΄λ²€νŠΈλŠ” λ°œν–‰ν–ˆμ§€λ§Œ, DB μ €μž₯ 쀑 λ‘€λ°± β†’ 주문은 μ—†λŠ”λ° μž¬κ³ κ°€ 쀄어듬 (데이터 뢈일치)

두 μž‘μ—…μ€ μ„œλ‘œ λ‹€λ₯Έ μ‹œμŠ€ν…œμ΄κΈ° λ•Œλ¬Έμ— ν•˜λ‚˜μ˜ μ›μžμ μΈ νŠΈλžœμž­μ…˜μœΌλ‘œ 묢을 수 μ—†μŠ΅λ‹ˆλ‹€.

이처럼 두 개의 이쒅 μ‹œμŠ€ν…œμ— λ™μ‹œμ— μ“°κΈ° μž‘μ—…μ„ μ§„ν–‰ν•˜λ‹€κ°€ μ‹€νŒ¨ν•œ 경우 μ˜ˆμ™Έ 핸듀링을 잘λͺ» ν•΄μ£Όλ©΄ 데이터 일관성이 깨질 수 μžˆμŠ΅λ‹ˆλ‹€.

Transactional Outbox Pattern

νŠΈλžœμž­μ…”λ„ μ•„μ›ƒλ°•μŠ€ νŒ¨ν„΄μ€ 단일 DB에 λŒ€ν•΄μ„œλŠ” νŠΈλžœμž­μ…”λ„ν•œ μ²˜λ¦¬κ°€ κ°€λŠ₯ν•˜λ‹€λŠ” 것을 ν™œμš©ν•©λ‹ˆλ‹€.

즉, β€œλ©”μ‹œμ§€ λ°œν–‰λ„ DB νŠΈλžœμž­μ…˜ μ•ˆμ—μ„œ μ²˜λ¦¬ν•˜μžβ€λŠ” μ˜λ―Έμž…λ‹ˆλ‹€.

Kafka에 직접 λ©”μ‹œμ§€λ₯Ό λ³΄λ‚΄λŠ” λŒ€μ‹ , β€˜λ³΄λ‚Ό 메세지’λ₯Ό DB ν…Œμ΄λΈ”(Outbox)에 μ €μž₯ν•˜λŠ” κ²ƒμž…λ‹ˆλ‹€. μ΄λ ‡κ²Œν•˜λ©΄ λΉ„μ¦ˆλ‹ˆμŠ€ 둜직과 λ©”μ‹œμ§€ μ €μž₯이 같은 DB νŠΈλžœμž­μ…˜μœΌλ‘œ λ¬Άμ΄λ―€λ‘œ μ›μžμ„±μ΄ 보μž₯λ©λ‹ˆλ‹€.

[λ™μž‘ 원리]

  1. νŠΈλžœμž­μ…˜ μ‹œμž‘

  2. λΉ„μ¦ˆλ‹ˆμŠ€ 둜직: Order ν…Œμ΄λΈ”μ— μ£Όλ¬Έ 정보 μ €μž₯ (INSERT)

  3. Outbox μ €μž₯: Outbox ν…Œμ΄λΈ”μ— λ°œν–‰ν•  이벀트 정보(Payload) μ €μž₯ (INSERT)

    • outbox_events table 예:

      • { "eventType": "ORDER_CREATED", "payload": { ... }, "status": "PENDING" }

      column_name
      type
      description

      id

      BIGINT

      PK

      event_type

      VARCHAR

      이벀트 μ’…λ₯˜ ꡬ뢄

      payload

      JSON, TEXT

      이벀트 κ΄€λ ¨ 핡심 데이터

      status

      VARCHAR

      이벀트 처리 μƒνƒœ

      created_date

      TIMESTAMP

      이벀트 생성 μ‹œκ°„

  4. νŠΈλžœμž­μ…˜ 컀밋: 두 데이터가 λ™μ‹œμ— μ €μž₯됨 (μ‹€νŒ¨ μ‹œ λ‘˜ λ‹€ λ‘€λ°±)

  5. λ©”μ‹œμ§€ 릴레이(Relay): λ³„λ„μ˜ ν”„λ‘œμ„ΈμŠ€κ°€ Outbox ν…Œμ΄λΈ”μ„ κ°μ‹œν•˜λ‹€κ°€, μƒˆλ‘œμš΄ 데이터가 보이면 Kafka둜 μ‹€μ œ λ©”μ‹œμ§€λ₯Ό λ°œν–‰ν•˜κ³  μƒνƒœλ₯Ό PUBLISHED둜 변경함.

Outbox Pattern μ˜ˆμ‹œ

1. 문제 상황 (Dual Write)

  • κ²°κ³Ό:

    • μ˜ˆμ™Έκ°€ λ°œμƒν•΄μ„œ DB의 μ£Όλ¬Έ μ •λ³΄λŠ” λ‘€λ°±λ˜μ–΄ μ‚¬λΌμ‘ŒμŠ΅λ‹ˆλ‹€.

    • ν•˜μ§€λ§Œ 이미 λ°œν–‰λœ Kafka λ©”μ‹œμ§€λŠ” μ·¨μ†Œν•  수 μ—†μ–΄, 재고 μ„œλΉ„μŠ€λŠ” 재고λ₯Ό μ€„μ—¬λ²„λ¦¬λŠ” 사고가 λ°œμƒν•©λ‹ˆλ‹€.

2. Outbox Pattern 적용

  • κ²°κ³Ό:

    • μ˜ˆμ™Έ λ°œμƒ μ‹œ DB νŠΈλžœμž­μ…˜μ΄ λ‘€λ°±λ˜λ―€λ‘œ, μ£Όλ¬Έ 정보와 Outbox μ΄λ²€νŠΈκ°€ λͺ¨λ‘ κΉ”λ”ν•˜κ²Œ μ‚¬λΌμ§‘λ‹ˆλ‹€.

    • Kafka λ©”μ‹œμ§€λŠ” μ•„μ˜ˆ λ°œν–‰ μ‹œλ„μ‘°μ°¨ ν•˜μ§€ μ•Šμ•˜μœΌλ―€λ‘œ 데이터 일관성이 μ™„λ²½ν•˜κ²Œ μ§€μΌœμ§‘λ‹ˆλ‹€.

μ•„μ›ƒλ°•μŠ€ 이벀트 μ‹€ν–‰ν•˜κΈ°: Message Relay

DB에 μ €μž₯된 이벀트λ₯Ό κΊΌλ‚΄μ„œ μ‹€μ œλ‘œ Kafka에 λ³΄λ‚΄μ£ΌλŠ” 역할이 ν•„μš”ν•©λ‹ˆλ‹€. 이λ₯Ό Message Relay라고 ν•©λ‹ˆλ‹€.

κ΅¬ν˜„ 방식 1: Polling Publisher

κ°€μž₯ 쉽고 직관적인 λ°©λ²•μž…λ‹ˆλ‹€. μŠ€μΌ€μ€„λŸ¬κ°€ 주기적으둜 DBλ₯Ό μ‘°νšŒν•΄μ„œ μƒˆλ‘œμš΄ μ΄λ²€νŠΈκ°€ 있으면 λ©”μ‹œμ§€ 브둜컀둜 λ°œν–‰ν•©λ‹ˆλ‹€.

  • μž₯점: κ΅¬ν˜„μ΄ 쉽고 DB만 있으면 λ©λ‹ˆλ‹€.

  • 단점:

    • Polling μ£ΌκΈ°:

      • λ„ˆλ¬΄ κΈΈλ©΄ μ‹€μ‹œκ°„μ„±μ΄ λ–¨μ–΄μ§ˆ 수 μžˆμŠ΅λ‹ˆλ‹€.

      • κ·Έλ ‡λ‹€κ³  μ£ΌκΈ°κ°€ λ„ˆλ¬΄ 짧으면 DB에 μ‹¬ν•œ λΆ€ν•˜λ₯Ό 주게 될 κ²ƒμž…λ‹ˆλ‹€.

    • 쀑볡 λ°œν–‰: μ—¬λŸ¬ μ„œλ²„κ°€ λ™μ‹œμ— Polling ν•˜λ©΄ 같은 λ©”μ‹œμ§€λ₯Ό 쀑볡 λ°œν–‰ν•  수 μžˆμŠ΅λ‹ˆλ‹€. (μŠ€μΌ€μ€„λŸ¬ 락 λ“±μœΌλ‘œ λ°©μ–΄ ν•„μš”)

μŠ€μΌ€μ€„λ§μ— μ˜ν•œ 폴링 방식이 μ•„λ‹ˆλ”λΌλ„ λ©”μ‹œμ§€ 릴레이λ₯Ό ν•  λ•Œ μ£Όμ˜ν•΄μ•Ό ν•  사항은 λ¬΄μ—‡μΌκΉŒμš”?

그쀑 ν•˜λ‚˜λŠ” λ©”μ‹œμ§€ 릴리에이 μ‹€νŒ¨ν–ˆμ„ λ•Œ μ–΄λ–»κ²Œ μ²˜λ¦¬ν•  것인지 μž…λ‹ˆλ‹€. (ex: μΉ΄ν”„μΉ΄ λ©”μ‹œμ§€ λ°œμƒ μ‹€νŒ¨)

μž¬μ‹œλ„λ₯Ό ν•˜λŠ” λ“± λ‹€μ–‘ν•œ μˆ˜λ‹¨μ„ λ„μž…ν•΄μ•Ό ν•˜κ³ , Posion Pill이라고 λΆ€λ₯΄λŠ” λ©”μ‹œμ§€ μžμ²΄κ°€ μœ νš¨ν•˜μ§€ μ•Šμ•„μ„œ 무쑰건 μ‹€νŒ¨ν•˜λŠ” λ©”μ‹œμ§€λŠ” λ¬΄ν•œμ • μž¬μ‹œλ„ν•˜λ„λ‘ κ΅¬ν˜„ν•˜λ©΄ μ•ˆ λ©λ‹ˆλ‹€.

이런 뢀뢄을 κ³ λ €ν•œλ‹€λ©΄ μ•„μ›ƒλ°•μŠ€ 이벀트 ν…Œμ΄λΈ”μ— μž¬μ‹œλ„ 횟수λ₯Ό ν•¨κ»˜ μ €μž₯ν•˜λŠ” 것도 쒋은 방법이 될 수 μžˆμŠ΅λ‹ˆλ‹€.

κ΅¬ν˜„ 방식 2: Transaction Log Tailing (CDC)

μ‹€λ¬΄μ—μ„œ λŒ€κ·œλͺ¨ νŠΈλž˜ν”½ μ²˜λ¦¬μ— ꢌμž₯λ˜λŠ” λ°©μ‹μž…λ‹ˆλ‹€. Debezium 같은 CDC(Change Data Capture) 도ꡬλ₯Ό μ‚¬μš©ν•©λ‹ˆλ‹€.

  • λ™μž‘: μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ€ Outbox ν…Œμ΄λΈ”μ— INSERT만 ν•©λ‹ˆλ‹€. CDC 도ꡬ가 DB의 νŠΈλžœμž­μ…˜ 둜그(Binlog)λ₯Ό μ‹€μ‹œκ°„μœΌλ‘œ κ°μ‹œν•˜λ‹€κ°€, λ³€κ²½ 사항을 κ°μ§€ν•˜μ—¬ μžλ™μœΌλ‘œ Kafka에 λ©”μ‹œμ§€λ₯Ό μ΄μ€λ‹ˆλ‹€.

  • μž₯점: μ‹€μ‹œκ°„μ„±μ΄ λ†’κ³  μ• ν”Œλ¦¬μΌ€μ΄μ…˜ λΆ€ν•˜κ°€ μ—†μŠ΅λ‹ˆλ‹€.

  • 단점: 인프라 ꡬ성(Kafka Connect, Debezium λ“±)이 λ³΅μž‘ν•©λ‹ˆλ‹€.

Summary

Transactional Outbox Pattern은 λ©”μ‹œμ§€ μœ μ‹€μ„ λ§‰λŠ” κ°•λ ₯ν•œ λ„κ΅¬μ§€λ§Œ, 만λŠ₯은 μ•„λ‹™λ‹ˆλ‹€.

  • λ©”μ‹œμ§€ μˆœμ„œ:

    • Polling λ°©μ‹μ΄λ‚˜ λ©€ν‹° μŠ€λ ˆλ“œ ν™˜κ²½μ—μ„œλŠ” λ©”μ‹œμ§€ λ°œν–‰ μˆœμ„œκ°€ λ’€λ°”λ€” 수 μžˆμŠ΅λ‹ˆλ‹€.

    • μ—„κ²©ν•œ μˆœμ„œκ°€ ν•„μš”ν•˜λ‹€λ©΄ Kafka νŒŒν‹°μ…”λ‹ μ „λž΅κ³Ό ν•¨κ»˜ κ³ λ €ν•΄μ•Ό ν•©λ‹ˆλ‹€.

  • λ©±λ“±μ„±(Idempotency) ν•„μˆ˜:

    • Relayκ°€ λ©”μ‹œμ§€λ₯Ό 보내고 DB μƒνƒœλ₯Ό PUBLISHED둜 λ°”κΎΈκΈ° 직전에 μ£½λŠ”λ‹€λ©΄? μž¬μ‹€ν–‰ μ‹œ λ©”μ‹œμ§€κ°€ 쀑볡 λ°œν–‰λ  수 μžˆμŠ΅λ‹ˆλ‹€.

    • Consumer μͺ½μ—μ„œλŠ” λ°˜λ“œμ‹œ 쀑볡 λ©”μ‹œμ§€λ₯Ό κ±ΈλŸ¬λ‚΄λŠ”(λ©±λ“±μ„±) 처리λ₯Ό ν•΄μ•Ό ν•©λ‹ˆλ‹€.

Last updated