홈으로 돌아가기

Fastify 최적화: Bloom Filters와 확장 가능한 백엔드

Fastify에서 고부하 백엔드를 만드는 실용적인 솔루션 탐색: 커스텀 ID, 사용자 및 이미지용 Bloom filters, 로그인 없는 인증, 비동기 DB 쓰기, API 보호, TypeScript 타이핑.

Fastify 백엔드 최적화: Bloom Filters & 확장 가능한 아키텍처
Advertisement 728x90

고부하 애플리케이션을 위한 Fastify 백엔드 최적화: 실용적인 해결책

확장 가능하고 고성능 백엔드를 구축하려면 아키텍처 트레이드오프에 대한 깊은 이해가 필수적입니다. 이 글에서는 Fastify를 사용하여 고부하 애플리케이션을 개발한 경험을 공유하며, 데이터베이스 부하를 최소화하고, 기존 인증 방식 없이 사용자를 효율적으로 식별하며, Bloom 필터 및 비동기 작업과 같은 도구를 활용하여 악성 요청으로부터 보호하는 전략에 중점을 둘 것입니다.

ID 생성 전략 및 효율적인 데이터 관리

사용자나 이미지와 같이 대량의 엔티티를 처리하는 시스템을 설계할 때, 식별자(ID) 형식의 선택은 매우 중요합니다. 표준 GUID 또는 UUID는 고유성을 보장하지만, 데이터베이스 인덱싱 및 백엔드 처리에는 과도하게 리소스를 많이 사용할 수 있습니다. 이러한 오버헤드를 줄이기 위해 우리는 19bc19e00ab-eh5kkeh99q7와 같은 하이브리드 ID 형식을 구현했습니다. 첫 번째 부분은 16진수로 변환된 Unix 타임스탬프를 나타내고, 두 번째 부분은 임의의 문자 시퀀스입니다. 하이픈은 순전히 가독성을 높이는 역할을 합니다.

다음은 이러한 ID를 생성하는 예시 함수입니다.

Google AdInline article slot
function generateID(): string {
  const timestamp = Date.now().toString(16);
  const randomPart = generateRandomSequence( 11 );
  return timestamp + '-' + randomPart;
}

이 접근 방식은 초기 부분이 매 밀리초마다 변경되므로 "조건부 증분" ID를 제공하며, 이는 데이터베이스 인덱싱 효율성을 높이는 데 기여합니다. 추가적인 이점은 ID에서 엔티티의 생성 날짜를 직접 디코딩할 수 있어 테이블에 별도의 created_at 필드가 필요 없다는 것입니다. 이는 저장되는 데이터의 양과 쓰기 작업 수를 줄여주지만, 날짜 기반 쿼리를 구성하는 편의성에는 약간의 제약이 따릅니다.

Bloom 필터 및 패스워드리스 인증을 통한 확장

수천 개의 동시 세션과 수만 개의 이미지를 처리하도록 설계된 애플리케이션을 개발하려면 CPU 및 메모리 소비를 최소화하는 접근 방식이 필요합니다. 조회된 이미지나 기존 사용자를 확인하기 위한 전통적인 SQL 쿼리는 빠르게 병목 현상이 됩니다. 이러한 맥락에서 Bloom 필터는 매우 중요한 역할을 합니다.

Bloom 필터는 요소가 집합에 없는지 효율적으로 (예: 99.9999% 확률로) 확인할 수 있는 확률적 데이터 구조입니다. 그 핵심 장점은 멤버십 확인 시 기존 데이터 구조에 비해 메모리 및 CPU 오버헤드가 극히 낮다는 것입니다. 우리 프로젝트에서 Bloom 필터의 주요 적용 사례는 다음과 같습니다.

Google AdInline article slot
  • 조회된 이미지 추적: 각 사용자마다 Bloom 필터를 유지하여 이미 표시된 이미지의 ID를 저장합니다. 이를 통해 이미지가 사용자에게 이미 제시되었는지 빠르게 확인하여 새 배치에서 중복을 제거할 수 있습니다.
  • 기존 사용자 식별: 백엔드 시작 시, 모든 등록된 사용자 ID가 전역 Bloom 필터에 로드됩니다. 이는 들어오는 device_id가 시스템에 이미 알려진 것인지 즉시 확인할 수 있도록 합니다.

사용자 편의성을 높이고 앱 스토어 심사를 간소화하기 위해 명시적인 인증 없이 식별하는 시스템이 구현되었습니다. 애플리케이션이 설치되면 고유한 device_id가 생성되며, 이는 백엔드에서 사용자를 식별하는 데 사용됩니다. 가짜 device_id 생성 및 악용을 방지하기 위해 다단계 검증 프로세스가 사용됩니다.

  • Fastify JSON 스키마 유효성 검사: device_id 형식의 초기 유효성 검사는 minLength, maxLength, pattern을 정의하는 JSON 스키마를 사용하여 Fastify 수준에서 이루어집니다.

