이중 요청 대응 핸드북

2重リクエスト完全攻略HANDBOOK / Shohei Mitani - Kaigi on Rails 2025

작성자
Kaigi on Rails
발행일
2025년 11월 25일

핵심 요약

  • 1 이중 요청(Double Request) 문제의 정의, 발생 원인 및 심각성을 이해하고, 이에 대한 포괄적인 방어 전략을 학습합니다.
  • 2 클라이언트 및 백엔드 측면에서 이중 요청 방어를 위한 9가지 주요 전략을 소개하고 각 방법론의 특성과 적용 시 고려사항을 설명합니다.
  • 3 실제 서비스(One Bank)에서 결제, 입출금 등 중요 비즈니스 로직에 적용된 이중 요청 방어 사례와 각 전략의 선택 기준을 분석합니다.

도입

본 발표는 웹 서비스 개발에서 흔히 발생하는 '이중 요청(Double Request)' 문제의 본질과 이에 대한 효과적인 대응 전략을 다룹니다. 이중 요청은 사용자의 실수(버튼 중복 클릭, 페이지 새로고침)나 시스템 오류(외부 서비스 리트라이, 배치 오류) 등으로 인해 동일한 요청이 여러 번 전송되어 데이터 불일치, 중복 처리, 심지어 보안 취약점으로 이어질 수 있는 심각한 문제입니다. 발표자는 웹 검색을 통해 얻을 수 있는 단편적인 정보의 한계를 지적하며, 복잡한 실제 애플리케이션 환경에서 적용 가능한 포괄적인 이중 요청 방어 핸드북의 필요성을 강조합니다. 본 핸드북은 갱신(POST, PUT, DELETE) 계열의 API를 중심으로, 의도치 않은 이중 요청과 악의적인 이중 요청 모두에 대한 방어책을 제시하여 안정적이고 견고한 시스템 구축에 기여하고자 합니다.

이중 요청 방어 전략 개요

이중 요청 방어는 클라이언트 측 제어, 백엔드 측 제어, 그리고 클라이언트와 백엔드의 연동을 통한 제어로 구분할 수 있습니다. 각 접근 방식은 고유한 장단점을 가지며, 애플리케이션의 특성과 중요도에 따라 적절히 조합하여 적용해야 합니다.

클라이언트 측 방어

  • 서브밋 버튼 제어: 가장 기본적인 방법으로, 요청 전송 시 버튼을 비활성화하여 중복 클릭을 방지합니다. Rails UJS(5.0-6.1)에서는 data-with 속성을 사용했으며, 현재는 Stimulus 등으로 직접 구현합니다.

백엔드 측 방어

  • PRG(Post-Redirect-Get) 패턴: POST 요청 완료 후 HTML을 직접 반환하는 대신 결과 페이지로 리다이렉트하여 브라우저 새로고침으로 인한 이중 요청을 방지합니다.

  • 배타 제어(Exclusive Control): 특정 리소스에 대한 동시 접근을 제한하여 데이터 불일치를 방지합니다. Active Record의 with_lock을 활용한 비관적 락(Pessimistic Lock), Advisory Lock, Redis를 이용한 락, 또는 FIFO(First-In, First-Out) 큐를 활용하는 방법이 있습니다. 락 해제 후의 대응과 불필요한 리소스에 대한 락 사용은 주의해야 합니다.

  • 테이블 설계: 이중 요청 발생 시에도 데이터 무결성을 유지할 수 있도록 테이블을 설계합니다. 상태 전이 규칙(State Transition Rule)을 정의하거나, 고유 제약(Unique Constraint)을 활용하여 중복 요청 시 데이터베이스 레벨에서 오류를 발생시킵니다. AASM Gem을 활용하여 상태 전이를 관리할 수 있습니다.

  • 레이트 리미트(Rate Limit): 일정 기간 동안 처리할 수 있는 요청 수를 제한하여 시스템 과부하를 방지하고 이중 요청을 제어합니다. 사용자 ID 등을 키로 사용하여 특정 API의 호출 횟수를 제한할 수 있으며, Rack Attack, Throttling 등의 Gem을 활용할 수 있습니다.

  • API 캐시: 이전에 처리된 요청의 응답을 캐시하여 동일한 요청이 다시 올 경우 캐시된 응답을 반환함으로써 멱등성을 보장합니다. 클라이언트 관점에서는 모두 성공으로 처리되지만, 캐시 키 설계, 유효 기간, 응답 크기, 캐시 저장 전 중복 요청 처리 등의 어려움이 있습니다.

