중복 요청, 어떻게 ‘한 번만’ 처리할 것인가 – 멱등성과 서버 설계의 핵심 원리

2026. 8. 2. 01:50·Language·CS/Backend Interview

이 글은 Velog에서 이전한 글입니다. Velog 원문 보기

문제 상황: 중복 요청이 일으키는 치명적 결과

게시판 서비스에서 사용자 글을 작성하고 '등록' 버튼을 눌렀습니다.
그런데 서버에서 늦어지고, 브라우저는 멈춘 것처럼 보입니다.
당황한 사용자는 새로고침을 누르거나 버튼을 또 누르게 됩니다.
이 결과는 동일한 게시글이 두 개 이상 등록되는 중복 현상이 발생합니다.

게시글의 중복 등록의 경우에는 "그냥 하나 삭제하면 되지" 싶을 수 있습니다.
하지만, 상황이 금융 서비스라면 이야기가 달라집니다.

예를 들어 토스나 카카오페이 같은 송금 기능에서, 사용자가 친구에게 10만 원을 보내는 중 네트워크 지연이 생겼다고 가정하겠습니다.
화면이 멈춘 것 같아 다시 버튼을 누른다면 어떻게 될까요?
같은 송금 요청이 두 번 처리되어 20만 원이 빠져나갑니다. 이건 사용자 입장에서 단순한 버그가 아니라, 재산 피해로 이어지는 치명적인 장애입니다.

이처럼 중복 요청은 단순 UI/UX 문제를 넘어 서비스 신뢰성, 법적 책임, 데이터 무결성 문제까지 불어올 수 있는 무서운 이슈입니다.
특히 다음과 같은 상황에서 중복 요청이 쉽게 발생할 수 있습니다:

  • 사용자의 브라우저에서 새로고침 / 뒤로가기
  • 모바일 앱의 재시도 로직 (네트워크 불안정 시)
  • 프론트엔드 또는 중간 서버의 재전송 로직
  • 클라이언트/서버간 타임아웃 미스매치

그렇다면 서버는 어떻게 한 번만 처리되게 만들 수 있을까요?
다음 섹션에서는 이 문제를 해결하기 위한 멱등성(idempotency) 개념에 대해 정리합니다.


Idempotency란 무엇인가

중복 요청 문제를 이야기할 때 가장 먼저 떠올려야 할 개념이 바로 멱등성(Idempotency)입니다.

API에서의 멱등성 개념
멱등성

  • 같은 요청을 여러 번 수행하더라도 결과가 같아야한다는 성질을 말합니다.

즉,

  • 한 번 요청을 보내든
  • 같은 요청을 두 번, 세 번 보내든

서버는 항상 한 번만 처리한 것과 동일한 결과를 반환해야 합니다.
이 개념은 HTTP 표준 메서드의 설계 철학에도 그대로 반영되어 있습니다.

GET은 괜찮고, POST는 왜 위험할까?
HTTP 메서드마다 멱등성 여부가 다릅니다.

메서드 멱등성 여부 설명
GET ✅ 멱등함 같은 URL로 조회하면 항상 같은 결과가 나와야 한다.
DELETE ✅ 멱등함 같은 자원을 여러 번 삭제해도 결과는 동일해야 한다.
PUT ✅ 멱등함 같은 리소스를 같은 값으로 덮어쓰면 변화 없음.
POST ❌ 멱등하지 않음 호출할 때마다 새로운 리소스를 생성하거나 행위를 유발함.

예: POST /posts로 글을 등록하는 경우,
같은 요청이 2번 가면 글이 2개 생성됩니다.

이처럼 POST는 기본적으로 멱등하지 않습니다.
때문에 결제, 송금, 주문, 회원가입 같은 행위는 POST 방식으로 이루어지는 경우가 많고,
중복 요청 시 매우 민감한 문제를 일으킵니다.

그래서 POST 요청도 멱등하게 만들기 위한 다양한 전략이 실무에서는 사용됩니다.
대표적인 것이 Idempotency-Key 기반의 UUID 요청 추적 방식입니다.


요청 중복을 어떻게 막을 것인가

문제의 본질은 명확합니다.
사용자는 여러 번 요청을 보낼 수 있지만, 서버는 단 한 번만 처리해야 합니다.

그렇다면 서버는 이 요청을 이미 처리했는지 아닌지를 어떻게 구분할 수 있을까요?

답은 요청을 식별하는 고유 키를 두는 것입니다.
가장 대표적인 방식이 UUID와 Idempotency-Key입니다.

UUID를 활용한 중복 식별

서버가 요청의 고유성을 판별하기 위해 가장 자주 쓰는 방식은,
클라이언트가 UUID (Universally Unique Identifier)를 생성해서 요청에 포함시키는 것입니다.

