네트워크 장애가 잦은 분산 환경에서도 API 통합을 일관된 상태로 유지하기 위해 서버의 멱등성, 멱등성 키, 재시도 전략(지수 백오프와 지터)을 활용하는 방법을 살펴봅니다.
Brandur Leach — API Experience
네트워크는 신뢰할 수 없습니다. 우리는 모두 Wi‑Fi 연결에 문제가 생기거나, 통화가 갑자기 끊기는 일을 겪어본 적이 있습니다.
우리 서버들을 연결하는 네트워크는 휴대전화망이나 가정용 ISP 같은 소비자 수준의 ‘라스트 마일’보다 평균적으로 더 안정적이긴 하지만, 충분히 많은 정보가 선로를 오가다 보면 여전히 기묘한 방식으로 실패하게 마련입니다. 장애, 라우팅 문제, 기타 간헐적인 실패는 전체적으로는 통계적으로 드물 수 있지만, 어떤 배경 잡음 같은 비율로 항상 발생하고 있다고 봐야 합니다.
이처럼 본질적으로 신뢰하기 어려운 환경을 극복하려면, 실패 상황에서도 견고하게 동작하고, 실패가 있더라도 복잡한 통합을 일관된 상태로 예측 가능하게 수렴시키는 API와 클라이언트를 설계하는 것이 중요합니다. 이를 위한 몇 가지 방법을 살펴보겠습니다.
어떤 두 노드 간의 호출을 생각해 봅시다. 다양한 실패가 발생할 수 있습니다.
이들 중 어느 하나라도 요청을 보낸 클라이언트는 불확실한 상황에 놓이게 됩니다. 어떤 경우에는 실패가 충분히 단정적이라서 클라이언트가 “그냥 재시도해도 안전하다”는 것을 상당히 확신할 수 있습니다. 예를 들어 서버와의 연결 자체가 전혀 수립되지 못한 경우가 그렇습니다. 하지만 다른 많은 경우에는, 클라이언트 입장에서 작업이 성공했는지 불명확하기 때문에 재시도가 안전한지 알 수 없습니다. 메시지를 주고받는 도중에 연결이 끊기는 경우가 대표적입니다.
이 문제는 분산 시스템에서 고전적으로 다뤄지는 주제입니다. 여기서 말하는 “분산 시스템”의 정의는 꽤 넓습니다. 네트워크를 통해 메시지를 주고받는 두 대의 컴퓨터만 있어도 해당될 수 있습니다. Stripe API와 그에 요청을 보내는 단 하나의 서버만으로도 분산 시스템을 이룬다고 할 수 있습니다.
실패로 인해 분산된 상태가 불일치하게 되는 문제를 해결하는 가장 쉬운 방법은 서버 엔드포인트를 멱등적(idempotent) 으로 구현하는 것입니다. 즉, 같은 요청을 몇 번을 호출하더라도 부작용(side effects)은 오직 한 번만 발생하도록 보장하는 것입니다.
클라이언트는 어떤 형태의 에러를 보더라도 재시도를 통해 자신의 상태를 서버 상태와 수렴시킬 수 있고, 검증 가능한 성공을 얻을 때까지 계속 재시도할 수 있습니다. 이렇게 하면 애매한 실패(ambiguous failure) 문제를 완전히 해결할 수 있습니다. 클라이언트는 하나의 간단한 기법(재시도)만으로 어떤 실패도 안전하게 처리할 수 있다는 것을 알기 때문입니다.
예를 들어, 가상의 DNS 제공업체 API가 있고 HTTP 요청으로 서브도메인을 추가할 수 있다고 해봅시다.
bashcurl https://example.com/domains/stripe.com/records/s3.stripe.com \ -X PUT \ -d type=CNAME \ -d value="stripe.s3.amazonaws.com" \ -d ttl=3600
레코드를 생성하는 데 필요한 모든 정보가 호출에 포함되어 있으며, 클라이언트가 이를 몇 번 호출해도 완전히 안전합니다. 서버는 도메인이 이미 존재해서 중복 요청임을 알게 되면, 요청을 단순히 무시하고 성공 상태 코드를 응답합니다.
HTTP 의미론에 따르면 PUT과 DELETE 동사는 멱등적입니다. 특히 PUT 동사는 요청 페이로드의 내용으로 대상 리소스를 전체 생성하거나 완전히 교체해야 함을 의미합니다(현대적인 REST 용어로는 부분 수정은 PATCH로 표현됩니다).
PUT과 DELETE의 본질적으로 멱등적인 HTTP 의미론은 많은 API 호출에 잘 맞지만, 어떤 작업은 정확히 한 번만 호출되어야 하고 그 이상은 안 되는 경우도 있습니다. 예를 들어 고객에게 과금을 하는 API 엔드포인트를 설계한다고 해봅시다. 실수로 두 번 호출되면 고객이 이중 청구되어 매우 심각한 문제가 됩니다.
이럴 때 멱등성 키(idempotency key) 가 등장합니다. 클라이언트는 요청을 수행할 때 해당 작업을 식별하는 고유 ID를 생성하고, 일반 페이로드와 함께 서버로 전송합니다. 서버는 이 ID를 받아 서버 측의 요청 상태와 연결합니다. 클라이언트가 실패를 감지하면 같은 ID로 요청을 재시도하고, 이후에는 서버가 이를 어떻게 처리할지 판단합니다.
앞서의 네트워크 실패 사례에 멱등성 키를 적용해 보면:
Stripe API는 변경을 일으키는 엔드포인트(즉, 여기서는 POST 아래에 있는 것들)에서 멱등성 키를 지원합니다. 클라이언트는 특별한 Idempotency-Key 헤더에 고유 값을 담아 보낼 수 있고, 이를 통해 분산 환경의 작업 안전성을 보장할 수 있습니다.
bashcurl https://api.stripe.com/v1/charges \ -u sk_test_BQokikJOvBiI2HlWgH4olfQ2: \ -H "Idempotency-Key: AGJ6FJMkGQIpHUTX" \ -d amount=2000 \ -d currency=usd \ -d description="Charge for Brandur" \ -d customer=cus_A8Z5MHwQS7jUmZ
위 Stripe 요청이 네트워크 연결 오류로 실패하더라도, 같은 멱등성 키로 안전하게 재시도할 수 있으며 고객은 한 번만 청구됩니다.
실패를 안전하게 처리하는 것은 매우 중요하지만, 그뿐 아니라 배려 있는 방식으로 처리하는 것도 권장됩니다. 클라이언트가 네트워크 작업 실패를 감지했을 때, 다음 재시도 때는 사라질 일시적(간헐적) 문제일 가능성이 큽니다. 하지만 더 심각하고 오래가는 문제일 가능성도 있습니다. 예를 들어 서버가 하드 다운타임을 유발하는 장애(incident) 진행 중이라면, 재시도는 성공하지 못할 뿐 아니라 추가적인 성능 저하를 초래할 수도 있습니다.
일반적으로 클라이언트는 에러를 만났을 때 지수 백오프(exponential backoff) 알고리즘과 유사한 방식을 따르는 것이 권장됩니다. 첫 실패 시에는 짧게 대기하지만, 실패가 반복될수록 대기 시간을 2^n(여기서 _n_은 발생한 실패 횟수)에 비례하도록 늘립니다. 지수적으로 백오프하면, 다운된 서버를 클라이언트들이 계속 두드려서(hammering) 문제를 악화시키는 일을 막을 수 있습니다.
지수 백오프는 컴퓨터 네트워킹 분야에서 길고 흥미로운 역사를 가지고 있습니다.
또한 무작위 요소를 섞는 것도 좋은 생각입니다. 서버 문제로 많은 클라이언트가 거의 동시에 실패하면, 백오프를 하더라도 재시도 스케줄이 서로 충분히 정렬(aligned)되어 있어 재시도가 문제를 겪는 서버를 또다시 두드릴 수 있습니다. 이를 ‘천둥 치는 소 떼(thundering herd) 문제’라고 합니다.
천둥 치는 소 떼 문제는 각 클라이언트의 대기 시간에 무작위 “지터(jitter)”를 추가해서 완화할 수 있습니다. 그러면 모든 클라이언트의 요청이 시간적으로 분산되어, 서버가 회복할 숨쉴 여유를 갖게 됩니다.
서버가 모든 클라이언트의 동시 재시도에 직면할 때 발생하는 천둥 치는 소 떼 문제.
Stripe Ruby 라이브러리는 실패 시 멱등성 키를 사용해 자동으로 재시도하며, 증가하는 백오프 시간과 지터를 적용합니다. 구현은 꽤 단순하며, 정확히 어떻게 동작하는지 보려면 GitHub의 해당 코드를 참고할 수 있습니다.
분산 시스템에서 실패 가능성과 그 처리 방법을 고려하는 일은 견고하면서도 예측 가능한 API를 구축하는 데 무엇보다 중요합니다. 클라이언트의 재시도 로직과 서버의 멱등성은 이 목표를 달성하는 데 유용한 기법이며, 어떤 기술 스택에서든 비교적 간단히 구현할 수 있습니다.
클라이언트와 API를 설계할 때 따를 몇 가지 핵심 원칙은 다음과 같습니다.
Stripe 소식을 계속 받아보고, 새 블로그 글을 받은편지함에서 받아보세요.
구독
Stripe는 개인정보 처리방침(Privacy Policy)에 따라 데이터를 처리합니다.
이 양식을 제출함으로써, 귀하는 Stripe의 개인정보 처리방침에 따라 개인 데이터가 처리되는 것에 동의하며, 귀하의 데이터가 Stripe의 개인정보 처리방침에 따라 중국 외 지역에 저장됨을 인지하고 동의합니다.
구독해 주셔서 감사합니다. 곧 받은편지함에서 블로그 업데이트를 받게 됩니다.
Stripe는 인터넷을 위한 금융 도구와 경제 인프라를 구축합니다.
여러분의 의견을 듣고 싶습니다.