Files
project-auth-server/docs/examples/integration/idempotency.md
T

3.4 KiB

Integration Idempotency 예시

좋은 예시

예시 1. provider 공식 header를 adapter에서 설정한다

@Service
@RequiredArgsConstructor
class StripePaymentClient {

    private final RestClient restClient;

    ChargeResult createCharge(CreateChargeCommand command) {
        return restClient.post()
                .uri("/v1/payment_intents")
                .header("Idempotency-Key", command.providerIdempotencyKey())
                .body(StripeCreateChargeRequest.from(command))
                .retrieve()
                .body(ChargeResult.class);
    }
}

좋은 이유:

  • provider 공식 idempotency header를 adapter 경계에서 설정한다
  • application/domain이 HTTP 헤더 이름을 몰라도 된다
  • 같은 command 재전송 시 같은 key를 쓸 수 있다.

예시 2. 내부 command id와 provider key를 분리해 관리한다

public record OutboundCallKey(
        String outboundCommandId,
        String provider,
        String operation,
        String providerIdempotencyKey
) {
}

public record OutboundFingerprint(
        String requestDigest
) {
}

좋은 이유:

  • 내부 추적 키와 provider 전송 키가 분리된다
  • provider별 operation scope 차이를 표현하기 쉽다
  • fingerprint 충돌 검사를 붙이기 좋다

예시 3. timeout 후 같은 key로 재전송한다

try {
    return paypalCaptureClient.capture(command);
} catch (ExternalTimeoutException ex) {
    return paypalCaptureClient.capture(command.withSameProviderIdempotencyKey());
}

좋은 이유:

  • side effect 재시도 시 새 key를 만들지 않는다
  • 같은 요청 의도에 같은 provider key를 재사용한다
  • PayPal/Stripe 문서 취지와 맞는다.

예시 4. 같은 key 동시 송신을 막는다

if (!outboundIdempotencyCoordinator.tryAcquire(command.provider(), command.operation(), command.providerIdempotencyKey())) {
    throw new DuplicateOutboundCallInProgressException();
}

좋은 이유:

  • 같은 key 두 번 동시 전송을 줄인다
  • PayPal이 설명한 concurrent duplicate 문제를 완화할 수 있다.

나쁜 예시

예시 1. timeout 후 새 key로 다시 보낸다

try {
    return stripeClient.createCharge(command.withNewProviderIdempotencyKey());
} catch (ExternalTimeoutException ex) {
    return stripeClient.createCharge(command.withNewProviderIdempotencyKey());
}

나쁜 이유:

  • 같은 외부 side effect 요청이 새 요청으로 처리될 수 있다
  • 중복 생성/중복 결제 위험이 커진다

예시 2. 같은 key를 다른 operation에 재사용한다

String key = "7f6d...";
authorizePayment(key);
capturePayment(key);

나쁜 이유:

  • provider마다 operation scope가 다를 수 있다
  • PayPal은 API call type 단위 고유성을 요구한다.

예시 3. provider key에 이메일을 넣는다

String providerIdempotencyKey = request.email() + ":" + request.orderId();

나쁜 이유:

  • PII가 key에 섞인다
  • Stripe도 민감정보를 key로 쓰지 말라고 권고한다.

예시 4. replay semantics를 무시하고 항상 “새 성공”으로 해석한다

return new PaymentCapturedResult(true, true);

나쁜 이유:

  • provider가 이전 결과 재생인지 최신 상태 조회인지 구분하지 못한다
  • 내부 감사/운영 추적이 부정확해진다