멱등성 (Idempotence)와 HTTP API 설계
개요
멱등성 (Idempotence)이란, 컴퓨터 과학과 수학에서 동일한 연산을 여러 번 한다면 결과값이 달라지지 않는 것을 뜻한다.
API 설계에서는 데이터의 일관성과 안정성을 보장하는데 중요한 개념이다.
RFC 7231에 따르면 HTTP 메소드 중 `HEAD, OPTIONS, GET, PUT, DELETE` 메소드들의 경우 멱등하지만
`POST, PATCH` 경우 멱등하지 않다고 한다.
예를 들어 HTTP API 요청으로 어떤 리소스 생성 혹은 수정을 한다고 하고 아직 API가 멱등하지 않다면 여러번 요청을 할 때 중복 혹은 잘못된 업데이트가 일어날 수 있다.
The Idempotency-Key HTTP Request Header Field
클라이언트는 동일한 HTTP 요청에 대해서 Request Header 에 Idempotency-Key
키와 Unique 값을 포함하여 보내야 한다.
Uniqueness of Idempotency Key
값은 문자열을 표준으로 하고 있고 보통 Unique 를 보장하기 위해 UUID 혹은 유사한 Identifier 을 권장한다. (RFC 8941, 4122)
개인적으로는 ULID를 사용하는게 어떨까 싶다.
Idempotency Key Validity and Expiry
서버에서는 키 값 삭제에 대한 만료 정책을 정해야 하고 문서에 작성해야 한다.
필요하다면 만료되어 없애지 않고 데이터베이스 등에 연관관계를 가지고 저장해도 무방하다.
Responsibilities
Client
클라이언트는 멱등성을 제공하는 API를 사용하기 위해서 적절한 키값 생성 알고리즘을 사용해야 하며
서버에서 꼭 한번만 수행되어야 하는지에 대해서 신중히 선택 해서 API 요청을 해야 한다.
Server
서버는 API가 멱등성을 제공하는 경우 API Spec에 만료시간과 같이 명시해야 하며 키 값에 대한 라이프 사이클을 관리할 수 있어야 한다.
- 각 요청에 대해서 HTTP Request Header에 Idempotency-Key를 포함하고 있는지 확인 해야 한다.
- 필요하다면 Idempotency-Key fingerprint 를 생성햐야 한다.
- 멱등성을 제공하기 위해서 다양한 상황을 고려해보고 런타임시 구현 문제가 없도록 해야 한다.
시나리오
- 첫번째 요청 (멱등성 키, fingerprint 없음)
서버는 정상적으로 수행을 하고 적절한 응답 값과 상태 코드를 응답한다. - 두번째 요청 (중복, 멱등성 키, fingerprint 확인)
첫번째 요청이 완료된 후 재요청 되었고 서버에서는 fingerprint가 확인 되었다면 이전에 완료된 이벤트라 간주하고 이전에 수행된 응답 값을 재응답 해주거나 에러로 응답을 해야 한다. - 동시 요청
첫번째 요청이 완료되기 전 여러개의 요청들이 동시에 요청되었다면 서버에서는409 CONFLICT
에러를 발생해야 한다.
즉 아직 수행 중이라면 409 에러를 응답해야 한다. - 이미 사용한 멱등성 키로 요청
이미 사용한 멱등성 키로 재요청을 하는경우422 Unprocessable Content
에러를 발생해야 한다.
데모 구현
@RestController
@RequestMapping("")
class SomethingController(
private val somethingService: SomethingService,
) {
private val logger = LoggerFactory.getLogger(this::class.java)
@PostMapping("/idem-1")
fun idempotency(
@RequestHeader(value = "Idempotency-Key", required = false) idempotencyKey: String
): ResponseEntity<UniqueData> {
return idempotencyService.validateIdempotency2(idempotencyKey) {
Thread.sleep(10_000)
UniqueData(1, "hello")
}
}
}
@Service
class IdempotencyService(
private val valueOps: ValueOperations<String, String>,
) {
private val logger = LoggerFactory.getLogger(this::class.java)
// RedisTemplate this.setEnableTransactionSupport(true)
@Transactional
fun <T> validateIdempotency(idempotencyKey: String, function: () -> T): ResponseEntity<T> {
val requestedKey = "idempotency-requested:$idempotencyKey"
val doneKey = "idempotency-done:$idempotencyKey"
val defaultPolicyOfTTL = Duration.ofSeconds(60)
if (valueOps.get(doneKey) == "1") {
logger.warn("$idempotencyKey is done")
return ResponseEntity(HttpStatus.UNPROCESSABLE_ENTITY)
}
if (valueOps.get(requestedKey) == "1") {
logger.warn("$idempotencyKey is requested")
return ResponseEntity(HttpStatus.CONFLICT)
}
logger.info("$idempotencyKey is processing")
valueOps.set(requestedKey, "1", defaultPolicyOfTTL)
return ResponseEntity.ok(function.invoke()).also {
logger.info("$idempotencyKey is finished")
valueOps.getAndDelete(requestedKey)
valueOps.set(doneKey, "1", defaultPolicyOfTTL)
}
}
}
전체 구현 코드는 아래 레포 주소를 통해서 확인할 수 있습니다.
https://github.com/sanggi-wjg/kotlin-spring-boot-demos/tree/main/redis/redis
Ref
https://datatracker.ietf.org/doc/draft-ietf-httpapi-idempotency-key-header/
https://docs.stripe.com/api/idempotent_requests
https://docs.tosspayments.com/blog/what-is-idempotency
'IT' 카테고리의 다른 글
의존성 주입에 대한 생각 (1) | 2024.12.19 |
---|---|
[Jetbrains] Intellij 인텔리제이 Live Template 사용 방법 (1) | 2024.02.26 |
[통계] 정규화(Normalization)와 표준화(Standardization) (1) | 2023.12.04 |
[Kubernetes] OpenLens 설치 (0) | 2023.01.09 |
API, 서비스, 도메인 테스트 및 TDD 에서의 기어비 (0) | 2022.12.24 |