멱등키 테이블 구현 의사결정

lilamaris
January 29, 2026 7 min read

분산 환경에서 요청 처리는 하나의 시스템에서 종결되는 흐름 뿐만 아닌, 여러 마이크로 서비스가 제공하는 기능을 조율하는 과정도 포함한다.

이 때 요청은 특정 비즈니스 목적을 달성하기 위한 마이크로 서비스들의 논리적 기능 집합으로 볼 수 있고, 마이크로 서비스 간 통신 시 최소 한 번 이상 전송을 보장하는 네트워크 환경에서는 동일한 요청이 여러 번 전송될 수 있기 때문에, 마이크로 서비스는 결제를 처리하거나 제한된 자원을 할당하는 등 되돌리기 곤란한 작업에 대해서는 사전에 중복된 요청을 판단하고 이를 방어할 수 있어야한다.

이렇듯 중복된 동일한 요청에 대해 방어하기 위한 기법 중 동일한 입력에 대해 항상 같은 결과를 반환하는 멱등성을 보장하는 방법이 있다. 예컨데, 하나의 요청마다 고유한 식별자를 부여하고 이를 관리할 수 있다면, 애플리케이션은 식별자를 바탕으로 중복된 요청인지 판단할 수 있다.

여기서 생각한 흐름

  1. 하나의 요청을 구성하는 기능 집합은 여러 시스템에 분산될 수 있다.
  2. 예시로 ‘주문’이라는 요청은 ‘재고 차감’, ‘결제’, ‘배송’ 등 관심사가 다른 여러 마이크로 서비스들이 제공하는 기능을 조율하는 시나리오로 볼 수 있다.
  3. 선제 내용을 바탕으로, 요청 흐름에 포함되는 기능들 또한 여러 번 실행될 수 있다.
  4. 요청이라는 논리적 단위에 포함되는 기능 또한 독립적인 멱등성을 가질 필요가 있다.
  5. 멱등키는 요청 맥락에서 부여받은 식별자 뿐만 아니라 작업을 구분할 수 있는 정보도 필요하다.

따라서 멱등키 범위를 요청 관점과 작업 관점에서 분리한 계약을 정의했다.

public interface IdempotencyKey {
	// Request scope idempotency key
	String subject();
	
	// Operate scope idempotency key
	String op()
}

멱등키 테이블은 이 두 개의 필드를 바탕으로 레코드 조회를 수행한다. 쿼리를 단순화하기 위해서 이 두 필드를 결합한 파생 필드를 사용할 수 있다.

IdempotencyKey idemKey = new IdempotencyKey(...);
String key = idemKey.subject() + ":" + idemKey.op();

추가로, 요청을 처리할 당시 전달된 파라미터를 실행 맥락으로 보고 이를 함께 저장할 수 있다. 이를 통해 동일한 멱등키로 중복 요청이 유입되는 상황에서 요청 본문(Request Payload 등)이 달라진 경우 충돌로 판단하고, 적절한 처리 로직을 분기할 수 있다.

public interface IdempotencyContext {
	String hash();
}

멱등성을 보장받아야할 작업의 현재 처리 상태를 표현하고, 상태 머신을 구현할 수 있다면, 이를 활용해서 효과적인 동시성 제어가 가능할 것이다.

public enum IdempotencyProgressStatus {  
    READY,  
    IN_PROGRESS,  
    COMPLETE,  
    FAIL,  
    RETRYABLE  
}

IdempotencyProgressStatus는 뒤이어 서술할 상태 머신 기반 멱등 실행기가 제어 흐름 분기를 위해 활용할 작업 처리 상태 값 정의이다.

  • READY: 멱등키를 부여받은 작업이 멱등 실행기에 의해 제어되는 흐름에 진입한 상태
  • IN_PROGRESS: 작업이 현재 처리되고 있는 상태

READY 상태는 멱등성이 보장될 작업이 현재 실행 준비 상태에 진입했음을 의미하지만, 아직 어떤 작업 맥락(IdempotencyContext)으로 실행되는지는 모른다. IN_PROGRESS 상태로 전이하면서 작업 맥락이 주어진다.

IN_PROGRESS 상태인 작업은 아래 상태 중 하나로 전이할 수 있다.

  • COMPLETE: 작업 종료 후 결과가 보존된 상태
  • FAIL: 비즈니스 로직 위반으로 인해 작업을 마칠 수 없는 원인이 보존된 상태
  • RETRYABLE: 작업 처리 중이던 프로세스가 비정상 종료되거나, 시스템의 일시적인 오류 등으로 도중 중단된 상태.

여기서 COMPLETEFAIL은 작업 내용 또는 발생한 예외 등 작업 결과가 있는 종결된 상태지만, RETRYABLE은 작업 도중 프로세스가 비정상 종료되거나 시스템의 일시적인 오류 등으로 인해 중단된 상태로, 같은 요청 맥락에서 다시 시도하면 작업 처리 결과를 기대할 수 있는 상태다.

여기서 상태 전이 규칙을 세울 수 있다.

  1. READYIN_PROGRESS로 전이한다.
  2. RETRYABLEIN_PROGRESS로 전이한다.
  3. IN_PROGRESS에서
    1. 작업 실행의 성공 여부에 따라 COMPLETE 또는 FAIL로 전이한다.
    2. 작업 종결이 시스템 상 불가능하면 RETRYABLE로 전이한다.

멱등성을 보장해야하는 작업은 동시성 환경에서 발생하는 경쟁 조건(Race Condition)을 고려하고 있기 때문에 상태 전이 행위 또한 원자적으로 수행되야한다.

PostgreSQL의 강한 쿼리 제약으로 하나의 트랜잭션 블럭 내에서 예외가 발생하면 그 트랜잭션은 Rollback-only로 마킹된다. Rollback-only가 마킹된 시점 이후 쿼리들은 전송되지 않으며, 메서드 블럭 종료 후 DataIntegrityViolationException을 발생시킨다.

Unique 제약 조건의 동작에 기대한 멱등키 조회를 위해서는 Save 후 Flush 흐름을 새로운 트랜잭션에서 열어서 이 후 동작에 영향을 최소화할 수 있다. 같은 클래스 내 다른 메서드 블럭이 아닌 프록시 객체를 사용하므로 서로 다른 클래스 내 정의되어야한다.

또는 원자적 상태 갱신을 수행할 수 있다. Repository에 커스텀 쿼리를 작성함

© 2025 All Rights Reserved. Made with 🤍 by lilamaris