잔소리 프로젝트 회고 - Redis와 첫 만남

신현철·2023년 9월 6일
0
post-thumbnail

프로젝트를 진행하면서 Redis를 처음 도입한 경험과 아쉬움을 STAR 방식으로 기록하려합니다.

📌 상황

JWT 토큰 방식으로 로그인을 구현하고, SNS 성격의 Feed가 메인 기능인 서비스를 개발했습니다. 또한 실시간은 아니지만 갱신 주기가 잦은 랭킹 시스템과 만료 기능이 필요한 Message Queue 기능이 필요했습니다.

Feed의 게시글은 개별 사용자들의 Todo로 구성되며, 사용자들은 해시태그를 걸고 Todo 또는 잔소리를 작성할 수 있는 서비스입니다. 해시태그가 같고, 각 작성자가 다르다면 Todo에 잔소리가 달리게됩니다.


📌 문제

문제1

JWT 토큰을 사용하는 목적에 매번 인증 과정마다 DB를 조회하는 것은 부합하지 않는다고 판단했습니다.

전통적인 세션 방식의 인증 과정에서는 DB를 매번 조회하여 session id 존재 여부를 확인함으로써 인증 여부를 판별하는 과정이 스토리지에 부하를 주기 때문에 JWT 토큰 방식에서는 이 과정을 대체하게 됩니다.

본 서비스에서는 로그아웃 기능을 위한 access token blacklist와 refresh token 관리를 위해 초기에는 이 두 가지를 DB에 저장하고 조회했습니다.

문제2

랭킹 변화를 갱신 주기마다 매번 조회하기 위해서 관련 엔티티를 full scan 하고 있었습니다. 주기마다 scheduler를 사용하여 fulll scan을 하기 때문에 사용자의 랭킹 조회가 없어도 무조건 full scan 쿼리가 나가는 현상이 발생했습니다. 즉 사용자의 요청에 있을 때에만 동적으로 랭킹 새로고침이 불가능했습니다.

문제3

피드에 노출되는 게시글들은 모두 1~3개의 해시태그를 갖고 있으며, 각 해시태그들은 게시글 또는 ‘잔소리’라는 덧글 성격의 엔티티에서 사용됩니다. 사용자가 게시글을 생성할 경우 해시태그에 걸맞는 적당한 타유저의 잔소리가 달리게됩니다. 그러나 잔소리가 데이터가 없는 상황에서 만들어진 게시글들은 잔소리를 얻지 못하고 넘어가게됩니다. 이러한 상황을 방지하기 위해 초기에는 Map으로 잔소리가 없는 Todo를 관리했습니다. 그러나 서버 다중화 논의가 진행되면서, 더 이상 로컬 서버의 메모리로는 관리할 수 없게되었습니다. 따라서 메세지 큐라는 개념이 필요하게 되었습니다.


📌 행동

위의 문제점 세 가지를 통해 글로벌 캐시를 도입하기로 결정했습니다.

Redis와 Memcached 중 Memcached도 key-value 형태의 글로벌 캐시이지만 string 이외의 다른 데이터 타입은 지원하지 않아서 메세지 큐로 활용하기에는 부적합했습니다. 그래서 Redis를 채택하였습니다.

이에 따라 access token blacklist와 refresh token은 Redis의 Key-Value String으로 관리하게 되었습니다.

또한 랭킹은 Read Through 방식을 통해 캐싱을 하도록 구현했습니다. 이를 통해 무조건 cache에서 랭킹 정보를 조회하게되며, 만약 갱신 주기와 동일한 expiration이 만료되어 캐싱된 데이터가 없다면, 그 순간에만 DB에서 랭킹을 새로 집계하고, 이를 바로 사용자에게 응답하는 것이 아닌 캐싱을 하게 됩니다. 따라서 사용자는 무조건 캐시 데이터만을 조회합니다.


📌 결과

개선된 점1

