backend

멱등성(Idempotency)이란 무엇인가? 안전한 API 설계를 위한 필수 가이드

개발자가 API를 설계할 때 가장 자주 마주치는 단어 중 하나가 바로 **멱등성(Idempotency)**입니다. 특히 네트워크 오류가 빈번한 분산 시스템이나 중복 결제 사고가 발생해서는 안 되는 금융 시스템에서 멱등성은 선택이 아닌 필수입니다. 오늘은 시니어 개발자의 시선으로 멱등성의 정확한 정의부터 실무 적용 사례까지 깊이 있게 다뤄보겠습니다.

목차

  1. 멱등성(Idempotency)의 정의
  2. HTTP 메서드와 멱등성의 관계
  3. 왜 멱등성이 중요한가? (분산 시스템과 보안)
  4. 실무에서 멱등성을 구현하는 방법: Idempotency Key
  5. 개발자의 팁: 안전한 API 설계를 위한 체크리스트

멱등성(Idempotency)의 정의

멱등성이란 수학적 용어로, 연산을 여러 번 적용하더라도 결과가 달라지지 않는 성질을 의미합니다. 수학식으로 표현하면 $f(f(x)) = f(x)$가 성립하는 경우를 말합니다.

이를 웹 API 관점에서 해석하면 다음과 같습니다.

“동일한 요청을 한 번 보내는 것과 여러 번 연속해서 보내는 것이 시스템의 상태에 미치는 영향이 동일해야 한다.”

중요한 점은 **응답 값(Response body)**이 매번 같아야 한다는 뜻이 아니라, 서버의 **상태(State)**가 추가적으로 변경되지 않아야 한다는 것입니다.

Illustration of idempotency: multiple identical requests resulting in the same server state after the initial operation

HTTP 메서드와 멱등성의 관계

RFC 7231 표준에 따르면 HTTP 메서드는 멱등성을 가진 것과 그렇지 않은 것으로 구분됩니다.

메서드멱등성 여부설명
GETO데이터를 조회만 하므로 서버 상태를 변경하지 않습니다.
PUTO대상을 요청 데이터로 완전히 대체합니다. 여러 번 수행해도 결과는 동일합니다.
DELETEO데이터를 삭제합니다. 이미 삭제된 데이터를 또 삭제해도 상태는 ‘삭제됨’입니다.
POSTX호출할 때마다 새로운 리소스를 생성하므로 서버 상태가 매번 변합니다.
PATCHX/O어떻게 구현하느냐에 따라 다릅니다. 부분 교체 시 값 누적 연산이 포함되면 비멱등적입니다.

왜 멱등성이 중요한가? (분산 시스템과 보안)

네트워크 세상에서는 **재전송(Retrying)**이 일상입니다. 클라이언트가 API를 호출했는데 응답을 받기 직전 네트워크가 끊겼다고 가정해봅시다.

  1. 결제 서비스: 사용자가 ‘결제하기’를 눌렀는데 타임아웃이 발생했습니다. 클라이언트 앱은 안전을 위해 재전송을 시도합니다. 만약 API가 멱등하지 않다면 사용자 통장에서는 돈이 두 번 빠져나갈 것입니다.
  2. 보안 사고 방지: 중복된 요청이 서버 리소스를 고갈시키는 DoS(Denial of Service) 공격의 빌미가 될 수 있습니다. 멱등성을 보장하면 중복된 요청에 대해 서버 리소스를 낭비하지 않고 즉시 이전 결과를 반환할 수 있습니다.

실무에서 멱등성을 구현하는 방법: Idempotency Key

주로 POST 메서드처럼 멱등하지 않은 요청에 멱등성을 부여하기 위해 멱등성 키(Idempotency Key) 패턴을 사용합니다. Stripe API가 이를 활용하는 대표적인 사례입니다.

/**
 * 멱등성을 보장하는 결제 요청 처리 로직 예시
 */
async function processPayment(requestId: string, amount: number) {
  // 1. 이미 처리된 요청인지 데이터베이스(Redis 등)에서 확인
  const existingRecord = await redis.get(`idempotency_key:${requestId}`);
  
  if (existingRecord) {
    // 2. 이미 처리된 이력이 있다면 기존에 저장된 응답을 그대로 반환
    return JSON.parse(existingRecord);
  }

  // 3. 실제 비즈니스 로직 수행 (결제 처리)
  const paymentResult = await paymentGateway.charge(amount);

  // 4. 요청 ID와 결과를 함께 저장 (TTL 설정으로 일정 시간 후 삭제)
  await redis.set(`idempotency_key:${requestId}`, JSON.stringify(paymentResult), 'EX', 86400);

  return paymentResult;
}

개발자의 팁: 안전한 API 설계를 위한 체크리스트

실무에서 멱등성을 구현할 때 자주 하는 실수 중 하나는 **부수 효과(Side Effect)**를 간과하는 것입니다.

Warning: DELETE 요청 시 응답 코드가 처음에는 204 No Content였더라도 두 번째는 404 Not Found가 될 수 있습니다. 이는 멱등성을 위반한 것이 아닙니다. 멱등성은 서버의 리소스 상태에 초점을 맞추기 때문입니다.

  1. Unique Key 활용: DB 수준에서 Unique Constraint를 활용하여 중복 생성을 원천 차단하세요.
  2. 멱등성 키 만료 시간: 멱등성 데이터는 무한히 저장할 수 없으므로 비즈니스 도메인에 맞는 적절한 TTL(Time-To-Live)을 설정해야 합니다.
  3. 에러 전파 주의: 이전 요청이 실패했다면, 재시도 시에도 실패 결과를 줄 것인지 아니면 재시도를 허용할 것인지 명확히 정의해야 합니다.

자주 묻는 질문 (FAQ)

Q1. 모든 API를 멱등하게 만들어야 하나요?

이론적으로는 좋지만 비용이 듭니다. 데이터 조회(GET)나 단순 상태 변경(PUT, DELETE)은 이미 멱등하므로 추가 처리가 필요 없지만, 결제나 알림 발송처럼 중복 실행 시 치명적인 영향을 주는 POST 요청 위주로 먼저 적용하는 것이 효율적입니다.

Q2. 멱등성 키로는 무엇을 쓰는 것이 좋나요?

보통 클라이언트가 생성한 UUID v4를 사용합니다. 혹은 비즈니스 로직에 따라 user_id + order_id와 같은 조합으로 결정론적인 키를 생성하기도 합니다.

Q3. PATCH 메서드는 왜 멱등할 수도 있고 아닐 수도 있나요?

{ "age": 30 } 처럼 절대적인 값으로 수정하는 경우 멱등합니다. 하지만 { "age": "+1" } 처럼 기존 값에 의존하는 상대적 연산을 수행한다면 호출 횟수만큼 나이가 늘어나므로 멱등하지 않게 됩니다.

안전한 시스템 구축은 멱등성을 이해하는 것에서 시작합니다. 여러분의 API가 예기치 못한 재시도 상황에서도 평온함을 유지할 수 있도록 설계해 보세요!

이 글이 마음에 드셨나요?

로딩 중...