CacheOps — ORM에 Redis Cache 쉽게 적용하기

Jimin Lee
29CM TEAM
Published in
7 min readMay 10, 2020

29CM 백엔드 서버는 Django으로 구성되어있습니다. 이 글에서는 Django ORM Cache인 Cacheops를 도입하면서 분석한 자료를 공유드리고자 합니다.

Cacheops란?

Django ORM Redis Cache 도입이 용이하고, 쉽게 데이터를 동기화할 수 있도록 도와주는 파이썬 라이브러리입니다.

Cacheops 적용하기

cacheops 패키지를 설치한 후, 자신의 환경에 맞는 설정을 Django settings에 추가합니다.

INSTALLED_APPS += ['django-cacheops']CACHEOPS_LRU = True
CACHEOPS_DEGRADE_ON_FAILURE = True
CACHEOPS_DEFAULTS = {
'timeout': 60*5,
'cache_on_save': True,
'local_get': False,
}
CACHEOPS_REDIS = {
'host': CACHEOPS_REDIS_HOST,
'port': CACHEOPS_REDIS_PORT,
'db': 1,
'socket_timeout': 0.5
}
CACHEOPS = {
'contents.Article': {'ops': 'get'},
}

이후, Cacheops ORM으로 지정한 모델과 함수를 호출하면, ORM이 DB에 질의하기 전에 Redis Cache를 먼저 질의하게 됩니다.

from contents.models import ArticleArticle.objects.get(pk=1)

장점

Cacheops는 ORM Cache 조회와 데이터 동기화를 추상화하였습니다. 그래서 ORM Cache와 Django Model의 관심사가 분리되어있습니다. 덕분에 백엔드 엔지니어는 비즈니스 로직에 집중할 수 있습니다.

ORM Cache 조회

Cacheops는 Cache Key 생성, Cache Read, Cache Fetch, Cache Deserialize를 담당합니다.

  • Cache Key : 특정 모델과 질의 조건 별로 Unique Key를 만듭니다.
  • Cache Read : Cacheops으로 지정한 모델의 Queryset 함수 호출 시, 알아서 Cache에서 데이터를 조회합니다.
  • Cache Fetch : Cache에서 값을 조회하지 못한 경우, DB에서 데이터를 질의한 후 이를 Cache에 저장합니다.
  • Cache Deserialize : Cache에 직렬화되어 저장된 데이터를 역직렬화합니다.

이와 같은 역할을 Cacheops가 담당하기 때문에, 백엔드 엔지니어는 Cache Read Process를 구현하지 않아도 됩니다.

ORM 동기화

Cacheops는 Django Model save, delete Signal을 사용하여 데이터를 동기화합니다. 이 때, Cacheops는 cache key 생성, cache write, cache delete, data serialize을 담당합니다.

덕분에 백엔드 엔지니어는 Cache Write Process에 관여하지 않아도 됩니다.

예외적으로 Queryset update 함수가 호출되는 경우에는 Cache 동기화를 하지 않습니다. 왜냐하면 Django Queryset update함수는 Signal을 발생시키지 않기 때문입니다. 그래서 이 경우에는 Cacheops Custom Function인 invalidated_update함수를 사용해야합니다.

Article.objects.filter(created_at__lte=datetime(2020,1,1)).invalidated_update(publish=False)

활용 범위

Cacheops는 Function, View, Template Cache도 지원합니다.

유의사항

Cacheops는 캐시 관리 로직을 추상화하였습니다. 그래서 백엔드 엔지니어는 비즈니스 로직 구현에만 집중할 수 있습니다.

그럼에도 Cacheops의 동작 방식과 유의사항을 인지하고 사용해야합니다.

동작 방식을 모르는 채 무분별하게 사용하면, Cache Hit Rate가 떨어지고 데이터 동기화가 누락되는 현상이 발생할 수 있습니다.

Join Filter 지양하기

