(https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F309p7u33h98257y0elqw.png)
프롬프트는 설정(configuration)처럼 느껴지기 때문에, 플랫폼에서 제공하는 관리형 프롬프트 관리 서비스(managed prompt-management service)에 저장하는 것이 매력적입니다. 버전 관리 기능, 콘솔, 그리고 깔끔한 API를 얻을 수 있고, 한동안은 그것이 올바른 선택입니다. 하지만 문제가 발생하는 시점은 나중에, 부하가 걸렸을 때입니다. 프롬프트 검색(prompt retrieval)이 모든 추론 호출(inference call)의 '핫 패스'(hot path)에 놓이고 서비스가 저항하기 시작할 때 말이죠.
제가 직면했던 것이 바로 그 벽이었습니다. 프롬프트 관리 계층에서 요청 제한(throttling)이 걸리고 있었습니다. 모든 호출은 실제 작업을 수행하기 전에 프롬프트를 가져와야 했기 때문에, 이 스로틀링이 전체 파이프라인의 천장 역할을 했습니다. 병목 현상은 모델 자체가 아니었습니다. 프롬프트 조회(Prompt lookup)가 문제였습니다.
저는 프롬프트 제공(prompt serving)을 DynamoDB로 옮기고 관리형 서비스를 작성 출처의 진실 공급원(authoring source of truth)으로 유지했습니다. 스로틀링이 사라졌고, 이후로 디자인은 잘 작동하고 있습니다.
왜 프롬프트 관리 서비스가 병목 현상이 되는가
프롬프트 관리 서비스는 작성을 하고 거버넌스(governance)를 하는 용도로 만들어진 것이지, 지연 시간에 민감한 경로에서 분당 수천 번 읽히기 위한 용도는 아닙니다. 이 서비스들은 요청 할당량(request quotas)을 가지고 오는데, 이 할당량은 보통 계정 전체에 걸쳐 공유되며 단일 워크로드의 읽기 트래픽에 맞춰 확장되지 않습니다. 읽기 볼륨이 증가하면, 프롬프트 자체와 관련된 제한에 도달하기 훨씬 전에 할당량에 부딪히게 됩니다.
징후는 처음에는 미묘합니다. 프롬프트 검색에서 스로틀링 및 too-many-requests 오류가 발생하기 시작하고, 그래서 지수 백오프(exponential backoff)를 사용한 재시도 로직과 서비스가 거부할 때의 폴백(fallback)을 추가하게 됩니다. 이것은 합리적인 위생 관행이지만, 순수한 설정 조회에 재시도 로직을 감싸게 되면, 그 스토어 자체가 애플리케이션의 처리량 제한(throughput limit)이 되어버립니다. 모델을 최적화하고, 작업을 배치하며, 다운스트림 모든 것을 조정할 수 있지만, 아무 소용이 없습니다. 왜냐하면 요청은 이미 프롬프트를 기다리며 멈춰 있기 때문입니다.
왜 DynamoDB가 이 접근 패턴에 적합한가
프롬프트 조회는 가장 단순한 접근 패턴입니다. 키를 가지고 있고, 해당 아이템을 가져오고 싶습니다. 이는 기본 키(primary key)를 통한 단일 항목 읽기이며, 바로 이것이 DynamoDB가 가장 잘하는 일입니다.
- 읽기 처리량은 테이블에 비례하며, 고정된 서비스 할당량에 의존하지 않습니다. 온디맨드 용량(On-demand capacity)은 급증을 흡수하고, 프로비저닝된 용량(provisioned capacity)과 자동 스케일링(auto scaling)은 꾸준한 부하를 처리합니다. 저를 제한하던 공유 할당량이 더 이상 병목 지점에 놓이지 않았습니다.
- 접근 패턴은 키 조회입니다. 프롬프트 식별자(prompt identifier)로 파티션을 구성하고, 모든 가져오기 작업은 단일
GetItem 호출입니다. 스캔(scans)도 없고, 필터링(filtering)도 없으며, 예측 가능한 한 자릿수 밀리초 읽기가 가능합니다.
- 읽기와 쓰기가 깔끔하게 분리됩니다. 대량의 읽기는 DynamoDB가 처리합니다. 드물게 발생하는 작성자 작성(authoring writes)은 검토 및 거버넌스가 이미 존재하는 관리형 서비스에 남아 있으며, 동기화 단계(sync step)가 변경 사항을 DynamoDB로 푸시합니다.
안전하게 만드는 부분: 체크섬 기반 동기화
프롬프트를 캐싱하는 것은 쉽습니다. 하지만 캐시를 정확하게 유지하는 것이 제대로 처리해야 할 부분입니다.
관리형 서비스는 프롬프트 콘텐츠의 진실 공급원(source of truth)으로 남습니다. DynamoDB는 핫 패스(hot path)가 읽어가는 제공 사본(serving copy)을 보관합니다. 모든 것을 배포할 때마다 다시 업로드하지 않으면서 이 둘을 일치하게 유지하기 위해, 저는 체크섬(checksum)을 이용해 동기화합니다.
이 동기화는 쓰기 전송(write-through) 방식입니다. 소스에서 변경 사항을 감지하면, 먼저 DynamoDB에 새 프롬프트를 작성하고, 이후 독자들(readers)은 그곳에서 데이터를 가져갑니다. 흐름은 간단합니다:
- 진실 공급원에 있는 각 프롬프트의 콘텐츠에 대해 체크섬을 계산합니다.
- 이 체크섬을 해당 프롬프트에 이미 저장된 값과 비교합니다.
- 일치하면 아무것도 하지 않습니다. 다르면, 프롬프트가 변경되었으므로 새 콘텐츠를 DynamoDB에 작성하고 새로운 체크섬을 기록합니다.
이렇게 하면 모든 동기화(sync)가 무작정 다시 쓰기(blind rewrite)하는 대신 저렴한 비교 작업으로 바뀝니다. 변경되지 않은 프롬프트는 비교 비용만 들고 쓰기는 발생하지 않습니다. 오직 원본에서 진정한 편집이 있을 때만 쓰기가 트리거됩니다. 쓰기가 읽기가 필요로 하기 전에 완료되므로, 새로운 프롬프트가 깔끔하게 전파됩니다. 즉, 서비스 제공 복사본(serving copy)은 '핫 패스'(hot path)가 요청할 때쯤 이미 업데이트되어 있습니다.
이 좋은 점은 체크섬(checksum) 자체가 변경 신호라는 것입니다. 별도의 '더티 여부' 플래그나 수동 버전 증가가 필요 없습니다. 내용이 동일하면 체크섬도 동일하고 쓰기는 건너뜁니다. 누군가 프롬프트를 편집하면, 체크섬이 바뀌고 다음 동기화에서 새로운 텍스트가 기록됩니다.
롤백 또는 감사(audit)가 필요한 경우: DynamoDB에서의 버전 관리
DynamoDB는 프롬프트 관리 서비스처럼 버전 관리를 제공하지 않지만, 모델링할 수 있으며 이 점을 명확히 언급할 가치가 있습니다. 왜냐하면 'DynamoDB에는 버전 관리가 없다'는 것은 흔한 오해이기 때문입니다.
프롬프트당 단일 현재 복사본(single current copy)만 필요한 경우, 위의 체크섬 레이아웃으로 충분합니다. 하지만 히스토리, 롤백 또는 감사 추적(audit trail)이 필요하다면, 키에 버전을 추가하세요:
- 파티션 키 (Partition key): 프롬프트 식별자.
- 정렬 키 (Sort key): 버전. 단조 증가 정수(monotonic integer), ISO 타임스탬프 또는 체크섬 자체가 모두 작동합니다.
- 현재 포인터 (A current pointer): 프롬프트당 하나의 추가 항목(예: 정렬 키
CURRENT)으로 활성 버전을 지정합니다.
핫 패스는 키를 통해 현재 포인터를 읽기 때문에, 서비스 제공은 여전히 단일 키 읽기로 유지됩니다. 히스토리는 파티션 키에 대한 Query로 쿼리 가능하게 유지됩니다. 체크섬은 여전히 새로운 버전 행이 언제 쓰여질지 결정하므로, 변경되지 않은 프롬프트에 대해 새로운 버전을 쌓을 필요가 없습니다. 동시 동기화(concurrent syncs)가 가능한 경우, 조건부 쓰기(ConditionExpression을 사용하여 저장된 체크섬에 적용)는 오래된 동기화가 더 최신인 것을 덮어쓰는 것을 방지합니다.
트레이드오프는 간단합니다. 단일 복사본 레이아웃은 관리할 것이 적지만, 버전 관리 레이아웃은 롤백과 감사를 제공합니다. 둘 다 핫 패스에서 키 읽기이므로, 어느 것도 스로틀링 문제를 재도입하지 않습니다.
변경의 형태(The shape of the change)
이전 마이그레이션 자체는 규모가 작았기 때문에 올바른 결정이었다고 할 수 있습니다.
- 조회(lookup)를 중심으로 테이블을 모델링합니다. 프롬프트 식별자(prompt identifier)를 파티션 키로 사용합니다. 이것이 전체 스키마이며, 접근 패턴이 사전에 알려져 있었기 때문입니다.
- 핫 패스(hot path)에서 키 기반으로 읽습니다. 각 추론 호출(inference call)은 단일 키 기반 읽기를 수행합니다. 프로덕션 환경에서는 이 테이블에 대해 스캔(scan) 작업이 실행되지 않으므로, 비용은 실제로 발생하는 읽기 횟수에 비례하여 유지됩니다.
- 타이머나 모든 배포 시가 아닌 체크섬(checksum)으로 동기화합니다. 신뢰할 수 있는 출처(source of truth)가 업데이트를 주도하며, 실제 변경된 내용만 DynamoDB로 전송됩니다.
그 결과, 프롬프트 검색은 공유되고 속도 제한이 걸리는 의존성에서 벗어나, 트래픽에 따라 확장되는 스토어에 대한 키 기반 읽기가 되었습니다.
이 글을 복사하기 전에 조언하고 싶은 것들
이것은 프롬프트 관리 서비스(prompt-management services)에 반대하는 주장은 아닙니다. 해당 서비스들은 작성, 검토 및 거버넌스 측면에서 그 자리를 얻습니다. 문제는 동일한 서비스를 자신이 감당할 수 있도록 설계되지 않은 고용량 읽기 경로에 두는 것입니다.
염두에 둘 몇 가지 사항:
- 증상은 모델 지연 시간(model latency)이 아닌 요청 볼륨을 추적하는 처리량 상한선입니다. 프롬프트 검색을 속도 제한 재시도(throttling retries)로 감싸고 있다면, 그것이 단서입니다. 어떤 다운스트림 튜닝으로도 이 문제를 해결할 수 없습니다.
- 접근 패턴을 중심으로 테이블을 먼저 설계합니다. 키 조회는 키 스키마를 원합니다. 실제로 실행하는 쿼리에서 파티션 키를 선택하고, 프로덕션 환경에서는 스캔 작업을 제외해야 합니다.
- 캐시가 자체적으로 수정되도록 만듭니다(self-correcting). 신뢰할 수 있는 출처와 비교하는 체크섬은 콘텐츠가 실제로 변경될 때만 서빙 사본을 업데이트하므로, 수동 버전 조작이나 변경되지 않은 데이터를 다시 작성할 필요가 없습니다.
- 버전 관리가 필요하면 모델링하고, 없다고 가정하지 마십시오. 정렬 키(sort-key) 버전과 현재 포인터는 핫 패스를 단일 키 기반 읽기로 유지하면서 롤백 및 감사 기능을 제공합니다.
프롬프트를 DynamoDB로 옮긴 것은 사소한 변경이었지만 그 보상은 엄청났습니다. 파이프라인을 제한하던 스로틀링(throttling) 문제가 사라졌고, 검색은 깔끔한 키 조회(key lookup)가 되었으며, 체크섬 동기화(checksum sync)는 서비스 복사본이 진실의 원천(source of truth)과 일치하도록 유지합니다. 이후로 프로덕션 환경에서 잘 작동하고 있습니다.