๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ

ํ”„๋กœ์ ํŠธ/Airbnb Clone

ํŠธ๋žœ์žญ์…˜ ๋กค๋ฐฑ ์‹œ ๊ฒฐ์ œ ์ทจ์†Œ ์š”์ฒญํ•˜๊ธฐ - @TransactionalEventListener & ApplicationEventPublisher ๋ชจํ‚น ์ด์Šˆ

๊ฒฐ์ œ ํ”„๋กœ์„ธ์Šค๋ฅผ ์„ค๊ณ„ํ•˜๋ฉด์„œ, ๊ธฐ์กด์—๋Š” try-catch๋ฅผ ํ™œ์šฉํ•ด ํŠธ๋žœ์žญ์…˜ ๋‚ด๋ถ€์—์„œ ๋ฐœ์ƒํ•˜๋Š” ๋Ÿฐํƒ€์ž„ ์˜ˆ์™ธ๋ฅผ ์ฒ˜๋ฆฌํ•˜๊ณ , ๊ฒฐ์ œ ์ทจ์†Œ ์š”์ฒญ์„ ๋ณด๋ƒˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜, ์ด์— ๋Œ€ํ•œ ํ•œ๊ณ„๋ฅผ ๋Š๊ผˆ๊ณ  ํŠธ๋žœ์žญ์…˜ ๋กค๋ฐฑ ์‹œ ๊ฒฐ์ œ ์ทจ์†Œ ์š”์ฒญ์„ ๋ช…ํ™•ํ•˜๊ณ  ์ง๊ด€์ ์œผ๋กœ ์ฒ˜๋ฆฌํ•˜๊ธฐ ์œ„ํ•ด @TransactionalEventListener๋ฅผ ๋„์ž…ํ–ˆ์Šต๋‹ˆ๋‹ค. 

 

๊ธฐ์กด try-catch ๋ฐฉ์‹์˜ ํ•œ๊ณ„

try-catch์—์„œ๋Š” ์ฃผ๋กœ DB ์˜ˆ์™ธ(์˜ˆ: DataIntegrityViolationException)๋‚˜ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์—์„œ ๋ฐœ์ƒํ•˜๋Š” ์˜ˆ์™ธ๋ฅผ ์ฒ˜๋ฆฌํ•˜๊ณ , ๊ฒฐ์ œ ์ทจ์†Œ ์š”์ฒญ์„ ๋ณด๋ƒˆ์Šต๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ, try-catch๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์˜ˆ์™ธ๋ฅผ ์ •์ƒ์ ์ธ ํ๋ฆ„์œผ๋กœ ์ œ์–ดํ•˜๋Š” ๊ฒƒ์ฒ˜๋Ÿผ ๋ณด์ด๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. ์ด๋Š” "ํŠธ๋žœ์žญ์…˜์ด ๋กค๋ฐฑ๋˜์—ˆ์„ ๋•Œ ๊ฒฐ์ œ ์ทจ์†Œ๋ฅผ ์ˆ˜ํ–‰ํ•œ๋‹ค"๋ผ๋Š” ์˜๋ฏธ๋ฅผ ์ •ํ™•ํžˆ ํ‘œํ˜„ํ•˜์ง€ ๋ชปํ•œ๋‹ค๊ณ  ์ƒ๊ฐํ–ˆ์Šต๋‹ˆ๋‹ค.

๊ฒฐ๊ณผ์ ์œผ๋กœ try-catch ์•ˆ์—์„œ RuntimeException์„ ์žก์•„ ๊ฒฐ์ œ ์ทจ์†Œ๋ฅผ ์š”์ฒญํ•˜๋Š” ๊ฒƒ์€ "์˜ˆ์™ธ๋ฅผ ํ•ธ๋“ค๋งํ•œ๋‹ค"๋Š” ์˜๋ฏธ์ผ ๋ฟ, ํŠธ๋žœ์žญ์…˜์˜ ๋กค๋ฐฑ ์ƒํƒœ๋ฅผ ๊ธฐ์ค€์œผ๋กœ ๊ฒฐ์ œ ์ทจ์†Œ ์š”์ฒญ์„ ๋ณด๋‚ธ๋‹ค๋Š” ์˜๋„๋ฅผ ๋ช…ํ™•ํžˆ ์ „๋‹ฌํ•˜์ง€ ๋ชปํ•œ๋‹ค๊ณ  ํŒ๋‹จํ–ˆ์Šต๋‹ˆ๋‹ค.

 

