멱등성은 응답이 아니라 상태다
- 개발
- 2025. 12. 23. 22:07
분산 환경에서 요청은 한 번만 도착한다는 보장은 없습니다.
네트워크 지연, 타임아웃, 클라이언트 재시도 등으로 동일한 요청은 언제든지 여러 번 전달될 수 있습니다. 이때 자주 등장하는 개념이 바로 멱등성입니다. 멱등성은 흔히 "여러 번 동작해도 같은 결과를 반환하는 성질"이라고 설명됩니다. 하지만 이 정의만으로는 멱등성과 재시도의 차이를 제대로 이해하기 어렵습니다. 재시도 역시 요청을 여러 번 수행한다는 점에서는 멱등성과 매우 유사해 보이기 때문입니다. 그럼에도 불구하고 두 개념은 결과와 책임의 관점에서 명확히 다릅니다. 재시도는 실패 가능성을 전제로 다시 요청을 보내는 행위인 반면, 멱등성은 중복 요청이 발생하더라도 시스템의 상태를 안전하게 유지하는 설계적 성질에 가깝습니다. 이 차이를 구분하지 못하면, 재시도는 곧바로 중복 실행과 데이터 불일치로 이어집니다. 보통 멱등성을 설명할 때 HTTP 메서드(GET, PUT, DELETE 등)를 떠올리지만, 이는 멱등성을 이해하기 위한 하나의 표현 수단일 뿐입니다. 만약 HTTP 메서드의 의미에 의존하지 않는다면, 혹은 HTTP가 아닌 환경이라면 멱등성은 어떻게 보장해야 할까요?
이 글에서는 HTTP 메서드에 의존하지 않고도 멱등성을 설계할 수 있는 방법들을 살펴봅시다.
멱등성을 사용하지 않는다면 어떤 일이 발생할까?
멱등성은 분산 환경에서 신뢰성과 일관성을 유지하는데 중요한 역할을 하게 됩니다. 네트워크 지연이나 타임아웃으로 인해 API 요청이 실제로는 성공했음에도 불구하고, 클라이언트가 이를 실패로 인식하는 상황은 흔하게 발생합니다. 이러한 경우 클라이언트는 동일한 요청을 다시 전송하게 되며, 서버가 중복 요청을 식별하지 못한다면 재시도는 의도하지 않은 중복 처리나 데이터 손상으로 이어질 수 있습니다.
멱등 연산은 동일한 요청이 여러 번 또는 동시에 처리되더라도 시스템의 상태가 한 번 실행한 결과로 수렴하도록 보장함으로써, 분산 환경에서 발생하는 중복 실행의 부작용을 최소화합니다. 이를 통해 시스템은 예측 가능하고 안정적인 동작을 유지할 수 있으며, 사용자에게 일관된 결과를 제공할 수 있도록 도와줍니다.
그럼 서버는 중복 요청을 어떻게 구분해야 할까?
멱등성을 처음 접할 때 가장 많이 떠올리는 예시는 HTTP 메서드일 것입니다. HTTP는 일부 메서드에 대해 멱등한 동작을 기대하도록 의미를 정의하고 있으며, 이는 멱등성을 이해하기 위한 대표적인 출발점으로 자주 활용됩니다.
멱등성을 지원하는 메서드와 멱등성을 지원하지 않는 메서드들을 구분하면서 얘기해 봅시다.
멱등 메서드
- GET
- 서버 상태를 수정하지 않고 정보를 조회하는 역할을 합니다.
- 리소스 상태가 변경되지 않는 이상, 동일한 응답을 리턴합니다. - PUT
- 리소스의 수정 또는 생성을 합니다.
- 같은 요청이 여러 번 적용되더라도 리소스의 최종 상태가 항상 동일하기 때문에 멱등합니다. - DELETE
- 리소스의 삭제를 합니다.
- 동일한 요청을 여러 번 수행해도 리소스가 ‘삭제된 상태’로 유지되기 때문에 멱등합니다.
비 멱등 메서드
- POST
- 새로운 리소스를 생성합니다.
- 사이드 이펙트가 발생할 수밖에 없는 행위에 사용됩니다.
- 결과가 요청 시점에 결정되지 않음
- 서버가 새 식별자를 만들거나
- 외부 시스템을 호출하거나
- 내부적으로 여러 단계를 거침
- PATCH
- 리소스를 부분 변경합니다.
- 최종 상태를 선언하지 않습니다.
- 전체를 덮어쓰지 않습니다.
- 변경 방법(연산)을 전달합니다.
-> 멱등할 수도 멱등하지 않는 메서드
그렇다면 HTTP에서 멱등 메서드로 분류된 요청들은 항상 멱등하다고 볼 수 있을까요?
반대로 비 멱등 메서드는 반드시 멱등하지 않을까요? 전혀 그렇지 않습니다. 예를 들어, 조회할 때마다 조회수가 증가하거나 최근 조회목록이 기록이 된다면 이는 요청할 때마다 상태가 변경이 되기 때문에 멱등성이 깨진다고 할 수 있습니다.
비 멱등성인 메서드도 설계에 따라 멱등할 수도 있습니다. (추후에 어떻게 설계할 수 있는지 알아봅시다.)
이처럼 HTTP 메서드는 멱등성에 대한 의도를 표현할 뿐이고, 실제 멱등성 보장은 서버 구현에 달려 있습니다.
멱등성을 지원한다는 것은 무엇을 의미할까?
트랜잭션 ID나 요청 ID와 같은 고유 식별자에 의해 멱등성을 제공받을 수 있습니다. 하지만 분산시스템에서 식별자를 일관되게 생성하고 관리하는 것은 어려울 수 있습니다. 왜냐하면, 동일한 ID를 가진 동시 요청은 경쟁 조건으로 이어질 수 있습니다. 스레드 안정성과 동시작업의 일관된 동작을 보장하려면 신중한 설계가 필요합니다.
첫 번째, 고려해야 될 사안은 여러 단계가 포함된 작업은 중간에서 실패할 수도 있습니다. 즉, 롤백 메커니즘을 설계하거나 궁극적인 일관성을 보장하는 것은 멱등성을 유지하는데 중요합니다.
두 번째, 상태를 어떻게 관리를 할 수 있을까요? 멱등성은 결국 상태를 저장하는 문제이며, 이 상태를 저장하는 순간 추가적인 비용이 발생하게 됩니다. 처리된 요청이나 리소스의 상태를 계속해서 확인하고 추적하는 과정 역시 요청 처리에 부담을 줄 수 있으며, 경우에 따라서는 별도의 인프라 구성이 필요할 수도 있습니다.
또한, 분산 캐시를 사용한다면, 멱등키를 추적할 때 캐시 만료를 관리하는 것이 중요합니다. 만약, 클라이언트가 키를 삭제한 후 요청을 다시 시도하면, 만료된 키가 중복 처리될 수 있기 때문입니다.
그렇다면 어떻게 구현을 할 수 있을지 여러 전략들을 살펴보겠습니다.
멱등성을 구현하기 위한 전략들
멱등성을 구현하기 위해 다양한 전략들이 존재합니다.
데이터베이스 고유 제약 조건
가장 간단한 방법으로 신뢰할 수 있는 방법 중 하나입니다.
transation_id나 request_id와 같은 고유한 키를 사용하여 데이터베이스 수준에서 중복 작업을 방지할 수 있습니다.
예를 들어, 다음과 같이 transaction_id에 UNIQUE 제약을 둔 테이블을 구성할 수 있습니다.
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
user_id INT NOT NULL,
product_id INT NOT NULL,
quantity INT NOT NULL,
transaction_id UUID UNIQUE NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ;
해당 테이블에는 transaction_id라는 값을 활용해서 동일한 결과인지 아닌지를 판명하게 됩니다.
테이블에 고유한 키를 사용하기 때문에 단순합니다. 추가적인 인프라는 필요하지 않습니다. 고유한 값을 사용하기 때문에 멱등하다는 신뢰도 쉽게 얻습니다.
다만, 쓰기 요청이 빈번하게 발생하는 시스템에서는 주의가 필요합니다. 동일한 키에 대한 쓰기 요청이 반복될 경우, 데이터베이스가 고유 제약을 유지하는 과정에서 병목 지점이 발생할 수 있으며, 이는 전체 처리 성능에 영향을 줄 수 있습니다.
인 메모리 추적
앞서 살펴본 데이터베이스 고유 제약 조건 방식과 인메모리 추적 방식은 모두 고유한 키를 통해 멱등성을 판단한다는 점에서는 동일합니다.
다만, 멱등 상태를 어디에 저장하고 관리하느냐에 따라 보장 범위와 신뢰성은 크게 달라집니다.
인 메모리는 Set이나 Map과 같은 데이터 구조를 통해 멱등 키를 관리하는 형태로도 구현될 수 있습니다.
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
public class IdempotencyStore {
// 이미 처리된 transactionId 목록
private static final Set<Uuid> processedIds =
ConcurrentHashMap.newKeySet();
public static boolean isProcessed(Uuid transactionId) {
return processedIds.contains(transactionId);
}
public static void markProcessed(Uuid transactionId) {
processedIds.add(transactionId);
}
}
표준 데이터 구조를 사용하기 때문에 구현이 쉬우며, 외부 종속성이나 인프라가 필요하지 않습니다. DB도 필요 없습니다.
또한, 인메모리 데이터 구조의 조회 및 삽입이 빠르기 때문에 조회 성능은 매우 좋습니다.
하지만 인메모리 방식은 데이터가 프로세스 메모리에 저장되기 때문에 몇 가지 명확한 한계를 가집니다.
서비스가 재시작될 경우 메모리에 저장된 멱등 정보는 모두 사라지며, 이는 동일한 요청이 다시 처리되는 중복 실행으로 이어질 수 있습니다.
또한 트래픽이 많거나 장시간 실행되는 서비스에서는, 메모리가 감당할 수 있는 범위를 초과할 가능성도 존재합니다.
인메모리 저장소에 기존 항목을 정리하는 메커니즘이 없다면, 멱등 키가 계속 누적되어 결국 메모리 소진으로 이어질 수 있습니다.
이를 방지하기 위해 TTL(Time To Live)을 적용할 수 있지만, 이 경우 구현과 운영 복잡성이 증가하게 됩니다.
더 큰 문제는 분산 환경입니다.
인메모리 추적은 각 서비스 인스턴스가 서로 다른 메모리 공간을 사용하기 때문에, 여러 노드가 동시에 실행되는 분산 시스템에서는 멱등 상태를 공유할 수 없습니다.
노드 간 상태를 동기화하려면 추가적인 조정 메커니즘이 필요하며, 이 시점부터 인메모리 방식의 단순함은 사라지게 됩니다.
따라서 인메모리 기반 멱등성은 다음과 같은 제한된 상황에서 사용하는 것이 적절합니다.
- 서비스 수명이 짧고, 멱등성 보장 범위가 제한적인 API
- 상태 공유가 필요 없는 단일 인스턴스 환경에서 실행되는 서비스
Redis와 같은 분산 캐시 사용

