Redis 좋아요 기능, 캐싱 전략 고민 과정

enjoy89·2024년 3월 10일
2
post-custom-banner

현재 진행 중인 프로젝트에서 좋아요 기능을 구현하기 위해 고민했던 것 + 이를 어떻게 해결하는지 과정을 기록한 글 입니다!


🤨 좋아요 기능은 이렇게 구현하면 되나? (고민 과정)

1. 처음 생각했던 시나리오

  1. Like Table을 새로 하나 만든다.
  2. Item과 Like 엔티티 간의 연관관계를 맺어주고, like_count 항목을 추가시켜 좋아요 요청이 올 때마다 likes_count+1 해주어 저장한다. (=업데이트 해준다.)
  3. Like Table을 통해 아이템의 좋아요 개수를 조회할 수 있다.

❗️문제점

  • 이는 실시간으로 사용자의 요청을 처리하기 위해서는 효율적인 방식이 아니었고, 많은 사용자가 한 꺼번에 요청이 올 경우를 대비하지 못한 방식이다.
  • 또한, 아이템에 좋아요를 누르는 사용자는 1명이 아닌, 동시에 여러명이 누를 수 있으므로 모든 서버에서 접근이 가능해야 한다. 즉, 동시성 문제까지 고려한 글로벌 한 캐싱 방식을 적용해야 한다.
  • 이에 따라 RDBMS의 접근을 최소화 하고, 효율적이고 빠르게 데이터를 가져오기 위해선 Redis라는 메모리 캐싱을 사용해야 한다.


2. 두 번째 시나리오

  1. 클라이언트로부터 좋아요를 한 아이템 ID 목록을 List로 받는다.
  2. Redis 저장소에 key: “user:{userId}:likes” value: List<ItemDto> 형태로 저장한다.
  3. 유저가 좋아요 한 아이템의 정보를 조회할 수 있다.

❗️문제점

  • 아이템 정보가 모두 포함된 DTO 자체를 캐시에 저장하려고 했다.
  • 어차피 아이템을 조회할 때 좋아요 개수를 포함해서 반환해야 되기 때문에 캐시에 likeCount를 추가한 DTO 정보를 저장해놓고, 이를 역직렬화해서 조회하면 효율적일 것이라고 생각했기 때문이다.
  • 결론부터 말하자면 아이템 전체 정보 자체를 캐시에 저장하는 것은 비효율적이다.
  • 왜냐하면, DB에 저장되어 있는 아이템 정보와 캐시에 저장된 코디룩 정보의 차이점은 좋아요 개수인데, 거의 동일한 데이터를 DB와 캐시 2곳에 중복 저장하는 꼴이 되어 버리기 때문이다.
  • 따라서 key 값에 해당하는 좋아요 개수만 캐싱해놓고, 캐시 데이터를 DB에 Write 하거나 Read할 때 기존 데이터에 좋아요 개수만 추가하면 효율적인 로직이 완성된다.
  • 즉, 아이템 정보를 조회할 때 좋아요 개수는 캐시에서 조회하고, 그 외 모든 정보는 DB에서 조회 해야 한다.

🔺  동시성 문제 고민

  • 만약 여러명의 사용자가 동시에 하나의 코디룩에 좋아요를 누른다면, 좋아요 개수 카운팅이 정확히 될까 의문
  • Redis를 사용할 때 INCR 같은 명령어를 사용하면, 내부적으로 동시성 문제를 해결 ⇒ Redis 선택 이유

Redis는 싱글 스레드 모델을 사용하기 때문에, 한 시점에 하나의 명령만 처리한다.
따라서 INCR 명령 같은 경우, 여러 클라이언트가 동시에 같은 키에 대해 증가 연산을 요청하더라도, Redis 서버는 이를 차례대로 순차적으로 처리한다.
이것은 데이터의 무결성을 보장하며, 복잡한 락(Lock)이나 트랜잭션 메커니즘 없이도 동시에 같은 데이터에 대한 업데이트가 안전하게 이루어질 수 있음을 보장한다.



3. 세 번째 시나리오 (최종)

  1. 클라이언트로부터 좋아요를 한 아이템 ID 목록을 List로 받는다.
  2. Redis 저장소에 아래와 같은 형태로 저장한다.
    • 좋아요를 누른 사용자와 아이템의 관계를 저장하는 캐시
      key: "user:{userId}:likes_item" value: Set<coordinateLookId>
    • 각 아이템의 좋아요 개수를 저장하는 캐시
      key: "item:{itemId}:likes" value: likeCount (Integer)
    1. 이렇게 2가지 구조로 특정 아이템의 좋아요 개수를 빠르게 증가 / 감소 / 조회 할 수 있다.
    2. 좋아요 개수가 업데이트 될 때마다 DB에 동기화 → 데이터 정합성 확보

🔺 데이터 정합성 문제 고민

  • 사용자가 좋아요를 누를 때마다 좋아요 개수를 계속해서 업데이트 되고, 이를 캐싱해두면서 언제 DB에 Write 하는게 좋을지 고민이 많았다.
  • 다양한 캐시 설계 전략이 있지만, 나는 그 중에 Write Through + Read Through 조합을 선택했다.

✔️ Write Through + Read Through 조합 ⇒ 쓰기 보다, 읽기 성능을 높이는 전략

  1. 사용자가 좋아요를 누르는 순간, 데이터의 Update 발생 → 캐시 데이터 Write (항상 캐시 먼저, Hit 확률 높아짐)
  2. 그 다음 캐시에서 DB로 데이터 Write (데이터 정합성 보장)
  3. 사용자의 좋아요 목록을 조회하는 순간, 데이터 Read 발생 → 캐시 데이터 Read
    • Hit ⇒ 캐시에서 데이터 조회
    • Miss ⇒ DB 조회 (확률 낮음)

👀 선택 이유

  • 우리 서비스는 데이터를 쓰는 순간 보다, 읽는 순간이 훨씬 많다. (맞춤형 코디룩 조회, 아이템 조회.. 등)
  • DB에 있는 데이터와, 캐시에 있는 데이터의 차이점은 좋아요 개수(likeCount)이다.
  • 즉, 데이터를 업데이트 해야되는 순간은 사용자가 좋아요를 누를 때 밖에 없다. (상대적으로 조회 보다 적다는 것을 의미)
  • 따라서 좋아요를 누를 때마다 캐시에 쓰고, 동시에 디비에도 쓴다. 그 외에 모든 조회 요청이 들어올 때는, 무조건 캐시에서 먼저 읽고, 캐시에 없다면 그때 DB에서 조회한다.
  • 캐시와 DB가 항상 동기화 되어 있기 때문에 캐시 서버의 장애가 생겨도 데이터의 손실 위험성이 낮아진다.
  • 하지만, 예외 상황에 대비한 로직을 고민해봐야 할 것 같다.
profile
Backend Developer 💻 😺
post-custom-banner

0개의 댓글