Technote

성능 최적화 심화

연재 REST API 와 헤드리스 8부 중 7부

API 응답 캐시 — 어느 계층에 두고 어떻게 무효화하나

캐시를 넣는 것은 쉽습니다. 어려운 것은 언제 버릴지 아는 일이고, 그 판단을 잘못하면 결제 상태가 갱신되지 않는 사고까지 이어집니다.

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 쿼리인데 응답을 캐시하면, 캐시가 만료되는 순간마다 같은 느린 요청이 그대로 돌아옵니다. 캐시는 개선을 대신하지 않고 개선의 효과를 넓혀 줄 뿐입니다.

계층별 캐시 설계는 성능 최적화 아카이브에서 더 다루고, 오브젝트 캐시와 페이지 캐시를 서로 어긋나지 않게 구성하는 작업은 최적화 지원 사업에 포함돼 있습니다.

다음 회차

연재의 마지막 회차입니다. 여기까지의 도구를 다 갖추고 나면 “그럼 프런트를 통째로 분리할까” 라는 질문이 옵니다. 헤드리스가 무엇을 주고 무엇을 가져가는지, 정직하게 정리합니다.

이 주제의 다른 글

노하우 목록으로

성능 최적화 심화

프론트엔드 예산 — 상한을 정하고 지키는 법

페이지는 한 번에 무거워지지 않습니다. 각각 합리적인 작은 추가가 쌓여 무거워집니다. 상한을 정하고 재는 절차가 그 누적을 끊습니다.

개발자 4분 읽기

성능 최적화 심화

전환 감사 — 유입부터 완료까지 새는 곳 찾기

아무도 도달하지 않는 페이지를 다듬는 일이 가장 흔한 낭비입니다. 경로 전체를 처음부터 끝까지 걸어 보면 어디서 새는지 먼저 보입니다.

마케터 4분 읽기

성능 최적화 실무

팝업의 대가 — 검색과 전환에 동시에 걸립니다

모바일에서 본문을 가리는 팝업은 검색 평가에서 불리하게 작용하고, 같은 팝업이 전환에서도 비용을 만듭니다. 조절할 수 있는 것은 타이밍 · 트리거 · 닫기 세 가지입니다.

마케터 4분 읽기

₩270,000 · 신청하기