해결된 질문
작성
·
129
1
선생님 안녕하세요.
MongoDB에 이어 infra 관련 강의도 잘 수강하고 있습니다.
캐싱 관련되어 선생님 의견이 궁금해 질문드립니다.
현재 MongoDB + Nest.JS를 사용해 WAS를 구축한 상태입니다.
도큐먼트 안에 서브 도큐먼트들이 array 형태로 들어가도록 Model을 설계했는데요, 서브 도큐먼트를 가져올 때 aggregation을 사용해 페이지네이션을 적용했습니다.
(처음에 설계 시, 잘 못 생각해 이러한 설계를 하긴 했습니다..)
aggregation에는 성능 문제가 따라올 위험이 있을까봐 해당 GET 요청 컨트롤러에는 캐싱을 적용했습니다.
문제가 되는 부분은 캐싱처리로 인한 코드의 가독성 저하 입니다.
WAS안에서 GET 요청 시 캐싱을 적용하고, POST, PUT, PATCH 요청시에는 캐싱을 지우고 있는데, 비즈니스 로직안에서 이를 처리하다보니 코드의 가독성이 급격히 떨어지더라고요.
이를 해결하기 위해 event emitter를 사용해 캐싱을 비즈니스 로직으로부터 떼어낼까 고민했습니다.
다만 이 해결방안은 캐싱처리하는 로직 자체가 하나의 WAS에 존재하고 있어 추후에 또 비슷한 문제가 발생하지 않을까 생각이 들더라고요.
선생님께서는 이러한 문제 발생 시 캐싱 처리는 프록시 서버를 띄워서 앞단에서 미리 처리를 하시는지... 아니면 또 다른 좋은 방식으로 설계를 하시는지 궁금해서 질문드립니다.
답변 1
1
캐싱..! 방법은 여러가지가 있고 모두 장단점이 있죠.
혹시 이 데이터들이 public data일까요? 만약 그렇다면 CDN을 서버에 연동해서 해당 endpoint에 Cache Control 해더를 적용하면 어떨까 합니다. 그러면 CDN 레벨에서 캐싱을 해줍니다. 장점은 일단 서버 캐시보다 보통 더 빠릅니다. 글로벌 서비스인 경우 CDN 캐시가 지역별로 있기 때문에 더더욱 성능이 뛰어나고요. 그리고 cache invalidate 요청할 수 있는 API를 제공하는 CDN들이 있는데요. invalidate 처리하는 부분에 그냥 이 API 호출만 해주기 때문에 코드도 상당히 간결해직거라고 봅니다.
만약 private data라면 지금 하신것처럼 서버에서 캐시를 관리해야되는데요.
Event Driven한 접근 방법(event emitter)도 나쁘지는 않아요. 특히 이건 transactional한 경우(카프카 같은 복잡한 메시지 브로커를 관리하지 않아도 되는상황)가 아니기 때문에 (이벤트가 누락 되도 크게 문제 되지 않은 상황) 노드의 EventEmitter로 해결하는 것도 좋아보입니다.
근데 굳이 event emitter로 분리할 필요가 있을까 싶어요. 전 그냥 지금 하신것처럼 GET에서 캐싱하고 뮤테이션 API에서 초기화합니다. create cache, invalidate cache만 함수화시켜서 분리시키면 충분히 깔끔하지 않을까 싶어요. Event Driven으로 한다고 해서 코드양이 줄어드는건 아닙니다. POST/PUT/DELETE할 때 invalidateCache함수를 호출하느냐 아니면 fooUpdated 이벤트를 발행하느냐 차이 밖에 없습니다. 그래서 일단 invalidate하는 쪽에서 코드가 줄지는 않습니다. subscribe하는 부분에서 동일한 invalidateCache를 호출해야하는데 여기서 오히려 subscribe하는 코드가 많지는 않지만 추가가 되죠. 그리고 캐싱 로직이 분리되면서 디버깅이 오히려 조금 더 어려워질 수도 있어요.
"처음에 설계 시, 잘 못 생각해 이러한 설계를 하긴 했습니다.." -> 모델을 어떻게 개선하면 좋을지 알고 계신듯 하네요. 데이터가 많이 축적되었다면 좀 "무서운" 작업이 될 수도 있지만 부담스러워도 모델 고치는게 근본적인 해결책(캐싱을 할필요도 없는 상황)이라고 봐요.
몽고디비 좋은 점 중 하나가 여러 스키마를 동시에 가질 수 있고 모든 문서들을 동시에 업데이트 할 필요 없다는 점이죠. 즉, 디비 서버에 무리가 가지 않는 선에서 migration 로직을 점진적으로 돌릴 수 있습니다. 예를 들어 subdocument를 별도 컬렉션으로 분리를 하고 싶다면 일단 mutation 쪽 API들을 모두 업데이트 해주세요. 기존 컬렉션의 subsocument에도 적용을 해주고 새로운 collection에도 이중으로 해주는거죠. 이 시점의 timestamp를 기억해두고요. 그 다음에 점진적(ex. 한번에 천개/만개 등)으로 subdocument들을 새로운 collection에 옮겨줍니다. bulkwrite, upsert를 적절하게 활용하면 좋습니다.
이 작업이 모두 성공적으로 끝났다면 이중으로 적용했던 뮤테이션 API들은 새로운 collection에만 적용하도록 해주고요. GET도 새로운 컬렉션에서만 요청할 수 있도록 해줍니다. 이제 마지막으로 한번 더 문제 없는지 확인하고 기존 컬렉션의 subdocument를 점진적으로 날려줍니다. (예를 들어 한번에 1000개 문서의 subdocument를 unset)
한번에 몇개의 문서를 업데이트할지는 migration 로직 만든걸 몇번 테스트해보면 바로 알 수 있을겁니다. 이건 모델 특성 그리고 디비 성능에 따라 다르니깐요. 괜히 무리하게 빨리 하려고 한번 수만, 수십만개를 업데이트하지는 않으시는게 좋습니다. 프러덕션에 무리가 가면 안되니깐요.
무서울 수 있지만 이걸 잘하고 못하고가 매우 중요한 역량이라고 봅니다! 아무리 좋은 코드를 짜도 리팩토링이 필요하듯이 모델도 마찬가지거든요. (빈도는 상대적으로 적으면 좋겠지만요 ㅎㅎ)
와 양질의 조언 감사합니다...!! 하나하나 곱씹어 보겠습니다 ㅎㅎ 정말 감사합니다