Skip to content

[성능 개선] 게시물 조회 캐시 적용

YoonTaeMin edited this page Sep 11, 2023 · 15 revisions

캐시 적용 위치

  • 메인페이지에서 [좋아요수를 가장 많이 받은 게시물 top5를 조회하여 보여주는 api]
  • 커뮤니티탭에서 [게시글 카테고리와 검색에 따른 게시물 조회 api(페이징처리)]

필요성

메인페이지는 사용자가 가장 많이 접속하는 페이지이기 때문에 메인페이지에 접속할 때마다 게시물 조회 api를 호출하여, db로부터 데이터를 조회해오는 것은 비효율적이라고 생각했습니다. 따라서, 메인페이지에서의 게시글 조회에 캐시를 적용하여 중복적인 쿼리의 수를 감소시켰습니다. 또한, 메인페이지 뿐 아니라 [검색조건과 페이징처리]가 되어있는 게시글 조회에서도 캐시를 적용하였습니다. 사용자는 1페이지에서 2,3,4..페이지로 이동 후, 다시 이전 페이지로 돌아와서 조회를 하는 경우가 많을 것이라고 판단하였기 때문에 캐시를 적용하여 반복적인 조회쿼리를 감소시키고자 하였습니다. 하지만, 하나의 게시글이라도 생성되거나 수정, 삭제가 발생할 경우, 캐시에 저장되어 있는 모든 데이터를 삭제하여 데이터의 불일치를 해결해줘야 합니다. 이는 trade-off 가 있지만, 서비스 초기 단계 즉, 사용자가 많지 않은 상황에서는 게시글의 변경이 빈번하게 일어나지 않을 것이기 때문에, 캐시적용을 시도하기에 적합하다고 판단했습니다.

구현

캐싱는 redis를 사용하여 구현하였습니다. local-memory 기반의 캐시인 ConcurrentMapCacheManager를 사용하여 구현할 수 있지만, 다중 서버에 대비하여 global하게 사용하기 위해 외부 캐시 db인 redis를 사용하였습니다.

메인페이지

  • 캐시 key : 게시글 카테고리
  • 캐시 value : 좋아요수 게시글 top5

페이징 조회

  • 캐시 key : 게시글 검색키워드 and 게시글 카테고리 and 페이지번호
  • 캐시 value : 페이지 별 게시글 목록

스프링에서 제공하는 @Cacheable , @CacheEvict 사용하여 구현하였습니다.
최초의 게시글 조회에서는 db에서 데이터를 조회하고, 조회한 데이터를 캐시에 key-value 형태로 저장합니다. 이 후, 요청에서는 db를 조회하지 않고, 캐시에서 데이터를 조회하여 반환합니다. -> @Cacheable
또한, 게시글의 변경(수정,삭제)가 발생할 경우, 캐시데이터를 삭제합니다. -> @CacheEvict

캐시 데이터의 유효기간은 3시간으로 설정하였습니다. 서비스 초기의 상태여서 캐시데이터의 변경빈도가 많이 발생하지 않을 것이라 판단하였고, 추후에 사용자가 늘어남에 따라 유효기간을 줄일 예정입니다.

PageImpl 커스터마이징

페이징객체를 반환하는 부분에 캐시를 적용하다가 org.springframework.data.redis.serializer.SerializationException 가 발생하였습니다.
object에서 json으로, json에서 object로 직렬화, 역직렬화를 수행하려면 dto class에 기본생성자가 존재하거나 @JsonProperty 어노테이션이 존재해야 합니다. 하지만, 페이징처리를 위해서 Page 인터페이스를 사용하기 때문에 이를 적용할 수 없었습니다. 따라서 Page의 구현체인 PageImpl 클래스를 상속하여 커스터마이징하였습니다. 스크린샷 2023-06-16 오전 3 44 56
스크린샷 2023-06-16 오전 3 51 42
추가적으로, Page에 포함된 Pageable 객체에 대해서도 직,역직렬화를 수행할 수 없기 때문에 @JsonIgnore를 사용하여 제외시켰습니다.

성능 개선

결과적으로 같은 요청에 대한 응답시간을 단축하였습니다. (158ms -> 39ms)
스크린샷 2023-05-29 오후 11 02 50

스크린샷 2023-05-29 오후 6 53 54

추가

(뒤늦게 offset 기반 페이지네이션의 단점에 대해 알게 되었습니다.😭😭)
offset기반의 페이지네이션은 특정 정렬조건에 대해서 특정 offset까지의 row들을 모두 순회해야만 합니다. 수천, 수만개의 데이터가 있는 테이블에서의 이 작업은 DB 퍼포먼스를 떨어뜨린다는 단점이 있습니다. 또한, 사용자가 1페이지를 조회하고 있는 도중 새로운 데이터가 추가된다면, 2페이지로 이동했을 때, 중복된 데이터를 보여줄 확률이 높습니다. 따라서 가장 마지막으로 조회한 row의 id를 기반으로 페이징을 구현하는 커서 기반 페이지네이션을 구현함으로서 이러한 문제를 해결할 수 있다고 생각합니다. 하지만, mvp 기능을 빠르게 구현하는 것이 우선적인 목표였고, 아직 사용자가 많지 않은 서비스이기 때문에 급하게 이를 수정하기 보다는 서비스가 기능적으로 완성이 되면, 꼭 적용해보려고 생각하고 있습니다.

Clone this wiki locally