@TransactionalEventListener ๋„์ž…

์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ์Šคํ”„๋ง 4.2๋ถ€ํ„ฐ ์ง€์›ํ•˜๋Š” @TransactionalEventListener๋ฅผ ๋„์ž…ํ–ˆ์Šต๋‹ˆ๋‹ค. ์ด ์–ด๋…ธํ…Œ์ด์…˜์€ ํŠธ๋žœ์žญ์…˜์˜ ์ƒํƒœ์— ๋”ฐ๋ผ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ๋ฅผ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ๋„๋ก ๋„์™€์ค๋‹ˆ๋‹ค. ์ด๋ฅผ ํ†ตํ•ด ํŠธ๋žœ์žญ์…˜์ด ๋กค๋ฐฑ๋˜์—ˆ์„ ๋•Œ๋งŒ ๊ฒฐ์ œ ์ทจ์†Œ ์š”์ฒญ์„ ๋ณด๋‚ผ ์ˆ˜ ์žˆ๋„๋ก ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

 

Spring-Events ์†Œ๊ฐœ

@TransactionalEventListener๋ฅผ ์†Œ๊ฐœํ•˜๊ธฐ ์•ž์„œ Baeldung-spring-events๋ฅผ ์ฐธ๊ณ ํ•˜์—ฌ ์Šคํ”„๋ง ์ด๋ฒคํŠธ๋ฅผ ์„ค๋ช…ํ•ฉ๋‹ˆ๋‹ค.

  • Spring Framework 4.2 ์ด์ „ ๋ฒ„์ „์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒฝ์šฐ ์ด๋ฒคํŠธ ํด๋ž˜์Šค๋Š” ApplicationEvent๋ฅผ ํ™•์žฅํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. 4.2 ๋ฒ„์ „๋ถ€ํ„ฐ ์ด๋ฒคํŠธ ํด๋ž˜์Šค๋Š” ๋” ์ด์ƒ  ApplicationEvent  ํด๋ž˜์Šค๋ฅผ ํ™•์žฅํ•  ํ•„์š”๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.
  • ์ด๋ฒคํŠธ ๋ฐœํ–‰: ApplicationEventPublisher ๊ฐ์ฒด๋ฅผ DIํ•˜๊ณ , publishEvent() ๋ฉ”์„œ๋“œ๋ฅผ ํ˜ธ์ถœ
  • ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ: ApplicationListener ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๊ตฌํ˜„ํ•˜์ง€ ์•Š๊ณ , @EventListener๋ฅผ ํ†ตํ•ด ๊ฐ„๋‹จํžˆ ๋ฉ”์„œ๋“œ ๋“ฑ๋ก
  • ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ๋Š” ์ด๋ฒคํŠธ ๋ฐœํ–‰์ž์™€ ๋™์ผ ์“ฐ๋ ˆ๋“œ์ด๋ฉฐ, ๋™๊ธฐ์ ์œผ๋กœ ํ˜ธ์ถœ๋ฉ๋‹ˆ๋‹ค. 

@TransactionalEventListener ์†Œ๊ฐœ

@TransactionalEventListener ๋Š” ํŠธ๋žœ์žญ์…˜ ๋‹จ๊ณ„(TransactionPhase) ์— ๋”ฐ๋ผ ํ˜ธ์ถœ๋˜๋Š” ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ๋ฅผ ์ •์˜ํ•˜๊ธฐ ์œ„ํ•œ ์–ด๋…ธํ…Œ์ด์…˜์ž…๋‹ˆ๋‹ค.

 

