GrowMe

[Spring] 이벤트를 처리하는 방법 본문

About Spring

[Spring] 이벤트를 처리하는 방법

오늘도 타는중 2022. 7. 12. 23:54
Event를 처리하는 방법
# Event
# 이벤트 처리하기
# ApplicationEvert
# ApplicationEventPublisher
# ApplicationListener
# 이벤트 발행
# ApplicationListener

*Event란?

'이벤트 발생' 이라는 의미는 '회원 정보 저장', '회원 정보 업데이트' 등의 어떤 기능이 처리됨을 의미합니다. 이처럼, 어떠한 기능을 이벤트로 정의하고, 그 이벤트가 발생할 때 어떠한 추가적인 처리를 하도록 구현을 할 상황이 필요하곤 합니다. 아래와 같이, 회원가입 후 축하 메시지 전송 및 쿠폰 전송하는 기능있다고 가정할 때, 아래의 코드는 몇 가지 문제점이 존재합니다.

@Service
@Transactional
public class RegisterService {

  @Autowired
  ApplicationEventPublisher publisher;

  public void register(String name) {
    // 회원가입 처리 로직
    System.out.println("회원 추가 완료");
    
    // 가입 축하 메세지 전송
    System.out.println(name + "님에게 가입 축하 메세지를 전송했습니다.");
    
    // 가입 축하 쿠폰 발급
    System.out.println(name + "님에게 쿠폰을 전송했습니다.");
  }
}
  1. 강한 결합도
    회원가입 서비스에 가입에 대한 기능 뿐만 아니라, 메시지 전송 기능 & 쿠폰 발급 기능이 모두 섞여 있습니다. 이처럼 결합도가 강할 경우, 추후에 유지보수가 어려워지고, 코드의 복잡도도 올라가게 될 것입니다.

  2. 트랜잭션
    메시지 전송 중 실패 시, 저장되었던 회원 정보까지 모두 롤백이 되기에 이는 적절치 못한 처리라고 할 수 있습니다.
    따라서, 회원 가입 / 축하 메시지, 쿠폰발급 <- 이렇게 나눠서 처리를 해주는 것이 필요합니다.

  3. 성능
    위 코드처럼 처리할 경우, 회원가입 요청 후, 메시지 전송, 쿠폰 발급이 완료될 때까지 회원가입 처리가 지연됩니다. 즉, 메시지 전송, 쿠폰발급하는데 1분씩 소요된다고하면, 고객이 회원가입 완료 화면을 보는데까지 2분이상 기다려야 된다는 것입니다.

 

-> 이와 같은 문제를 해결하기위해, 원하는 기능을 이벤트로 정의하고 본기능과 분리하여 처리해봅시다.


*이벤트 처리 방법

1. 이벤트 클래스 만들기

@Getter
public class PostEvent extends ApplicationEvent {

    private final Member savedmember;

    public PostEvent(Object obj, Member savedmember){
        super(obj);
        this.savedmember = savedmember;
    }
  • ApplicationEvert를 상속받아야 하나의 이벤트로 인식된다.
  • 생성자로 이벤트를 발생시킨 주체 클래스(서비스)를 필수로 입력받아야 하며, 이벤트 발생시 생겨난 데이터(savedmember)를 전달받아 이벤트 후처리를 할 EventLister에서 접근할 수 있도록 가능하다.
  • 전달받은 주체 클래스(서비스)는 super()을 통해 ApplicationEvent 클래스로 넘겨주어야 한다.

2. 서비스에서 이벤트를 정의하고, 발행하기

@Slf4j
@Transactional
@Service
public class MemberService {
    private final MemberRepository memberRepository;
    private final ApplicationEventPublisher applicationEventPublisher;

    public MemberService(MemberRepository memberRepository, ApplicationEventPublisher applicationEventPublisher) {
        this.memberRepository = memberRepository;
        this.applicationEventPublisher = applicationEventPublisher;
    }

    @Transactional
    public Member createMember(Member member) {
        verifyExistsEmail(member.getEmail());
        Member savedMember = memberRepository.save(member);
        PostEvent postEvent = new PostEvent(this, savedMember);
        applicationEventPublisher.publishEvent(postEvent);
        
        return savedMember;
    }
}
  • 이벤트 발행 기능을 사용하기 위해, ApplicationEventPublisher 을 주입받는다.
  • 위에서 만든 PostEvent 클래스를 생성(이벤트 발생 주체인 MemberServic를 넘겨주고, 생성된 데이터 savedMember도 넘겨준다)하여 이벤트를 정의한다.
  • ApplicationEventPublisher의 publishEvent를 통해 정의한 이벤트를 발행한다.

3. 발행한 이벤트를 받아서 처리할 이벤트 핸들러 만들기

@EnableAsync
@RequiredArgsConstructor
@Configuration
@Component

public class EventLister implements ApplicationListener<PostEvent> {

    private final EmailSender emailSender;
    private final MemberService memberService;

    @Async
//    @EventListener
    public void onApplicationEvent(PostEvent postEvent) {
            try {
            // 이메일을 보낸다
                emailSender.sendEmail("any email message");
            } catch (Exception e) {
            // 이메일 발송 실패 시, 저장했던 회원 정보를 삭제한다
                log.error("MailSendException happened: ", e);
                memberService.deleteMember(postEvent.getSavedmember().getMemberId());
                throw new RuntimeException(e);
            };
    }
}
  • 이벤트 핸들러는 빈으로 등록이 되어야 작동을 한다. (@Component 추가)
  • 이벤트 리스너로서 작동하기 위해, ApplicationListener<정의한 이벤트>을 상속받아야 한다.
  • onApplicationEvent 메서드를 오버라이드하여, 2번에서 발행한 이벤트를 넘겨받는다.
  • PostEvent를 정의할 때 넘겨받았던 savedmember 데이터를 다시 넘겨받아, 가공하여 deleteMember 등 후처리 가능.
  • @EventLister : 스프링 4.2 이후부터 추가된 애너테이션으로, 이것을 사용하면 ApplicationListerner을 상속받지 않아도 되며, 정의했던 PostEvent에서도 ApplicationEvert을 상속받지 않아도 되고, super()로 이벤트 발생 주체 클래스(서비스)도 안넘겨줘도 된다. 대신 리스너의 핸들러 메서드명은 onApplicationEvent여야 한다.
  • @Async : 비동기적으로 이벤트 핸들러를 처리하고 싶을때 붙여준다. @EnableAsync를 리스너 클래스에 @Configuration과 함께 붙여줘야한다. 이렇게하면, 멀티쓰레드로 작동하여, 회원정보 저장 -> 회원가입 완료 ->
    이메일 발송 -> 이메일 발송완료, 즉 Service 계층에서 한 트랜잭션으로 묶여있어도 분리되어 동작한다.
Comments