[트러블 슈팅 - 중복 API 요청] API 멱등성 (feat: redis) 해결 완료

2024. 9. 11. 13:37Spring

이번 문제는 API 요청이 중복 클릭과 네트워크 지연으로 동일한 요청을 반복하는 문제가 발생하는 일이 생겨 이를 해결하는 과정을 작성할 예정입니다.

 

멱등성이란

 

멱등성은 동일한 연산을 여러 번 수행해도 결과가 달라지지 않는 성질을 말한다. 이는 클라이언트에서 같은 요청을 여러 번 보내거나 네트워크 지연으로 인한 오류로 인한 중복 요청이 오더라도 서버는 상태가 변하지 않게 유지하도록 보장하는 것을 말한다.

 

멱등성이 보장되지 않는다면 어떻게 될까? 멱등성이 보장되지 않는 요청의 종류에는 무엇이 있을까?

 

서버 리소스 조회 혹은  대체하는 GET, PUT의 경우 여러번 요청해도 멱등성이 보장된다. 불가피하게 같은 요청이 발생하더라도 데이터 변경이 없기 때문이다. 문제가 생길 거 같은 DELETE의 경우에도 데이터의 삭제 요청이 중복되어 오더라도 이미 데이터는 삭제되고 또 한 번 삭제 요청을 한다 해서 바뀌는 것이 없기 때문이다.

 

반면에 서버 데이터를 변경하는 요청의 경우 멱등성 보장이 중요하다. 특히 POST, PATCH의 경우 멱등성 보장이 되지 않는데 문제가 발생하는 상황의 예를 들자면 음식을 주문하는 POST 요청이 중복되면 동일한 음식을 또 주문하는 문제가 발생할 수 있다.

메서드 멱등성
POST X
GET O
DELETE O
PUT O
PATCH X

 

한마디로 멱등성이 보장되는 API의 경우 요청이 여러번 반복되더라도 DB 데이터 변경을 유발하지 않기에 문제가 발생하지 않지만 멱등성이 보장되지 않는 API의 경우 큰 문제가 발생할 수 있다. 때문에 별도의 안전장치를 마련해야 한다. 

 

발생한 문제 & 해결 방식

 

상황: 일명 따닥 문제(사용자가 클라이언트가 콜백이 느려 여러번 누름)와 네트워크 지연의 콜라보로 일어난 문제이다. 이틀에 걸쳐 2번 발생했다. 발생 시나리오는 알 수 없는 이유로 네트워크 지연이 발생했고 때문에 사용자는 API 요청 결과에 대한 피드백이 늦어지니 버튼을 계속 누르게 된 것이다.

 

원인: 기존 클라이언트 버튼에는 재클릭 방지를 위해 별도의 코드를 작성해 놨다. 하지만 실수로 클라이언트의 코드 변경 과정 중 해당 코드를 삭제 후 추가하지 않았다. 때문에 네트워크 지연 발생 시 다시 클릭을 하면 서버에 동일한 요청이 보내지는 문제가 발생한 것이다.

 

해결 방식: 문제 발생 후 해당 문제를 해결하기 위한 방법들을 찾아보게 되었는데 일반적으로 클라이언트에서는 중복 클릭을 못하게 방지하는 코드를 작성하고 서버에서는 멱등키를 이용해 중복요청을 방지하는 식으로 구현하는 방식을 찾게 되었다.

 

2024-XX-XXT18:48:47.753+09:00 : sendMessage Controller
.....
2024-XX-XXT18:48:47.713+09:00 : sendMessage Controller

 

해당 로그는 실제 발생한 시스템 로그를 필요한 부분만 잘라서 가져왔다. 시간을 자세히 살펴보면 약간의 밀리초 단위로 중복된 API 요청을 보낸 것을 알 수 있다. 이후 로그를 쭉 살펴보면 동일한 내용의 요청을 두 번 연속 보내고 실제 수신자들에게도 동일한 문자 메시지가 2번 전송되는 치명적인 문제가 발생했다.

 

그렇다면 멱등성 보장이 되지 않는 요청을 보장되게 만들려면 어떻게 해야할까?

멱등성 보장 방식에 대한 구현 힌트는 토스 페이먼츠에서 블로그에서 찾을 수 있었다.

 

https://docs.tosspayments.com/reference/using-api/authorization

 

인증 및 기타 헤더 설정 | 토스페이먼츠 개발자센터

토스페이먼츠 API를 사용하기 위해 필요한 인증과 헤더 설정 방법입니다.

docs.tosspayments.com

 

