Redis sorted set과 비관적락을 활용한 핫딜 구매 대기열 구현 로직에서 트래픽이 몰렸을 때 구매가 잘 되는지 확인하는 데이터 정합성 테스트와 성능 테스트를 ngrinder로 진행
우리 프로젝트는 다수의 사용자가 한 번에 요청을 보내어 트래픽이 대용량으로 발생하는 상황을 대처해보고자 핫딜 서비스를 구현하였다.
우선 핫딜 레포지토리에 비관적락을 적용하였다. 그러나 비관적락만으로는 유저의 요청 순서를 보장할 수 없어서 앞단에 대기열을 구현하기로 하였고 이를 위해 redis sorted set을 선택하였다. 구현에 대한 글을 앞전에 있으니 이제 부하 테스트를 진행해보자.
ngrinder로 테스트를 진행하기 위해 다음과 같은 테스트 스크립트를 작성하였다.
// 로그인
@Test
public void test1() {
// 로그인 API에 요청을 보내서 JWT 토큰을 받아옴
HTTPResponse response = request.POST("http://localhost:8080/api/members/login",
[email: userEmail, password: userPassword])
token = response.getHeader("Authorization").toString().substring(15);
headers.put("Authorization", token)
def result = response.getHeader("Authorization");
}
// 핫딜 구매
@Test
public void test2() {
request.setHeaders(headers)
// JSON 형식의 requestDto를 body로 추가하여 POST 요청 수행
HTTPResponse response = request.POST("http://localhost:8080/api/hotdeals/purchase/sortedset",
[hotdealId: 3, quantity: 1, address: address, phone: phone])
assertThat(response.statusCode, is(200))
}
해당 테스트는 사용자들이 로그인을 해야 구매를 할 수 있기 때문에 로그인 요청을 동시에 전송하였다. 그러나 로그인에는 동시접속에 대한 대비가 되어 있지 않아 오류가 종종 발생하는 문제가 있었다. 우리 서비스는 사용자들이 이미 로그인이 되어 있다는 전제하에 동시에 핫딜을 구매하는 환경만을 테스트해야 하므로 추후 스크립트를 변경할 예정이다.
ngrinder 테스트의 또 다른 문제가 있다. 이 로직은 사용자가 purchaseHotdeal API 요청을 보내면 sorted set에 add후 해당 요청이 완료된다. 스케줄러가 1초에 한 번씩 지정한 인원만큼 구매 로직을 실행시킬 수 있도록 이벤트를 발생시키고 다른 사용자들에게는 대기번호를 부여한다. ngrinder는 요청을 보내고 해당 요청이 완료되면 그 요청의 성능 측정을 종료하므로 대기열에 들어가는 순간 성능 측정이 종료되는 것이다. 즉, 실제 구매 로직은 스케줄러에서 발생시킨 이벤트를 통해 실행되므로 정확한 성능측정이 어렵다는 문제가 있다. 다만 한번에 구매 가능한 인원은 처음 핫딜 세팅 시에 관리자가 지정할 수 있으므로 1초에 실행할 수 있는 트랜잭션은 정해져 있어 우선은 요청한만큼의 구매가 잘 이루어지는지 데이터 정합성을 중점적으로 테스트하였다.
116건의 오류는 대부분 로그인 오류로 확인 되었으나 구매 요청에 성공한 약 10,804건 중 3,372건만이 성공적으로 구매한 것을 확인할 수 있었다. 구매에 실패한 나머지 요청의 경우 오류가 발생한 것은 아니었기 때문에 애초에 대기열에 들어가지 못한 것인지 대기열에 들어갔으나 구매 로직에 입장하지 못한것인지는 확인하지 못하였다.
이번에는 초당 구매 인원을 줄여서 테스트를 진행해보았다.
약 11,184건 중 1993건만이 구매에 성공하였다.
대기열에 입장이 된 사용자들이 초당 구매 인원만큼 구매가 이루어지는지를 중간에 확인해보았다. 1초에 30명씩 구매가 이루어져 30개씩 핫딜의 수량이 줄어드는 것을 확인할 수 있었다. 이렇게 중간 확인을 거쳤을 때 대기열에 30명 이상의 인원이 존재한다면 30개씩 구매가 됨을 여러 번 확인하였으므로 대기열에 입장 후 구매를 못하는 경우보단 대기열에 애초에 입장하지 못한 경우가 많은 것으로 추측된다.
EX) 998,602개에서 ➡ 998,572개로 30개 감소
세번째 테스트는 시간을 줄여 1분동안 요청을 보내도록 하였다.
약 887건의 요청 중 230건이 구매에 성공하였다.
약 768건중 469건이 구매에 성공
300건중 300건이 구매에 성공
구매가 제대로 이루어지지 않는 문제의 원인은 ngrinder에서 요청을 보낼 때 같은 유저가 여러번 반복해서 요청을 보내는 경우를 포함하고 있기 때문임을 깨달았다. sorted set은 중복을 허용하지 않으므로 같은 유저가 여러 번 요청을 보냈는데 이미 대기열에 존재한다면 중복해서 대기열에 저장되지 않는다. 즉, 10번 요청을 성공했어도 대기열에는 1번 들어가므로 1번 구매에 성공하는 것이고 당연히 오류 메세지도 출력되지 않았다.
정확한 테스트를 위해 로그인을 테스트에 포함하지 않도록하고, 한 명의 유저가 여러번 요청을 보내지 않도록 시나리오를 변경하여 다시 테스트해야겠다...