요구사항
멀티스레드 환경에서 쿠폰 발급 요청 시 동시성 처리하기 위한 방법으로 synchronized를 사용해보았습니다.
Synchronized의 용도
멀티스레드 환경에서 여러 스레드가 하나의 공유 자원에 동시에 접근하지 못하도록 막는 것을 말합니다. 만약 여러 스레드가 하나의 공유 자원에 동시에 접근했을 경우 값을 올바르게 변경하거나 가져오지 못하는 경우를 막기위해 사용합니다.
Synchronized의 사용법
메소드에 synchronized 설정
메소드 이름 앞에 synchronized를 설정하면 메소드 전체를 임계영역으로 설정할 수 있습니다.
synchronized void decrease() {
count--;
System.out.println(count);
}
코드블럭에 synchronized 설정
동기화를 많이 사용하면 효율이 떨어지게 되므로 꼭 필요한 부분만 블록으로 임계영역을 설정할 수 있습니다. synchronized(this)로 지정하게 되면 참조변수(this) 객체가 기준이 됩니다.
void decrease() {
synchronized(this) {
count--;
}
System.out.println(count);
}
다음예제로 synchronized를 이용한 쿠폰 발급에 대한 동시성 처리를 테스트해보겠습니다.
쿠폰 발급 기능
쿠폰 감소 기능에만 초점을 두었기때문에 유저에 따라 쿠폰 제공 횟수나 기간 등의 요구사항은 배제하여 개발하였습니다.
쿠폰 엔티티
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private UUID couponId;
//...
/** 비용 */
private int cost;
/** 쿠폰 발급 매수 */
private int numberOfCoupons;
/**
* 쿠폰 발급 시 쿠폰 갯수 감소처리
*
*/
public void minusCouponCount() {
if (this.numberOfCoupons < 0) {
throw new IllegalArgumentException("쿠폰 다운이 만료되었습니다.");
}
this.numberOfCoupons--;
}
Synchronized를 이용한 쿠폰 발급 기능
메소드에 synchronized를 선언해 thread-safe하도록 해주었습니다.
/**
* 쿠폰 발급
*
* @param couponId 쿠폰 아이디
*/
public synchronized CouponResponse issuanceCoupon(String couponId) {
Coupon coupon = couponRepository.findById(UUID.fromString(couponId))
.orElseThrow(() -> new NoSuchFieldError("쿠폰을 찾을 수 없습니다."));
coupon.minusCouponCount();
couponRepository.save(coupon);
return CouponResponse.from(coupon);
}
쿠폰 발급 테스트
int threadCount = 100;
UUID couponId = couponList.get(0)
.getCouponId();
// when
ExecutorService executor = Executors.newFixedThreadPool(5);
CountDownLatch countDownLatch = new CountDownLatch(threadCount);
// then
for (int i = 0; i < threadCount; i++) {
executor.submit(() -> {
try {
couponService.issuanceCoupon(couponId.toString());
} finally {
countDownLatch.countDown();
}
});
}
countDownLatch.await();
assertEquals(9900, compareCoupon.getNumberOfCoupons());
결과
synchronized를 이용해 값이 정상적으로 줄어드는 것을 확인할 수 있었습니다.
겪었던 문제 상황
쿠폰 발급 메소드에 @Transactional을 붙였을 경우 값이 정상적으로 줄어들지 않는 현상을 발견 할 수 있었습니다.
@Transactional을 사용하면 메소드가 종료된 이후에 commit을 요청하는데 이 사이에 다른 스레드가 메소드를 실행시키다보니 다른 스레드는 결국 변경되기 이 전의 값을 보게 됩니다. 그래서 결국 @Transactional을 붙이면 정상적인 값을 얻을 수 없게 됩니다.
synchronized 문제
synchronized는 동시성 처리를 위한 한 방법이지만 결코 좋은 방법은 아니라고 생각합니다.
제가 생각한 이유는 2가지입니다.
1) 서버가 여러 대인 경우에 대한 대응을 할 수 없다.
2) 결국 @Transactional을 붙이지 않아야 정상적으로 동작된다.
Scale-out한 상황에서는 해당 문제를 해결할 수 없을 뿐더러 @Transactional을 빼면 데이터 일관성 유지 방면에서 큰 문제가 될 것입니다. 이러한 문제를 해결하고자 synchronized가 아닌 다른 방법으로 해결하는 방법에 대해서 다음 글을 작성해보겠습니다.
소스코드 첨부
https://github.com/cyeji/coupon-service
'Spring' 카테고리의 다른 글
쿠폰발급 서비스 동시성 처리하기3 - (3/3) (feat.Redis) (0) | 2024.02.13 |
---|---|
쿠폰 발급 서비스 동시성 처리하기 2 - (2/3) (feat. Database Lock) (1) | 2024.01.30 |
[CountDownLatch] 동시성 처리 테스트를 위한 CountDownLatch 사용법 (0) | 2024.01.15 |
[Test] 의미있는 단위 테스트 Naming Convention 작성하기 (0) | 2023.06.21 |
Spring에서 지연로딩 데이터 갖고오기 (0) | 2023.06.02 |