1. ํŠธ๋žœ์žญ์…˜ ๋‹จ๊ณ„๋ณ„ ๋™์ž‘

  • AFTER_COMMIT (๊ธฐ๋ณธ๊ฐ’): ํŠธ๋žœ์žญ์…˜์ด ์„ฑ๊ณต์ ์œผ๋กœ ์™„๋ฃŒ๋œ ๊ฒฝ์šฐ ์ด๋ฒคํŠธ๋ฅผ ๋ฐœ์ƒ์‹œํ‚ค๋Š” ๋ฐ ์‚ฌ์šฉ
  • AFTER_ROLLBACK: ํŠธ๋žœ์žญ์…˜์ด ๋กค๋ฐฑ ๋œ ํ›„ ์‹คํ–‰
  • AFTER_COMPLETION: ํŠธ๋žœ์žญ์…˜์ด ์™„๋ฃŒ๋œ ํ›„ ์‹คํ–‰( AFTER_COMMIT ์™€ AFTER_ROLLBACK ํฌํ•จ )
  • BEFORE_COMMIT: ํŠธ๋žœ์žญ์…˜์ด ์ปค๋ฐ‹๋˜๊ธฐ ์ง์ „์— ์ด๋ฒคํŠธ๋ฅผ ๋ฐœ์ƒ์‹œํ‚ค๋Š” ๋ฐ ์‚ฌ์šฉ

 

2. fallbackExecution=false ๋ฅผ ํ†ตํ•ด ํŠธ๋žœ์žญ์…˜์ด ์—†๋Š” ๊ฒฝ์šฐ ๋ฆฌ์Šค๋„ˆ๋Š” ์‹คํ–‰๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

/**
 * Whether the event should be handled if no transaction is running.
 */
boolean fallbackExecution() default false;

 

3. Javadoc์— ๋”ฐ๋ฅด๋ฉด @TransactionalEventListener ์—์„œ ๋ณ€๊ฒฝ๋œ ๋ฐœ์ƒ ์‚ฌํ•ญ์€ ํŠธ๋žœ์žญ์…˜์— ์ปค๋ฐ‹๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

** @TransactionalEventListener Javadoc **
WARNING: if the TransactionPhase is set to AFTER_COMMIT (the default), AFTER_ROLLBACK, or AFTER_COMPLETION, the transaction will have been committed or rolled back already, but the transactional resources might still be active and accessible. As a consequence, any data access code triggered at this point will still "participate" in the original transaction, but changes will not be committed to the transactional resource. See TransactionSynchronization.afterCompletion(int) for details.
  • ํ•ด์„
    ํŠธ๋žœ์žญ์…˜์ด ์ปค๋ฐ‹๋˜๊ฑฐ๋‚˜ ๋กค๋ฐฑ๋œ ํ›„์— ์‹คํ–‰๋˜๋Š” ์ž‘์—…์—์„œ, ํŠธ๋žœ์žญ์…˜ ๋ฆฌ์†Œ์Šค๋Š” ์•„์ง ํ™œ์„ฑ ์ƒํƒœ์ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ฆ‰, ํŠธ๋žœ์žญ์…˜์˜ ๋ฌผ๋ฆฌ์  ์—ฐ๊ฒฐ(๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ปค๋„ฅ์…˜ ๋“ฑ)์ด ์™„์ „ํžˆ ๋‹ซํžˆ์ง€ ์•Š์•˜๋‹ค๋Š” ๋œป์ž…๋‹ˆ๋‹ค. ์ด ์‹œ์ ์—์„œ DB ์“ฐ๊ธฐ ์ž‘์—…(INSERT, UPDATE ๋“ฑ)์„ ์‹œ๋„ํ•˜๋ฉด, ํŠธ๋žœ์žญ์…˜์— "์ฐธ์—ฌ"ํ•  ์ˆ˜๋Š” ์žˆ์ง€๋งŒ, ํŠธ๋žœ์žญ์…˜์ด ์ด๋ฏธ ์ข…๋ฃŒ๋˜์—ˆ๊ธฐ ๋•Œ๋ฌธ์— ์‹ค์ œ ๋ณ€๊ฒฝ ์‚ฌํ•ญ์€ ์ปค๋ฐ‹๋˜์ง€ ์•Š์Œ. ํŠธ๋žœ์žญ์…˜์ด ๋๋‚ฌ๋”๋ผ๋„ ๋ฆฌ์†Œ์Šค๊ฐ€ ์•„์ง ์™„์ „ํžˆ ํ•ด์ œ๋˜์ง€ ์•Š์•˜์œผ๋ฏ€๋กœ, DB ์“ฐ๊ธฐ ์ž‘์—… ์‹œ ์˜ˆ์ƒ์น˜ ๋ชปํ•œ ๋™์ž‘์ด ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Œ.

