동시성 문제 해결하기 - 1(동시성 문제 파악)

Hyeseong_M·2025년 3월 23일

동시성 문제

목록 보기
1/2
post-thumbnail

진행하는 프로젝트의 쿠폰 발급 서비스에 동시성 문제가 있음을 파악했다.
이를 테스트하고 해결하는 과정을 기록해보고자 한다.


목차

  1. 동시성 문제란?
  2. 배경지식
  3. 테스트 - 동시성 문제 확인
  4. 테스트 결과 - 동시성 문제 발생
  5. 동시성 문제 해결 방법

1. 동시성 문제란?

동시성 문제란 하나의 자원에 두개 이상의 스레드, 세션에서 가변 데이터를 동시에 제어할 때 나타나는 문제이다.
하나의 세션이 데이터에 접근중일 때, 다른 세션에서 동시에 접근할 경우 동시성 문제로 인해 데이터 정합성에 문제가 생긴다.

현재 프로젝트의 쿠폰 발급 서비스에서 한정된 쿠폰을 여러 사람이 동시에 발급하고자 할 때 동시성 문제가 발생할 가능성이 있어 이를 파악하고 해결해보고자 한다.

2. 배경 지식

  • Race condition(경쟁상태) : 두 개 이상의 프로세스가 공통 자원을 병행적으로 접근할 때, 접근 순서에 따라 실행 결과가 달라지는 상황. 두개의 스레드 혹은 세션 등이 자원을 놓고 경쟁하는 상황
  • Dead Lock(교착상태) : 프로세스가 두 자원 모두에 접근해야 하는 상황에서, 두 개의 프로세스가 각자 한 자원을 점유하였을 때, 각자 소유한 리소스를 해제하지 않고 무한정 대기하게 되는 상황
  • Starvation(기아상태) : 특정 프로세스의 우선순위가 낮아 원하는 자원을 할당받지 못하는 상황

3. 테스트 - 동시성 문제 확인

동시성 문제가 발생하는지 확인하기 위해 Lock 등 동시성문제 처리가 되지 않은 상태에서 테스트를 진행한다.
Locust를 통해 서버에 요청을 보내 결과를 확인하는 방식으로 진행한다.

3.1. 테스트 시나리오

1번 테스트: 100매 발급 가능한 쿠폰을 120명이 발급 시도한다. -> 정상적인 경우 100매 성공, 20매 실패
2번 테스트: 1일당 1매 발급 가능한 쿠폰을 1인이 동시에 여러번 발급 시도한다. -> 정상적인 경우 1매만 발급 성공

3.2. 테스트 코드

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)

3.3. 테스트 진행

1번 테스트
시나리오: 120명의 사용자가 랜덤 회원가입 후 로그인하여 인증 키를 얻고, 해당 키를 첨부해 쿠폰 발급을 시도한다.
기대 결과: 120명의 사용자가 생성된 후, 100매의 쿠폰이 발급된다. 20개의 요청은 에러가 발생한다.
참고 사항: 환경상 발생할 수 있는 차이를 고려해 10회 실시한다.

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

4. 테스트 결과 - 동시성 문제 발생

4.1. 1번 테스트 결과

회차12345678910평균
생성된 쿠폰 수105101101101101101100103101101101.5

Locust를 통해 10회의 테스트를 진행하였으며, 매 회 100개의 쿠폰만 생성될 것을 기대하였으나, 평균 1.5개의 쿠폰이 더 발급되며, 동시성 문제가 발생하는 것을 확인했다.

4.2. 2번 테스트 결과

회차12345678910평균
생성된 쿠폰 수978484105626.3

Locust를 통해 10회의 테스트를 진행하였으며, 매 회 1개의 쿠폰만 발급될 것을 기대하였으나. 평균 6.3개의 쿠폰이 더 발급되며, 동시성 문제가 발생하는 것을 확인했다.

5. 동시성 문제 해결 방법

동시성 문제에는 비관적 락(Pessimistic Lock), 낙관적 락(Optimistic Lock), 분산락 등의 해결방법이 있는데, 이를 알아보고 알맞은 방법을 이용해 해결해보고자 한다.

참고자료

profile
Dev_Hyeseong

0개의 댓글