🔷 GraphQL5. 캐시 & 성능06 · Automatic Persisted Queries (APQ)

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의 구현 상세

import { createPersistedQueryLink } from "@apollo/client/link/persisted-queries";
import { sha256 } from "crypto-hash";
 
const link = createPersistedQueryLink({
  sha256,
  useGETForHashedQueries: true, // ← CDN 친화 핵심
}).concat(httpLink);

이 link의 프로토콜:

  1. query → sha256 hash 계산.
  2. 첫 요청: GET /graphql?extensions={"persistedQuery":{"version":1,"sha256Hash":"abc"}}&variables=...
  3. 서버 응답 코드:
    • PersistedQueryNotFound → 자동 재시도, 이번엔 POST에 query 포함.
    • PersistedQueryNotSupported → 모든 요청을 그대로 (서버가 APQ 미지원).
  4. 성공이면 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 PQAPQ
등록 시점빌드 타임 (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)는 서버가 자기 응답 자체를 캐시하는 마지막 레이어를 다룬다.