๊ฒฐ์ ํ๋ก์ธ์ค๋ฅผ ์ค๊ณํ๋ฉด์, ๊ธฐ์กด์๋ 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.
์ด๋ฌํ ์ด์ ๋ ์คํ๋ง์ ํ๊ณ๋ก ApplicationEventPublisher๋ regular bean ์ด๊ธฐ ๋๋ฌธ์ด๋ผ๊ณ Issue ์์๋ ์ค๋ช ํฉ๋๋ค.
์ด๋ฅผ ํ์ด ์ค๋ช ํ์๋ฉด ApplicationEventPublisher๋ ์คํ๋ง ์ปจํ ์คํธ์ ์ฌ๋ฌ ์ด๋ฒคํธ ๊ธฐ๋ฐ ์ปดํฌ๋ํธ(ex. @EventListener๋ก ๊ตฌํ๋ ๋ฆฌ์ค๋)์์ ์ฌ์ฉ๋๋ฉฐ, ๋ชจํนํ๋ฉด ๋ค๋ฅธ ์ด๋ฒคํธ ๊ธฐ๋ฐ์ ์ปดํฌ๋ํธ์ ์์์น ๋ชปํ ๋ฌธ์ ๊ฐ ๋ฐ์ํ๊ธฐ ๋๋ฌธ์ @MockBean ์ฒ๋ฆฌ๊ฐ ์๋์ง ์์๊น ์๊ฐํด๋ด ๋๋ค.
ํด๊ฒฐ ๋ฐฉ๋ฒ: @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 ๋ชจํน ์ด์๋ฅผ ๋ง์น๊ฒ ์ต๋๋ค.