랭킹 조회의 경우 스케줄링으로 랭킹 새로고침이 아닌, Read Through 방식의 새로고침을 하기 때문에 새벽 시간과 같이 유저의 랭킹 조회가 드문 시간대에는 랭킹 집계를 하지 않게 되어 서버의 리소스 사용 효율을 높일 수 있었습니다.

개선된 점2

Todo의 기획 의도가 “매일 할 일”이기 때문에 잔소리가 달리지 않은 Todo이더라도 무한정 잔소리를 기다릴 필요가 없었습니다. 따라서 메세지 큐에 들어가더라도 자연스레 24시간이 지난다면 expire 되는 로직을 Redis 도입을 통해 추가할 수 있었습니다.

아쉬운 점1

로컬 캐시와 Redis를 함께 사용하고 Redis의 pub/sub 구조를 가져가지 못한 점이 정말 아쉬운 것 같습니다. 팀원 모두가 Redis 사용 경험이 없었고, 서버 다중화 또한 결국엔 기한 문제로 실현하지 못하여 pub/sub 구조에 대한 필요성을 느끼지 못했던 점이 크게 작용한 것 같습니다. Redis를 도입하기 직전까지도 계속 팀원들 간 논의로 오버엔지니어링을 피하자라는 의견 때문에 도입할 생각이 없었기에 제대로 된 기술조사가 이뤄지지 못하였습니다. 되돌아보니 로컬 캐시로 해결할 수 있었던 문제가 아니였을까?라는 생각도 들었습니다.

아쉬운 점2

사실 캐싱이 가장 필요했던 부분은 바로 피드 캐싱과 좋아요 캐싱이었습니다. 트위터 같은 대형 SNS 서비스에서 가져가는 뉴스 피드 모델처럼 Push 모델, Pull 모델 혹은 Push & Pull 모델을 구현해보고 싶은 마음이 컸습니다. 그러나 이 부분 또한 기한 문제로 구현하지 못한 것이 가장 아쉽습니다.
트위터 같은 서비스들은 보통 친구 관계가 있고 이를 바탕으로 피드가 구성되기에, 개개인마다 피드가 캐싱되고 친구의 게시글이 생성될 때마다 팔로워들의 캐시 데이터에 게시글을 push 해주는 형태로 가게됩니다.
본 서비스에서는 피드가 해쉬태그 별로 구성되기 때문에, 해쉬태그마다 push 이벤트를 주고 피드 조회 로직에서 이러한 캐시 데이터를 사용하게 설계를 변경하고 싶었습니다.

또한 게시글의 좋아요 갯수 또한 처음부터 캐싱을 염두에 두고 점차 바꿔나가고 있었지만 결정적으로 캐싱은 하지 못했습니다. 매번 좋아요 갯수를 count 쿼리로 세는 로직에서 like count를 컬럼으로 두고 좋아요 토글이 발생할 때마다 like count를 변경해주는 방식으로 변경했습니다. 그러나 이 방식은 like count 컬럼 요청이 몰릴 경우, lock 발생하여 응답 지연으로 이어지게 되었습니다.

이를 해결하기 위해 좋아요라는 액션을 redis의 list 기능으로 게시글-멤버아이디 형태로 캐싱하고 스케줄링을 통하여 캐싱된 좋아요 갯수를 like count에 반영시켜주는 eventual consistency 방식을 도입하려 했지만 역시 기한 문제로 구현하지 못한 점이 상당히 아쉽습니다.

아쉬운 점3

위에 언급한 여러 로직들은 Redis에 상당히 의존적입니다. 따라서 Redis에 장애가 발생할 경우에는 무결성 문제가 발생하고, 서버 전체에 장애가 발생합니다.

이를 해결하기 위해서는 Redis의 Replica를 두어서 master-slave 노드 구조로 가져갔어야 했지만, 이 또한 기획은 제외한 5주가 채 안되는 기간의 벽에 부딪혀 구현하지 못했습니다.

profile
DB는 두부

0개의 댓글

관련 채용 정보