0. 들어가며

올리브영 테크 블로그를 보다가 대규모 트래픽 상황에서 선착순 쿠폰 발급 시스템의 문제에 대해 궁금해졌다.

티켓 오픈 시간에 많은 사용자가 동시에 티켓 예매를 처리하면 어떠한 문제가 발생할까? 그리고 그 문제를 어떻게 해결할 수 있을까? 여기서 가장 큰 문제는 요청 순서에 따라 티켓을 발급하되, 발급 가능 수량만큼만 발급해야 된다는 점이다.

이번 칼럼에서는 해당 문제를 어떻게 해결할 수 있는지 공유해보고자 한다.

1. 예제 코드

아래 코드는 티켓 예매 상황을 간단하게 구현한 예제 코드이다.

IssueCouponService

@Service
@RequiredArgsConstructor
public class IssueCouponService {

    private final IssueCouponRepository issueCouponRepository;
    private final MemberRepository memberRepository;
    private final CouponRepository couponRepository;

    @Transactional
    public IssueCouponResponse issueCoupon(IssueCouponRequest issueCouponRequest, MemberDetails memberDetails, Long couponId) {
        Member member = memberRepository.findById(memberDetails.getMemberId()).orElseThrow(
                () -> new MemberNotFoundException(ErrorCode.NOT_FOUND_MEMBER)
        );

        Coupon coupon = couponRepository.findById(couponId).orElseThrow(
                () -> new CouponNotFoundException(ErrorCode.NOT_FOUND_COUPON)
        );

        coupon.increaseIssueAmount();
        IssueCoupon issueCoupon = issueCouponRepository.save(issueCouponRequest.toEntity(member, coupon));

        return IssueCouponResponse.fromEntity(issueCoupon);
    }
}

아래는 Coupon 도메인 코드 중 일부이다.

@Entity
@Table(name = "coupons")
@SuperBuilder
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
public class Coupon extends BaseTimeEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long couponId;

    @Column(nullable = false)
    private String name;

    @Column(nullable = false)
    private Integer totalCount;

    @Column(nullable = false)
    private Integer issuedCount = 0;

    public void increaseIssueAmount() {
        if (issuedCount >= totalCount) {
            throw new CouponSoldOutException(ErrorCode.COUPON_SOLD_OUT);
        }
        issuedCount++;
    }
}

2. 순차적 발급

모든 사용자가 순차적으로 쿠폰 발급을 요청한다고 가정하자.

아래 테스트에서는 10장의 티켓을 30명의 사용자가 예매하는 상황을 가정했다.