API 응답에 딜레이가 있던 문제

2024년 12월 10일

문제 상황

1. 관리자가 API를 통해 투표방 시작 요청을 서버에 보낸다. 2. 서버는 이 요청을 받고 두 가지 작업을 수행해야 합니다 - 방의 상태를 'waiting'에서 'active'로 변경 - 소켓을 통해 방 시작 메시지를 전송 3. 하지만 실제로는 사용자들이 방 참여 버튼을 눌렀을 때 여전히 방의 상태가 'waiting'인 상태여서 참여가 불가능한 상황이 발생 4. 관리자가 발에 참여하기 버튼을 누를 경우 방의 상태가 아직 변경이 되지 않아 투표 창으로 이동할 수 없는 문제 발생 5. 하지만 일정 시간이 지난 후 다시 버튼을 눌렀을 경우 정상적으로 페이지의 이동이 이루어짐, 또한 투표 방의 시간 또한 방을 시작 하였을 당시의 시간과 일치
위와 같이 문제가 발생 하였고 프로젝트를 진행할 경우에는 이 문제가 정확히 어떤 부분에서 발생하는 것인지 추측할 수 없었습니다. 여기서 단지 이 문제가 외부 서버에서 API 를 이용하여 요청을 하고 이 물리적인 거리가 멀기 때문에 생기는 네트워크 지연이지 않을까? 라는 추측만을 했을 뿐 이 문제를 정확히 어떠한 문제이거 이 문제를 어떻게 해결해야 하는 지에 대해서는 깊게 생각하지 못했습니다.

해결 방안

프론트에서 네트워크 요청을 확인하였을 경우에 해당 네트워크 요청이 정상적으로 가는 것을 확인하였기 때문에 비지니스 로직의 문제는 아니라는 판단이 있었습니다. 하지만 이 문제가 진짜 네트워크 지연 문제를 어떻게 해결해야할 지 몰랐기 때문에 현재 할 수 있는 최선의 선택을 하자고 생각했습니다.
이렇게 기능을 분리한 이유는 아직 문제를 특정하지 못했지만 현재 상황으로써 판단할 수 있는 상황은 다음과 같았습니다.
  • API 요청은 정상적으로 전송이 된다.
  • 이 API 요청에 대한 결과 또한 딜레이가 존재하지만 정상적으로 동작을 한다.
  • 대략 이 딜레이는 1~3초 사이의 시간이 소요가 된다.
이러한 사실을 바탕으로 문제를 어떻게 해결할 지에 대해서 고민해봤습니다.

사용자 경험을 개선하기

기존의 기능 - 방 시작하기 버튼 클릭 -> API 요청 전송 -> 성공할 경우 배팅 페이지로 리다이렉트 분리한 기능 - 방 시작하기 버튼 클릭 -> API 요청 전송 - API 요청 전송 성공 시 socket을 통해서 방이 생성 데이터를 입력 받은 -> 방의 상태가 active 상태인지 확인 -> active일 경우 방에 참여
위와 같이 기존의 하나로 작동하던 기능을 두 가지 기능으로 분리하였고 추가적으로 현재 시작된 투표 방의 상태를 추적 가능하게 끔 기능을 수정하였습니다.
그리고 여기서 추가적으로 사용자에게 피드백을 제공하고자 생각했습니다. Toast 메시지를 통해 현재 상황을 사용자에게 명확히 알리고, 다시 시도할 수 있는 기회를 제공하여 사용자가 일정 시간 후 다시 참여하기 버튼을 누를 수 있게 끔 유도하자고 생각했습니다.
저는 이러한 방법을 통해서 원인을 완전히 특정할 수 없는 기술적 문제를 사용자 경험을 개선하고, 시스템의 안정성을 높이고자 하였습니다. 그래서 서비스의 근본적인 문제가 해결이 되지는 않았지만 현재 상황에서 최선의 절충안을 찾아 문제를 해결하였습니다.

tanstack query의 자동 재시도 매커니즘 활용하기

