인기게시글 도입을 위한 과정 - 추가적인 고민

2025. 4. 25. 22:08개발

 

이전 글. 

https://loftspace.tistory.com/46

 

인기 게시글 도입을 위한 과정 - 자료구조의 선택 그리고 추가적인 고민

좋아요 수, 유저-좋아요, 인기 게시글 목록을 저장할 자료구조레디스의 공식문서를 보았을 때 다음과 같은 방식을 고안했다. 방법 1. 좋아요 수 카운팅을 HashSet으로 관리, 랭킹 목록은 List로 저장

loftspace.tistory.com

 

Redis에 캐싱되어 있는 인기 게시글 기본 정보(제목,인원 수 등) 변경에 대해 write through 방식을 채택하였다.

 

서비스 상황 : 인기 게시글 목록에서는 각 게시글의 기본 정보만 표시가 되어있고 이 정보를 Redis에 적재한 상태다.

 

이번에는 다음 문제 상황에 대해 고민해 보았다.

 

캐시에 write후 DB에 write해야하는데 커넥션 타임아웃으로 인해 데이터가 유실된다면?

즉 레디스에는 요청이가고 이제 MySQL에 요청을 보내야 하는데 이거를 못한다면, 레디스에는 최신데이터가 있고 디비에는 최신 데이터가 없을 것이다. 

주기적으로 디비에서 새로운 랭킹목록을 위해 레디스를 업데이트 하기 때문에 이 최신데이터는 결국 디비의 이전 데이터에 의해 덮어 씌워지고 결국 사라질 것이다.

 

이 문제를 해결하기 위해서, 실패시 어떻게 처리할 것인가?에 초점을 맞췄다.

 

1. 실패시, 롤백

디비에 실패시, 롤백. 그러니까 레디스에 있는 데이터도 요청 이전으로 복구하고 사용자에게 실패 응답을 보내는 방식이다.

위의 문제에서, 이미 캐시에 업데이트는 된상태고 디비가 업데이트가 안됐다. 사용자 입장에서는 레디스 서버가 다운되지 않는 한, read요청시 최신데이터를 볼 것이다. 
=> 즉 사용자 입장에서는 경험 저하 없이 실패 여부를 모르고 깔끔하게 서비스를 이용할 수 있다.

 

굳이 사용자 경험을 저하시키는 방식을 선택할 이유가 없다고 판단했다.

무엇보다, 사용자는 실패 응답을 받으면 다시 요청을 보낼 것이다. 이럴 거면 차라리 사용자 경험에 문제가 없으면서 내부적으로 DB요청만 재시도 하는 로직을 짜면 되지 않을까?

 

2. 별도의 큐에 저장 후 일정 시간 단위로 재시도

이 방식은 DB 요청 실패시, 별도의 큐에 담아두고 일정 시간 단위로 DB에 배치처리하는 방식이다.

그런데 만약 레디스 서버가 1대라면 레디스 다운시, 복구를 해야하는데 그 지연 시간동안 DB에서 이전 데이터를 보게 될 것이다. 배치처리 주기 동안은 DB에 업데이트가 되지 않은 상태기 때문이다.

 

3. DB요청만 재시도 로직

위의 이유로 실패시, DB요청만 재시도하는 방식을 선택했다. 이 api에서는 기존에 Redisson 클라이언트를 사용했었다.

Redisson에서 제공하는 MapWriter 인터페이스에서는 MapOption에 재시도 속성을 둘 수 있다.

 

그러나 재시도가 성공한다는 보장이 있던가?

따라서 위의 이유로 DB에 먼저 write 후에 redis에 write을 하는 방안에 대해 생각해 보았다.

 

DB에 먼저 write시 발생할 수 있는 문제점

1. DB에 commit, 레디스에 write 실패시

redis에 먼저 write을 하는 방법에서는 redis에 실패시, db에 요청을 보내지 않고 실패 처리를 할 수 있지만 db에 먼저 write를 한다면 redis에서 write를 실패 시점이 이미 커밋이 된 상태기 때문에 정합성 문제가 있다.

즉 일정 기간동안의 데이터 불일치는 여전히 존재하지만, 주기적인 redis update로 해결이 가능하다. 또한 redis에 요청을 재전송 하는 방법이 있다. 결국 즉지 정합성을 보장해줘야 하느냐 혹은 일정 주기(최대 3분)동안의 동기화 문제를 감당할 것이냐 인데 감당하다는 것이 옳다고 판단했다. (그 이유는 아래 있다)

2. DB에 commit, 레디스에 request delay 시

