134 lines
3.4 KiB
Markdown
134 lines
3.4 KiB
Markdown
# Integration Idempotency 예시
|
|
|
|
## 좋은 예시
|
|
|
|
### 예시 1. provider 공식 header를 adapter에서 설정한다
|
|
|
|
```java
|
|
@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를 분리해 관리한다
|
|
|
|
```java
|
|
public record OutboundCallKey(
|
|
String outboundCommandId,
|
|
String provider,
|
|
String operation,
|
|
String providerIdempotencyKey
|
|
) {
|
|
}
|
|
|
|
public record OutboundFingerprint(
|
|
String requestDigest
|
|
) {
|
|
}
|
|
```
|
|
|
|
**좋은 이유:**
|
|
|
|
- 내부 추적 키와 provider 전송 키가 분리된다
|
|
- provider별 operation scope 차이를 표현하기 쉽다
|
|
- fingerprint 충돌 검사를 붙이기 좋다
|
|
|
|
### 예시 3. timeout 후 같은 key로 재전송한다
|
|
|
|
```java
|
|
try {
|
|
return paypalCaptureClient.capture(command);
|
|
} catch (ExternalTimeoutException ex) {
|
|
return paypalCaptureClient.capture(command.withSameProviderIdempotencyKey());
|
|
}
|
|
```
|
|
|
|
**좋은 이유:**
|
|
|
|
- side effect 재시도 시 새 key를 만들지 않는다
|
|
- 같은 요청 의도에 같은 provider key를 재사용한다
|
|
- PayPal/Stripe 문서 취지와 맞는다.
|
|
|
|
### 예시 4. 같은 key 동시 송신을 막는다
|
|
|
|
```java
|
|
if (!outboundIdempotencyCoordinator.tryAcquire(command.provider(), command.operation(), command.providerIdempotencyKey())) {
|
|
throw new DuplicateOutboundCallInProgressException();
|
|
}
|
|
```
|
|
|
|
**좋은 이유:**
|
|
|
|
- 같은 key 두 번 동시 전송을 줄인다
|
|
- PayPal이 설명한 concurrent duplicate 문제를 완화할 수 있다.
|
|
|
|
## 나쁜 예시
|
|
|
|
### 예시 1. timeout 후 새 key로 다시 보낸다
|
|
|
|
```java
|
|
try {
|
|
return stripeClient.createCharge(command.withNewProviderIdempotencyKey());
|
|
} catch (ExternalTimeoutException ex) {
|
|
return stripeClient.createCharge(command.withNewProviderIdempotencyKey());
|
|
}
|
|
```
|
|
|
|
**나쁜 이유:**
|
|
|
|
- 같은 외부 side effect 요청이 새 요청으로 처리될 수 있다
|
|
- 중복 생성/중복 결제 위험이 커진다
|
|
|
|
### 예시 2. 같은 key를 다른 operation에 재사용한다
|
|
|
|
```java
|
|
String key = "7f6d...";
|
|
authorizePayment(key);
|
|
capturePayment(key);
|
|
```
|
|
|
|
**나쁜 이유:**
|
|
|
|
- provider마다 operation scope가 다를 수 있다
|
|
- PayPal은 API call type 단위 고유성을 요구한다.
|
|
|
|
### 예시 3. provider key에 이메일을 넣는다
|
|
|
|
```java
|
|
String providerIdempotencyKey = request.email() + ":" + request.orderId();
|
|
```
|
|
|
|
**나쁜 이유:**
|
|
|
|
- PII가 key에 섞인다
|
|
- Stripe도 민감정보를 key로 쓰지 말라고 권고한다.
|
|
|
|
### 예시 4. replay semantics를 무시하고 항상 “새 성공”으로 해석한다
|
|
|
|
```java
|
|
return new PaymentCapturedResult(true, true);
|
|
```
|
|
|
|
**나쁜 이유:**
|
|
|
|
- provider가 이전 결과 재생인지 최신 상태 조회인지 구분하지 못한다
|
|
- 내부 감사/운영 추적이 부정확해진다
|