06 · Automatic Persisted Queries — 자동 등록의 트레이드오프
질문: Persisted Query의 효과는 매력적인데 빌드 타임 manifest 배포가 운영 부담이다. 자동으로 풀 수 없나? 한 줄 답: Automatic Persisted Query는 첫 요청에서 hash만 보냈다가 miss면 query와 함께 재시도하는 단순 프로토콜이다 — 운영 부담 0, CDN 친화적, 하지만 allowlist 보안 효과는 없다.
Pyramid Top
05의 build-time persisted query는 3가지 효과(대역폭·보안·CDN)를 모두 얻지만 manifest 배포라는 운영 부담이 있다. 그 부담이 팀 규모가 작거나 query 변경이 잦은 환경에서는 진입장벽이 된다. APQ는 그 부담을 런타임으로 옮긴다 — 첫 요청은 자동 등록, 이후는 hash만. 보안은 약해지지만 대역폭과 CDN cacheability는 보존된다. 적절한 자리에서만 쓰는 것이 핵심 — 어디가 그 자리인지가 이 문서의 답이다.
사고 흐름 — APQ의 핸드셰이크
핵심: 2번의 round trip은 첫 요청 한 번뿐. 그 이후 모든 클라이언트가 GET URL 캐시 hit.
Why — Build-time persisted의 3가지 약점을 푼다
| Build-time PQ의 약점 | APQ가 어떻게 푸나 |
|---|---|
| manifest 빌드·배포 파이프라인 필요 | 등록이 런타임 자동 — 빌드 도구 무관 |
| 서버와 클라이언트 동시 배포 동기화 | 새 query도 자동 등록되므로 비동기 배포 OK |
| 새 query 추가 시 manifest 재생성 | 클라이언트만 배포해도 됨 |
즉, APQ는 운영 단순성을 위해 보안을 거래한 변종이다.
How — Apollo의 구현 상세
클라이언트 (createPersistedQueryLink)
import { createPersistedQueryLink } from "@apollo/client/link/persisted-queries";
import { sha256 } from "crypto-hash";
const link = createPersistedQueryLink({
sha256,
useGETForHashedQueries: true, // ← CDN 친화 핵심
}).concat(httpLink);이 link의 프로토콜:
- query → sha256 hash 계산.
- 첫 요청:
GET /graphql?extensions={"persistedQuery":{"version":1,"sha256Hash":"abc"}}&variables=... - 서버 응답 코드:
PersistedQueryNotFound→ 자동 재시도, 이번엔POST에 query 포함.PersistedQueryNotSupported→ 모든 요청을 그대로 (서버가 APQ 미지원).
- 성공이면 hash만 보내는 모드 유지.
서버 (Apollo Server 기본 내장)
import { ApolloServer } from "@apollo/server";
const server = new ApolloServer({
typeDefs, resolvers,
// APQ는 기본 활성화
// 비활성화하려면 persistedQueries: false
persistedQueries: {
cache: "bounded", // LRU 캐시
ttl: 900, // 15분 (없으면 영구)
},
});서버 내부 동작:
extensions.persistedQuery.sha256Hash추출.- LRU 캐시에서 hash로 query 조회.
- hit: 그 query 실행.
- miss:
PersistedQueryNotFound반환 (클라이언트가 재시도). - 재시도 (query 포함): hash 검증(
sha256(query) === hash) 후 캐시에 저장, 실행.
hash 검증은 무결성 보호다. “이 query는 이 hash”라는 self-describing이라 서버는 클라이언트를 믿지 않아도 된다. 그러나 어떤 query든 자동 등록되니 — allowlist 효과는 사라진다.
What — Build-time vs APQ 비교 표
| 차원 | Build-time PQ | APQ |
|---|---|---|
| 등록 시점 | 빌드 타임 (manifest) | 런타임 (첫 요청) |
| 대역폭 절약 | 100% (모든 요청) | ~99.x% (첫 요청만 큰 본문) |
| 보안 (allowlist) | ✅ 강함 | ❌ 거의 없음 |
| CDN cacheability | ✅ (GET) | ✅ (GET, 등록 후) |
| 운영 부담 | 높음 | 낮음 |
| 클라이언트·서버 배포 동기 | 필수 | 불필요 |
| 새 query 추가 | manifest 재배포 | 자동 |
| 적합한 자리 | 모바일 SDK·금융·공개 SDK | 일반 웹 앱·SaaS dashboard |
CDN 캐시 친화성 — APQ의 진짜 가치
APQ의 가장 큰 효과는 대역폭이 아니라 CDN 친화성이다.
GET /graphql?ext=...&variables={"id":42}- URL이 완전한 캐시 key가 된다.
- query string과 variables가 모두 URL에 있으므로 CDN이 서로 다른 요청을 분리할 수 있다.
Cache-Control: max-age=60을 서버가 보내면 CDN이 그 시간 동안 같은 URL을 hit시킨다.
CDN cache가 진짜로 듣는다 — Cloudflare Workers·Fastly Compute가 GraphQL endpoint를 fully cacheable하게 만드는 핵심 메커니즘이다.
APQ는 모든 캐시 문제를 풀지 않는다
여기서 자주 헷갈리는 점이 있다.
APQ는 네트워크 본문과 서버 hit은 줄이지만, 클라이언트 측 일관성은 못 푼다.
같은 사용자가 두 화면에서 같은 entity를 보면 — 여전히 각 화면이 따로 fetch한다. 한 화면의 갱신이 다른 화면에 자동 반영되지 않는다. 그건 정규화 캐시의 일이다.
| 캐시 레이어 | APQ가 푸나? |
|---|---|
| HTTP/CDN 대역폭 | ✅ 푼다 |
| 동일 요청의 서버 fan-out | ✅ (CDN이 흡수) |
| 화면 간 entity 일관성 | ❌ 정규화 캐시의 일 |
| resolver 실행 비용 | ❌ 서버 response cache의 일 |
| 화면 첫 진입 비용 | ❌ 첫 요청은 여전히 서버까지 |
즉, APQ는 한 레이어를 풀고 나머지에 영향 없음. 다른 레이어들과 겹쳐서 쓴다.
What-if — APQ를 보안 목적으로 쓰면
| 잘못된 가정 | 실제 |
|---|---|
| ”APQ를 켰으니 임의 query 불가” | ❌ — 모든 query가 자동 등록된다. 공격자도 마찬가지. |
| ”introspection을 그래도 차단해야 하나?” | ✅ 그래야 한다 — APQ는 임의 query 차단이 아니라 자동 등록. |
| ”depth limit 등은 안 두어도 되나?” | ❌ 그것도 따로 필요 — 07-security-governance |
APQ는 대역폭/CDN 캐시 도구다. 보안 도구가 아니다. 보안이 본 목적이면 build-time persisted + introspection 차단 + 별도 allowlist가 필요.
클라이언트 측 동작 모드 매트릭스
흥미로운 이야기
APQ는 Apollo의 비공개 패키지에서 생태계 표준이 되었다
2018년 Apollo가
apollo-link-persisted-queries를 공개했을 때, 이건 Apollo 전용 link였다. urql·Relay는 안 썼고 비표준 프로토콜에 가까웠다. 그런데 Cloudflare Workers의 GraphQL 캐시가 APQ 프로토콜을 공식 지원하면서 — de facto 표준이 되었다. 2020년에는 Hasura·Yoga·Mercurius가 모두 APQ를 서버에서 지원하기 시작했고, 클라이언트와 서버 사이 비공식 약속이 생태계 약속으로 굳어졌다. 한 회사의 내부 도구가 생태계 표준이 되는 패턴 — GraphQL 도메인에서 놀라울 정도로 자주 일어난다 (DataLoader·Connection spec·APQ가 모두 같은 경로). Apollo의 가치는 제품 그 자체가 아니라 생태계 규약을 먼저 정한 사람이라는 데 있다.
Insight — 2단계 핸드셰이크가 자동 등록을 풀었다
어렵게 푸는 대신 한 번의 round trip을 추가한다 — 시스템 설계의 흔한 패턴 (lazy initialization, write-back cache). APQ는 그 패턴을 GraphQL 위에 옮긴 것뿐이다.
한 단락 요약
Automatic Persisted Query는 첫 요청에서 hash만 보냈다가 miss면 query를 함께 재시도하는 단순 프로토콜이다. Build-time persisted의 manifest 운영 부담을 런타임 자동 등록으로 옮겨, 대역폭·CDN cacheability는 보존하면서 진입장벽을 0에 가깝게 낮춘다. 단, allowlist 보안 효과는 사라진다 — 모든 query가 자동 등록되니까. 적절한 자리는 일반 웹 앱·SaaS dashboard·prototyping이고, 공개 API·모바일 SDK·금융은 여전히 build-time이 정답. 또한 APQ는 클라이언트 정규화 캐시도, 서버 response cache도 대체하지 않는다 — 그저 네트워크 본문 레이어만 푼다. 다음 문서(
07-response-cache-server-side)는 서버가 자기 응답 자체를 캐시하는 마지막 레이어를 다룬다.