<aside> 👨‍💻 9개 프로젝트로 경험하는 대용량 트래픽 & 데이터 처리 초격차 패키지 Online. 오늘은 패스트 캠퍼스 대용량 트래픽 처리 강의를 보면서 동시성 문제에 대해서 배웠다.

</aside>

개요: 어떤 상황에 Lock을 걸까? (속도 이슈)


synchronized 키워드를 사용하여 락을 걸면 해당 블록이 실행되는 동안 다른 스레드는 그 블록에 접근할 수 없다. 따라서, synchronized 블록 내에서 시간이 많이 소요되는 작업을 수행하면 다른 스레드가 블록에 접근하는 데 지연이 발생할 수 있다.

이러한 이유로, synchronized 블록은 최소한으로 유지하고, 필요한 부분에만 락을 적용하는 것이 좋다. 예를 들어, 쿠폰 발급과 같은 특정 API 로직에만 synchronized를 적용하고, 다른 API 로직에는 적용하지 않을 수 있다. 이렇게 하면 다른 API의 속도에는 영향을 미치지 않으면서 동시성 문제를 해결할 수 있다.

그러나 synchronized 키워드를 사용할 때는 주의가 필요하다. synchronized 블록 내에서 오랜 시간이 걸리는 작업을 수행하거나, 데드락(deadlock)을 발생시킬 수 있는 상황을 피해야 한다. 또한, 가능하다면 더 세밀한 동기화를 위해 java.util.concurrent 패키지의 도구들을 사용하는 것을 고려해 볼 수 있다.

1. 동시성 문제 발생과 @Transactional에 lock을 걸면 발생하는 문제


Untitled

@RequiredArgsConstructor
@Service
public class CouponIssueService {

    private final CouponJpaRepository couponJpaRepository;
    private final CouponIssueJpaRepository couponIssueJpaRepository;
    private final CouponIssueRepository couponIssueRepository;

    @Transactional
    public void issue(long couponId, long userId) {
        // 동시성 문제를 해결하기 위해 lock 처리
        synchronized (this) {
            var coupon = findCoupon(couponId);
            coupon.issue(); // 발급된 수량을 하나 증가시킨다.
            saveCouponIssue(couponId, userId);
        }
    }

    // 쿠폰 조회
    @Transactional
    public Coupon findCoupon(long couponId) {
        return couponJpaRepository.findById(couponId)
                .orElseThrow(() -> new CouponIssueException(COUPON_NOT_EXIST, "쿠폰이 존재하지 않습니다. %s".formatted(couponId)));
    }

    @Transactional
    public CouponIssue saveCouponIssue(long couponId, long userId) {
        // 이미 발급된 쿠폰인지 확인
        checkAlreadyIssuance(couponId, userId);
        var issue = CouponIssue.builder()
                .couponId(couponId)
                .userId(userId)
                .build();
        return couponIssueJpaRepository.save(issue);
    }

    private void checkAlreadyIssuance(long couponId, long userId) {
        var issue = couponIssueRepository.findFirstCouponIssue(couponId, userId);
        if (issue != null) {
            throw new CouponIssueException(DUPLICATE_COUPON_ISSUE, "이미 발급된 쿠폰입니다. user_id: %s, coupon_id: %s".formatted(userId, couponId));
        }
    }

}

@Transactional
public void issue(long couponId, long userId) {
    // 동시성 문제를 해결하기 위해 lock 처리
    synchronized (this) {
        var coupon = findCoupon(couponId);
        coupon.issue(); // 발급된 수량을 하나 증가시킨다.
        saveCouponIssue(couponId, userId);
    }
}