간단히 요약하자면 토스 페이에서는 별도의 시크릿 키를 생성해 헤더에 전달하고 토스의 별도 Idempotency DB에 저장되어 매칭을 통해 멱등한 요청인지를 판별한다.

 

즉 유일한 키값을 생성해 중복되는 요청인지 아닌지 판단하는 로직이 추가되어야 한다.

 

기존 API 로직 순서

 

기존 API 로직 순서를 보면 별도의 응답에 대한 검증 없이 요청이 들어오면 들어오는 데로 해당 요청을 처리했다. 하지만 이 경우 중복 요청인지에 대한 판단이 되지 않으므로 각종 예외 상황이 발생하면 중복 요청이라는 문제가 발생하게 된다.

 

때문에 기존에 사용 중인 로직에 REDIS를 추가해 멱등성 보장 문제를 해결했다.

  1. 메시지 전송 요청 시 멱등키가 존재하는지 확인
  2. REDIS에 조회 후 멱등키가 존재하지 않다면 저장 후 나머지 로직 진행 
  3. 만약 이미 동일한 멱등키가 존재한다면 중복되는 요청이므로 리다이렉트 반환

 

멱등키의 VALUE 값의 경우 단순 요청이기 때문에 UUID만을 이용해 유일성을 보장해 주었다.

 

결론

  • 멱등성을 보장하기 위해 클라이언트와 서버 양쪽에서 중복 요청 방지 로직을 구현해야 한다.
  • 클라이언트에서 중복 클릭 방지 코드를 추가하고 서버에서는 멱등성 키를 활용하여 중복 요청을 감지하고 차단하는 방식으로 문제를 해결할 수 있다.

 

수정한 코드 - 서버

 

@Service
@RequiredArgsConstructor
@Slf4j
public class IdempotencyRedisService {

    private final RedisTemplate<String, Object> redisTemplate;

    private static final String IDEMPOTENCY_KEY_PREFIX = "idempotency:";

    public boolean isDuplicateRequest(String idempotencyKey) {
        return Boolean.TRUE.equals(redisTemplate.hasKey(IDEMPOTENCY_KEY_PREFIX + idempotencyKey));
    }

    public void saveIdempotencyKey(String idempotencyKey) {
        // 5분(300초) 동안 유효하도록 설정
        redisTemplate.opsForValue().set(IDEMPOTENCY_KEY_PREFIX + idempotencyKey, "true", 300, TimeUnit.SECONDS);
    }

}

 

    // 메시지 전송
    @PostMapping("/message/content")
    public String sendMessage(@ModelAttribute("messageForm") MessageListDTO messageListDTO,
                              @RequestParam("idempotencyKey") String idempotencyKey,
                              RedirectAttributes redirectAttributes) {
        log.info("sendMessage Controller");

        log.info("idempotencyKey = {}", idempotencyKey);

        //중복 요청 체크
        if (idempotencyRedisService.isDuplicateRequest(idempotencyKey)) {
            log.info("중복 요청 감지: " + idempotencyKey);
            redirectAttributes.addFlashAttribute("responses", "중복데이터 발생");
            return "redirect:/automessage/message/result";
        }

        idempotencyRedisService.saveIdempotencyKey(idempotencyKey);

        List<Integer> errorMessage = new ArrayList<>();
//      메시지 전송
        List<MessageResponseDTO> responses = messageService.checkMessageTransmission(messageListDTO, errorMessage);

//      전송 결과를 모델에 추가
        redirectAttributes.addFlashAttribute("responses", responses);
        redirectAttributes.addFlashAttribute("errorMessage", errorMessage);

        // 전송 결과 페이지로 리다이렉트
        return "redirect:/automessage/message/result";
    }

 

이후 해당 클라이언트에는 멱등키가 존재하기 때문에 만약 동일한 요청을 또 보내더라도 redis에 있는 검증 코드에서 false를 반환하기에 동일한 요청을 더 보내지 않게 된다.

 

2025년 02월 01일 원인 발견

영원히 알 수 없을 것 같던 api 중복 호출 원인을 알아냈다. redis를 도입해 멱등성 인증을 도입한 이후 볼 수 없었던 원인을 우연히 알게 되었다. 가게에서 사용 중이던 마우스를 연휴에 집에서 사용하며 알게 되었는데 로지텍의 마우스는 오래 사용할 경우 마우스 클릭에서 더블클릭 문제가 발생할 수 있다 한다. 웹 사이트 드레그 시 자꾸 마우스가 이상한 곳을 드레그하여 살펴보며 알게 되었다.

프로그래밍 외적인 문제였지만 문제 원인을 파악하게 되어 기분은 좋네요 :)