다음 상황을 고려해보자 (게시글 세부 정보는 DB에서 읽으므로 수정 또한 DB read에서부터 출발한다)

 

 

  • Thread A : 게시글 내용 'a' -> 'b' 로 업데이트
  • Thread A : DB Commit, Redis에 요청 (딜레이)
  • Thread B : 게시글 내용 'b' -> 'c' 로 업데이트 
  • Threa B : DB Commit, Redis에 요청
  • Thread B : Redis에 'b' -> 'c' 업데이트
  • Thread A : Redis에 'c' -> 'a' 업데이트

DB에는 최신데이터가 잘 저장이 되었으나, 커밋 후 레디스로의 요청이 지연되어서 레디스에는 이전 데이터가 저장되어있는 상황이다.

 

해결책 : 

  1. Redis에 update가 아닌 delete로 요청
    1. 그럼 레디스에 write마다 해당 데이터가 삭제될 것이고 다음 read연산은 cache miss로 db에서 읽어 올 것이기 때문에 정합성 문제는 해결된다.
    2. 하지만 위의 방식은 특정 key를 찾을 경우에 해당된다. 인기 게시글 목록 불러오기 api는 Redis에 read 쿼리를 날리는데 이때 특정 key를 찾는 방식이 아니라 자료구조에 저장이 되어있는 모든 데이터를 반환하므로 적용하기가 어렵다.
  2. 요청에 time stamp를 부여
    1. 요청의 time stamp를 붙여서 redis에 자신보다 이전의 time stamp인 경우에만 update 하는 것이다.
    2. 이 방식의 문제점은 서버간 clock drift 문제가 있겠으나... 그거를 고려해야 할정도로 하나의 게시글에 대해 동시적으로 변경이 이루어 질수가 있나...? 없다고 생각한다
    3. 또다른 문제점은 데이터 형식이 key : value -> key : {timestamp, payload} 포맷을 유지해야 하므로 기존 코드들을 싹다 바꿔야 한다. 저장할때, 포맷 변경, 읽을 때도 payload부분만 읽기 등...
  3. 그냥 어짜피 3분 단위로 db에서 인기 게시글 목록을 레디스로 업데이트 하니까 냅두기
    1. 현재 데이터베이스에서 변경된 인기게시글 목록을 3분 단위로 레디스로 업데이트한다.
    2. '에브리 타임-실시간 핫 게시글'과 '네이버 실시간 인기 검색어'기능을 참조했을 때, 보통 3분주기던데 사용자들한테 그렇게 큰 영향을 줄 정도로 빠른 업데이트 반영이 중요하지는 않은 것 같다. (이전 포스팅에 자세히 있다)
      1. 예를들어 목록에서의 데이터와 게시글 전체 내용페이지에서의 데이터가 다르다.

 

결국에는 순서 변경 + 그냥 냅두기

캐시 -> 디비 업데이트에서, 디비 업데이트 -> 캐시 업데이트로 순서를 변경했다.

디비에 업데이트 -> 캐시에 업데이트시, 발생할 수 있는 위의 문제점은 발생 가능성이 낮다.

안그래도 한번 작성된 게시글은 수정 가능성이 낮은데 수정을 연속으로 두번하는 상황, 더 나아가 연속으로 두번 수정의 간격이 네트워크 딜레이로 순서가 뒤바뀔 정도로 짧은 상황은 훨씬 더 낮을 것이다.

그런 상황을 위해서 time stamp를 붙여 레디스에 데이터 저장 형식을 수정, 기존 read, write시 코드들을 모두 수정하는 것은 과하다고 판단을 했다.

 

만약 낮은 확률이더라고 위의 상황을 해결 할 방법이 아예 없다면 해결을 해야겠지만 기본적으로 우리 서비스는 모든 좋아요수 write back 이후에, db에서부터 새로 갱신된 인기 목록을 레디스에 업데이트 하기 때문에 stale 데이터에 관한 문제에 대한 최후 대비책은 마련된 셈이다.

 

또한 프론트 단에서 5초 이내로 수정을 연속적으로 못하게 하는 방법도 있겠다.

 

결국에 요약하자면 낮은 확률로 발생하는 문제가 사용자 경험에 큰 영향을 미치지 않을 뿐더러 완벽한 정합성을 위해서 위에 언급한 데이터 포맷 변경, 코드 수정등의 작업을 추가로 진행할 필요가 없다고 판단했다. 

 

다음글

https://loftspace.tistory.com/51

 

Redis의 응답이 느릴때

현재 서비스에서, 레디스에는 많은 종류의 데이터가 저장되어있다.각 게시글에 대한 락정보인기 게시글 정보유저-좋아요 정보게시글의 좋아요 카운팅하나의 레디스 서버에서 4개의 역할을 수

loftspace.tistory.com