Published on

가벼운 캐시시스템 memcached 사용하기

Authors

기존에 애플리케이션에 캐시 시스템을 적용하고 설정할 때 Redis를 사용해왔습니다.

최근 프로젝트에서 퍼시스턴스(데이터 영구 저장)나 메시지 큐 같은 기능이 필요없는 아주 단순한 캐시 저장소를 사용해보고자 했습니다.

단순한 캐시 저장소로서 빠르고 많은 기능이 있지 않아서 가벼운 캐시 시스템으로 사용하기 좋은 Memcached(Local)를 적용하고 쿼리에 적용한 부분을 기록합니다.

Install Memcached (Local)

Brew 설치

brew install memcached
brew services start memcached

종료 할때는 stop 명령어로 종료합니다.

brew services stop memcached

Docker 설치

docker run -d --name memcached -p 11211:11211 memcached

Django Setting

가상환경에 python memcahe 라이브러리를 설치합니다.

pip install pymemcache

settings.py 에 CACHES 를 작성합니다.

기본적으로 11211 port 를 사용하며 우선 로컬에서 사용하기 때문에 LOCATION 은 127.0.0.1:11211 로 입력합니다.

AWS Elasticcache 사용 시 해당하는 memcached-cluster-endpoint 를 대체하면 됩니다.

CACHES = {
    'default': {
        'BACKEND': 'django.core.cache.backends.memcached.PyMemcacheCache',
        'LOCATION': '127.0.0.1:11211',
    }
}

코드레벨

Simple description

  • cache.get(key)cache.set(key) 로 캐싱된 데이터를 가져오고 세팅할 수 있으며, redis와 같은 타 캐시시스템과 사용방법은 동일합니다.
cache.set(cache_key, data, timeout=3600)
  • cache_key: 캐시를 저장할 때 사용하는 고유한 키 값입니다. 나중에 이 키를 사용해 캐시된 데이터를 조회할 수 있습니다.
  • data: 캐시에 저장할 실제 데이터입니다. 이 데이터는 키와 함께 메모리에 저장됩니다.
  • timeout=86400: 1 단위 당 1초 입니다. 캐시된 데이터의 유효 기간을 설정합니다. (86400초=24시간) 시간이 지나면, 해당 캐시가 만료되어 다시 데이터를 캐시에 저장해야 합니다.

예제

하단의 코드는 상위직종 / 하위직종에 대한 쿼리를 사전에 캐시해두고 가져오는 로직입니다.

하위직종 - 상위직종 간 연계되어 있으며, 예시에서는 생략했지만 cache_key를 상위직종과 연관지어 set하면 캐시 사용에 대한 효과가 확실하게 나타납니다.

from django.core.cache import cache

def get_job_upper_dict():
    cache_key = "job_upper_dict"
    job_upper_dict = cache.get(cache_key)
    if job_upper_dict is None:
        print("Cache miss and fetch.")
        job_upper_dict = dict(JobUpper.objects.values_list("code", "name"))
        cache.set(cache_key, job_upper_dict, timeout=86400)
    else:
        print("Cache hit.")
    return job_upper_dict

@생략
class SomeViewSet:
    queryset = WorkerProfile.objects.all()
		"""생략"""

   def get_queryset(self):
        job_upper_dict = get_job_upper_dict()
        job_lower_dict = get_job_lower_dict()
        upper_when_conditions = [When(job_upper_code=code, then=Value(name)) for code, name in job_upper_dict.items()]
        lower_when_conditions = [When(job_lower_code=code, then=Value(name)) for code, name in job_lower_dict.items()]
        queryset = (
            super()
            .get_queryset()
            .select_related(
                "construction",
                "user",
            )
        )
				# 캐시
        queryset_from_cache = queryset.annotate(
            job_upper_name=Case(*upper_when_conditions, default=Value(""), output_field=CharField()),
            job_lower_name=Case(*lower_when_conditions, default=Value(""), output_field=CharField()),
        )

        # 서브쿼리
        queryset_from_subquery = queryset.annotate(
            job_upper_name=Subquery(JobUpper.objects.filter(code=OuterRef("job_upper_code")).values("name")[:1]),
            job_lower_name=Subquery(JobLower.objects.filter(code=OuterRef("job_lower_code")).values("name")[:1]),
        )

        return queryset_from_cache

데이터 결과는 캐시와 서브쿼리 방식 모두 동일합니다.

쿼리속도로 보면 캐시 방식이 서브쿼리 방식보다 0.022초에서 0.015초로 감소(30%) 되었습니다.

속도측면에서 보나 자주 업데이트 되지 않을 데이터를 캐싱해서 사용하는 것으로 보나 이점은 확실합니다.

하지만, 시스템이 확장된 것이기 때문에, 서브쿼리를 사용하여 가져오는 비용과 별도의 cache 모듈을 사용하는 것에 대한 비용을 잘 고려 해야겠습니다.

hongreat 블로그의 글을 봐주셔서 감사합니다!