이번에도 멱등 상태를 어딘가에 저장하는 방식은 동일합니다. 다만 이번에는 애플리케이션 내부 메모리가 아닌 Redis에 멱등 상태를 저장합니다. Redis는 고성능 인메모리 기반의 데이터 저장소로, 빠른 읽기와 쓰기를 제공하기 때문에 대량의 요청이 발생하는 환경에서도 멱등성 판단에 따른 처리 비용을 효과적으로 줄일 수 있습니다.
또한 Redis는 TTL(Time To Live)을 기본적으로 지원하므로, 멱등 키가 무한정 누적되는 문제를 자연스럽게 방지할 수 있습니다. 별도의 정리 로직 없이도 일정 시간이 지난 멱등 상태를 자동으로 제거할 수 있어, 메모리 관리에 대한 부담을 줄일 수 있습니다.
이러한 특성 덕분에 Redis를 활용한 멱등성은 단일 인스턴스 환경을 넘어, 여러 서버가 동시에 요청을 처리하는 분산 환경에서도 일관된 중복 요청 처리를 가능하게 합니다.
다만 Redis를 사용하기 위해서는 별도의 인스턴스 또는 클러스터를 구성하고 운영해야 하므로, 시스템 전반의 운영 복잡도가 증가하고 외부 인프라에 대한 의존성이 생길 수 있습니다. 또한 Redis 인스턴스가 장애로 인해 사용할 수 없게 되는 경우, 이를 대체할 추가적인 메커니즘이 없다면 멱등성 추적 자체가 실패할 수 있다는 점도 고려해야 합니다.
메시지 중복 탐지 사용

