본문으로 바로가기 본문 바로가기
WIKI 기술 지식 베이스

키-값 쌍 쓰기

원문 보기 위키 갱신

Workers KV에서 키-값 쌍을 만들거나 업데이트하는 put() 메서드 사용법을 알아봐요. 만료 설정, 메타데이터 첨부, 동시 쓰기와 벌크 쓰기, 그리고 같은 키에 대한 쓰기 제한까지 함께 살펴봐요.

출처: 문서

본문

새 키-값 쌍을 만들거나 특정 키의 값을 업데이트하려면 Worker 코드에 바인딩한 아무 KV 네임스페이스에서 KV 바인딩의 put() 메서드를 호출하면 돼요:

env.NAMESPACE.put(key, value);
self.env.NAMESPACE.put(key, value)

예시

Worker 안에서 키-값 쌍을 쓰는 예시예요:

export default {
	async fetch(request, env, ctx) {
		try {
			await env.NAMESPACE.put("first-key", "This is the value for the key");

			return new Response("Successful write", {
				status: 201,
			});
		} catch (e) {
			return new Response(e.message, { status: 500 });
		}
	},
};
from workers import WorkerEntrypoint, Response

class Default(WorkerEntrypoint):
    async def fetch(self, request):
        try:
            await self.env.NAMESPACE.put("first-key", "This is the value for the key")

            return Response("Successful write", status=201)
        except Exception as e:
            return Response(str(e), status=500)

참조

KV에 쓰기 위해 제공되는 메서드는 다음과 같아요:

put() 메서드

새 키-값 쌍을 만들거나 특정 키의 값을 업데이트하려면 Worker 코드에 바인딩한 아무 KV 네임스페이스에서 put() 메서드를 호출하면 돼요:

env.NAMESPACE.put(key, value, options?);
self.env.NAMESPACE.put(key, value, options)

매개변수 (Parameters)

  • key: string
    • 값과 연관시킬 키예요. 키는 비어 있을 수 없고 . 또는 .. 와 정확히 같을 수 없어요. 그 외의 모든 키는 유효해요. 키의 최대 길이는 512바이트예요.
  • value: string | ReadableStream | ArrayBuffer
    • 저장할 값이에요. 타입은 추론돼요. 값의 최대 크기는 25MiB예요.
  • options: { expiration?: number, expirationTtl?: number, metadata?: object }
    • 선택 사항. expiration (선택), expirationTtl (선택), metadata (선택) 속성을 포함하는 객체예요.
      • expiration 은 epoch 이후 초 단위로 키-값 쌍을 만료시킬 시각을 나타내는 숫자예요.
      • expirationTtl 은 지금부터 몇 초 후에 키-값 쌍을 만료시킬지 나타내는 숫자예요. 최소값은 60이에요.
      • metadata 는 JSON으로 직렬화되어야 하는 객체예요. 메타데이터 객체의 직렬화된 JSON 표현의 최대 크기는 1024바이트예요.

응답 (Response)

  • response: Promise<void>
    • 업데이트가 성공하면 resolve되는 Promise 예요.

put() 메서드는 성공적인 업데이트를 확인하기 위해 await 해야 하는 Promise를 반환해요.

가이드

같은 키에 대한 동시 쓰기

KV의 eventual consistency(최종 일관성) 특성 때문에 같은 키에 대한 동시 쓰기가 서로를 덮어쓸 수 있어요. 단일 프로세스에서 Wrangler, Durable Objects, 또는 API로 데이터를 쓰는 것이 일반적인 패턴이에요. 이렇게 하면 단일 스트림 때문에 경쟁하는 동시 쓰기를 피할 수 있어요. 모든 데이터는 네임스페이스에 바인딩된 모든 Worker 내에서 여전히 쉽게 사용할 수 있어요.

같은 키에 동시 쓰기가 이루어지면 마지막 쓰기가 우선해요.

쓰기는 같은 글로벌 네트워크 위치의 다른 요청에는 즉시 보이지만, 세계의 다른 지역에서는 최대 60초 (또는 get() / getWithMetadata() 메서드의 cacheTtl 매개변수 값)가 걸려 보일 수 있어요.

이 주제에 대한 자세한 내용은 KV 작동 방식을 참조하세요.

데이터 대량 쓰기

Wrangler 또는 REST API를 통해 한 번에 여러 개의 키-값 쌍을 쓸 수 있어요.

벌크 API는 한 번에 최대 10,000개의 KV 쌍을 받을 수 있어요.

각 KV 쌍에는 key 와 value 가 필요해요. 전체 요청 크기는 100메가바이트 미만이어야 해요. 벌크 쓰기는 KV 바인딩을 사용해서는 지원되지 않아요.

키 만료시키기

