[Spring Boot Event] ApplicationEventPublisher를 통해 스프링 부트 이벤트 적용하기 - 1편

서예진·2024년 4월 5일
0

SPRING

목록 보기
5/5
post-thumbnail

📖 스프링 부트 이벤트


  • 스프링 부트에서는 이벤트 기반 아키텍처를 지원하고 있다. 이벤트는 애플리케이션에서 발생하는 주요 사건이나 상태 변경 등을 나타내며, 다른 컴포넌트들에게 알림을 전달하여 처리할 수 있도록 돕는다.
  • 스프링 부트에서는 이러한 이벤트 처리를 위해 ApplicationEvent 클래스와 ApplicationEventPublisher 인터페이스를 제공한다.
  1. ApplicationEvent: 스프링에서 발생하는 모든 이벤트는 ApplicationEvent 클래스를 상속하여 정의된다. 이 클래스는 이벤트를 나타내는 데이터를 포함하고 있으며, 이벤트를 발생시킬 때 전달된다.
  2. ApplicationEventPublisher: ApplicationEventPublisher는 이벤트를 발생시키고 이벤트를 구독하는 리스너에게 전달하는 역할을 한다. 스프링 컨텍스트에서 주입하여 사용할 수 있으며, publishEvent() 메서드를 통해 이벤트를 발생시킬 수 있다.

스프링 부트 이벤트 구현

  1. 이벤트 정의: 먼저 애플리케이션에서 발생할 수 있는 이벤트를 정의 한다. 이벤트는 ApplicationEvent 클래스를 상속하여 구현된다.
  2. 이벤트 발생: 이벤트를 발생시키는 코드에서 ApplicationEventPublisher를 주입받아 publishEvent() 메서드를 호출하여 이벤트를 발생시킨다.
  3. 이벤트 리스너 등록: 이벤트를 처리할 리스너를 정의하고, 스프링 빈으로 등록한다. 이벤트가 발생하면 해당 이벤트를 처리할 수 있는 리스너가 호출된다.
  4. 이벤트 처리: 이벤트를 처리하는 리스너에서 필요한 작업을 수행한다. 예를 들어, 이메일 발송, 로깅, 데이터 업데이트 등의 작업을 수행할 수 있다.
  • 즉, 스프링 부트 이벤트를 구현하려면 **event class**, **event publisher**, **event listener** 가 필요하다.

스프링 부트 이벤트 사용 이유

  • 이번 포스팅에서는 쿠폰 발행 로직에 스프링 이벤트를 활용해보고자 한다.
  • 선착순 쿠폰이 유저에게 발행이 되었다면 유저에게 푸시알림을 보내는 등의 추가 기능, 로직이 구현될 수 있다.
  • 이때, 스프링 이벤트 활용을 하지 않는다면, EventService 단에서 PushService(예를 들어)를 주입받아와서 해당 메서드를 호출해야한다.
  • 즉, 새로운 도메인의 service를 만들었다면 그 service를 주입받아와서 사용해야한다.
  • 이렇게 되면 서비스 간의 의존성이 높아지기 때문에 지금보면 하나쯤은 주입해서 사용해도 괜찮겠지만 나중가면 엄청 복잡한 로직을 발견할 것이다. 서비스 간의 의존성이 높다면.
  • 스프링 이벤트를 활용하면 이러한 서비스 간의 의존성을 낮출 수 있다.

📖 스프링 부트 이벤트 적용 - 동기 (쿠폰 발행)

Event.class

CouponEvent

```java
@Getter
public class CouponEvent {

	private Long couponId;

	public CouponEvent(Long couponId) {
		this.couponId = couponId;
	}

}
```

Event Publisher

EventServiceImpl

