
진행하는 프로젝트의 쿠폰 발급 서비스에 동시성 문제가 있음을 파악했다.
이를 테스트하고 해결하는 과정을 기록해보고자 한다.
동시성 문제란 하나의 자원에 두개 이상의 스레드, 세션에서 가변 데이터를 동시에 제어할 때 나타나는 문제이다.
하나의 세션이 데이터에 접근중일 때, 다른 세션에서 동시에 접근할 경우 동시성 문제로 인해 데이터 정합성에 문제가 생긴다.
현재 프로젝트의 쿠폰 발급 서비스에서 한정된 쿠폰을 여러 사람이 동시에 발급하고자 할 때 동시성 문제가 발생할 가능성이 있어 이를 파악하고 해결해보고자 한다.
동시성 문제가 발생하는지 확인하기 위해 Lock 등 동시성문제 처리가 되지 않은 상태에서 테스트를 진행한다.
Locust를 통해 서버에 요청을 보내 결과를 확인하는 방식으로 진행한다.
1번 테스트: 100매 발급 가능한 쿠폰을 120명이 발급 시도한다. -> 정상적인 경우 100매 성공, 20매 실패
2번 테스트: 1일당 1매 발급 가능한 쿠폰을 1인이 동시에 여러번 발급 시도한다. -> 정상적인 경우 1매만 발급 성공
1번 테스트
from locust import HttpUser, task, between, SequentialTaskSet
import requests
import random
import string
#시나리오:
#다수의 사용자가 한번에 동시에 쿠폰 발급을 시도하는 시나리오
HOST = "http://localhost:8080"
class BoardServiceUser(SequentialTaskSet):
def on_start(self):
# 1. 회원가입
randEmail = self.generate_random_email()
response = requests.post(HOST + "/api/v1/users", json={
"email" : randEmail,
"password": "test1234",
"nickname": "testUser",
"userRole" : "USER",
"phoneNumber" : "01012340000"
})
data = response.json()
if data["status"] == "success":
print("회원가입 성공")
else:
print("회원가입 실패:", data["message"])
self.interrupt()
# 2.로그인 요청
response = requests.post(HOST + "/api/v1/users/login", json={
"email": randEmail,
"password": "test1234"
})
data = response.json()
if data["status"] == "success":
token = data["data"]
self.client.headers.update({'Authorization': f'Bearer {token}'})
else:
print("로그인 실패:", data["message"])
self.interrupt()
@staticmethod
def generate_random_email():
random_string = ''.join(random.choices(string.ascii_lowercase + string.digits, k=8))
return f"test_{random_string}@example.com"
#쿠폰 발급
@task
def issueCoupone(self):
response = self.user.client.post(HOST + "/api/v1/coupons/user-coupons/1")
print(response)
class WebsiteUser(HttpUser):
host = HOST
tasks = [BoardServiceUser]
wait_time = between(1, 1)
2번 테스트
from locust import HttpUser, task, between, SequentialTaskSet
import requests
import random
import string
#시나리오:
#단일 사용자가 하나의 쿠폰에 대해 여러번 쿠폰 발급을 시도하는 시나리오
HOST = "http://localhost:8080"
class BoardServiceUser(SequentialTaskSet):
def on_start(self):
# 2.로그인 요청
response = requests.post(HOST + "/api/v1/users/login", json={
"email": "test@test.com",
"password": "test1234"
})
data = response.json()
if data["status"] == "success":
token = data["data"]
self.client.headers.update({'Authorization': f'Bearer {token}'})
else:
print("로그인 실패:", data["message"])
self.interrupt()
#쿠폰 발급
@task
def issueCoupone(self):
response = self.user.client.post(HOST + "/api/v1/coupons/user-coupons/1")
print(response)
class WebsiteUser(HttpUser):
host = HOST
tasks = [BoardServiceUser]
wait_time = between(1, 1)
1번 테스트
시나리오: 120명의 사용자가 랜덤 회원가입 후 로그인하여 인증 키를 얻고, 해당 키를 첨부해 쿠폰 발급을 시도한다.
기대 결과: 120명의 사용자가 생성된 후, 100매의 쿠폰이 발급된다. 20개의 요청은 에러가 발생한다.
참고 사항: 환경상 발생할 수 있는 차이를 고려해 10회 실시한다.

2번 테스트
시나리오: 한명의 사용자가 100번의 쿠폰 발급 요청을 동시에 시도하는것을 모사하여 쿠폰발급을 시도한다.
기대 결과: 1명의 사용자에 1매의 발급이 가능한만큼, 매 회 1매의 쿠폰만이 발급되고. 99개의 에러가 발생한다.
참고사항: 환경 상 발생할 수 있는 차이를 고려해 10회 실시한다.

| 회차 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 평균 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 생성된 쿠폰 수 | 105 | 101 | 101 | 101 | 101 | 101 | 100 | 103 | 101 | 101 | 101.5 |

Locust를 통해 10회의 테스트를 진행하였으며, 매 회 100개의 쿠폰만 생성될 것을 기대하였으나, 평균 1.5개의 쿠폰이 더 발급되며, 동시성 문제가 발생하는 것을 확인했다.
| 회차 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 평균 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 생성된 쿠폰 수 | 9 | 7 | 8 | 4 | 8 | 4 | 10 | 5 | 6 | 2 | 6.3 |

Locust를 통해 10회의 테스트를 진행하였으며, 매 회 1개의 쿠폰만 발급될 것을 기대하였으나. 평균 6.3개의 쿠폰이 더 발급되며, 동시성 문제가 발생하는 것을 확인했다.
동시성 문제에는 비관적 락(Pessimistic Lock), 낙관적 락(Optimistic Lock), 분산락 등의 해결방법이 있는데, 이를 알아보고 알맞은 방법을 이용해 해결해보고자 한다.