캐싱을 적용해보자!

JeongYong Park·2023년 4월 6일
2

이미지 다운로드 / 업로드 서버를 개발 중에 해결했던 문제를 적어보고자 합니다.
현재 서버는 이미지를 다운로드할 때 이미지의 크기를 조정한 후 조정된 이미지를 내려주는 로직을 수행하고 있습니다.

웹 서버

이미지같은 정적 리소스는 애플리케이션 서버에서 반환하는 것이 아닌 웹 서버에서 반환을 할 수 있습니다. 이를 위해 애플리케이션 서버 앞에 웹 서버를 두기로 결정했습니다.

어떤 웹 서버를 선택할까?

  • Apache 웹 서버
    • 가장 오래된 웹 서버 중 하나이며, 많은 사용자와 커뮤니티가 있다.
    • 모듈 기반 아키텍처로 인해 다양한 기능과 유연성을 제공한다.
    • 정적 콘텐츠와 동적 콘텐츠 모두를 다룰 수 있다.
    • 모듈 및 구성 설정이 복잡할 수 있다.
  • Nginx 웹 서버
    • 빠른 성능으로 알려져 있으며, 아파치 웹 서버보다 더 빠릅니다.
    • 이벤트 기반 아키텍처를 사용하여 처리 능력과 확장성을 높입니다.
    • 주로 정적 콘텐츠를 다루지만, 프록시 서버나 로드 밸런서 등으로 사용할 수 있습니다.
    • 모듈이 적지만, 설정이 비교적 단순합니다.

이미지 파일은 정적 리소스이기 때문에 NGINX의 빠른 처리 능력과 단순한 구성설정이 이점이 될 것으로 판단되고 프록시 서버로서의 기능도 사용할 수 있어 결국 NGINX 웹 서버를 선택하기로 하였습니다.

여기까지의 개발한 흐름을 이미지로 나타내면 다음과 같습니다.

캐싱 적용

아직 클라이언트가 동일한 이미지를 계속해서 요청할 때 이미지 리사이징 로직을 매번 수행해야하는 문제가 있습니다. 이를 위해 캐싱을 적용해보도록 하겠습니다.

NGINX 캐싱

NGINX의 reverse proxy 캐싱을 적용해 보겠습니다.

NGINX는 애플리케이션 서버로 가는 요청을 앞에서 처리하는 reverse proxy 기능과 처리된 요청을 캐싱할 수 있는 기능을 제공합니다. 이를 이용하여 동일한 크기의 이미지를 요청하는 경우, 애플리케이션에서 처리한 이미지를 NGINX에 캐싱하도록 설정하겠습니다.

NGINX에서 캐싱에 대한 기능을 제공해주고 있기 때문에 nginx.conf 파일을 수정해 캐싱을 적용했습니다.

  • NGINX 변수들
    • proxy_cache_key 지시어를 사용하여 캐시 키를 생성합니다. 이때, $scheme
      , $request_method, $host, $request_uri 등의 변수를 사용하여 캐시 키를 생성합니다.
    • $scheme 변수는 요청에 사용된 프로토콜 (http 또는 https)을 나타내며,
      $request_method변수는 요청에 사용된 HTTP 메소드 (GET, POST 등)를 나타냅니다.
      $host변수는 요청한 호스트 이름을 나타내며,
      $request_uri 변수는 요청된 URI를 나타냅니다.
    • $upstream_cache_status 변수를 사용하여 프록시 캐시에서 응답을 받았는지를 확인합니다. 이 변수는 다음과 같은 값을 가질 수 있습니다. ($upstream_cache_status 변수는 Nginx의 proxy_cache 모듈에서 사용되는 변수입니다.)
      • MISS: 캐시에서 응답을 받지 못함
      • BYPASS: 캐시를 건너뛰고 원본 서버에서 응답을 받음
      • EXPIRED: 캐시의 유효 기간이 지나 응답을 받을 수 없음
      • HIT: 캐시에서 응답을 성공적으로 받음

여기까지의 흐름을 이미지로 나타내보면 다음과 같습니다.

브라우저 캐싱