[server] API 요청 처리 [client] 방 상태가 바뀌었을 것을 기대하고 참여하기 위한 API 전송 [server] 방 상태를 확인 했지만 방 상태가 아직 변하지 않음 [client] API 에러 발생 [client] retry, retryOn 속성에 의해 다시 API 요청을 전송
프로젝트에서 겪었던 문제를 tanstack query를 이용하여 해결한 흐름을 작성해보면 위와 같습니다. 그리고 이러한 자동 재시도 매커니즘은 어렵지 않습니다. 자동 재시도 매커니즘을 코드로 한 번 구현해보겠습니다.
let maxCount = 5;
let currentRetry = 0;
let retryInterval = 100;
async function retry(url, options = {}) {
  try {
    const response = await fetch(url, options);
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }
    return response.json();
  } catch (error) {
    if (currentRetry < maxCount) {
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          currentRetry += 1;
          retry(url, options).then(resolve).catch(reject);
        }, retryInterval);
      });
    } else {
      console.error(error);
      throw error;
    }
  }
}
위의 로직에서 확인할 수 있는 것 처럼 재시도 매커니즘은 생각보다 복잡하지 않고 간단하게 구현할 수 있습니다. 하지만 이 방법은 강력한 효과를 발휘합니다. 특히 앞서 설명했던 네트워크 지연 문제를 겪고 있을 경우에는 더 큰 효과를 볼 수 있습니다.
네트워크 지연으로 인한 일시적인 오류는 재시도를 통해 자동으로 복구되므로, 개발자는 진정한 오류 상황에만 집중할 수 있습니다. 이는 코드의 복잡성을 낮추고 유지보수성을 향상시키는 효과가 있습니다.
더불어 사용자 입장에서도 큰 이점이 있습니다. 네트워크 지연이 발생했을 때 사용자가 직접 새로고침을 하거나 다시 시도할 필요 없이, 시스템이 자동으로 이를 처리합니다. 이는 사용자의 불편함을 최소화하고, 애플리케이션에 대한 신뢰도를 높이는 데 기여합니다.
뿐만 아니라 특히 모바일 환경에서 자동 재시도 매커니즘의 효과가 두드러집니다. 모바일 기기는 이동 중에 네트워크 신호 강도가 수시로 변할 수 있으며, Wi-Fi에서 셀룰러 데이터로 전환되는 상황도 빈번하게 발생합니다. 이러한 환경에서 자동 재시도 매커니즘은 일시적인 연결 문제를 자연스럽게 해결하여 사용자 경험을 크게 향상시킵니다.
하지만 자동 재시도 매커니즘이 만능이라고 생각해서는 안됩니다. 자동 재시도 매커니즘이 유용한 경우와 그렇지 않은 경우를 구분할 수 있어야 합니다.

자동 시도 매커니즘이 유용한 경우

  1. 우선 가장 먼저 GET과 같은 HTTP Method는 멱등성이 보장되는 요청이므로 여러 번 실행되어도 서버의 상태가 변하지 않으므로 재시도가 안전합니다.
  2. 서버의 상태 변화를 기다리는 폴링 상황입니다. 예를 들어 대용량 파일 처리나 복잡한 연산의 결과를 기다리는 경우, 적절한 간격을 두고 재시도하는 것이 효과적일 수 있습니다.

자동 시도 매커니즘이 유용하지 못한 경우

  1. HTTP Method 중 POST나 PUT과 같이 서버의 상태를 변경하는 요청은 중복 실행의 위험이 있으므로 신중히 접근해야 합니다.
  2. 비즈니스 로직의 중요도를 고려해야 합니다. 결제나 주문과 같이 중요한 트랜잭션의 경우, 실패 시 즉각적인 사용자 피드백이 더 적절할 수 있습니다. 반면 알림이나 로그 기록과 같은 부가적인 기능의 경우 자동 재시도가 유용할 수 있습니다.
위에서 설명한 경우 이외에도 자동 시도 매커니즘이 유용한 경우와 유용하지 못한 경우가 있을 수 있으니 적용하고자 하는 비즈니스 로직의 성격을 분석하여 적용할 경우 좋은 효과를 보실 수 있습니다. 또한 사용자가 어떠한 작업이 이루어지고 있는 지에 대한 정보를 얻을 수 있어야 합니다.재시도가 진행 중임을 사용자에게 적절히 피드백하고, 필요한 경우 사용자가 재시도를 중단할 수 있는 옵션을 제공하는 것이 좋습니다.