클라이언트-백엔드 연동 방어

  • 멱등성 키 헤더(Idempotency Key Header): 클라이언트가 요청에 고유 식별 키를 부여하여 전송하고, 서버는 이 키를 기반으로 요청의 멱등성을 보장합니다. API 캐시보다 더 복잡하지만, 유연하고 견고한 API 서버 구축이 가능하며, 미처리/처리 중/처리 완료 등 세부 상태 관리가 가능하여 악의적인 리트라이와 우발적인 중복 요청을 구분할 수 있습니다.

  • Etag 및 If-Match: Etag는 리소스의 버전을 태그로 관리하고, 클라이언트가 If-Match 헤더를 통해 특정 버전과 일치할 경우에만 요청을 처리하도록 합니다. 이는 낙관적 락(Optimistic Lock) 방식과 유사하며, 동시 업데이트 방지에 효과적입니다. 변경 빈도가 낮은 리소스(예: 사용자 프로필)에 적합합니다.

  • 원타임 토큰(One-time Token): 한 번만 사용 가능한 토큰을 발행하여 이중 요청을 방지하고, 정당한 요청임을 보증하여 리플레이 공격(Replay Attack)과 같은 부정 행위를 방지합니다. CSRF 토큰과 유사하지만, 토큰의 일회성이 필수적입니다.

One Bank의 이중 요청 방어 사례

  • 멱등성 키 헤더: 외부 서비스와의 연동(결제, 입출금)과 같이 복잡하고 데이터 불일치 위험이 큰 중요 작업에 사용됩니다. 우발적인 이중 요청과 의도적인 다른 요청을 구분하여 처리합니다.

  • 원타임 토큰: 패스코드 등록, 카드 번호 표시 등 보안 강도가 높은 앱 내 중요 처리에 적용됩니다. 리플레이 공격 방지가 주 목적이며, 이중 요청 방지를 위해 멱등성 키와 함께 사용되는 경우도 많습니다.

  • 레이트 리미트: 가상 카드 재발급과 같이 비정상적인 빈도로 요청될 가능성이 있는 작업에 적용됩니다. 낮은 비용으로 잠재적 문제를 해결할 수 있습니다.

  • 테이블 설계 + 캐시: 외부 서비스 웹훅 처리와 같이 비동기적이고 리트라이가 발생하는 상황에서 데이터 일관성을 유지하고 리트라이 루프를 방지하는 데 활용됩니다. 외부 서비스의 ID를 기반으로 멱등성을 보장합니다.

결론

이중 요청 문제는 모든 개발자에게 친숙하지만, 그 복잡성과 다양한 발생 원인으로 인해 효과적인 대응책 마련이 쉽지 않습니다. 본 발표는 문제의 구조를 이해하고, 클라이언트 및 백엔드 측면에서 적용 가능한 9가지 방어 전략을 제시했습니다. 대부분의 경우, 서브밋 버튼 제어, PRG 패턴, 배타 제어, 그리고 견고한 테이블 설계를 조합하는 것만으로도 충분한 방어가 가능합니다. 그러나 결제, 중요 데이터 변경 등 치명적인 피해를 초래할 수 있는 상황에서는 멱등성 키 헤더, 원타임 토큰과 같은 보다 정교한 기법을 적용하여 보안과 데이터 무결성을 극대화해야 합니다. 각 전략의 장단점과 적용 비용을 고려하여 서비스의 특성에 맞는 최적의 방어 체계를 구축하는 것이 중요합니다.

댓글 0

로그인이 필요합니다

댓글을 작성하거나 대화에 참여하려면 로그인이 필요합니다.

로그인 하러 가기

아직 댓글이 없습니다

첫 번째 댓글을 작성해보세요!