Backend 성능 향상을 위한 작고 소중한 시도들

chloe·2022년 12월 15일
0
post-thumbnail
post-custom-banner

개요

Backend 개발을 진행하면서 고민한 성능 개선 방안들을 실제 실험을 통해 유의차를 확인했습니다.

Postman이나 Client 요청으로 일회성 측정시 응답 시간의 variation이 있어, 반복 측정이 필요했습니다.

성능 Test Tool인 Artillery를 사용하여 반복적인 요청을 보내고, Artillery의 report를 확인하여 결과 확인 및 정리를 했습니다.

개별 응답시간 측정을 위해, 서버가 같은 시간에 하나의 요청을 처리할 수 있도록, 가상 사용자는 1명으로 설정하여 진행했습니다.

성능 개선 방안은 다음과 같습니다.

  1. Data Caching
  2. 비동기 병렬 실행을 통한 속도 개선
  3. Array와 Set의 검색 속도 차이
  4. DB Indexing

Data Caching

실험 조건

  • 1초당 사용자 : 1명
  • 테스트 시간 : 40초
  • 확인한 http response time (ms) : max / medium / p95

실험

  1. Cache Target Data 선정

    우선 Client가 가장 자주 요청을 보내는 API Request를 조사했습니다.

    • POST 채팅 저장
    • GET 나의 커뮤니티 정보
    • GET 특정 채널 정보

    채팅 저장 로직은 READ 수행이 적고 WRITE는 비동기로도 수행할 수 있기 때문에 제외했습니다.

    나의 커뮤니티 정보는 커뮤니티 뿐만 아니라 커뮤니티에 속한 채널과 사용자의 정보를 요구하고 있습니다.

    특정 채널 정보는 채널의 기본 정보 및 참여한 사용자의 정보를 요구하고 있습니다.

    (DB Collections : 사용자, 채널, 커뮤니티, 채팅)

    전달하는 커뮤니티, 채널 정보의 경우 채널이 생성되거나 새로운 사용자의 참여의 로직이 실행되면 UPDATE가 빈번하게 발생했습니다.

    그에 비해 사용자 기본 정보는 UPDATE가 적고 앞서 말한 상위 두 개의 GET 요청에서 필요하고있어 READ가 빈번하게 발생했습니다.

    사용자 Collection 전체로 본다면 UPDATE가 자주 일어나지만, 변경이 적은 사용자 기본 정보만 조회하는 경우에 Cache Data를 사용하기 적합했습니다.

  2. Caching을 위한 Code 작성

    Look Aside 전략으로 Code를 작성했습니다.

    사용자의 기본 정보를 필요로 하는 API는 4개가 있었으며, 이 요청들에 대하여 Cache에 먼저 접근하도록 구현했습니다.

    const cache = await this.redis.get(`user/${_id}`);
        if (cache) {
          return JSON.parse(cache);
        }
        const result = await this.userModel.findById(_id);
        await this.redis.set(`user/${_id}`, JSON.stringify(result));
        return result;
    }
  3. Test Sequence

    Artillery Test Script를 위의 실험 조건을 기반으로 작성했습니다.

    config:
      target : 'http://localhost:3000'
      phases:
        - duration: 40
          arrivalRate: 1
          name: Warm up
    before:
      flow:
        - post:
            url : '/api/user/auth/signin'
            json:
              id: '*@gmail.com'
              password: '*'
            capture:
              json: '$.result.accessToken'
              as: accessToken
            expect:
              - statusCode: 201
    scenarios:
      - flow:
          - get:
              url: '/api/channels/639846568ad1c10ed60d9653'
              headers:
                authorization: 'Bearer {{accessToken}}'
          - get:
              url: '/api/communities/63997033a59cf95317119f62/users'
              headers:
                authorization: 'Bearer {{accessToken}}'
          - get:
              url: '/api/communities'
              headers:
                authorization: 'Bearer {{accessToken}}'

결과

max / medium / p95 (ms)

