1. ์๋ก
์ ๊ฒ์๊ธ์์ Spring Event๋ก ์์กด์ฑ์ ๊ฐ์ ํ๋ ๋ฒ์ ๋ค๋ค๋ณด์๋ค. ํ์ง๋ง ํ ํธ๋์ญ์ ์์์ ์ฒ๋ฆฌํ๋ ๊ฒ๋ค์ด ํฉ์ด์ง๋ฉด์ ๋ฌธ์ ๋๋ ๋ถ๋ถ๋ ์๋ค. ์ด๋ฒ์ ๊ทธ ๋ถ๋ถ์ ๊น๊ฒ ๋ค๋ค๋ณผ ์๊ฐ์ด๋ค.
Event๋ฅผ ์ ์ฉํ๊ธฐ ์ ์ฝ๋์ ํ ์ฝ๋๋ฅผ ๋จผ์ ์ดํด๋ณด๋ฉด ์๋์ ๊ฐ๋ค.
- ์ ์ฉ ์
@Service
@RequiredArgsConstructor
public class MemberService {
private final AlarmService alarmService;
private final AlimTalkService alimTalkService;
public void register(String name) {
// ํ์๊ฐ์
์ฒ๋ฆฌ ๋ก์ง
System.out.println("ํ์ ์ถ๊ฐ ์๋ฃ");
// ๊ฐ์
์ถํ ํธ์ ์๋ฆผ
alarmService.send(name);
// ์นด์นด์ค ์๋ฆผํก ์ ์ก
alimTalkService.send(name);
}
}
์ ๋ก์ง์ ํต์ฌ ๋ก์ง๊ณผ ๋ถ๊ฐ์ ์ธ ๋ก์ง์ด ๋ฌถ์ฌ์์ด ํ๋์ ๋ณด๊ธฐ ์ด๋ ค์ธ ๋ฟ๋๋ฌ ์์กด์ฑ์ด ๋์ ์ ์ง๋ณด์๊ฐ ์ด๋ ต๋ค.
์์ ์์ ๋ฅผ ์ ๋ฆฌํด๋ณด๋ฉด ๋ค์๊ณผ ๊ฐ์ ๋น์ฆ๋์ค ๋ก์ง์ ํํํ๊ณ ์๋ค.
- ํ์๊ฐ์
๋ก์ง์ ์ฒ๋ฆฌํ ๋
- ๊ฐ์ ์ถํ ํธ์ ์๋ฆผ
- ์นด์นด์ค ์๋ฆผํก ์ ์ก
- ์ ์ฉ ํ
@Service
@RequiredArgsConstructor
@Transactional
public class MemberService {
private final ApplicationEventPublisher publisher;
public void register(String name) {
// ํ์๊ฐ์
์ฒ๋ฆฌ ๋ก์ง
System.out.println("ํ์ ์ถ๊ฐ ์๋ฃ");
// ์ด๋ฒคํธ ๋ฑ๋ก
publisher.publishEvent(new RegisteredEvent(name));
}
}
@Component
@RequiredArgsConstructor
public class SmsEventHandler {
private final AlarmService alarmService;
private final AlimTalkService alimTalkService;
@Async
@EventListener
public void sendFCM(RegisteredEvent event) throws InterruptedException {
alarmService.send(event.getName());
}
@Async
@EventListener
public void sendAlimTalk(RegisteredEvent event) throws Exception {
alimTalkService.send(event.getName());
}
}
@Service
@RequiredArgsConstructor
@Transactional
public class AlarmService {
private final AlarmRepository alarmRepository;
public void send(String name) throws InterruptedException {
alarmRepository.save(Alarm.builder().createdAt(LocalDateTime.now()).build());
System.out.println(name + "์๊ฒ push ์๋ฆผ ๋ฐ์ก");
}
}
@Service
@Transactional
public class AlimTalkService {
public void send(String name) throws Exception {
Thread.sleep(3000); // 3์ด sleep
System.out.println(name + "์๊ฒ ์๋ฆผํก ๋ฐ์ก");
throw new Exception("์๋ฌ ๋ฐ์");
}
}
์ ์ฝ๋์์ ๋ง์ฝ ํ์์ด ์ ์ฅ๋๋ ์ค ์๋ฌ๊ฐ ๋ฐ์ํ๋ฉด ์๋ฆผ์ ๋ณด๋ด๋ ๋ก์ง๋ค์ ์ด๋ป๊ฒ๋ ๊น??
Async๋ฅผ ์ฌ์ฉํ๊ณ ๋ค๋ฅธ ์ฐ๋ ๋์์ ํธ๋์ญ์ ์ด ์คํ๋๊ธฐ ๋๋ฌธ์ ์๋ก ๋ ๋ฆฝ์ ์ผ๋ก ์ปค๋ฐ๋๋ค. ๋ฐ๋ผ์ ํ์๊ฐ์ ์ ๋์ง ์์์ง๋ง ์๋ฆผ์ ์ ์ก๋๋ ์ข์ง ๋ชปํ ์ํฉ์ด ๋ฐ์ํ๋ค.
2. ๋ฌธ์ ๊ฐ ๋๋ ์ํฉ๋ค
- 1. event๋ฅผ ๋น๋๊ธฐ๋ก ์ฒ๋ฆฌ & memberService ์์ ์์ธ๊ฐ ๋ฐ์ํ ๊ฒฝ์ฐ
@Transactional
public void register(String name) throws Exception {
// ํ์๊ฐ์
์ฒ๋ฆฌ ๋ก์ง
memberRepository.save(Member.builder().name(name).build());
// ์ด๋ฒคํธ ๋ฑ๋ก
publisher.publishEvent(new RegisteredEvent(name));
// ์์ธ ๋ฐ์
throw new RuntimeException("member service ์๋ฌ");
}
}
memberService์์ ์์ธ๊ฐ ํฐ์ก์ง๋ง ๋น๋๊ธฐ๋ก ์คํ๋ ํธ๋์ญ์ ์ ์ ์์ ์ผ๋ก ์ปค๋ฐ๋จ
- 2. event๋ฅผ ๋๊ธฐ๋ก ์ฒ๋ฆฌ & ์๋์ ๋ณด๋ด๋ ์ค ์๋ฌ๊ฐ ๋ ๊ฒฝ์ฐ
@Service
@RequiredArgsConstructor
public class MemberService {
private final ApplicationEventPublisher publisher;
private final MemberRepository memberRepository;
@Transactional
public void register(String name) throws Exception {
// ํ์๊ฐ์
์ฒ๋ฆฌ ๋ก์ง
memberRepository.save(Member.builder().name(name).build());
// ์ด๋ฒคํธ ๋ฑ๋ก
publisher.publishEvent(new RegisteredEvent(name));
}
}
@Service
@RequiredArgsConstructor
@Transactional
public class AlarmService {
private final AlarmRepository alarmRepository;
public void send(String name) throws InterruptedException {
alarmRepository.save(Alarm.builder().createdAt(LocalDateTime.now()).build());
System.out.println(name + "์๊ฒ push ์๋ฆผ ๋ฐ์ก");
// ์์ธ ๋ฐ์
throw new RuntimeException("member service ์๋ฌ");
}
}
member ์ ์ฅ์ ์ ์์ ์ผ๋ก ๋์์ง๋ง ๋ถ๊ฐ์ ์ธ ์์ ์ ํ๋ ์ค ์๋ฌ๊ฐ ๋ ๊ฒ์ด๋ค. ํ์ง๋ง member ์ ์ฅํ๋ ๋ถ๋ถ๊น์ง ๋กค๋ฐฑ๋๋ฉด์ ์ฟผ๋ฆฌ๊ฐ ๋๊ฐ์ง ์์๋ค.
์ด๋ฐ์๋ ๋ง์ ๊ฒฝ์ฐ์ ์๊ฐ ์์ง๋ง ์ด์ ๋๊น์ง๋ง ์๊ฐํ๊ฒ ๋ค. ๊ทธ๋ผ ์ด๋ป๊ฒ ํด๊ฒฐํ ์ ์์๊น??
3. TransactionalEventListener
์ด๋ฌํ ๋ฌธ์ ๋ @TransactionEventListener๋ก ํด๊ฒฐํ ์ ์๋ค. @TransactionEventListener๋ Event์ ์ค์ง์ ์ธ ๋ฐ์์ ํธ๋์ญ์ ์ ์ข ๋ฃ๋ฅผ ๊ธฐ์ค์ผ๋ก ์ผ๋๋ค.
- @TransactionalEventListener ์ต์
@TransactionalEventListener์ ์ด์ฉํ๋ฉด ํธ๋์ญ์ ์ ์ด๋ค ํ์ด๋ฐ์ ์ด๋ฒคํธ๋ฅผ ๋ฐ์์ํฌ ์ง ์ ํ ์ ์๋ค. ์ต์ ์ TransactionPhase์ ์ฌ์ฉํ์ฌ ์ง์ ํ ์ ์๊ณ , ๋ค์๊ณผ ๊ฐ์ ์ต์ ์ด ์๋ค.
- AFTER_COMMIT (๊ธฐ๋ณธ๊ฐ) - ํธ๋์ญ์ ์ด ์ฑ๊ณต์ ์ผ๋ก ๋ง๋ฌด๋ฆฌ(commit)๋ฌ์ ๋ ์ด๋ฒคํธ ์คํ
- AFTER_ROLLBACK – ํธ๋์ญ์ ์ด rollback ๋ฌ์ ๋ ์ด๋ฒคํธ ์คํ
- AFTER_COMPLETION – ํธ๋์ญ์ ์ด ๋ง๋ฌด๋ฆฌ ๋์ ๋(commit or rollback) ์ด๋ฒคํธ ์คํ
- BEFORE_COMMIT - ํธ๋์ญ์ ์ ์ปค๋ฐ ์ ์ ์ด๋ฒคํธ ์คํ
- Handler ํด๋์ค์ AFTER_COMPLETION ์ต์ ์ ๋ฌ๊ณ
- ์๋ฆผ์ ๋ฐ์กํ๋ค ์์ธ๊ฐ ํฐ์ง๋ ์ํฉ์ ๊ฐ์ ํด๋ณด๊ฒ ๋ค.
@Component
@RequiredArgsConstructor
public class SmsEventHandler {
private final AlarmService alarmService;
private final AlimTalkService alimTalkService;
// @Async
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void sendFCM(RegisteredEvent event) throws InterruptedException {
alarmService.send(event.getName());
}
// @Async
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void sendAlimTalk(RegisteredEvent event) throws Exception {
alimTalkService.send(event.getName());
}
}
์๊น ์์์ ๋ฌธ์ ๋๋ ์ํฉ์ ์๊ฐํ๋๋ฐ, ๊ทธ๋ member๊น์ง ์ ์ฅ๋์ง ์์๋ค.
ํ์ง๋ง ์ด๋ฒ์ member๋ ์ ์ ์ฅ๋๊ณ ์์ธ๊ฐ ํฐ์ง๋ ๋ชจ์ต์ ๋ณผ ์ ์๋ค.
AFTER_COMPLETION ์ฃผ์ ์ฌํญ
TransactionalEventListener์ ์ฌ์ฉํ์ฌ ์ด๋ฒคํธ ๊ตฌ์กฐ๋ฅผ ๋์ ํ์ฌ ๊ฐ๊ฒฐํ ์ฝ๋ ๊ตฌ์กฐ๋ฅผ ์ ์งํ ์ ์์ด์ ์ฅ์ ์ด ์์ง๋ง, ํธ๋์ญ์ ๊ณผ ํจ๊ป ์ด๋ฒคํธ๋ฅผ ์ฒ๋ฆฌํ ๋ ์ฃผ์์ฌํญ์ด ์๋ค. phase ๊ฐ์ด AFTER_COMMIT ์ผ๋ก ์ ์ํด๋์ ๊ฒฝ์ฐ ๋ฆฌ์ค๋ ์ฝ๋ ์์์ ๋ค์ ํธ๋์ญ์ ์ ์ฒ๋ฆฌํ๋ฉด ํด๋น ํธ๋์ญ์ ์ ์ปค๋ฐ๋์ง ์๋ ํ์์ด ๋ฐ์ํ๋ค.
@Component
@RequiredArgsConstructor
public class SmsEventHandler {
private final AlarmService alarmService;
private final AlimTalkService alimTalkService;
// @Async
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void sendFCM(RegisteredEvent event) throws InterruptedException {
alarmService.send(event.getName());
}
// @Async
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void sendAlimTalk(RegisteredEvent event) throws Exception {
alimTalkService.send(event.getName());
}
}
@Service
@RequiredArgsConstructor
@Transactional
public class AlarmService {
private final AlarmRepository alarmRepository;
// ํธ๋์ญ์
commit ์ ๋จ!!!!
public void send(String name) throws InterruptedException {
alarmRepository.save(Alarm.builder().createdAt(LocalDateTime.now()).build());
System.out.println(name + "์๊ฒ push ์๋ฆผ ๋ฐ์ก");
}
}
์ด ํ์์ ๋ํ ์์ธ์ TransactionSynchronization ์ afterCommit ์ฃผ์๋ถ๋ถ์ ์ค๋ช ๋์ด ์๋ค.
/** * Invoked after transaction commit. Can perform further operations right * <i>after</i> the main transaction has <i>successfully</i> committed. * <p>Can e.g. commit further operations that are supposed to follow on a successful * commit of the main transaction, like confirmation messages or emails. * <p><b>NOTE:</b> The transaction will have been committed 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, allowing to perform some cleanup (with no commit following * anymore!), unless it explicitly declares that it needs to run in a separate * transaction. Hence: <b>Use {@code PROPAGATION_REQUIRES_NEW} for any * transactional operation that is called from here.</b> * @throws RuntimeException in case of errors; will be <b>propagated to the caller</b> * (note: do not throw TransactionException subclasses here!) **/
์์ฝํ์๋ฉด, ์ด์ ์ ์ด๋ฒคํธ๋ฅผ publish ํ๋ ์ฝ๋์์ ํธ๋์ญ์ ์ด ์ด๋ฏธ ์ปค๋ฐ ๋์๊ธฐ ๋๋ฌธ์ AFTER_COMMIT ์ดํ์ ์๋ก์ด ํธ๋์ญ์ ์ ์ํํ๋ฉด ํด๋น ๋ฐ์ดํฐ์์ค ์์์๋ ํธ๋์ญ์ ์ ์ปค๋ฐํ์ง ์๋๋ค๋ ๊ฒ์ด๋ค.
๋ฐ๋ผ์ @Transactional ์ด๋ ธํ ์ด์ ์ ์ ์ฉํ ์ฝ๋์์ PROPAGATION_REQUIRES_NEW ์ต์ ์ ์ง์ ํ์ง ์๋๋ค๋ฉด (๋งค๋ฒ ์๋ก์ด ํธ๋์ญ์ ์ ์ด์ด์ ๋ก์ง์ ์ฒ๋ฆฌํ๋ผ๋ ์๋ฏธ) ์ด๋ฒคํธ ๋ฆฌ์ค๋์์ ํธ๋์ญ์ ์ ์์กดํ ๋ก์ง์ ์คํํ์ ๊ฒฝ์ฐ ์ด ํธ๋์ญ์ ์ ์ปค๋ฐ๋์ง ์๋๋ค.
ํด๊ฒฐ ๋ฐฉ๋ฒ 1)
์ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๋ ์ฒซ ๋ฒ์งธ ๋ฐฉ๋ฒ์ผ๋ก๋ AFTER_COMMIT ์ดํ์ ๋์ผํ ๋ฐ์ดํฐ์์ค๋ฅผ ์ฌ์ฉํ์ง ์๋ ๋ฐฉ๋ฒ์ด ์๋ค. ์ด๋ฅผ ์ํด์๋ ์ด๋ฒคํธ ๋ฆฌ์ค๋๋ฅผ ๋ณ๋์ ์ค๋ ๋์์ ์คํํ๋ ๋ฐฉ๋ฒ์ด ์๋ค. ๋ฐ๋ก @Async ์ด๋ ธํ ์ด์ ์ ์ถ๊ฐํ๋ ๋ฐฉ๋ฒ์ด๋ค.
@Async
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void sendAlimTalk(RegisteredEvent event) throws Exception {
alimTalkService.send(event.getName());
}
์ ์ฝ๋๋ ์ด๋ฒคํธ ๋ฆฌ์ค๋ ๋ก์ง์ด ๋ณ๋์ ์ค๋ ๋์์ ์คํ๋์ด ํธ๋์ญ์ ์ด ์ปค๋ฐ๋๊ธฐ ๋๋ฌธ์ ์๋ํ ๊ฒฐ๊ณผ๋ฅผ ์ป์ ์ ์๋ค.
๋ํ ๋ฌธ์ ๊ฐ ๋๋ ์ํฉ 1๋ฒ์งธ, 2๋ฒ์งธ ๋ฌธ์ ๋ ์ ๋ถ ํด๊ฒฐํ ์ ์๋ค.
ํด๊ฒฐ ๋ฐฉ๋ฒ 2)
๋๋ฒ์งธ ๋ฐฉ๋ฒ์ผ๋ก @TransactionalEventListener ์ด๋ ธํ ์ด์ ์ ์ถ๊ฐ๋ก @Transactional(propagation = Propagation.REQUIRES_NEW) ์ ๋ถ์ฌ์ฃผ๋ ๋ฐฉ๋ฒ์ด๋ค.
์ด๋ ๊ฒ ํ๋ฉด ์ด๋ฒคํธ ๋ฆฌ์ค๋์ ๋ก์ง ์์์ ์คํ๋๋ @Transactional ๋ก์ง์ ์ํ ์๋ก์ด ํธ๋์ญ์ ์ด ์ด์ ์ ํธ๋์ญ์ ๊ณผ ๊ตฌ๋ถ๋์ด ์๋กญ๊ฒ ์์ํ๋ค. ๋ฐ๋ผ์ ์ด๋ฒคํธ๋ฅผ ๋ฐ์์ํจ ํธ๋์ญ์ ๊ณผ๋ ๋ณ๋์ ๋ถ๋ฆฌ๋ ํธ๋์ญ์ ์์์ ์ด๋ฒคํธ ๋ฆฌ์ค๋ ๋ก์ง์ด ์คํ๋๋ค.
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void sendAlimTalk(RegisteredEvent event) throws Exception {
alimTalkService.send(event.getName());
}