클라이언트에서 서버로: Alova 3의 풀스택 요청 전략 실습

발행: (2025년 12월 4일 오후 11:49 GMT+9)
6 min read
원문: Dev.to

Source: Dev.to

BFF 레이어에서 클라이언트 요청 전달하기

BFF 레이어에서는 클라이언트 요청을 백엔드 마이크로서비스로 전달해야 하는 경우가 자주 있습니다. async_hooks를 사용해 각 요청의 컨텍스트에 접근하고 이를 Alova의 beforeRequest에 추가하면 사용자와 관련된 데이터를 전달할 수 있습니다.

import { createAlova } from 'alova';
import adapterFetch from '@alova/fetch';
import express from 'express';
import { AsyncLocalStorage } from 'node:async_hooks';

// Create async local storage instance
const asyncLocalStorage = new AsyncLocalStorage();

const alovaInstance = createAlova({
  requestAdapter: adapterFetch(),
  beforeRequest(method) {
    // Get request headers from async context and pass to downstream
    const context = asyncLocalStorage.getStore();
    if (context && context.headers) {
      method.config.headers = {
        ...method.config.headers,
        ...context.headers,
      };
    }
  },
  responded: {
    onSuccess(response) {
      // Data transformation processing
      return {
        data: response.data,
        timestamp: Date.now(),
        transformed: true,
      };
    },
    onError(error) {
      console.error('Request failed:', error);
      throw error;
    },
  },
});

const app = express();

// Set once in middleware, automatically passed throughout
app.use((req, res, next) => {
  const context = {
    userId: req.headers['x-user-id'],
    token: req.headers['authorization'],
  };
  asyncLocalStorage.run(context, next);
});

// Business code focuses on business logic
app.get('/api/user-profile', async (req, res) => {
  // No need to manually pass context anymore!
  const [userInfo, orders] = await Promise.all([
    alovaInstance.Get('http://gateway.com/user/profile'),
    alovaInstance.Get('http://gateway.com/order/recent'),
  ]);

  res.json({ user: userInfo.data, orders: orders.data });
});

API 게이트웨이에서의 활용 사례

게이트웨이에서는 인증, 요청 속도 제한, 요청 분배 등이 자주 필요합니다. Alova 3의 Redis 스토리지 어댑터와 rateLimiter를 활용하면 분산 인증 서비스와 요청 속도 제한을 효과적으로 구현할 수 있습니다.

인증은 이렇게 할 수 있습니다

인증 토큰에 만료 시간이 있는 경우, 게이트웨이에 Redis 스토리지 어댑터를 설정해 토큰을 재사용하도록 저장할 수 있습니다. 단일 머신 클러스터 서비스라면 @alova/storage-file 파일 스토리지 어댑터를 사용할 수도 있습니다.

import { createAlova } from 'alova';
import RedisStorageAdapter from '@alova/storage-redis';
import adapterFetch from '@alova/fetch';
import express from 'express';

const redisAdapter = new RedisStorageAdapter({
  host: 'localhost',
  port: '6379',
  username: 'default',
  password: 'my-top-secret',
  db: 0,
});

const gatewayAlova = createAlova({
  requestAdapter: adapterFetch(),
  async beforeRequest(method) {
    const newToken = await authRequest(
      method.config.headers['Authorization'],
      method.config.headers['UserId']
    );
    method.config.headers['Authorization'] = `Bearer ${newToken}`;
  },
  // Set L2 storage adapter
  l2Cache: redisAdapter,
  // ...
});

const authRequest = (token, userId) =>
  gatewayAlova.Post('http://auth.com/auth/token', null, {
    // Cache for 3 hours; subsequent requests with the same parameters will hit Redis
    cacheFor: {
      mode: 'restore',
      expire: 3 * 3600 * 1000,
    },
    headers: {
      'x-user-id': userId,
      Authorization: `Bearer ${token}`,
    },
  });

const app = express;

// Forward all incoming requests to alova
const methods = [
  'get',
  'post',
  'put',
  'delete',
  'patch',
  'options',
  'head',
];
methods.forEach((m) => {
  app[m]('*', async (req, res) => {
    const { method, originalUrl, headers, body, query } = req;

    const response = await gatewayAlova.Request({
      method: method.toLowerCase(),
      url: originalUrl,
      params: query,
      data: body,
      headers,
    });

    // Forward response headers
    for (const [key, value] of response.headers.entries()) {
      res.setHeader(key, value);
    }

    // Send response data
    res.status(response.status).send(await response.json());
  });
});

app.listen(3000, () => {
  console.log('Gateway server started on port 3000');
});

Note: 매 요청마다 재인증이 필요하다면 authRequestcacheFor 옵션을 삭제해 캐싱을 비활성화하면 됩니다.

속도 제한 전략

Alova의 rateLimiter는 내부적으로 node-rate-limiter-flexible을 사용해 분산 속도 제한 전략을 구현할 수 있습니다. 아래는 모든 게이트웨이 라우트에 제한자를 적용한 리팩터링 예시입니다.

import { createRateLimiter } from 'alova/server';

const rateLimit = createRateLimiter({
  /**
   * Time for points reset, in ms
   * @default 4000
   */
  duration: 60 * 1000,
  /**
   * Maximum consumable quantity within duration
   * @default 4
   */
  points: 4,
  /**
   * Namespace, prevents conflicts when multiple rateLimits use the same storage
   */
  keyPrefix: 'user-rate-limit',
  /**
   * Lock duration in ms; when the limit is reached, the block lasts this long.
   */
  blockDuration: 24 * 60 * 60 * 1000,
});

const methods = [
  'get',
  'post',
  'put',
  'delete',
  'patch',
  'options',
  'head',
];
methods.forEach((m) => {
  app[m]('*', async (req, res) => {
    const { method, originalUrl, headers, body, query } = req;

    const alovaRequest = gatewayAlova.Request({
      method: method.toLowerCase(),
      url: originalUrl,
      params: query,
      data: body,
      headers,
    });

    const response = await rateLimit(alovaRequest, {
      key: req.ip, // Use IP as tracking key to prevent frequent requests from the same IP
    });

    // Forward response (similar to the previous example)
    for (const [key, value] of response.headers.entries()) {
      res.setHeader(key, value);
    }
    res.status(response.status).send(await response.json());
  });
});

서드파티 서비스 연동: 자동 토큰 관리

외부 API와 연동할 때는 access_token 수명 주기를 관리해야 하는 경우가 많습니다. Alova 3와 Redis 스토리지 어댑터, 그리고 atom 훅을 함께 사용하면 토큰을 분산 환경에서 자동으로 유지 보수할 수 있습니다.

import { createAlova, queryCache, atom } from 'alova';
import RedisStorageAdapter from '@alova/storage-redis';
import adapterFetch from '@alova/fetch';

// Configure Redis for token caching
const redisAdapter = new RedisStorageAdapter({
  host: 'localhost',
  port: 6379,
});

const alovaInstance = createAlova({
  requestAdapter: adapterFetch(),
  l2Cache: redisAdapter,
});

// Atomic operation to refresh token only once across distributed instances
const refreshTokenAtom = atom(async (currentToken) => {
  if (currentToken && Date.now()  {
  const res = await requestWithToken({
    method: 'GET',
    url: 'https://thirdparty.com/api/resource',
  });
  console.log(res.data);
})();
Back to Blog

관련 글

더 보기 »