```javascript

fastify.post( '/backend', {

Google AdInline article slot

  schema: {

    body: {

      type: 'object',

      required: [ 'device_id' ],

      properties: {

        device_id: {

          type: 'string',

          minLength: 23,

          maxLength: 23,

          pattern: '^[a-z0-9]{11}-[a-z0-9]{11}$'

        }

      }

    }

  }

} );

```

  • 사용자 Bloom 필터: Fastify 스키마를 통과한 후, device_id는 기존 사용자 Bloom 필터와 비교하여 확인됩니다.
  • ID 정확성 확인:

* 생성 날짜: 타임스탬프를 인코딩하는 ID의 첫 번째 부분은 프로젝트 시작일과 현재 날짜 사이의 기간에 속해야 합니다.

* 무작위 부분 엔트로피: 임의의 시퀀스를 나타내는 ID의 두 번째 부분은 엔트로피를 확인합니다. 이는 단순하고 예측 가능한 ID(예: aaaaaaaaaaaa)의 사용을 방지합니다. 엔트로피는 특정 임계값(예: 2) 이상이어야 합니다.

```javascript

function getEntropyString( str: string ): number {

  const frequencies = new Map();

  for (const char of str) {

    frequencies.set(char, (frequencies.get(char) || 0) + 1);   }

  return [...frequencies.values()].reduce((acc, count) => {

    const p = count / str.length;

    return acc - p * Math.log2(p);

  },

0);

}

```

  • 비동기 사용자 삽입: ID가 새롭고 유효하다고 판단되면, 기존 사용자 Bloom 필터에 추가되고 INSERT 쿼리가 데이터베이스로 비동기적으로 전송됩니다. await를 생략하면 요청 처리가 차단되는 것을 방지하여 백엔드 부하를 크게 줄입니다. 이 접근 방식은 예기치 않은 백엔드 재시작 시 새로운 사용자 데이터가 손실될 매우 낮은 확률을 감수하는데, 이는 해당 사용자의 스와이프 기록이 재설정되는 것을 의미할 뿐이므로 이러한 유형의 애플리케이션에서는 허용 가능한 트레이드오프입니다.

클라이언트 상호작용 및 API 보호 최적화

모바일 애플리케이션과 백엔드 간의 효율적인 상호작용은 성능에 매우 중요합니다. 이미지와 같은 정적 콘텐츠를 제공하기 위해 Nginx가 별도의 도메인에서 운영됩니다. 이는 Fastify 서버의 부하를 최대한 줄여 비즈니스 로직에 집중할 수 있도록 하며, 효과적인 콘텐츠 캐싱을 가능하게 합니다.

모바일 애플리케이션은 10개씩 이미지 배치를 받습니다. 이 이미지들의 ID는 각 사용자에게 특정한 "조회된 이미지" Bloom 필터에도 추가됩니다. 이 접근 방식은 API 요청 수를 최소화합니다. 애플리케이션은 시작 시 두 배치를 로드하고, 10개의 좋아요마다 새 배치를 요청합니다. 이는 사용자가 현재 배치와 상호작용하는 동안 지속적인 콘텐츠 스트림을 보장합니다.

과도한 부하 및 악용으로부터 API를 보호하기 위해 Fastify용 rateLimit 플러그인을 사용하여 속도 제한(rate limiting)이 구현됩니다. 아키텍처에 따라 분산 데이터 저장을 위해 Redis를 사용하거나, 단일 인스턴스의 경우 Node.js 애플리케이션의 메모리에서 작동할 수 있습니다.

중요한 측면은 10개씩 배치로 전송되는 사용자 반응(좋아요/싫어요)의 유효성 검사입니다. 이 프로세스에는 여러 수준의 유효성 검사가 포함됩니다.

  • Fastify 스키마 유효성 검사: 사용자 ID, 이미지 ID, 그리고 반응 자체(열거형 'like' 또는 'dislike'로)는 JSON 스키마에 대해 확인됩니다.
  • 사용자 Bloom 필터: "기존 사용자" Bloom 필터를 통해 user_id의 존재 여부를 확인합니다.
  • 조회된 이미지 Bloom 필터: 현재 사용자에게 image_id가 실제로 표시되었는지 개인 "조회된 이미지" Bloom 필터를 사용하여 확인합니다.
  • 반응 배치 진위 확인: 10개 반응 배치 내의 패턴을 분석하는 trustLikeInBatch10 함수가 구현됩니다. 모든 반응이 동일하거나(모두 좋아요 또는 모두 싫어요) 지나치게 빈번한 교차(예: 8회 이상의 전환)가 있는 경우, 해당 배치는 신뢰할 수 없는 것으로 간주되어 무시됩니다. 이는 자동화되거나 악의적인 행동을 걸러내는 데 도움이 됩니다.

