django cacheops 주의 사항

이태성·2022년 9월 25일

cacheops

장고로 개발을 하는 사람이라면 db 캐시 관련하여 한 번쯤 들어봤을 캐시 라이브러리이다.

보통 django의 orm을 사용하여 캐시가 가능한 라이브러리로 cachealot, cacheops 이 두 가지가 가장 유명한데 현재 회사는 cacheops를 사용하고 있다.

둘의 차이를 간단하게 설명하면 cachealot은 테이블 단위 캐시를 하고 cacheops는 row 단위로 캐시를 하여 더 높은 hit율이 나올 수 있다?가 가장 큰 차이이다.

현재 회사에서도 cachealot에서 cacheops로 캐시 라이브러리를 변경했는데 변경하면서 발생한 문제를 정리해 본다.

join 캐시 탈락 이슈

db 캐시를 사용한다는 것은 어느 정도 트래픽이 있다는 것이고 일정 트래픽과 사용 경험 개선을 위해 복잡한 비즈니스 로직도 동반이 될 것이라고 생각한다.

복잡한 비즈니스 로직 이 부분이 회사마다 다르겠지만 보통 서비스가 고도화됨에 따라 화면에 표시해야 될 정보들이 초기에 비해 달라질 수 있다. 초기엔 간단한 select 쿼리에 간단한 필터 조건으로 해결이 될 수 있지만 점점 시간이 지날수록 join을 활용한 복잡한 쿼리로 진화할 수 있다.

문제는 cacheops가 join절에 걸린 Filter 쿼리의 캐시 탈락을 지원하지 않는다.

예를 들어 User와 UserProfile이라는 테이블이 존재할 여 join을 한다면

User

idname
1여진구
2임시완
3천우희

UserProfile

idphoneuserId
1010123412341
2010123456782
3010567856783
select * from User join UserProfile on User.id = UserProfile.userId where UserProfile.phone = '01012341234'

이런 형식의 쿼리가 발생할 시 join으로 가져온 필드에 걸린 where 절의 쿼리 문에 변경이 생길 시 cache된 쿼리가 탈락하여야 하는데 cacheops의 경우 join 절의 캐시 탈락을 지원하지 않아 01012341234번호가 변경이 되어도 01012341234로 시간 만료로 캐시가 탈락될 때까지 호출이 된다.

다만 해당 조건에 걸린 User 테이블의 row의 데이터의 변경이 발생하면 캐시가 탈락된다.

장고의 경우 orm으로 join을 잘 지원하는 편(select_related)인데 해당 문제가 발생하여 서비스 전반적인 쿼리를 전수 조사해서 변경하여 대응했던 적이 있다.

이때 적용했던 해결 방안으로 join > select where in 절 형태(prefetch_related)로 변경하였다.
문제는 이렇게 in 절 형태로 변경했을 때 in 절에 많은 조건이 걸리면 슬로쿼리가 발생하기 때문에 과도하게 걸린다 싶을 때 join을 한 후 캐시에 저장하지 않게(nocache) 하는 방향으로 해결하였다

서브쿼리도 캐시 탈락 문제 발생

여기서 동일한 문제가 서브 쿼리에서도 발생하였는데
장고 orm으로 Subquery 문을 의도적으로 만들 수 있지만 orm 안에 orm? 사용으로도 서브 쿼리가 발생할 수 있다.

ex)

서브쿼리 발생할 수 있음
UserProfile.objects.filter(user_id=User.objects.get(id=1))

서브쿼리 발생 안함
user = User.objects.get(id=1)
UserProfile.objects.filter(user_id=user.id)

이 문제는 처음에 join으로 캐시 탈락이 안 되는 것을 파악한 후에 개선 방안 중에 하나로 캐싱을 하지 않는 것보다 캐싱을 최대한 하는 게 효율적이지 않을까란 생각에 몇몇 쿼리에 도입을 하다 발생한 문제였다.

결국은 다시 join을 하고 캐싱을 하지 않는 방향으로 되돌아갔다.

결론

사실 위해서 설명한 내용들은 cacheops 깃헙에서 일정 부분 설명하고 있는 내용이다.
cacheops github

cacheops가 join Filter를 지원하지 않기 때문에 불편한 것은 사실이다.
다만 cahceops가 row 단위 캐싱을 하다 보니 cache의 hit 율을 높이고 캐시 동기화 문제를 해결하기 위해 join을 지양하는 것이 이해는 된다.

또한 index 설계와 모델링을 통해 충분히 낮은 비용으로 캐시를 하지 않아도 쿼리가 가능하기 때문에 치명적인 단점은 아니라고 생각한다.(물론 내 능력이 그렇게 안된다ㅠ)

이 부분이 단점이어도 더 많은 장점(캐시 읽기, 쓰기, 직렬화)이 존재하기 때문에 (사실상 설치 후 약간의 설정만 해주면 바로 운영 레벨에 적용할 수 있다) 위의 단점들만 고려해서 코딩을 한다면 굉장히 쉽게 cache를 사용할 수 있다.

0개의 댓글