[내일배움캠프 Spring 4기 - 최종 프로젝트] 95일차 TIL : 얻어걸린 최적화 | 스프링 이벤트 적용

서예진·2024년 4월 9일
0

오늘의 학습 키워드

📖 프로젝트 문제점
📖 코드 리팩토링
📖 최적화
📖 스프링 부트 이벤트 적용

📖 프로젝트 문제점


  • 현재 문제점은 이벤트 서비스 단에서 쿠폰을 발행하고 있다.
  • 즉, 이벤트 서비스 단에 쿠폰을 생성하는 책임이 같이 부여되고 있다.
  • 이렇게 로직을 구현한 이유는 Event 객체에 오픈일자가 있었기 때문이다.
  • 그런데 생각해보니 Event와 Coupon은 연관관계 매핑이 되어있었기 때문에 Coupon에서는 Event의 오픈 일자에 접근이 가능했다.
  • 따라서, 쿠폰 발행은 쿠폰 서비스단에서 처리해야하는게 맞다고 생각해서 코드를 리팩토링 하기로 했다.

📖 코드 리팩토링

리팩토링 전

  • Event 단에서 IssueCoupon을 생성하는 것을 확인할 수 있다.

EventController.java

@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();
    }

EventServiceImpl.java

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);

					if (coupon.getQuantity() <= 0) {
						throw new InvalidCouponException(INVALID_COUPON);
					}
					coupon.decrease();
					IssuedCoupon issuedCoupon = new IssuedCoupon(user, coupon);
					issuedCouponRepository.save(issuedCoupon);
					log.info(
						String.format("쿠폰 발행 처리 [쿠폰 ID : %s]", issuedCoupon.getCoupon().getId()));
					publisher.publishEvent(
						new CouponEvent(issuedCoupon.getCoupon().getId(),
							issuedCoupon.getUser().getId()));
				} finally {
					lock.unlock();
				}
			}
		} catch (InterruptedException e) {
			Thread.currentThread().interrupt();
		}
	}

	private Event findEvent(Long eventId) {
		return eventRepository.findById(eventId)
			.orElseThrow(() -> new NotFoundException(NOT_FOUND));
	}

	private Coupon findCoupon(Long couponId) {
		return couponRepository.findById(couponId)
			.orElseThrow(() -> new NotFoundException(NOT_FOUND));
	}

리팩토링 후

  • 쿠폰 발행 기능을 Coupon 단으로 옮겼다.

CouponController.java

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

        couponService.issueCoupon(couponId, customUserDetails.getUser(),
            LocalDateTime.now());

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

CouponServiceImpl.java

    public void issueCoupon(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);
                    }

                    LocalDateTime openAt = eventQuery.getOpenDate(couponId);
                    if (now.isBefore(openAt)) {
                        throw new InvalidCouponException(INVALID_COUPON);
                    }

                    Coupon coupon = findCoupon(couponId);

                    if (coupon.getQuantity() <= 0) {
                        throw new InvalidCouponException(INVALID_COUPON);
                    }
                    coupon.decrease();
                    IssuedCoupon issuedCoupon = new IssuedCoupon(user, coupon);
                    issuedCouponRepository.save(issuedCoupon);
                    log.info(
                        String.format("쿠폰 발행 처리 [쿠폰 ID : %s]", issuedCoupon.getCoupon().getId()));
                    publisher.publishEvent(
                        new CouponEvent(issuedCoupon.getCoupon().getId(),
                            issuedCoupon.getUser().getId()));
                } finally {
                    lock.unlock();
                }
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }

EventQueryImpl.java

  • 이벤트의 오픈 일자 전의 유저의 접근을 막기 위해서는 이벤트의 오픈 일자를 가져와야 했다.
public LocalDateTime getOpenDate(Long couponId) {
        QEvent qevent = QEvent.event;

        return jpaQueryFactory.select(qevent.openAt)
            .from(qevent)
            .where(qevent.coupon.id.eq(couponId))
            .fetchOne();
    }

📖 최적화


  • 위에서 처럼 QueryDSL로 openAt 컬럼을 가져오는 코드를 작성하던 중 어쩌면 이 쿠폰 발행 로직의 수행시간이 줄어들었을 수도 있겠다 라는 생각이 들었다.
  • 그 이유는, 리팩토링 전에는 JPA Repository를 통해 Event 객체 전체를 가져온다.
    Event event = findEvent(eventId);
    if (now.isBefore(event.getOpenAt())) {
    	throw new InvalidCouponException(INVALID_COUPON);
    }
  • 리팩토링 후에는 QuertDSL통해 Event 객체의 openAt 컬럼만 가져오기 때문이다.
    LocalDateTime openAt = eventQuery.getOpenDate(couponId);
    if (now.isBefore(openAt)) {
     throw new InvalidCouponException(INVALID_COUPON);
    }
  • 따라서, 얼만큼의 시간이 줄었는지 확인하기 위해 JMeter를 활용했다.
  • 아래는 JMeter를 활용해 쿠폰 발행 API 테스트를 50명이 동시 접속한다고 가정하고 11번씩 진행한 결과이다.
  • 리팩토링 전 걸리는 시간

  • 리팩토링 후 걸리는 시간

  • 리팩토링 결과를 보면, 평균적으로 247 → 141 의 결과를 얻을 수 있었다.
  • 꽤나 뿌듯한 성과였다.

📖 스프링 부트 이벤트 적용


  • 이렇게 쿠폰 발행을 Coupon 단으로 옮기면서 여러 Service 단에서 CouponService를 주입받아서 사용해야 했다.
  • 이렇게 되면 서비스 간의 의존도가 높아지기 때문에 서비스 간의 의존도를 낮추고 싶었다.
  • 구글에서 서비스 간의 의존도를 낮추는 방법에 대해서 찾아보던 중 스프링 부트 이벤트에 대해서 발견했고 본 프로젝트에도 스프링 부트 이벤트를 적용했다.
  • 아래 블로그는 그 과정에 대한 내용이 담긴 블로그다.

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

profile
안녕하세요

0개의 댓글