모든 글

공통 집계와 사용자별 캐시 상태를 분리하라

리액션 상태를 재사용 가능한 조회로 조합하고, 한 회원의 개인 상태가 모두에게 노출되지 않도록 공통 합계만 분리해 캐시한다.

출처 및 AI 안내: 이 글은 제미니의 개발실무 유튜브를 기반으로 작성되었습니다. gpt-5.6-sol 모델을 사용해 생성·편집했습니다.

전체 리액션 수와 현재 사용자의 반응 여부는 하나의 상태 응답처럼 보인다. 그러나 둘을 한 덩어리로 캐시하면 위험하다. 합계는 모두가 공유하지만 reactedByMe는 한 사용자에게 속한다. 이 결합 응답을 호출자 사이에서 재사용하면 이후 모든 호출자에게 캐시를 채운 회원의 상태가 보일 수 있다.

이 문제는 캐시 키 하나를 잘못 정한 데서 끝나지 않는다. 재사용 범위가 다른 두 정보가 한 응답에 들어 있음을 드러낸다. 캐시를 붙이기 전에 코드가 그 차이를 보여 줘야 한다.

유스케이스가 상태를 조합하게 한다

카운트만 제공하던 서비스는 프레젠테이션 계층이 숫자를 조회해 API 응답에 넣는 형태로 시작할 수 있다. 현재 사용자의 상태까지 응답에 들어가면 이 작업은 리액션 상태 조회 유스케이스에 가까워진다. 비즈니스 계층이 두 읽기를 조율하는 것이 자연스럽다.

  1. 대상의 활성 리액션 수를 센다.
  2. 현재 사용자가 대상에 활성 리액션을 남겼는지 확인한다.

그러면 프레젠테이션 계층은 비즈니스 의미를 직접 만들지 않고 완성된 상태를 받는다. 사용자 정보를 유스케이스에 전달하면 자신의 콘텐츠에 남긴 리액션을 제외하는 정책도 나중에 다룰 수 있다. 자기 리액션을 허용할지는 제품이 결정할 일이다. 이 구조는 그런 규칙을 적용할 사용자 맥락을 알맞은 위치에 남겨 둘 뿐이다.

아래의 조회는 각각 쓸모 있게 유지한다

리더나 저장소까지 하나의 status 메서드를 밀어 넣고 합계와 개인 상태를 한 번에 반환하고 싶을 수 있다. 현재 규모에서는 어느 쪽도 동작하지만, 결합한 메서드는 재사용 범위를 줄인다.

어떤 호출자는 합계만 필요하고, 다른 호출자는 개인 상태나 마지막 리액션 시각만 필요할 수 있다. 앞으로 한 계산의 정책만 바뀔 수도 있다. 구현 경계에서는 카운트와 개인 상태 연산을 분리해 두면 유스케이스가 필요한 값만 조합하고, 관련 없는 호출자가 두 비용을 모두 치르지 않게 할 수 있다.

무조건 잘게 쪼개자는 뜻은 아니다. 경계는 팀이 정할 문제다. 이 단계의 코드에서는 합계나 개인 상태만 따로 필요할 수 있고, 마지막 리액션 시각 같은 값이 더해질 수도 있으므로 조회를 분리하는 편이 낫다. 이 리팩터링은 모든 쿼리를 별도 클래스로 만들지 않으면서도 그런 선택지를 남긴다.

공통인 부분만 캐시한다

리액션 합계는 많은 사용자가 같은 대상에 같은 질문을 하므로 캐시 후보가 될 수 있다. 개인 상태의 답은 사용자마다 다르고, 그 사용자가 반응하거나 취소할 때 바뀐다.

전체 API 응답이나 전체 상태를 사용자 사이에서 재사용하도록 캐시하면 이 범위를 섞게 된다. 한 회원이 반응한 상태에서 채운 캐시 때문에 만료될 때까지 모든 방문자의 버튼이 활성화돼 보일 수 있다. 숫자는 맞아도 개인화 불리언은 분명히 틀린다.

하나의 응답으로 반환한다는 이유만으로 한 단위로 캐시할 필요는 없다. 이 사례에서는 공통 합계만 캐시 구현을 사용하고 개인화 조회는 그 캐시 밖에 둘 수 있다.

캐시 동작이 보이는 구현을 둔다

캐시 위치는 호출자가 원본과 캐시 중 무엇을 받는지도 설명해야 한다. 캐시 저장소 구현을 둘 수도 있고, 원래 카운트 리더를 감싸는 캐시 리더를 둘 수도 있다. 클래스 이름 자체보다 동작을 알아볼 수 있게 하고 캐시 정책을 관련 없는 조율 코드와 섞지 않는 것이 중요하다.

어떤 경로는 캐시된 합계가 필요하고 다른 경로는 원본 값이 필요할 때 명시적인 구현이 유용하다. 캐시 조회와 원본 조회가 한 구현에 섞이면 호출자가 무엇을 받는지 구분하기 어려워진다. 별도 캐시 구현은 연결하는 시점에 그 선택을 보이게 한다.

경계는 서비스마다 달라진다. DB 조회 자체가 캐시 대상이면 캐시 저장소가 맞을 수 있고, 그 선택을 별도 구현으로 드러내고 싶다면 캐시 리더가 맞을 수 있다. 이 리액션 상태 흐름에서는 어느 쪽이든 공통 합계와 그 옆에 조합되는 사용자별 답을 분리한다.

지금처럼 합계 조회만 캐시 경계로 내리면 원본 값이 필요한 경로와 캐시 값을 쓰는 경로도 코드에서 구분할 수 있다. 나중에 캐시 전략이 달라져도 사용자별 상태 조회까지 함께 옮길 이유가 없다. 한 응답으로 조합되는 값이라도 저장과 재사용의 수명주기는 서로 다를 수 있다는 점을 구조에 남기는 것이다.