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

[ํŠธ๋Ÿฌ๋ธ”์ŠˆํŒ…] 1. Outbox ์Šค์ผ€์ค„๋Ÿฌ์˜ ๋น„๋™๊ธฐ ์ฝœ๋ฐฑ ์ง€์—ฐ์œผ๋กœ ์ธํ•œ ์ƒํƒœ ๋ฎ์–ด์“ฐ๊ธฐ(Race Condition) ๋ฐฉ์–ด

by ์ ผ์ ผ39 2026. 5. 19.
  • ๋ถ„์•ผ: ๋™์‹œ์„ฑ ์ œ์–ด, Event-Driven Architecture
  • ๊ด€๋ จ ํŒŒ์ผ: InventoryOutboxHelper.java

1. ๋ฐœ๋‹จ ๋ฐ 1์ฐจ ์‹œ๋„ (๋ถˆ์™„์ „ํ•œ ํ•ด๊ฒฐ…)

Outbox ํŒจํ„ด์„ ๋„์ž…ํ•˜์—ฌ DB์— ์ด๋ฒคํŠธ๋ฅผ ์ ์žฌํ•œ ํ›„, 5์ดˆ๋งˆ๋‹ค ๋„๋Š” ์Šค์ผ€์ค„๋Ÿฌ๊ฐ€ ์นดํ”„์นด๋กœ ์ด๋ฒคํŠธ๋ฅผ ๋ฐœํ–‰ํ•˜๋„๋ก ๊ตฌ์„ฑํ•จ. ์ดˆ๊ธฐ์—๋Š” ๋™๊ธฐ(get()) ๋ฐฉ์‹์œผ๋กœ ๋ฐœ์†กํ–ˆ์œผ๋‚˜, ์Šค์ผ€์ค„๋Ÿฌ์˜ ์ฒ˜๋ฆฌ๋Ÿ‰์„ ๋†’์ด๊ธฐ ์œ„ํ•ด ๋น„๋™๊ธฐ(whenComplete()) ๋ฐฉ์‹์œผ๋กœ ๋ณ€๊ฒฝํ•˜๊ณ  ์‹คํŒจ ์‹œ retryCount๋ฅผ ์ฆ๊ฐ€์‹œํ‚ค๋„๋ก ๋กœ์ง์„ ์ž‘์„ฑํ–ˆ์Œ

2. ๋ฌด์—‡์ด ๋ฌธ์ œ์˜€๋‚˜?

๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ ๋„์ž… ํ›„, ๊ฐ„ํ—์ ์œผ๋กœ "์ด๋ฏธ ์นดํ”„์นด ๋ฐœ์†ก์— ์„ฑ๊ณตํ•œ ์ด๋ฒคํŠธ๊ฐ€ FAILED ์ƒํƒœ๋กœ ๋ฎ์–ด์”Œ์›Œ์ง€๋Š” ํ˜„์ƒ"์ด ๋ฐœ๊ฒฌ๋จ. ์›์ธ์€ ์นดํ”„์นด ๋ธŒ๋กœ์ปค์˜ ์ผ์‹œ์  ์ง€์—ฐ์œผ๋กœ ์ธํ•œ Race Condition์ด์—ˆ์Œ

  • ์Šค๋ ˆ๋“œ A(์ฒซ ๋ฒˆ์งธ ํด๋ง)๊ฐ€ ๋ฉ”์‹œ์ง€๋ฅผ ์˜๊ณ  ํƒ€์ž„์•„์›ƒ์„ ๊ธฐ๋‹ค๋ฆผ
  • ํƒ€์ž„์•„์›ƒ์ด ๋ฐœ์ƒํ•˜๊ธฐ ์ง์ „, ์Šค๋ ˆ๋“œ B(๋‹ค์Œ ํด๋ง)๊ฐ€ ๋™์ผํ•œ ๋ฉ”์‹œ์ง€๋ฅผ ๋‹ค์‹œ ์ฝ์–ด ์นดํ”„์นด๋กœ ์˜๊ณ  ์„ฑ๊ณต(PUBLISHED) ์ฒ˜๋ฆฌํ•จ
  • ์งํ›„, ์Šค๋ ˆ๋“œ A์˜ ์‹คํŒจ ์ฝœ๋ฐฑ์ด ๋’ค๋Šฆ๊ฒŒ ๋„์ฐฉํ•˜์—ฌ ์ƒํƒœ๋ฅผ ๋‹ค์‹œ FAILED๋กœ ๋ฎ์–ด์”Œ์›€