๊ทธ๋ž˜์„œ, @TransactionalEventListener ์—์„œ DB ์“ฐ๊ธฐ ์ž‘์—…์„ ํ•  ๊ฒฝ์šฐ, ํŠธ๋žœ์žญ์…˜ ์ „ํŒŒ ์†์„ฑ์„ REQUIRES_NEW ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ƒˆ๋กœ์šด ํŠธ๋žœ์žญ์…˜์„ ์—ด์–ด์ค˜์•ผ ํ•ฉ๋‹ˆ๋‹ค.

 

4. TransactionPhase.AFTER_COMMIT ๋‹จ๊ณ„์˜ ์˜ˆ์™ธ๋Š” DEBUG๋ชจ๋“œ

์ถ”๊ฐ€๋กœ, ๋ Œ๋”ง ๊ฐœ๋ฐœํŒ€์—์„œ ์“ด ๊ธ€์ธ ์˜ˆ์™ธ ๋จน๋Š” @TransactionalEventListener์—์„œ 'TransactionPhase.AFTER_COMMIT ๋‹จ๊ณ„์˜ ์˜ˆ์™ธ๋Š” DEBUG ๋ชจ๋“œ๋กœ ๋ฐœ์ƒ' ํ•œ๋‹ค๊ณ  ํ•ฉ๋‹ˆ๋‹ค. ํ•จ๊ป˜ ์ฝ์œผ๋ฉด ์ข‹์„ ๊ฒƒ ๊ฐ™์•„ ์ถ”๊ฐ€๋กœ ๋‚จ๊น๋‹ˆ๋‹ค.

 

๊ตฌํ˜„ ๋‹จ๊ณ„

* ๊ฐ„๋‹จํ•˜๊ฒŒ ์„ค๋ช…ํ•˜๊ธฐ ์œ„ํ•ด ์ฝ”๋“œ๋ฅผ ๊ฐ„์†Œํ™”ํ–ˆ์Šต๋‹ˆ๋‹ค.

 

1. ์ด๋ฒคํŠธ ๋ฐœํ–‰์ž: ๊ฒฐ์ œ ์„œ๋น„์Šค

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

@Slf4j
@RequiredArgsConstructor
@Service
@Transactional(readOnly = true)
public class PaymentService {

    ...
    private final ApplicationEventPublisher eventPublisher;

    @Transactional
    public PaymentReservationResponse confirmReservation(
            TossPaymentConfirm tossPaymentConfirm, PaymentConfirmServiceRequest request) {
        
        // ์ด๋ฒคํŠธ ๋ฐœํ–‰
        eventPublisher.publishEvent(new PaymentEvent(request.paymentKey()));

        // ๊ฒฐ์ œ ์Šน์ธ ๋ฐ์ดํ„ฐ INSERT
        // ์˜ˆ์•ฝ ํ™•์ • ๋ฐ์ดํ„ฐ INSERT

        return PaymentReservationResponse.of(paymentConfirmResponse, reservationResponse);
    }
}

 

2. ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ: ๊ฒฐ์ œ ์ทจ์†Œ ์š”์ฒญ

์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ๋Š” ํŠธ๋žœ์žญ์…˜ ๋กค๋ฐฑ ์‹œ์—๋งŒ ๋™์ž‘ํ•˜๋„๋ก AFTER_ROLLBACK์„ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.

์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ๋ฅผ Bean ์œผ๋กœ ๋“ฑ๋กํ•ด์ฃผ๊ธฐ ์œ„ํ•ด @Component ์–ด๋…ธํ…Œ์ด์…˜์„ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.

@Slf4j
@Component
@RequiredArgsConstructor
public class PaymentEventListener {

    private final TossClient tossClient;

