본문 바로가기
Back-End/Spring MVC

ApplicationEventPublisher: How to use events in Spring | @Async

by 달의 조각 2022. 9. 5.

 

ApplicationEventPublisher는 ApplicationContext가 상속받는 인터페이스(기능) 중 하나이다.

 

 

이벤트란?

 

회원 가입을 하면 이메일을 전송하는 서비스가 있다고 가정하자.
핵심 로직은 회원 가입 서비스이고, 이메일 전송은 부가적인 이벤트라고 볼 수 있다.

  1. 하나의 서비스에 여러 로직이 섞여 있으면 유지보수가 어렵고 코드가 복잡해진다.
  2. 이메일 전송 중에 문제가 생긴다면? 여러 로직이 존재할 때 이 모든 로직을 롤백 하는 것은 비효율적이다.

Service 클래스에는 회원 가입 로직만 남기고 이메일 전송 로직은 개별 이벤트로 처리할 수 있다!

 

로직 사이에 이벤트 Publisher 배치 → 이벤트를 던진다! → 리스너가 이를 캐치해서 이벤트 로직 실행

 

🌿 이벤트 클래스 생성하기

이 클래스는 정보를 담는 용도로 사용한다.
@EventListener 애너테이션을 붙이면 ApplicationEvent 상속을 생략할 수 있다.

@Getter
public class RegisteredEvent {

    private Member member;

    public RegisteredEvent(Member member) {
        this.member = member;
    }
}

 

🌿 서비스 구성하기

ApplicationEventPublisher를 주입받으면 이벤트 알림 기능을 사용할 수 있다.
필요한 부분에 배치해서 필요한 정보를 담아서 이벤트를 던진다!

@Service
public class MemberService {

    @Autowired
    ApplicationEventPublisher eventPublisher;

    public Member createMember(Member member) {
        Member savedMember = memberRepository.save(member);
        
        eventPublisher.publishEvent(new RegisteredEvent(savedMember)); //회원가입 알림
        return savedMember;
    }
}

 

🌿 이벤트 핸들러 생성하기

메서드에 @EventListener를 붙인다.
이벤트 클래스를 매개변수로 받아서 데이터를 수신하여 이벤트 처리를 할 수 있다.

🌟 Point!
회원 가입 후 이메일을 전송하는 기능은 중간에 메일 서버를 거치기 때문에 시간이 소요된다.
핵심 로직이 아닌 기능에서 발생하는 대기 시간은 불필요하므로 별도의 쓰레드를 둬서 비동기적으로 처리하자.

  1. ExecutorService는 재사용이 가능한 쓰레드풀이다.
    newSingleThreadExecutor()은 쓰레드 1개인 ExecutorService를 리턴 한다.
    싱글 쓰레드에서 동작해야 하는 작업을 처리할 때 사용한다.
    submit() 메서드 안에 싱글 쓰레드로 처리할 작업을 넣는다.
  2. 메서드 앞에 @Async를 붙이면 비동기적으로 처리할 수 있다.
    Application에는 @EnableAsync를 붙이자.

이렇게 하면 회원 가입을 하는 동시에 비동기적으로 별도의 쓰레드 풀에서 이메일 전송 로직이 실행된다!

@Component
@Slf4j
public class EmailEventHandler {

    private final EmailSender emailSender;
    private final MemberService memberService;

    public EmailEventHandler(EmailSender emailSender, MemberService memberService) {
        this.emailSender = emailSender;
        this.memberService = memberService;
    }

    @Async
    @EventListener
    public void memberDataRollback(RegisteredEvent event) throws InterruptedException {

        ExecutorService executorService = Executors.newSingleThreadExecutor();
        executorService.submit(() -> {
            try {
                emailSender.sendEmail("any email message");
            } catch (Exception e) {
                log.error("MailSendException happened: ", e);
                Member member = event.getMember();
                memberService.deleteMember(member.getMemberId());
                throw new RuntimeException(e);
            }
        });
    }
}

 

학습을 위해 try 안의 sendEmail() 메서드는 예외가 발생되도록 작성되어 있다.
트랜잭션 학습을 위해 이메일 전송 중 오류가 발생하면 롤백을 하도록 처리하는 의도였지만,
코드상 두 개의 쓰레드에서 로직이 실행되고, 다른 쓰레드에 접근하는 것은 복잡하고 단점이 존재한다.
그래서 catch문에서는 memberService 클래스의 deleteMember를 통해 멤버가 삭제되도록 했다.

 

++

Member를 삭제할 때, 연관 관계에 있는 Stamp 엔티티가 존재해서 외래키 제약 조건을 위반한다는 에러가 발생한다. 그래서 아래와 같이 orphanRemoval = true 속성을 넣었다.

CascadeType.ALL이나 CascadeType.REMOVE를 사용해도 삭제가 되는데 무슨 차이일까?

부모가 자식의 전체 생명 주기를 관리한다는 측면에서는 동일하지만,
CascadeType의 경우 부모 엔티티가 자식 엔티티의 관계를 삭제해도 자식 엔티티는 삭제되지 않는다.
orphanRemoval의 경우 부모 엔티티가 삭제되면 자식 엔티티도 삭제된다.

@OneToOne(mappedBy = "member", cascade = CascadeType.PERSIST, orphanRemoval = true)
private Stamp stamp;

 

 

📚 Reference

댓글