```java
@Slf4j
@Service
@RequiredArgsConstructor
public class EventServiceImpl implements EventService {

	...
	private final ApplicationEventPublisher publisher;

  ...
  
	public void issueCoupon(Long eventId, Long couponId, User user, LocalDateTime now) {
		RLock lock = redissonClient.getFairLock(LOCK_KEY);
		try {
			boolean isLocked = lock.tryLock(10, 60, TimeUnit.SECONDS);
			if (isLocked) {
				try {
					boolean isDuplicatedIssuance = issuedCouponRepository.existsByCouponIdAndUserId(
						couponId, user.getId());
					if (isDuplicatedIssuance) {
						throw new InvalidCouponException(INVALID_COUPON);
					}
					Event event = findEvent(eventId);
					if (now.isBefore(event.getOpenAt())) {
						throw new InvalidCouponException(INVALID_COUPON);
					}

					Coupon coupon = findCoupon(couponId);

					👉🏻 log.info(String.format("쿠폰 발행 처리 [쿠폰 ID : %s]", coupon.getId()));
					👉🏻 publisher.publishEvent(new CouponEvent(coupon.getId()));

					if (coupon.getQuantity() <= 0) {
						throw new InvalidCouponException(INVALID_COUPON);
					}
					coupon.decrease();
					IssuedCoupon issuedCoupon = new IssuedCoupon(user, coupon);
					issuedCouponRepository.save(issuedCoupon);
				} finally {
					lock.unlock();
				}
			}
		} catch (InterruptedException e) {
			Thread.currentThread().interrupt();
		}
	}
  ...
}
```

Eventlistener

CouponEventListener

@Slf4j
@Component
public class CouponEventListener {

	@EventListener
	public void sendPush(CouponEvent couponEvent) throws InterruptedException {
		log.info(String.format("푸시 알림 발송 [쿠폰 ID: %s | 유저 ID: %s]", couponEvent.getCouponId(),
			couponEvent.getUserId()));
	}
}

EventController

  	@PatchMapping("/{eventId}/coupons/{couponId}/issued-coupons")
    @PreAuthorize("hasAnyRole('ROLE_USER')")
    public ResponseEntity<Void> issueCoupon(
        @PathVariable Long eventId, @PathVariable Long couponId,
        @AuthenticationPrincipal CustomUserDetails customUserDetails) {

        eventService.issueCoupon(eventId, couponId, customUserDetails.getUser(),
            LocalDateTime.now());

        log.info(String.format("쿠폰이 발행되었습니다."));
        return ResponseEntity.ok().build();
    }

  • 위에서 처럼 스프링 이벤트를 구현하면 동기 방식으로 동작한다.
  • 기본적으로 스프링 이벤트는 동기 방식으로 동작하기 때문이다.
  • 이렇게 되면 하나의 프로세스가 끝나기를 기다리고 다음 프로세스가 이루어지기 때문에 오래걸리는 프로세스면 그보다 많은 시간을 기다려야할 수 있다.
  • 따라서, 지금부터 비동기 처리에 대해 알아보고자 한다.

📖 스프링 이벤트 구현 - 비동기 처리


  • 비동기 처리는 @EnableAsync 와 @Async만 적용해주면 된다.

Application.class

@EnableAsync
@EnableScheduling
@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

}

Eventlistener

@Slf4j
@Component
public class CouponEventListener {

  @Async
	@EventListener
	public void sendPush(CouponEvent couponEvent) throws InterruptedException {
		log.info(String.format("푸시 알림 발송 [쿠폰 ID: %s | 유저 ID: %s]", couponEvent.getCouponId(),
			couponEvent.getUserId()));
	}
}
  • 발행된 CouponEvent는 이 CouponEventListener가
  • 발행된 Event는 EventListener 객체가 수신한다. Spring에서 EventListener는 Bean으로 등록되어야 하며, 특정 Event가 발생한 경우 EventListener 객체 내에서 @EventListener 어노테이션을 부여받고 발행된 Event 객체를 인자로 받는 메서드가 동작하는 콜백 방식으로 호출이 이루어진다.

결과

  • Jmeter로 테스트 50개의 쿠폰에 55명의 유저가 접근하는 상황을 테스트 해보았을 때, 동기 방식이였을 때는 로그가 아래 같이 50번 찍혔다.
  • 비동기 처리 후에는 로그가 아래와 같이 찍혔다.
    • 먼저 위와 같은 로그가 50번 찍히고 아래 로그가 찍혔다.
      업로드중..

📖 느낀점


  • 위에서 스프링 이벤트를 적용한 것은 로그 찍기에 불과하다.
  • 그렇다면 실제로 eventlistener에서 메서드를 호출하려면 그냥 호출하면 되는 것인지 조금 더 공부해 봐야 겠다.
profile
안녕하세요

0개의 댓글