    @TransactionalEventListener(phase = TransactionPhase.AFTER_ROLLBACK)
    public void handleRollback(PaymentEvent paymentEvent) {
        log.info("์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ์‹œ์ž‘");
        tossClient.cancelPayment(paymentEvent.getPaymentKey());
    }
}

 

3. ์ด๋ฒคํŠธ ํด๋ž˜์Šค

PaymentEvent๋Š” ๊ฐ„๋‹จํ•œ POJO ๊ฐ์ฒด๋กœ ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค. ์š”๊ตฌ์‚ฌํ•ญ์— ๋”ฐ๋ผ ์—”ํ‹ฐํ‹ฐ๋กœ ํ™•์žฅ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.

@Getter
public class PaymentEvent {

    private final String paymentKey;

    public PaymentEvent(String paymentKey) {
        this.paymentKey = paymentKey;
    }
}

 

์ด๋ฒคํŠธ ๊ธฐ๋ฐ˜์˜ ์žฅ์ ๊ณผ ๋‹จ์ 

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

 

๋‹ค๋งŒ, ๋‹จ์ ์€ Ctrl B ๋กœ ์ฝ”๋“œ๋ฅผ ๋”ฐ๋ผ๊ฐ€๊ธฐ๊ฐ€ ์–ด๋ ค์›Œ ์ฝ”๋“œ ํ๋ฆ„์„ ์ถ”์ ํ•˜๊ธฐ๊ฐ€ ์–ด๋ ค์›Œ ์ด๋ฒคํŠธ๋ฅผ ์–ด๋–ค ๋ฆฌ์Šค๋„ˆ๊ฐ€ ์ฒ˜๋ฆฌํ•˜๋Š”์ง€ ์ง์ ‘ ํ™•์ธํ•ด์•ผ ํ•˜๋Š” ๋ถˆํŽธํ•จ์ด ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

 

์ด๋ฒคํŠธ ๋ฐœํ–‰ ํ…Œ์ŠคํŠธ: ApplicationEventPublisher ๋ชจํ‚น ์ด์Šˆ์™€ ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•

์ด๋ฒคํŠธ๊ฐ€ ์ œ๋Œ€๋กœ ๋ฐœํ–‰๋˜์—ˆ๋Š”์ง€ ํ…Œ์ŠคํŠธํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ๊ณ ๋ฏผํ•ด๋ณด์•˜์Šต๋‹ˆ๋‹ค. ์ฒ˜์Œ์—๋Š” ApplicationEventPublisher๋ฅผ @MockBean์œผ๋กœ ์ฒ˜๋ฆฌํ•˜์—ฌ publishEvent() ๋ฉ”์„œ๋“œ์˜ ํ˜ธ์ถœ ํšŸ์ˆ˜๋ฅผ ๊ฒ€์ฆํ•˜๋ ค ํ–ˆ์ง€๋งŒ ๋ชจํ‚น์ด ๋˜์ง€ ์•Š๋Š” ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.

 

์—๋Ÿฌ ๋ฉ”์‹œ์ง€๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

-> at org.springframework.context.ApplicationEventPublisher.publishEvent(ApplicationEventPublisher.java:65)
Actually, there were zero interactions with this mock.
๊ทธ๋ฆฌ๊ณ , ์ด์™€ ๊ด€๋ จํ•ด์„œ Spring GitHub Issue๊ฐ€ ์กด์žฌํ•˜๋ฉฐ ApplicationEventPublisher ๋Š” ๋ชจํ‚น์ด ๋  ์ˆ˜ ์—†๋‹ค๊ณ  ํ•ฉ๋‹ˆ๋‹ค.

์ด๋Ÿฌํ•œ ์ด์œ ๋Š” ์Šคํ”„๋ง์˜ ํ•œ๊ณ„๋กœ ApplicationEventPublisher๋Š” regular bean ์ด๊ธฐ ๋•Œ๋ฌธ์ด๋ผ๊ณ  Issue ์—์„œ๋Š” ์„ค๋ช…ํ•ฉ๋‹ˆ๋‹ค.

 