캐싱은 NGINX에서도 할 수 있지만 브라우저단에서도 적용할 수 있습니다. 바로 ETag 값과 If-None-Match 헤더를 사용해서 캐싱에 적용할 수 있습니다.

서버에서 응답하는 결과가 자주 바뀌는 API에 대해서는 ETag 값을 적용하면 오히려 애플리케이션 서버에 부하를 야기할 수 있지만, 개발하는 서버에서 동일한 크기의 이미지 요청이 많이 온다고 가정했기 때문에 ETag를 적용하기로 했습니다.

ETag는 HTTP 프로토콜에서 캐시 유효성 검증을 위한 방법 중 하나로 클라이언트가 조건부검사를 할 수 있도록 해줍니다. ETag는 웹 서버가 URL에서 탐색한 리소스의 특정 버전에 할당한 식별자 값입니다. 클라이언트는 이 식별자 값을 If-None-Match 헤더에 담아 값이 일치하지 않으면 서버에서 변경된 리소스를 가지고 오게 됩니다.

Spring framework 에서 ETag를 사용하고 싶다면 ShallowEtagHeaderFilter 를 빈으로 등록해주면 됩니다.

@Configuration(proxyBeanMethods = false)
public class ETagHeaderFilter {

    @Bean
    public ShallowEtagHeaderFilter shallowEtagHeaderFilter() {
        return new ShallowEtagHeaderFilter();
    }
}

API 테스트코드로 다음을 검증합니다.

  • 응답헤더에 ETag가 존재하는지
  • If-None-Match 헤더로 응답받은 ETag 값을 같이 넘겨 재요청시 응답코드가 304 NOT MODIFIED 인지
    @DisplayName("한 번 응답을 받은 후 동일한 요청을 etag 헤더와 같이 보내면 304응답코드가 반환된다.")
	@Test
	void givenResponseWasRetrieved_whenRetrievingAgainWithEtag_thenNotModified() {
		// given
		ResponseEntity<byte[]> res = testRestTemplate.getForEntity(
			"http://localhost:8080/api/resize/images/1cad34b7-55bc-49c3-a5a8-d384c3d30c26.jpeg?width=500",
			byte[].class
		);
		SoftAssertions.assertSoftly(softAssertions -> {
			softAssertions.assertThat(res.getStatusCode()).isEqualTo(HttpStatus.OK);
			softAssertions.assertThat(res.getHeaders().getCacheControl()).isNotNull();
			softAssertions.assertThat(res.getHeaders().getETag()).isNotNull();
		});

		HttpHeaders headers = new HttpHeaders();
		headers.setIfNoneMatch(res.getHeaders().getETag());
		HttpEntity<Object> request = new HttpEntity<>(headers);

		// when
		ResponseEntity<byte[]> secondRes = testRestTemplate.exchange(
			"http://localhost:8080/api/resize/images/1cad34b7-55bc-49c3-a5a8-d384c3d30c26.jpeg?width=500",
			HttpMethod.GET,
			request,
			byte[].class
		);

		// then
		assertThat(secondRes.getStatusCode()).isEqualTo(HttpStatus.NOT_MODIFIED);
	}

이후 실제로 서버를 동작시켜 서버로부터 온 응답값을 확인해보면 다음과 같습니다.

ETag 헤더에 해시값을 같이 넘겨주는 것을 확인할 수 있었습니다.

이후 Request Headers에도 If-None-Match 헤더에 ETag 값을 넘겨 요청을 보내는 것을 확인할 수 있습니다.

캐시 히트되어 StatusCode가 304 Not Modified 로 오는 것을 확인했고 기존 5.59s 정도 걸리던 요청이 캐시 적용시 68ms로 단축된 것을 확인할 수 있습니다.

결론

캐시를 통해 동일한 응답에 대해서는 응답시간을 대폭 줄일 수 있었습니다. 하지만 여전히 캐시히트에 의존하고 있고 해당 서버를 확장한다면 어떻게 데이터를 넣을지, 경로는 어떻게 설정해야할 것인지도 고민해야 할 것 같습니다.

참고자료

이동욱님 NGINX Cache 문제 해결
NGINX reverse proxy caching
Tecoble - ETag

profile
다음 단계를 고민하려고 노력하는 사람입니다

0개의 댓글