3. ํ•ด๊ฒฐ๋ฐฉ๋ฒ•: ์ƒํƒœ ๊ฐ€๋“œ

๋ถ„์‚ฐ ํ™˜๊ฒฝ์˜ ๋น„๋™๊ธฐ ์ฝœ๋ฐฑ์€ ๋„์ฐฉ ์ˆœ์„œ๋ฅผ ๋ณด์žฅํ•˜์ง€ ์•Š์Œ. ๋”ฐ๋ผ์„œ DB์˜ ์ƒํƒœ๋ฅผ ๋ณ€๊ฒฝํ•˜๊ธฐ ์ „, ํ˜„์žฌ ๋ฐ์ดํ„ฐ๊ฐ€ ๋‚ด๊ฐ€ ๊ธฐ๋Œ€ํ•˜๋Š” ์ƒํƒœ(์˜ˆ: INIT)๊ฐ€ ๋งž๋Š”์ง€ ํ™•์ธํ•˜๋Š” ๋กœ์ง์ด ํ•„์ˆ˜์ ์ด๋ผ๊ณ  ์ƒ๊ฐํ•จ

4. ์ตœ์ข… ํ•ด๊ฒฐ ๋ฐ ์‹ค์ œ ์ฝ”๋“œ

DB์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ์กฐํšŒํ•œ ์งํ›„, ์ƒํƒœ๊ฐ€ INIT์ด ์•„๋‹ ๊ฒฝ์šฐ ์ฆ‰์‹œ ๋กœ์ง์„ ์ค‘๋‹จํ•˜๋Š” ๋ฉฑ๋“ฑ์„ฑ ๋ณด์žฅ ๋กœ์ง์„ ์ถ”๊ฐ€ํ•จ

// ๋น„๋™๊ธฐ ์นดํ”„์นด ์ „์†ก ์‹คํŒจ ์‹œ ์žฌ์‹œ๋„ ํšŸ์ˆ˜๋ฅผ ์˜ฌ๋ฆฌ๊ณ  3ํšŒ ์ดˆ๊ณผ ์‹œ FAILED ์ฒ˜๋ฆฌํ•˜๋Š” ๋กœ์ง
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void handleFailure(UUID outboxId) {
    outboxRepository.findById(outboxId).ifPresentOrElse(
        outbox -> {
        // ๋น„๋™๊ธฐ ์ฝœ๋ฐฑ ์ง€์—ฐ์œผ๋กœ ์ธํ•œ Race Condition ๋ฐฉ์–ด (์ƒํƒœ ๊ฐ€๋“œ)
        // ๋‹ค๋ฅธ ์Šค๋ ˆ๋“œ๊ฐ€ ์ด๋ฏธ ์„ฑ๊ณต(PUBLISHED)์‹œ์ผฐ๊ฑฐ๋‚˜ ์˜๊ตฌ ์‹คํŒจ(FAILED) ์ฒ˜๋ฆฌํ–ˆ๋‹ค๋ฉด ๋ฌด์‹œ
            if (outbox.getStatus() != OutboxStatus.INIT) {
		            log.warn("Outbox ์ƒํƒœ ๋ณ€๊ฒฝ ๋ฌด์‹œ(์ด๋ฏธ ์ฒ˜๋ฆฌ๋จ). outboxId={}", outboxId);
                return;
            }
            outbox.incrementRetryCount();
            if (outbox.getRetryCount() >= maxRetries) {
                outbox.markAsFailed();
                log.error("[CRITICAL] Order Outbox ์ตœ๋Œ€ ์žฌ์‹œ๋„ ํšŸ์ˆ˜ ์ดˆ๊ณผ. FAILED ์ƒํƒœ๋กœ ๋งˆํ‚น. ์ˆ˜๋™ ์กฐ์น˜ ์š”๋ง. outboxId={}", outboxId);
            }
            outboxRepository.save(outbox);
        },
        () -> log.warn("[Order Outbox] ์‹คํŒจ์ฒ˜๋ฆฌ๋Œ€์ƒ ์—†์Œ outboxId={}", outboxId)
    );
}