์ด๋ฅผ ํ’€์–ด ์„ค๋ช…ํ•˜์ž๋ฉด ApplicationEventPublisher๋Š” ์Šคํ”„๋ง ์ปจํ…์ŠคํŠธ์˜ ์—ฌ๋Ÿฌ ์ด๋ฒคํŠธ ๊ธฐ๋ฐ˜ ์ปดํฌ๋„ŒํŠธ(ex. @EventListener๋กœ ๊ตฌํ˜„๋œ ๋ฆฌ์Šค๋„ˆ)์—์„œ ์‚ฌ์šฉ๋˜๋ฉฐ, ๋ชจํ‚นํ•˜๋ฉด ๋‹ค๋ฅธ ์ด๋ฒคํŠธ ๊ธฐ๋ฐ˜์˜ ์ปดํฌ๋„ŒํŠธ์— ์˜ˆ์ƒ์น˜ ๋ชปํ•œ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•˜๊ธฐ ๋•Œ๋ฌธ์— @MockBean ์ฒ˜๋ฆฌ๊ฐ€ ์•ˆ๋˜์ง€ ์•Š์„๊นŒ ์ƒ๊ฐํ•ด๋ด…๋‹ˆ๋‹ค.

Spring Context์˜ ๊ธฐ๋ณธ Bean๋“ค

 

ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•: @RecordApplicationEvents์™€ ApplicationEvents ํ™œ์šฉ

@RecordApplicationEvents๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ApplicationContext์—์„œ ๋ฐœ์ƒํ•œ ๋ชจ๋“  ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ด๋ฒคํŠธ๋ฅผ ๊ธฐ๋กํ•˜๊ณ , ์ด๋ฅผ ํ…Œ์ŠคํŠธ ๋‚ด์—์„œ ๊ฒ€์ฆํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

* ์ฐธ๊ณ : Make it possible to mock ApplicationEventPublisher in Spring Boot tests

 

@RecordApplicationEvents๋ž€?

  • @RecordApplicationEvents๋Š” ํ…Œ์ŠคํŠธ ํด๋ž˜์Šค์— ์ ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ์–ด๋…ธํ…Œ์ด์…˜์œผ๋กœ, Spring TestContext Framework์— ํ…Œ์ŠคํŠธ ์‹คํ–‰ ์ค‘ ๋ฐœ์ƒํ•œ ๋ชจ๋“  ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ด๋ฒคํŠธ๋ฅผ ๊ธฐ๋กํ•˜๋„๋ก ์ง€์‹œํ•ฉ๋‹ˆ๋‹ค.
  • ๊ธฐ๋ก๋œ ์ด๋ฒคํŠธ๋Š” ApplicationEvents API๋ฅผ ํ†ตํ•ด ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค

* ์ฐธ๊ณ : @RecordApplicationEvents Spring Docs 

 

ํ…Œ์ŠคํŠธ ํด๋ž˜์Šค์— @RecordApplicationEvents ์–ด๋…ธํ…Œ์ด์…˜์„ ์ถ”๊ฐ€ํ•˜๊ณ , ApplicationEvents ๋ฅผ ์ฃผ์ž…ํ•ฉ๋‹ˆ๋‹ค.

๊ทธ๋ฆฌ๊ณ , events.stream(T.class).count() ๋ฅผ ํ†ตํ•ด ์ด๋ฒคํŠธ ๋ฐœํ–‰ ์ˆ˜๋ฅผ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค.

 

๋‹ค์Œ์€ ์ „์ฒด ํ…Œ์ŠคํŠธ ์ฝ”๋“œ์ž…๋‹ˆ๋‹ค.

@RecordApplicationEvents
class PaymentServiceTest extends IntegrationTestSupport { // IntegrationTestSupport ๋Š” ์ „์ฒด ํ…Œ์ŠคํŠธ ์‹œ 1๋Œ€์˜ ์Šคํ”„๋ง๋งŒ ๋„์šฐ๊ธฐ ์œ„ํ•œ ์ถ”์ƒ ํด๋ž˜์Šค๋กœ @SpringBootTest ์–ด๋…ธํ…Œ์ด์…˜์ด ์กด์žฌ

    @Autowired
    private PaymentService paymentService;