KV는 자동으로 만료되는 키를 만들 수 있는 기능을 제공해요. 만료를 특정 시점 ( expiration 옵션 사용) 또는 키가 마지막으로 수정된 후 일정 시간이 지난 뒤 ( expirationTtl 옵션 사용)에 발생하도록 구성할 수 있어요.

만료 키의 만료 시각에 도달하면 시스템에서 삭제돼요. 삭제된 후에는 키를 읽으려는 시도가 키가 존재하지 않는 것처럼 동작해요. 삭제된 키는 과금 목적의 KV 네임스페이스 저장 사용량에 계산되지 않아요.

cacheTtl 이 더 높은 (더 긴) 값으로 설정된 경우에도 키에 expiration 설정이 있으면 해당 키가 삭제돼요. 만료가 항상 우선해요.

키가 만료되어야 하는 시점을 지정하는 방법은 두 가지가 있어요:

  • UNIX epoch 이후 초 ↗︎단위로 지정된 절대 시간을 사용해 키의 만료를 설정해요. 예를 들어 2019년 4월 1일 UTC 자정에 키가 만료되길 원한다면 키의 만료를 1554076800 로 설정하면 돼요.
  • 지금부터 상대적인 초 단위를 사용해 키의 만료 TTL(time to live)을 설정해요. 예를 들어 키가 만들어진 지 10분 후에 만료되길 원한다면 만료 TTL을 600 으로 설정하면 돼요.

미래로부터 60초 미만인 만료 대상을 지원하지 않아요. 이는 두 만료 방법 모두에 해당돼요.

만료 키 만들기

만료 키를 만들려면 put() 옵션의 expiration 을 epoch 이후 초 단위를 나타내는 숫자로 설정하거나, put() 옵션의 expirationTtl 을 지금부터 초 단위를 나타내는 숫자로 설정하면 돼요:

await env.NAMESPACE.put(key, value, {
	expiration: secondsSinceEpoch,
});

await env.NAMESPACE.put(key, value, {
	expirationTtl: secondsFromNow,
});
await self.env.NAMESPACE.put(key, value, expiration=seconds_since_epoch)

await self.env.NAMESPACE.put(key, value, expirationTtl=seconds_from_now)

이 예시들은 secondsSinceEpoch / seconds_since_epoch 와 secondsFromNow / seconds_from_now 가 Worker 코드의 다른 곳에서 정의된 변수라고 가정해요.

메타데이터

키-값 쌍에 메타데이터를 연관시키려면 put() 옵션의 metadata 를 (JSON으로 직렬화 가능한) 객체로 설정하면 돼요:

await env.NAMESPACE.put(key, value, {
	metadata: { someMetadataKey: "someMetadataValue" },
});
await self.env.NAMESPACE.put(key, value, metadata={"someMetadataKey": "someMetadataValue"})

같은 키에 대한 KV 쓰기 제한

Workers KV는 같은 키에 대해 초당 최대 1회의 쓰기를 허용해요. 1초 안에 같은 키에 이루어진 쓰기는 속도 제한 (429) 오류를 발생시켜요.

같은 키에 1초에 두 번 이상 쓰면 안 돼요. Worker 호출 내에서 키에 대한 쓰기를 단일 쓰기로 통합하거나, 쓰기 사이에 최소 1초를 기다리는 것을 고려해 보세요.

다음 예시는 단일 Worker 호출 내에서 동시 쓰기를 강제함으로써 같은 키에 대한 여러 쓰기가 어떻게 오류를 반환할 수 있는지 보여주는 데모예요. 이 패턴은 프로덕션에서 사용하면 안 돼요.

export default {
	async fetch(request, env, ctx): Promise<Response> {
		// Rest of code omitted
		const key = "common-key";
		const parallelWritesCount = 20;

		// Helper function to attempt a write to KV and handle errors
		const attemptWrite = async (i: number) => {
			try {
				await env.YOUR_KV_NAMESPACE.put(key, `Write attempt #${i}`);
				return { attempt: i, success: true };
			} catch (error) {
				// An error may be thrown if a write to the same key is made within 1 second with a message. For example:
				// error: {
				//	"message": "KV PUT failed: 429 Too Many Requests"
				// }

				return {
					attempt: i,
					success: false,
					error: { message: (error as Error).message },
				};
			}
		};

		// Send all requests in parallel and collect results
		const results = await Promise.all(
			Array.from({ length: parallelWritesCount }, (_, i) =>
				attemptWrite(i + 1),
			),
		);
		// Results will look like:
		// [
		// 	  {
		// 		  "attempt": 1,
		// 		  "success": true
		// 	  },
		//    {
		// 		  "attempt": 2,
		// 		  "success": false,
		// 		  "error": {
		// 			  "message": "KV PUT failed: 429 Too Many Requests"
		// 		  }
		// 	  },
		// 	  ...
		// ]

		return new Response(JSON.stringify(results), {
			headers: { "Content-Type": "application/json" },
		});
	},
};
from workers import WorkerEntrypoint, Response
import asyncio