POST /transfer
Idempotency-Key: 7f4a2d21-8b74-4e6c-9d4b-9dfb1a3023c2

이렇게 요청마다 유일한 키를 붙여두면, 서버는 이 키를 보고 중복 여부를 판단할 수 있습니다.
즉, "이 키로 처리한 요청이 있나?" -> 있다면 처리하지 않습니다.

Q: 요청마다 UUID를 생성해서 보낸다 -> 그렇다면 중복 요청이면 서로 다른 UUID가 아닐까?
핵심은 클라이언트가 "요청 단위"로 UUID를 "직접" 생성해야 한다는 것입니다.
예를 들어 사용자 A가 10만원을 송금했다고 하겠습니다.

  • 이 요청을 만들 때 클라이언트가 하나의 UUID를 생성해서 Idempotency-Key로 서버에 보낸다.
  • 이후 서버 응답이 늦거나 실패했을 때, 프론트는 같은 UUID로 다시 요청을 보내야 합니다.

즉, 중복인지 아닌지를 구분할 책임은 클라이언트에 있습니다.
클라이언트는 '사용자 행동 = 하나의 트랜잭션'임을 판단할 수 있는 구조를 잡아야 합니다.

예시: 송금 요청 처리 로직

  1. 송금 요청을 만들 때 클라이언트가 UUID를 생성해 함께 전송한다.
  2. 서버는 이 UUID를 기준으로 Redis나 DB에 저장된 이전 요청과 비교한다.
  3. 만약 처리 이력이 있다면: 응답을 재전송만 하고, 실제 처리는 생략한다.
  4. 만약 처리 이력이 없다면: 비즈니스 로직을 실행하고, 결과와 함께 UUID를 저장한다.

MSA에서는 서버가 어디에 저장된 UUID와 비교하는 것일까? Redis? 메시지 큐?
MSA 구조에서는 중복 요청을 감지할 책임이 각 마이크로서비스에 분산되어 있어야 합니다.

일반적인 처리 방식:

  • API Gateway나 BFF에서 Idempotency-Key를 받아서
    -> 각 서비스의 Redis 또는 DB에 저장/검사한다.

메시지 큐(Kafka/SQS) 사용 시:

  • 메시지에 Idempotency-Key 포함
  • Consumer 단에서 Redis나 DB에 처리 여부 기록
    • 메시지 큐는 "At Least Once"가 기본
    • 메시지 큐는 메시지를 줄 뿐이지, "이 메시지가 이미 처리되었나?"를 판단할 수 없습니다.
    • 이것은 Consumer의 책임이고, 그래서 Redis나 DB를 사용해서 검사를 진행합니다.
  • 이미 처리한 메시지는 skip

Kafka는 Exactly Once는 존재하긴 하지만
구성 복잡도도 크고, 성능 저하도 있어서 모든 경우에 사용하진 않습니다.

  • 그래서 보통은 Idempotency-Key + Redis/DB 체크 방식을 선호합니다.

Idempotency-Key를 메타데이터(HTTP 헤더)로 사용하는 패턴

실제 실무에서는 위에서 본 UUID를 Idempotency-Key라는 이름으로 HTTP 헤더에 명시하는 패턴이 표준처럼 사용됩니다.

POST /payments
Host: api.example.com  
Content-Type: application/json  
Idempotency-Key: 9dcba6ee-fb5e-4d0c-950e-5a3df49f3067

서버는 이 키를 기준으로 요청 중복을 감지하고,
같은 키에 대해 처리 결과를 재사용하거나 중복을 거부합니다.

클라이언트가 키를 안 보냈다면?
서버가 자동 생성할 수도 있지만, 멱등성을 보장하려면 클라이언트 측에서 명확히 관리하는 게 바람직합니다.


실제 시스템에서는 어떻게 처리할까?

앞에서 멱등성 개념과 Idempotency-Key의 동작 원리를 알아봤습니다.
그렇다면 실제로 송금, 결제, 주문과 같은 실무 시스템에서는 이걸 어떻게 구현하고 있을까요?

정답은 하나입니다.

❗️사용자 요청이 아무리 여러 번 오더라도, 서버는 단 한 번만 처리되어야 한다.
❗️그리고 이걸 정확하고, 신뢰할 수 있게 보장해야 한다.

송금/결제 시스템의 중복 방지 전략 사례

Idempotency-Key 기반 처리

  • 클라이언트가 요청마다 Idempotency-Key UUID를 생성해서 메타데이터에 포함
  • 서버는 이 키를 Redis 또는 DB에 기록
  • 같은 키로 다시 요청이 들어오면, 기존 처리 결과를 그대로 반환
  • 키는 TTL(예: 24시간)을 두고 자동 만료되도록 설계

멱등성을 API 스펙으로 강제함 -> 결제 API는 Idempotency-Key 없으면 요청 자체를 거부할 수 있음

