redis를 이용한 중복 요청 방지하기

yeon·2021년 9월 1일
2

ITDA 프로젝트는 소비자와 쉽게 접근할 여력이 되지 않는 1차산업 종사자 (판매자)와 소비자를 연결해주는 서비스이다.

백엔드단에서 주문하기 기능을 구현한 내용을 정리해보려고 한다.

🧐 고민사항

이용자가 주문 버튼을 두 번 빠르게 눌러서 주문 요청이 두 번 들어올 수 있다. 주문 객체가 두 개가 생겨서 저장되는 문제가 생긴다. 결제까지 이어진다면 큰 문제가 될 수 있다.

이를 어떻게 해결할 것인가?

👉 redis를 이용한다.

redis를 이용하여 구현할 방법이 구체적으로 떠오르지 않았다. 참고할 내용들을 찾아봤다.

검색 결과 중복 요청을 방지하는 방법 이런 글을 보게 되었고 redis에 주문한 유저의 id를 저장하고 만료시간을 지정하면 될 거 같다는 생각을 했다.

장바구니 구현을 이미 redis를 이용해서 redis 관련 세팅은 이미 완료된 상태이다.


key, value 모두 문자열로 저장할 예정이어서 StringRedisTemplate을 사용하기로 했다.

@Service
@RequiredArgsConstructor
public class OrderValidationService {

    private static final String REDIS_KEY = "order";

    private final StringRedisTemplate stringRedisTemplate;

    public boolean isDuplicatedOrder() {
        return stringRedisTemplate.opsForValue().get(REDIS_KEY) != null;
    }

    public void save(Long userId) {
        stringRedisTemplate.opsForValue().set(REDIS_KEY, String.valueOf(userId), 1, TimeUnit.SECONDS);
    }
}

key: "order", value: "유저의 id 값" 으로 넣고, 만료시간을 1초로 지정하였다.


아래는 주문 기능이 구현된 OrderService 클래스이다.

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class OrderService {

    private final OrderValidationService orderValidationService;
  	// 이외에 필요한 빈들이 추가되어 있음 

    @Transactional
    public OrderResponseDto order(Long userId, OrderRequestDto orderRequest) {

        if (orderValidationService.isDuplicatedOrder()) {
            throw new OrderDuplicationException();
        }

	orderValidationService.save(userId);
        
        // 주문 로직

        return getOrderResponse(orders);
    }
}

주문 로직을 수행하기 전에 OrderValidService의 isDuplicatedOrder() 를 호출해서 중복으로 요청된 주문인지를 확인한다.

만약 1초 이내로 빠르게 들어온 요청이라면 isDuplicatedOrder() 가 true를 반환하여서 예외를 발생시킨다.

이렇게 중복으로 들어온 요청을 방지할 수 있다.


실제로 요청을 넣고 빠르게 레디스에서 키를 확인해보면 order 키가 생긴다. 다시 한번 확인해보면 만료시간이 지나서 사라져 있는 것을 확인할 수 있다.


테스트 코드

@SpringBootTest
public class OrderValidationTest {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Test
    @DisplayName("레디스 order 키의 TTL은 1초이다")
    void order_redis() throws InterruptedException {
        Long userId = 1L;
        String redisKey = "order";

        stringRedisTemplate.opsForValue().set(redisKey, String.valueOf(userId), 1, TimeUnit.SECONDS);

        assertThat(stringRedisTemplate.opsForValue().get(redisKey)).isNotEmpty();

        TimeUnit.SECONDS.sleep(2);

        assertThat(stringRedisTemplate.opsForValue().get(redisKey)).isNull();
    }
}

@SpringBootTest
public class OrderValidationServiceTest {

    @Autowired
    private OrderValidationService orderValidationService;

    @Autowired
    private StringRedisTemplate stringRedisTemplate;


    @Test
    @DisplayName("주문 중복 확인 기능 테스트")
    void duplicateOrder() {

        stringRedisTemplate.opsForValue().set("order", String.valueOf(1L), 1, TimeUnit.SECONDS);

        assertThat(orderValidationService.isDuplicatedOrder()).isTrue();

    }
}

GitHub repository 👉 https://github.com/Team-IT-DA/Backend

0개의 댓글