class Default(WorkerEntrypoint):
    async def fetch(self, request):
        key = "common-key"
        parallel_writes_count = 20

        async def attempt_write(i):
            try:
                await self.env.YOUR_KV_NAMESPACE.put(key, f"Write attempt #{i}")
                return {"attempt": i, "success": True}
            except Exception as error:
                # An error may be thrown if a write to the same key is made
                # within 1 second with a message like:
                # "KV PUT failed: 429 Too Many Requests"
                return {"attempt": i, "success": False, "error": {"message": str(error)}}

        results = await asyncio.gather(
            *[attempt_write(i + 1) for i in range(parallel_writes_count)]
        )

        # Results will look like:
        # [
        #     {
        #         "attempt": 1,
        #         "success": True
        #     },
        #     {
        #         "attempt": 2,
        #         "success": False,
        #         "error": {
        #             "message": "KV PUT failed: 429 Too Many Requests"
        #         }
        #     },
        #     ...
        # ]

        return Response.json(list(results))

이러한 오류를 처리하려면 지수 백오프(exponential backoff)를 사용한 재시도 로직을 구현하는 것을 권장해요. 위 코드에 재시도를 추가하는 간단한 방법은 다음과 같아요.

export default {
	async fetch(request, env, ctx): Promise<Response> {
		// Rest of code omitted
		const key = "common-key";
		const parallelWritesCount = 20;

		// Helper function to attempt a write to KV with retries
		const attemptWrite = async (i: number) => {
			return await retryWithBackoff(async () => {
				await env.YOUR_KV_NAMESPACE.put(key, `Write attempt #${i}`);
				return { attempt: i, success: true };
			});
		};

		// Send all requests in parallel and collect results
		const results = await Promise.all(
			Array.from({ length: parallelWritesCount }, (_, i) =>
				attemptWrite(i + 1),
			),
		);

		return new Response(JSON.stringify(results), {
			headers: { "Content-Type": "application/json" },
		});
	},
};

async function retryWithBackoff(
	fn: Function,
	maxAttempts = 5,
	initialDelay = 1000,
) {
	let attempts = 0;
	let delay = initialDelay;

	while (attempts < maxAttempts) {
		try {
			// Attempt the function
			return await fn();
		} catch (error) {
			// Check if the error is a rate limit error
			if (
				(error as Error).message.includes(
					"KV PUT failed: 429 Too Many Requests",
				)
			) {
				attempts++;
				if (attempts >= maxAttempts) {
					throw new Error("Max retry attempts reached");
				}

				// Wait for the backoff period
				console.warn(`Attempt ${attempts} failed. Retrying in ${delay} ms...`);
				await new Promise((resolve) => setTimeout(resolve, delay));

				// Exponential backoff
				delay *= 2;
			} else {
				// If it's a different error, rethrow it
				throw error;
			}
		}
	}
}
from workers import WorkerEntrypoint, Response
import asyncio

class Default(WorkerEntrypoint):
    async def fetch(self, request):
        key = "common-key"
        parallel_writes_count = 20

        async def attempt_write(i):
            return await retry_with_backoff(
                lambda: self.env.YOUR_KV_NAMESPACE.put(key, f"Write attempt #{i}"),
                success_result={"attempt": i, "success": True},
            )

        results = await asyncio.gather(
            *[attempt_write(i + 1) for i in range(parallel_writes_count)]
        )

        return Response.json(list(results))

async def retry_with_backoff(fn, success_result, max_attempts=5, initial_delay=1.0):
    attempts = 0
    delay = initial_delay

    while attempts < max_attempts:
        try:
            await fn()
            return success_result
        except Exception as error:
            if "KV PUT failed: 429 Too Many Requests" in str(error):
                attempts += 1
                if attempts >= max_attempts:
                    raise Exception("Max retry attempts reached")

                print(f"Attempt {attempts} failed. Retrying in {delay}s...")
                await asyncio.sleep(delay)

                delay *= 2
            else:
                raise

KV에 접근하는 다른 방법

또한 명령줄에서 Wrangler로 키-값 쌍을 쓰고 REST API로 데이터를 쓰을 수 있어요.

더 알아보기 (Learn more)

KV의 쓰기 일관성 동작에 대한 깊은 이해는 KV 작동 방식 문서를 참고하세요. 읽기와 메타데이터 조회 방법은 읽기 문서에서 확인할 수 있어요.