```javascript

function trustLikeInBatch10( batch: string[] ): boolean {

  if( batch.length != feedLimit ) return false;

  const likes = batch.filter( v => v === 'like' ).length;

  if( likes === 0 || likes === feedLimit ) return false;

  const switches = batch.slice(1).reduce( ( acc, val, i ) => acc + ( val !== batch[ i ] ? 1 : 0 ), 0 );

  if( switches >= 8 ) return false;

  return true;

}

```

  • 비동기 반응 쓰기: 새 사용자와 마찬가지로, 반응은 작업 완료를 기다리지 않고 FOREIGN KEY 제약 조건 없이 데이터베이스에 비동기적으로 기록됩니다. 이 접근 방식은 비전통적이지만, 높은 강도의 데이터 스트림에서 데이터베이스 부하를 크게 줄여 처리량을 위해 엄격한 일관성을 희생합니다. 이 맥락에서 몇몇 반응의 잠재적 손실은 전체 통계 및 추천 시스템에 중요하지 않습니다.

API 유효성 검사 세부 정보와 관련된 보안을 강화하고 익스플로잇을 방지하기 위해 JSON 스키마 불일치로 인해 Fastify에서 발생하는 오류는 마스킹됩니다. 상세한 문제 설명 대신 백엔드는 일반적인 응답을 반환합니다. 이는 공격자가 데이터 스키마를 리버스 엔지니어링하기 어렵게 만듭니다.

fastify.setErrorHandler((
  error: FastifyError,
  request: FastifyRequest,
  reply: FastifyReply ) =>
{
  console.error( 'setErrorHandler', error );
  countErrorStatistics();
  reply.code(200).send( 'Hello World!' );
});

클라이언트 애플리케이션은 이러한 일반적인 응답을 처리할 준비가 되어 있어야 하며, 이는 전반적인 보안 전략의 일부입니다.

코드 신뢰성을 위한 TypeScript 활용

TypeScript를 사용하면 코드의 신뢰성과 유지보수성이 크게 향상됩니다. 특히 고정된 문자열 상태 집합(예: 이미지 상태)을 다룰 때, 문자열 배열에서 타입을 우아하게 생성할 수 있습니다. 이는 변수에 대한 엄격한 타입 지정을 보장하여 오타나 유효하지 않은 값 사용과 관련된 오류를 제거합니다.

// Usually it's like this:
const imageStateArray1 = [ 'new', 'broken', 'gpt', 'ready', 'download', 'done', 'bad' ];
type tpImageState1 = 'new' | 'broken' | 'gpt' | 'ready' | 'download' | 'done' | 'bad';

// or better yet:
const imageStateArray2 = [ 'new', 'broken', 'gpt', 'ready', 'download', 'done', 'bad' ] as const;
type tpImageState2 = ( typeof imageStateArray2 )[ number ];

as const(typeof array)[number]를 사용하는 두 번째 접근 방식은 배열에서 리터럴 타입을 자동으로 추론하여 배열 값과 해당 타입 간의 동기화를 중복 없이 보장하므로 더 선호됩니다.

핵심 요약

  • Bloom 필터 사용은 요소 존재 여부 확인 시 데이터베이스 부하를 크게 줄여주며, 이는 고부하 시스템에 매우 중요합니다.
  • 타임스탬프를 포함하는 맞춤형 ID 생성은 메타데이터 임베딩 및 인덱싱 최적화를 가능하게 하며, 중복되는 created_at 필드를 제거합니다.
  • 비동기 데이터베이스 쓰기 작업과 FOREIGN KEY 제약 조건 생략은 고강도 애플리케이션에서 장애 발생 시 잠재적이지만 허용 가능한 데이터 손실을 대가로 백엔드 처리량을 향상시킵니다.
  • Fastify 수준에서의 상세한 수신 요청 유효성 검사 및 데이터 엔트로피 확인은 API를 악용 및 잘못된 형식의 데이터로부터 보호합니다.
  • Fastify 유효성 검사 오류를 마스킹하는 것은 스키마의 내부 구조를 모호하게 하고 잠재적 공격자가 리버스 엔지니어링하기 어렵게 만들어 API 보안을 강화합니다.

— Editorial Team

Advertisement 728x90

다음 읽기