    @MockBean
    private ReservationServiceV2 reservationServiceV2;

    @Autowired
    private ReservationRepository reservationRepository;

    @Autowired
    private PaymentTemporaryRepository paymentTemporaryRepository;

    @Autowired
    private TossPaymentConfirmRepository tossPaymentConfirmRepository;

    @Autowired
    private ApplicationEvents events;

    @AfterEach
    void tearDown() {
        ...
    }
    
    @DisplayName("๊ฒฐ์ œ ๋ฐ ์˜ˆ์•ฝ์„ ํ™•์ •์ง€์„ ๋•Œ, ๋…ผ๋ฆฌ ํŠธ๋žœ์žญ์…˜์ด ๋กค๋ฐฑ๋˜๋ฉด ๋ฌผ๋ฆฌ ํŠธ๋žœ์žญ์…˜์ด ๋กค๋ฐฑ๋˜๋ฉฐ ์ด๋ฒคํŠธ๊ฐ€ ๋ฐœํ–‰๋œ๋‹ค")
    @Test
    void confirmReservationWithException() {
        // given
        TossPaymentConfirm tossPaymentConfirm = TossPaymentConfirm.builder()
                .paymentKey("๊ฒฐ์ œ ๊ณ ์œ  ํ‚ค")
                .orderId("์ฃผ๋ฌธ ๋ฒˆํ˜ธ")
                .orderName("์ฃผ๋ฌธ ์ด๋ฆ„")
                .mId("์ƒ์  ์•„์ด๋””")
                .taxExemptionAmount(50000)
                .status(TossPaymentStatus.READY)
                .requestedAt("๊ฒฐ์ œ ์‹œ๊ฐ„")
                .build();

        PaymentConfirmServiceRequest request = new PaymentConfirmServiceRequest(
                1L,
                1L,
                1L, "paymentKey", "amount", "orderId"
        );

        given(reservationServiceV2.confirmReservation(anyLong(), anyLong()))
                .willThrow(new RuntimeException());

        // when
        assertThatThrownBy(() -> paymentService.confirmReservation(tossPaymentConfirm, request))
                .isInstanceOf(RuntimeException.class);

        // then
        assertThat(tossPaymentConfirmRepository.findAll()).hasSize(0);
        assertThat(reservationRepository.findAll()).hasSize(0);

        long count = events.stream(PaymentEvent.class).count();
        assertThat(count).isEqualTo(1);
    }
}

 

์ด๋ฅผ ํ†ตํ•ด, ์„œ๋น„์Šค ํŠธ๋žœ์žญ์…˜์ด ๋กค๋ฐฑ๋์„ ๋•Œ ์ด๋ฒคํŠธ๊ฐ€ ๋ฐœํ–‰๋˜์—ˆ๋Š”์ง€ ๊ฐ„์ ‘์ ์œผ๋กœ ๊ฒ€์ฆํ•  ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

๋‹ค๋งŒ ์—ฌ๋Ÿฌ ์ด๋ฒคํŠธ๊ฐ€ ์žˆ์„ ๊ฒฝ์šฐ, count() ๋กœ๋งŒ ๊ฒ€์ฆํ•˜๊ธฐ์—๋Š” ๋ฌด๋ฆฌ๊ฐ€ ์žˆ๋‹ค๊ณ  ํŒ๋‹จ์ด ๋˜๋Š”๋ฐ์š”. ์ด ๋ถ€๋ถ„์€ ์•ž์œผ๋กœ ํ•™์Šต์ด ํ•„์š”ํ•œ ๋ถ€๋ถ„์ด๋ผ ์ƒ๊ฐํ•ฉ๋‹ˆ๋‹ค.

 

์ด์ƒ์œผ๋กœ ํŠธ๋žœ์žญ์…˜ ๋กค๋ฐฑ ์‹œ ๊ฒฐ์ œ ์ทจ์†Œ ์š”์ฒญํ•˜๊ธฐ - @TransactionalEventListener &  ApplicationEventPublisher ๋ชจํ‚น ์ด์Šˆ๋ฅผ ๋งˆ์น˜๊ฒ ์Šต๋‹ˆ๋‹ค.