DB 기반 멱등성 보장

  • 송금 요청 시 내부적으로 transactionId나 requestId 같은 고유 식별자를 생성
  • 이 식별자에 대해 DB Unique Key 제약조건을 걸어 중복 INSERT 자체를 방지
  • 이미 처리된 요청이면 -> 에러가 아니라, 기존 결과를 응답하도록 설계

"한 번만 처리한다"를 보장하기 위한 고민

실제 시스템에서 이걸 지키기 위해 정말 다양한 고민들이 필요합니다.

문제 상황 해결 전략
요청이 중복으로 도달 Idempotency-Key로 요청 식별, Redis/DB로 중복 확인
응답 실패로 사용자 재시도 기존 키로 처리 결과 재사용
멀티 인스턴스 환경에서 동시에 처리될 위험 Redis Lock / DB Unique 제약으로 Race Condition 방지
Consumer에서 메시지 중복 수신 메시지에 ID 포함 + 처리 이력 Redis에 저장

💬 고민 포인트들

  • 처리 결과를 어디에 저장할 것인가? (Redis, DB, Disk?)
  • 저장된 결과는 언제까지 유지할 것인가? (TTL 10분? 24시간?)
  • 트랜잭션이 실패했는지, 성공했는데 응답이 실패한 건지 어떻게 구분할 것인가?
  • Concurrent 요청이 동시에 처리되지 않도록 어떻게 막을 것인가?

이 모든 것들이 한 번만 처리되게 만드는 아키텍처 설계의 고민입니다.


서버에서 중복 요청을 방어하는 기술들

서버는 언제든지 같은 요청을 여러 번 보낼 수 있습니다.
심지어 의도치 않게, 또는 네트워크 불안정 때문에 자동으로 여러 번 요청이 발생할 수도 있습니다.

그래서 서버는 반드시, 요청이 여러 번 오더라도 단 한 번만 처리되도록 방어 로직을 갖춰야 합니다.

Redis 분산 락을 이용한 선점 처리

중복 요청이 동시에 들어오는 경우, Race Condition이 발생할 수 있습니다.
이때 한 요청이 먼저 선점하고, 나머지를 차단되도록 하기 위해 분산 락(distributed lock)을 사용합니다.

💡 대표 기술: Redis + SETNX or Redisson

Boolean locked = redisTemplate.opsForValue()
  .setIfAbsent("lock:transfer:abc-123", "1", Duration.ofSeconds(10));
if (!locked) {
    throw new DuplicateRequestException("이미 처리 중인 요청입니다.");
}
  • 첫 번째 요청만 락을 획득하고 처리
  • 나머지 요청은 처리하지 않고 바로 리턴
  • 락은 일정 시간 후 만료되도록 TTL 설정 필요

✅ 선점 처리 -> “처리 중인 요청은 기다리지 않고 거절”

DB Unique Key로 중복 저장 방지

한 번 처리된 요청에 대해 동일한 결과가 DB에 중복 저장되지 않도록 하는 가장 확실한 방법은,
데이터베이스에 Unique 제약조건을 설정하는 것입니다.

💡 예시: 송금 요청에 대해 request_id를 유니크하게 관리

CREATE TABLE transfer (
  id SERIAL PRIMARY KEY,
  request_id VARCHAR(36) UNIQUE,
  from_user_id UUID,
  to_user_id UUID,
  amount INT
);
  • 동일한 request_id로 INSERT 시도 시, DB가 중복을 막고 예외 발생
  • 서버는 이 예외를 캐치해서 -> 기존 요청으로 간주하고 처리 결과를 재전송
try {
    transferRepository.save(transfer);
} catch (DataIntegrityViolationException e) {
    return getPreviousTransferResult(requestId);
}

✅ 장점: 동시성 이슈까지 포함해서 가장 안전하게 중복 방지 가능

메시징 시스템의 Deduplication (Kafka, SQS 등)

MSA 구조에서는 서비스 간 통신에 Kafka, SQS 같은 메시징 큐를 많이 사용합니다.
하지만 이 시스템들은 기본적으로 At-Least-Once (최소 1회 전송)을 보장하기 때문에,
같은 메시지를 중복 소비하는 상황이 반드시 발생합니다.

이를 방지하기 위해 메시지 자체에 식별자(ID)를 포함하고, 처리 이력을 기록합니다.

💡 예시: Kafka Consumer에서 Redis 기반 Deduplication

String messageId = message.getHeader("eventId");
if (redis.exists("processed:" + messageId)) {
    // 이미 처리된 메시지
    return;
}
processMessage(message);
redis.set("processed:" + messageId, "1", Duration.ofHours(1));
  • 메시지에 포함된 고유 ID로 처리 여부를 체크
  • 이미 처리된 메시지면 무시
  • Redis TTL을 두어 메모리 낭비 최소화