요청 APIcache Xcache O성능 비교
나의 정보24/13.1/21.117/4/7227%
특정 채널의 정보44/23.8/4028/13.9/21.171%
커뮤니티 내 사용자 정보38.4/14.7/22.431/12.1/25.818%
나의 커뮤니티 정보190/92.8/122.7204/90.9/117.92%

API 별로 비지니스 로직이 상이하기 때문에 전체 코드에서 사용자 정보 조회가 차지하는 비율은 다르지만, 성능 개선이 된 것을 확인할 수 있습니다.

비동기 병렬 실행을 사용한 비지니스 로직 수행

실험 조건

  • 1초당 사용자 : 1명
  • 테스트 시간 : 20초
  • 확인한 http response time (ms) : max / medium / p95

실험

  1. 사용자는 여러 커뮤니티를 가지고, 커뮤니티는 여러 채널을 가지는 형식의 N:M 구조가 많아, 배열을 순회하며 DB 순회가 필요했습니다.
  2. 처음 기술스택 선택 시 NodeJS와 MongoDB의 Async + Non-blocking의 장점을 활용하기로했습니다.
  3. 배열 순회 시, Promise.all을 사용하여 배열의 각 요소가 병렬적으로 실행할 수 있도록 Code를 작성하였습니다.
  4. 순차 실행과 유의차를 확인 하기 위해 동일 코드에서 해당 부분만 수정하여 시간 측정을 진행 했습니다.

결과

max / medium / p95 (ms)

요청 APIFor ofPromise.all성능 비교
나의 커뮤니티 정보77/55.2/68.747/30.9/4479%

2중 중첩문이 있는 나의 커뮤니티 정보를 요청하는 API 수행 시 응답 속도가 약 79% 좋아졌다.

사용자가 속한 커뮤니티 및 채널이 많다면 병렬 실행으로 인한 속도 개선은 더 일어날 것이다.

Array와 Set의 검색 속도 차이

배열에 특정 값이 있는지를 확인할 때 Array.includes를 사용했다.
코드 상으론 가벼운 한 줄이지만, 개념 적으로 접근한다면 시간 복잡도는 O(n)이다.
여기서 Array를 Set으로 변경하게 되면 Set는 해시 테이블 자료 구조처럼 키가 O(1)의 복잡도를 가질 것이라 생각했다.
팀원이 속도 차이를 코드로 직접 작성했는데, 내가 작성한 코드는 아니기 때문에 블로그에 자세히 적진 않겠다.

DB Indexing

MongoDB Indexing 전략

mongoDB도 Index를 사용하여 검색을 한다.

효율적인 Index 활용을 위하여 설정 전략이 필요하다.

Indexing 전략을 학습으로 얻은 요약 내용은 다음과 같다.

정확하게 검색하기 위한 좁은 범위의 Index를 만들자.
복합 인덱스는 ERS 규칙을 사용해 인덱스 키를 정렬하면 효율적인 복합 인덱스를 만들 수 있다.
최고의 인덱스 전략은 반복 테스트다.
인덱스 설정 시에도 안타는 것 같다면 hint로 강제로 태워 유의차를 확인 해보자.

이까지 학습 후, 이제 우리가 설계한 Database 구조에서 Indexing을 할 수 있는 부분이 어디가 있을지 확인 해 보았다.

이미 대부분 접근은 기본키를 참조해 가져오고있다.

우리는 이미 설계할 때 부터 대부분 기본키를 참조하도록 했다. 유일하게 기본키로 검색을 하지 않는 경우가 사용자 검색이었다.

기본키는 이미 default로 Indexing을 하고 있기 때문에, 성능 개선에 크게 도움 될 Field가 없었다.

내가 순간 뭔가 잘못된 포인트를 잡았나 고민이 되어 멘토님에게 질문드렸을 때, 이미 잘 설계를 했으니 추가로 indexing을 할 필요가 없는 것 아니겠냐고 해주셨다.

이번 프로젝트는 사용자가 특정 Field를 검색하는 부분이 없었기 때문에 그럴 수 있을 것 같았다.

추후 다른 프로젝트를 하게 된다면, Indexing Test를 하면서 유효성 검증을 해보고 싶다.

profile
삽질전문 아티스트
post-custom-banner

0개의 댓글