Skip to content

데이터 관찰, 쿼리 발생 최적화 과정

Summer Min edited this page Dec 5, 2024 · 2 revisions

1. 문제 상황

현재 워크스페이스의 실시간 Node데이터를 보관하는 Y.Doc은

(1) Y.Doc의 데이터에 변경 사항이 하나라도 생길 경우

(2) 서버의 nodesMap.obeserve() 에 감지가 되어,

(3) 서버가 nodesMap에 존재하는 노드를 모두 순회하며 변경사항이 있는 노드를 찾은 뒤,

(4) 해당 변경사항을 Redis에 업데이트 해주며

사용자(들)의 문서 변경 사항을 DB로 업데이트 하고 있다.

하지만 해당 로직을 상용화해본 결과, 사용자가 캔버스에서 노드를 드래그하며 위치를 옮길때마다 서버가 해당 노드의 실시간 위치를 모두 감지, 너무 많은 데이터베이스 쿼리가 발생하여 서버가 다운되는 일이 생겼다.

어떻게 이 문제를 해결할까?

2. 위치 변경 업데이트 쿼리 디바운싱

사용자가 노드를 드래그앤드롭으로 옮길 때 사용자의 행동을 관찰해보자. 사용자는 노드를 마우스 클릭으로 hold한뒤, 해당 노드가 옮겨질 위치를 선택한 후, 최종 위치에 마우스 클릭의 hold를 놓아 위치를 변경한다.

즉, 우리는 사용자가 노드를 hold하고 있지 않을 때의 위치 변경사항만 데이터베이스에 반영하면 되는 것이다!

FE: isHolding property 추가

const holdingNodeRef = useRef<string | null>(null);

useEffect(() => {
    
...
    
    const yNodes = Array.from(nodesMap.values()) as YNode[];

    const initialNodes = yNodes.map((yNode) => {
      const nodeEntries = Object.entries(yNode).filter(
        ([key]) => key !== "isHolding",
      );
      return Object.fromEntries(nodeEntries) as Node;
    });

...

  const onNodeDragStart = useCallback(
    (_event: React.MouseEvent, node: Node) => {
      holdingNodeRef.current = node.id;
    },
    [],
  );

  const onNodeDragStop = useCallback(
    (_event: React.MouseEvent, node: Node) => {
      if (ydoc) {
        const nodesMap = ydoc.getMap("nodes");
        const yNode = nodesMap.get(node.id) as YNode | undefined;
        if (yNode) {
          nodesMap.set(node.id, { ...yNode, isHolding: false });
        }
      }
    },
    [ydoc],
  );
  • 이를 위해 클라이언트 쪽, 프런트엔드에서는 Y.Map에 넣을 노드 데이터에 isHolding 프로퍼티를 추가하여주었다.
  • 사용자가 Drag를 시작할 때 False로 초기화되었던 노드의 isHolding 프로퍼티는 True가 되며, Drag를 멈췄을 때 해당 노드의 isHolding은 다시 False가 된다.

BE: 위치 변경 알림 ⇒ isHolding이 False일 때만 DB에 반영

  async isHoldingStatusChanged(
    nodeId: number,
    isHolding: boolean,
  ): Promise<boolean> {
    const savedCacheValue = await this.get(nodeId);
    return !!savedCacheValue && savedCacheValue.isHolding !== isHolding;
  }
for await (const node of nodes) {
          const { title, id } = node.data; // TODO: 이모지 추가
          const { x, y } = node.position;
          // 만약 캐쉬에 노드가 존재하지 않다면 갱신 후 캐쉬에 노드를 넣는다.
          if (!this.nodeCacheService.has(id)) {
            this.nodeService.updateNode(id, { title, x, y });
            this.nodeCacheService.set(id, title);
            return;
            
          const isHolding = node.isHolding;
          const updateCondition =
            !(await this.nodeCacheService.has(id)) ||
            !(await this.nodeCacheService.hasSameTitle(id, title)) ||
            !(await this.nodeCacheService.isHoldingStatusChanged(
              id,
              isHolding,
            ));

          if (updateCondition) {
            await this.nodeService.updateNode(id, { title, x, y });
            await this.nodeCacheService.set(id, { title, isHolding });
          }
 ...
  • 이제 서버에서는 위치 변경 데이터를 변경할 조건에 노드의 isHolding이 false일 때도 추가하여 사용자가 위치 변경을 종료하였을 때만 해당 데이터를 DB에 변경한다.
  • 이제 사용자가 노드를 조금만 드래그 하더라도 수십개의 쿼리가 발생하는 상황을 관찰하지 않아도 된다!

3. 변경사항만 DB에 반영

하지만 여전히 우리는 node가 하나만 변경되더라도, 모든 노드의 정보를

      // node의 변경 사항을 감지한다.
      nodesMap.observe(async () => {
        const nodes = Object.values(doc.getMap('nodes').toJSON());

        // 모든 노드에 대해 검사한다.
        for await (const node of nodes) {
          const { title, id } = node.data; // TODO: 이모지 추가
          const { x, y } = node.position;
          const isHolding = node.isHolding;
          const updateCondition =
            !(await this.nodeCacheService.has(id)) ||
            !(await this.nodeCacheService.hasSameTitle(id, title)) ||
            !(await this.nodeCacheService.isHoldingStatusChanged(
              id,
              isHolding,
            ));

          if (updateCondition) {
            await this.nodeService.updateNode(id, { title, x, y });
            await this.nodeCacheService.set(id, { title, isHolding });
          }
        }
      });

이런 식으로 순회하며 조회해, 무엇이 변경되었는지를 찾아야한다. 즉, 데이터가 변경되지 않은 노드들까지 검사해야하는 것이다.

하지만 YMap이 제공해주는 observe 메소드에는 내부에서 변경된 key 값만 감지한 후 해당 value만 가져오는 기능 이 있었다!

      // node의 변경 사항을 감지한다.
      nodesMap.observe(async (event) => {
        for (const [key, change] of event.changes.keys) {
          if (change.action === 'update') {
            const node: any = nodesMap.get(key);
            const { title, id } = node.data; // TODO: 이모지 추가
            const { x, y } = node.position;
            const isHolding = node.isHolding;
            if (!isHolding) {
              await this.nodeService.updateNode(id, { title, x, y });
            }
          }
        }
      });

이런 식으로 변경 이벤트를 감지하면 해당 변경 이벤트가 발생한 노드까지 특정할 수 있어 바로 해당 노드의 정보를 변경할 수 있다.

이렇게 쿼리를 최적화하였고, 변경된 데이터를 관찰하는 과정을 단순화시켰다!

개발 문서

⚓️ 사용자 피드백과 버그 기록
👷🏻 기술적 도전
📖 위키와 학습정리
🚧 트러블슈팅

팀 문화

🧸 팀원 소개
⛺️ 그라운드 룰
🍞 커밋 컨벤션
🧈 이슈, PR 컨벤션
🥞 브랜치 전략

그룹 기록

📢 발표 자료
🌤️ 데일리 스크럼
📑 회의록
🏖️ 그룹 회고
🚸 멘토링 일지
Clone this wiki locally