Cacheops는 모델의 필드 값이 변경되는 경우, 해당 필드로 질의하여 캐싱했던 데이터를 삭제합니다. 이는 캐시 동기화의 정확도가 높아진다는 장점이 있지만, Queryset의 유형에 따라 캐시 효율이 현저하게 떨어진다는 단점이 있습니다.

예시로 특정 회원이 북마크한 기사 목록을 조회하는 Queryset을 살펴봅시다.

Bookmark.objects.filter(user_id=1, article__publish=True)

이 때, Bookmark의 user_id 필드가 변경되거나, Article의 publish 필드가 변경될 때마다 Cache가 재동기화됩니다. 이는 모든 Article이 변경될 때마다 북마크 캐시가 삭제된다는 것을 의미합니다.

때문에 이와 같이 Join Filter Queryset을 사용하는 경우, Cache Hit Rate가 현저하게 떨어질 수 있습니다.

Select Related 지양하기

cacheops는select_related로 선언된 모델도 함께 캐싱합니다. 그러나 select_related으로 캐싱된 객체는 cacheops 데이터 동기화 대상에 포함되지 않습니다.

그래서 캐시와 DB의 데이터 동기화가 깨질 수 있기 때문에 이 경우 prefetch_related함수를 사용하도록 변경해야합니다.

Data Structure

Cacheops는 Redis Cache에 특화된 라이브러리입니다. 내부적으로 Lua script와 Set Data Type 등을 사용하기 때문에 다른 Cache와는 호환되지 않습니다.

Redis Cache 데이터는 데이터 동기화를 위한 메타 데이터와 실 데이터로 구성됩니다. Redis 자료구조는 다음과 같습니다.

크게 3 Depth으로 데이터가 관리됩니다.

  • 1 depth ( schema:XXX ): 캐시 동기화 대상인 테이블명과 컬럼명을 추적할때 사용하는 데이터입니다.
smembers "schemes:article"1) "id"
2) "publish"
  • 2 depth ( conj:XXX ): Queryset Cache를 추적할 때 사용되는 데이터입니다. KEY는 캐시를 삭제해야하는 질의 조건, Value는 삭제 대상 data 캐시 리스트로 구성됩니다.
smembers "conj:article:id=1"1) "q:17c1cefe3cc9ccb29cdfdcb08b5f66dd"
  • 3 depth ( as:XXX / q:XXX ): 실 데이터 저장소입니다. 데이터가 직렬화되어 String 데이터 타입으로 저장됩니다.
GET "q:17c1cefe3cc9ccb29cdfdcb08b5f66dd"
"\x94\x85\x94R\x94h"

마무리

Django Application에 Cache를 빠르게 도입해야할 때, Cacheops의 강점이 더 빛을 발합니다.

그러나 캐시 동기화 코드의 은닉화는 코드 디버깅을 어렵게 한다는 약점도 있습니다.

그래서 29CM에서는 캐시를 적용할 쿼리셋에만 cache함수를 명시하는 방식으로 Cacheops를 보수적으로 사용하고 있습니다.

이만, 저의 첫 Medium 포스팅을 마치겠습니다. 이 글이 Cacheops 도입을 검토하는 다른 분들께 도움이 되었으면 좋겠네요. 😊

[함께 성장할 동료를 찾습니다]

29CM ((주)무신사)는 3년 연속 거래액 2배의 성장을 이루었습니다.
이제 더 큰 성장을 위해 기존 모놀리틱 서비스 구조를 마이크로서비스 구조로 전환하고, 앵귤러 기반 프론트엔드 코드를 리액트로 전환하는 등의 기술적인 시도를 진행하고 있습니다. 모바일 앱 내부 구조도 모듈러 아키텍처로 개선하는 과정에 있습니다. 함께 성장하고 유저 가치를 만들어낼 동료 개발자분들을 찾습니다.

🚀 29CM 채용 페이지 : https://www.29cmcareers.co.kr/

--

--