✅ 정리: 결국 중요한 건 ‘한 번만 처리되게 만들기’

지금까지 중복 요청에 대한 개념부터

  • 멱등성(Idempotency)의 정의,
  • UUID 기반 요청 식별,
  • 실제 시스템에서의 중복 방지 전략,
  • 그리고 서버 측의 방어 기술까지 살펴봤습니다.

많은 기술이 등장했고, 아키텍처도 복잡했습니다.
하지만 결국 그 모든 복잡함은 단 하나의 목적을 위해 존재합니다.

❗️사용자의 요청은 반드시 단 한 번만 처리돼야 한다.

실무에서 정말 중요한 건

  • 사용자는 요청이 여러 번 전송되는 걸 모를 수 있다.
  • 버튼을 두 번 눌렀거나, 네트워크가 불안정했거나, 앱이 멈췄거나.

하지만 시스템은 항상
이 사용자가 의도한 요청이 단 한 번만 수행되었는가?
이 질문에 "예"라고 답할 수 있어야 합니다.

그리고 그걸 보장하기 위해:

  • 클라이언트는 요청 단위로 Idempotency-Key를 생성하고 재사용해야 하고,
  • 서버는 그 키를 기준으로 중복 처리 여부를 식별하고, 처리 결과를 기억해야 하며,
  • DB나 메시지 시스템, 분산 락 등의 하위 시스템과의 연동까지 고려해야 합니다.

💬 마무리하며
저는 이 주제에 대해 예전에는 단순히

“프론트에서 버튼을 비활성화시키면 되지 않을까?”
생각했지만, 실제로 면접에서 이 질문에 제대로 답변하지 못한 경험을 통해
“한 번만 처리되게 만드는 것”은 백엔드 시스템의 본질적인 과제라는 걸 알게 되었습니다.

이 글이 중복 요청 문제에 대해 고민 중인 개발자들에게
“어떻게 동작해야 하는가”보다 “어떻게 책임져야 하는가”를 먼저 묻는 계기가 되기를 바랍니다.


🔗 참고 자료
Stripe 공식 문서 – Idempotency
카카오페이 기술 블로그
토스 기술 블로그
MDB - HTTP 멱등성

'Language·CS > Backend Interview' 카테고리의 다른 글

DB Replication 구조와 Read/Write 분리 전략의 장단점  (0) 2026.08.02
무중단 배포 전략(Rolling, Blue-Green, Canary ): 트래픽 제어부터 롤백까지  (0) 2026.08.02
JPA - Cascade와 orphanRemoval, 진짜 이해하고 쓰고 있나요?  (0) 2026.08.02
Redis 키 네임스페이스와 Hash 구조, 실무에서 어떻게 설계할까?  (0) 2026.08.02
'Language·CS/Backend Interview' 카테고리의 다른 글
  • DB Replication 구조와 Read/Write 분리 전략의 장단점
  • 무중단 배포 전략(Rolling, Blue-Green, Canary ): 트래픽 제어부터 롤백까지
  • JPA - Cascade와 orphanRemoval, 진짜 이해하고 쓰고 있나요?
  • Redis 키 네임스페이스와 Hash 구조, 실무에서 어떻게 설계할까?
pp8817
pp8817
공부한 내용, 개발 관련 지식, 트러블 슈팅 등을 기록합니다. 이전 블로그: https://velog.io/@pp8817/posts
  • pp8817
    끄적이는 개발 log
    pp8817
  • 전체
    오늘
    어제
    • 분류 전체보기 (273)
      • Project (71)
        • 척척학사 (29)
        • Book (23)
        • Saynow (3)
        • YAPP 27기 (4)
        • 나의 작은 프로젝트 (10)
        • Landit (2)
      • Backend (88)
        • Spring (13)
        • Spring MVC (13)
        • Spring Security (4)
        • JPA (26)
        • Database (19)
        • HTTP·Web (13)
        • Architecture (0)
      • Language·CS (59)
        • Java·Kotlin (5)
        • CS Interview (17)
        • Backend Interview (5)
        • Concepts (32)
      • Algorithm (25)
        • Algorithm (24)
        • 소마 알고리즘 스터디 (1)
      • Infra (8)
      • Troubleshooting (11)
      • Retrospective (6)
      • Etc (5)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    Spring
    BOJ
    http
    jpa
    interview
    트러블슈팅
    개념 정리!
    Project
    CS Interview
    척척학사
    object
    게시판
    Spring MVC
    Python
    Algorithm
    Book
    OS
    java
    나의 작은 프로젝트
    HTTP WEB 기본 지식
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.6
pp8817
중복 요청, 어떻게 ‘한 번만’ 처리할 것인가 – 멱등성과 서버 설계의 핵심 원리
상단으로

티스토리툴바