평소 동시성 관련된 문제에 대해 관심이 많았기에 토이 프로젝트를 통해 학습하고자 해당 프로젝트를 시작했다!
또한 서비스를 운영하면서 특정 이벤트를 진행할 경우가 많은데 (쿠폰, 할인 이벤트)
이럴 경우 일시적으로 트래픽이 폭발적으로 증가하게 되는 현상이 발생한다.
단순히 스케일아웃을 통해 트래픽을 해결하는 것이 아닌,
작업 큐를 활용하여 일시적인 트래픽을 제어하는 방법 또한 이번 프로젝트를 통해 학습하고자 한다.
위 코드는 쿠폰을 발급과 관련된 정말 간단한 로직을 담고있다.
쿠폰 발급 수량이 100개가 넘어가거나, 동일 회원이 쿠폰을 중복으로 발급받으려 할 때 체크하는 유효성 검증 로직과
UUID를 바탕으로한 쿠폰 발급 로직이다.
아무런 동기화 처리를 해주지 않은 상태인데, 이 상태에서 테스트를 해보면 어떤 결과가 나올까?
테스트 코드를 통해 30개의 스레드를 생성하여 1000번 쿠폰을 발급하게 된다.
하지만 어플리케이션 로직에 있는 쿠폰 최대 발급 갯수가 100개로 설정되어있기 때문에,
내가 원하는 결과는 1000개의 쿠폰 발급이 아닌 100개의 쿠폰이 발급되어야 한다.
예상과 다르게 결과를 보면 총 108개의 쿠폰 갯수가 발급되는 것을 볼 수 있다.
동시성 관련된 처리를 안해주었으니 당연한 결과이다.
특정 스레드가 카운트 쿼리를 날릴 때 쿠폰 발급 갯수가 99개 여서 추가 발급을 하였는데
그런 동일한 스레드가 여러개라면 정확히 100개만 발급되는게 아니라 99개로 조회한 스레드 갯수 만큼 발급이 되기 때문이다.
해당 문제를 어떻게 해결할 수 있을까?
해결 방법 1. Synchronized 키워드를 붙여 해결하기
synchronized 키워드는 자바에서 지원하는 문법인데
여러 스레드가 변경 가능한 공유 데이터를 동시에 수정하려 할 때 위와 같은 레이스 컨디션이 발생한다
이때 synchronized 키워드를 붙이면 해당 블록에는 오직 하나의 스레드만 접근 가능하게 된다!
원래 코드에 다음과 같이 synchonized 키워드를 붙여주었다. 결과는?
그 전보다 차이가 적긴 한데, 여전히 정확한 100개 수량을 맞춰서 발급하지 못하는 것을 볼 수 있다.
한번에 하나의 스레드만 접근하게 하는데도 왜 이러한 현상이 발생하는 것일까?
Transactional과 Syncrhonized 키워드를 동시에 사용하게 될 경우 발생하는 문제
트랜잭션 어노테이션은 AOP 방식으로 동작한다.
즉 기존의 메소드를 지닌 클래스를 상속한 클래스 내부 메서드에서
앞 뒤로 transaction.start, commit, rollback 과 관련된 로직들이 붙게되는데,
syncrhonized 키워드는 메서드 호출 시점에만 적용되기 때문에, 트랜잭션 범위를 포함할 수가 없다.
쉽게말하면 트랜잭션이 끝나지 않았는데, 다른 스레드가 접근할수 있게 되는 것이다.
그렇다면 synchronized 키워드를 붙인 메서드에서 transactional 메서드를 호출하게 된다면 어떻게 될까?
만일 클래스 분리를 하지 않고, CouponService 내부에서 synchronized 키워드 메서드와 transactional 메서드를 분리하여
호출하게 된다면 AOP를 활용한 트랜잭션 처리가 정상적으로 이루어지지 않는다.
다음과 같이 issueCoupon 메서드에서 Transactional 어노테이션이 붙어있는 makeCoupon 메서드를 호출한다면,
이는 트랜잭션 로직 관련 AOP가 적용된 프록시 객체의 메서드를 호출하는 것이 아닌,
원래 클래스의 메서드를 호출하는 것이기 때문에 트랜잭션 적용이 정상적으로 되지 않는 것이다!
따라서 이를 분리하려면 새로운 클래스가 하나 더 필요하다.
CouponSyncService 라는 클래스를 하나 더 생성하여, 트랜잭션이 적용된 issueCoupon 메서드를 외부에서 호출하는 방식으로 변경하였다.
이렇게 하게 되면, 같은 클래스 내부에서 호출하는 방식이 아니기 때문에, 정상적으로 트랜잭션 로직이 적용되게 된다.
또한 Transaction 어노테이션과 synchronized 키워드가 같이 쓰이는게 아니라,
synchronized 키워드가 먼저 적용되고 그 안에 있는 Transactional 메서드를 호출하는 로직이기 때문에
syncrhonized가 적용이 안되는 문제도 해결할 수 있다. 이를 테스트한 결과는 다음과 같다.
다음과 같이 기존 테스트 코드 대신에 couponSyncService의 메서드를 호출하는 식으로 변경하면
정상적으로 테스트가 통과된 것을 볼 수 있다. 즉 정확히 쿠폰을 100개만 발급한 것이다.
하지만 테스트 수행에 걸린 시간이 16초나 소요된 것이 보인다.
syncrhonized 키워드를 사용하게 될 경우 가장 조심해야되는 부분이 바로 이 부분이다.
메서드 블럭에 하나의 스레드만 접근이 가능하기 떄문에 성능적으로 문제가 생길 수 밖에 없어서
가능한 synchronized 영역을 최소화해야 한다.
하지만 정상적으로 쿠폰 발급 시에 유효성 검증과 트랜잭션 처리를 하려면, 최소화하는 것도 한계가 있는데
때문에 syncrhonized를 통해 동시성 문제는 해결 할 수 있지만, 성능적으로 문제가 생기게 된다.
다음 글에서는 이를 해결하기 위해 비관적 락, 낙관적 락을 통한 해결 방법을 소개하고자 한다.
'Projects' 카테고리의 다른 글
쿠폰 발급 프로젝트 (2) (3) | 2023.12.02 |
---|