API 응답이 느리다는 이야기가 나오면 대개 캐시부터 붙입니다. 붙이는 것은 쉽습니다. 어려운 것은 언제 버릴지 정하는 일이고, 그 판단이 잘못되면 느린 것보다 나쁜 상태 — 틀린 데이터를 빠르게 주는 상태 — 가 됩니다.
세 계층, 서로 다른 일
오브젝트 캐시는 응답을 캐시하지 않습니다. 응답을 만드는 데 필요한 조각(글 · 메타 · 텀)을 캐시해서 같은 요청 안의 중복 쿼리를 없애고, Redis 를 붙이면 요청을 넘어 유지됩니다. 무효화는 코어가 알아서 하므로 가장 안전한 계층입니다.
트랜지언트는 우리가 이름을 붙인 계산 결과입니다. 외부 API 호출이나 무거운 집계처럼 “이건 10분쯤 낡아도 된다” 는 값에 맞습니다.
페이지 · HTTP 캐시는 응답 전체를 앞단에서 돌려줍니다. 가장 빠르지만 PHP 가 돌지 않으므로 우리 코드가 무효화에 참여할 수 없습니다. 그래서 이 계층은 무엇을 담을지보다 무엇을 담지 않을지를 먼저 정해야 합니다.
무효화를 열거로 하지 않습니다
캐시 키를 잘게 나눌수록 무효화 때 지워야 할 키가 늘어납니다. 목록 캐시가 정렬 · 페이지 · 필터 조합마다 있다면, 글 하나가 바뀔 때 어떤 키를 지워야 하는지 아무도 정확히 모릅니다. 해결은 지우지 않는 것입니다 — 키에 세대 번호를 넣고, 무효화할 때 그 번호만 올립니다.
function wper_tools_payload() {
$gen = (int) get_option( 'wper_tools_gen', 1 );
$key = 'tools:' . $gen . ':' . get_locale();
$hit = wp_cache_get( $key, 'wper_api' );
if ( false !== $hit ) {
return $hit;
}
$payload = wper_build_tools_payload(); // 비싼 쪽
wp_cache_set( $key, $payload, 'wper_api', 10 * MINUTE_IN_SECONDS );
return $payload;
}
// 무효화는 키를 지우는 것이 아니라 세대를 올리는 것이다.
add_action( 'save_post_wper_tool', function () {
update_option( 'wper_tools_gen', (int) get_option( 'wper_tools_gen', 1 ) + 1, false );
} );
세대가 오르면 옛 키는 아무도 찾지 않고 만료되어 사라집니다. 지울 키 목록을 관리하지 않아도 되고, 조합이 몇 가지든 무효화는 한 줄입니다.
키에 get_locale() 이 들어간 것도 우연이 아닙니다. 응답을 가르는 모든 축이 키에 있어야 합니다 — 언어 · 인증 상태 · context · 쿼리 인자. 하나라도 빠지면 어떤 사용자의 응답이 다른 사용자에게 나갑니다.
앞단 캐시는 무엇을 담지 않을지부터
코어는 로그인 상태의 REST 응답에 no-cache 헤더를 붙입니다. 하지만 앞단 캐시가 그 헤더를 존중한다는 보장은 없습니다 — nginx FastCGI 캐시는 우리가 적어 준 조건대로만 움직입니다. 그래서 우회 조건을 명시적으로 정합니다.
네 번째 줄은 이론이 아니라 실제로 나는 사고입니다. 웹훅 라우트가 캐시되면 결제 서비스는 200 을 받는데 우리 DB 는 그대로입니다. 로그에는 이상이 없고 입금만 반영되지 않습니다. 앞 회차에서 같은 이야기를 한 이유가 이것입니다 — 이 경계는 보안과 성능 양쪽에서 같은 자리를 가리킵니다.
마지막으로 순서입니다. 캐시는 측정한 뒤에 붙입니다. 느린 이유가 N+1 쿼리인데 응답을 캐시하면, 캐시가 만료되는 순간마다 같은 느린 요청이 그대로 돌아옵니다. 캐시는 개선을 대신하지 않고 개선의 효과를 넓혀 줄 뿐입니다.
계층별 캐시 설계는 성능 최적화 아카이브에서 더 다루고, 오브젝트 캐시와 페이지 캐시를 서로 어긋나지 않게 구성하는 작업은 최적화 지원 사업에 포함돼 있습니다.
다음 회차
연재의 마지막 회차입니다. 여기까지의 도구를 다 갖추고 나면 “그럼 프런트를 통째로 분리할까” 라는 질문이 옵니다. 헤드리스가 무엇을 주고 무엇을 가져가는지, 정직하게 정리합니다.