<aside> 👨💻 9개 프로젝트로 경험하는 대용량 트래픽 & 데이터 처리 초격차 패키지 Online. 오늘은 패스트 캠퍼스 대용량 트래픽 처리 강의를 보면서 동시성 문제에 대해서 배웠다.
</aside>
synchronized
키워드를 사용하여 락을 걸면 해당 블록이 실행되는 동안 다른 스레드는 그 블록에 접근할 수 없다. 따라서, synchronized
블록 내에서 시간이 많이 소요되는 작업을 수행하면 다른 스레드가 블록에 접근하는 데 지연이 발생할 수 있다.
이러한 이유로, synchronized
블록은 최소한으로 유지하고, 필요한 부분에만 락을 적용하는 것이 좋다. 예를 들어, 쿠폰 발급과 같은 특정 API 로직에만 synchronized
를 적용하고, 다른 API 로직에는 적용하지 않을 수 있다. 이렇게 하면 다른 API의 속도에는 영향을 미치지 않으면서 동시성 문제를 해결할 수 있다.
그러나 synchronized
키워드를 사용할 때는 주의가 필요하다. synchronized
블록 내에서 오랜 시간이 걸리는 작업을 수행하거나, 데드락(deadlock)을 발생시킬 수 있는 상황을 피해야 한다. 또한, 가능하다면 더 세밀한 동기화를 위해 java.util.concurrent
패키지의 도구들을 사용하는 것을 고려해 볼 수 있다.
@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);
}
}