앞서 살펴본 방식들과 마찬가지로, 메시지 중복 탐지 역시 멱등성과 관련된 상태를 관리한다는 점에서는 공통점을 가집니다. 다만 그 상태를 애플리케이션의 비즈니스 로직 내부에서 직접 관리하는 대신, 메시지 전달을 담당하는 계층에서 관리한다는 점에서 차이가 있습니다.
메시지 큐는 이미 전달된 메시지 ID, 이미 소비된 메시지의 offset, 또는 일정 시간 내 처리된 중복 메시지 ID 중 하나를 기억합니다. 이는 특정 메시지가 이미 한 번 전달되었거나 소비되었다는 사실을 의미하며, 이 정보는 메시지 전달 단계에서의 중복 여부를 판단하는 기준으로 사용됩니다. 메시지 큐의 목적은 데이터를 장기적으로 저장하는 데 있지 않고, 메시지를 안전하고 신뢰성 있게 전달하는 데 있습니다.
이러한 특성으로 인해 메시지 중복 탐지를 사용한다는 것은, 멱등성 판단 중 메시지 전달 여부에 대한 책임을 메시지 큐에 위임하는 선택이라고 볼 수 있습니다. 이를 통해 메시지 전달 과정에서 발생할 수 있는 중복은 효과적으로 제어할 수 있습니다.
다만 메시지 큐가 비즈니스 상태를 직접 관리하거나, 메시지가 유발하는 비즈니스 로직의 중복 실행까지 보장해 주는 것은 아닙니다. 동일한 메시지가 여러 번 전달되더라도 최종 결과가 한 번 처리된 것처럼 유지되도록 만드는 책임은 여전히 애플리케이션에 남아 있습니다.
즉, 메시지 큐는 멱등한 상태를 직접 만들어 주는 것이 아니라, 메시지를 식별하고 전달 이력을 명확히 제공함으로써 애플리케이션이 멱등한 처리를 설계할 수 있는 기반을 제공합니다.
그렇다면 어떻게 멱등 상태를 설계할 수 있을까?
분산 시스템에서 메시지 전달 방식은 보통 at-least-once, at-most-once, exactly-once로 구분됩니다. 이 중 exactly-once는 메시지가 정확히 한 번만 처리되는 것을 목표로 하지만, 실제로는 구현 비용과 복잡도가 매우 높습니다. 또한 메시지 전달과 비즈니스 로직의 실행을 완전히 하나의 트랜잭션으로 묶는 것은 대부분의 시스템에서 현실적인 선택이 아닙니다.
그 결과, 많은 시스템은 메시지는 at-least-once로 전달하고, 비즈니스 로직은 멱등하게 처리하는 방식을 선택합니다. 이 접근 방식은 중복 전달을 허용하되, 중복 실행의 결과가 한 번 실행한 것과 동일하도록 설계함으로써 시스템 전체의 신뢰성과 단순성을 동시에 확보할 수 있습니다.
그렇다면 실패가 전제된 환경에서, 중복 처리와 유실은 어떻게 관리해야 할까?
분산 환경에서 실패는 피해야 할 예외가 아니라, 반드시 고려해야 할 전제입니다. 네트워크 오류나 시스템 재시작으로 인해 요청이 중복되거나 메시지가 다시 전달되는 상황은 자연스럽게 발생합니다.
이러한 환경에서 중요한 것은 실패를 완전히 제거하는 것이 아니라, 실패 이후에도 시스템이 일관된 상태로 수렴하도록 만드는 것입니다. 이를 위해 많은 시스템은 유실을 방지하기 위해 중복을 허용하고, 중복 실행의 부작용을 제거하기 위해 멱등성을 설계합니다.
마무리
멱등하다는 것은 동일한 요청에도 같은 응답을 받아야 된다고 생각했었습니다. 하지만 멱등성은 응답이 아닌 상태로 결정이 되는 성질이라는 사실을 깨달았습니다. 멱등성을 이야기할 때, 종종 "같은 요청을 보내면 같은 응답이 온다"라는 관점으로 이해가 된 경우가 많았습니다. 하지만 응답은 요청에 대한 결과 일 뿐이며, 멱등성의 핵심은 데이터의 상태가 무엇인지에 있다고 생각합니다. 동일한 요청이 여러번 실행되더라도 데이터의 최종 상태가 한 번 실행한 것과 같다면 그 연산은 멱등하다고 볼 수 있습니다. 반대로 응답이 같더라도 요청이 실행될 때마다 달라진다면 멱등하다고 할 수 없습니다.
지금까지 멱등 상태를 DB에 저장하는 방법, 인 메모리에 저장하는 방법, 분산 캐시에 저장하는 방법, 메시지 큐를 활용하는 방법 등 등 학습했습니다. 각각의 특징들이 존재하지만, 제가 생각할 때 이들이 멱등 상태를 말해주지는 않는다고 생각합니다. 결국 어떻게 사용하냐에 따라 달라지는 영역이라고 생각합니다.
출처
https://blog.bytebytego.com/p/mastering-idempotency-building-reliable
'개발' 카테고리의 다른 글
| 깃 플로우: 브랜치로 읽는 코드의 현재 상태 (0) | 2025.12.20 |
|---|---|
| RDB vs Nosql (0) | 2025.12.12 |
| 스프링 시큐리티에는 보안 6요소를 어떻게 사용할까? (0) | 2025.11.12 |