오픈소스 기여하기) NestJS Express↔Fastify 미들웨어 등록 일관성 개선하기

OpenSource 2025. 7. 16. 23:24
728x90

nest의 e2e

이 글을 읽으시는 nest를 활용하는 개발자분들은, e2e를 작성하시나요? 어떻게 작성하고 계신가요?
보통은 nest의 공식 문서 가이드에 따라 TestingModule을 통해 NestApplicationinit한 후 사용하실 겁니다.

beforeAll(async () => {
  const moduleRef = await Test.createTestingModule({
    imports: [CatsModule],
  })
  .overrideProvider(CatsService)
  .useValue(catsService)
  .compile();
  
  app = moduleRef.createNestApplication();
  app.use(thirdPartyMiddleWare()); // injected middleware
  await app.init();
});

 

express에서는 미들웨어를 테스트 애플리케이션에 등록해야한다면, 반드시 초기화 전에 등록해야합니다. 그렇지 않으면 미들웨어가 등록되지 않죠.

 

 

 

 

일관되지 않은 개발 경험

아시겠지만, 이 테스팅 과정은 실제 프로덕션의 NestApplication을 생성하는 과정과는 조금 상이합니다.

 

 

nest 컨테이너가 어떻게 생성되고, 모듈 간의 의존성을 어떻게 효율적으로 찾아가는지 등은 이 포스팅에서 다루지 않습니다. 관련해서 궁금하시다면, 정리해놓은 포스팅들(포스팅1, 포스팅2)이 있으니 참고해보시면 좋을 것 같습니다.

 

요약하자면, NestFactory를 통해 NestApplication을 create 함수를 사용하여 내부적으로 초기화가 되어 나온 NestApplication 인스턴스를 사용할 수 있고, 이 애플리케이션에 필요한 미들웨어들을 주입하여 사용하게 됩니다.

const app = await NestFactory.create<NestExpressApplication>(AppModule);
app.use(something);

 

 

 

여기까지 오셨다면, 뭔가 이상함을 느꼈을 수도 있습니다. 테스트 환경에서는 TestingModule의 createNestApplication 함수는 NestApplication을 초기화해주지 않습니다. 그래서 개발자가 직접 create > use > init 과정을 모두 호출하여 테스트에 필요한 애플리케이션을 생성하게 됩니다.

 

제가 감히 추측해보자면, 테스트 환경에서는 초기화 전 상태를 테스트하거나 모킹 등 더 세밀한 제어들이 필요하기 때문에 테스트 환경과 운영 환경의 애플리케이션 초기화 방식이 다를 것이라 생각합니다.

 

다시 돌아와서, 문제는 하나 더 발생합니다. express에서는 이 순서를 지키지 않아도, 테스트 애플리케이션의 초기화 과정에서 에러가 발생하지 않습니다.  하지만 fastify에서는 미들웨어를 등록하는 구간에서 에러가 발생합니다. 

 

describe("express (e2e)", () => {
  let app: NestExpressApplication;
  beforeAll(async () => {
    const module = await Test.createTestingModule({
      imports: [AppModule],
    }).compile();
    app = module.createNestApplication<NestExpressApplication>({
      bufferLogs: true,
    });
    await app.init();
    app.use(requestLoggerMiddleware);
  });
  
  describe("health", () => {
    it("should return healthy", async () => {
      await request(app.getHttpServer()).get("/health").expect(200);
    });
  });
});
describe("fastify (e2e)", () => {
  let app: NestFastifyApplication;
  beforeAll(async () => {
    const module = await Test.createTestingModule({
      imports: [AppModule],
    }).compile();
    app = module.createNestApplication<NestFastifyApplication>(
      new FastifyAdapter(),
      { bufferLogs: true }
    );
    app.use(requestLoggerMiddleware);
    await app.init();
    await app.getHttpAdapter().getInstance().ready();
  });
  describe("health", () => {
    it("should return healthy", async () => {
      await request(app.getHttpServer()).get("/health").expect(200);
    });
  });
});

 

 

 

 

 

 

왜 다른걸까?

express에서는 모든 것이 use를 통해 사용되죠. 이는 미들웨어 뿐 아니라 라우트 등도 모두 동일합니다. 하지만 fastify는 공식 문서에서도 나와있듯이 3.0.0버전 이후부터 @fastify/express, @fastify/middie등의 플러그인을 통해 미들웨어를 주입하도록 되어있습니다. 

 

// FastifyAdapter
public async init() {
  if (this.isMiddieRegistered) {
    return;
  }
  await this.registerMiddie();

  // Register any pending middlewares that were added before init
  if (this.pendingMiddlewares.length > 0) {
    for (const { args } of this.pendingMiddlewares) {
      (this.instance.use as any)(...args);
    }
    this.pendingMiddlewares = [];
  }
}
  
private async registerMiddie() {
  this.isMiddieRegistered = true;
  await this.register(
    import('@fastify/middie') as Parameters<TInstance['register']>[0],
  );
}

 

nest는 fastify를 초기화하는 시점에, @fastify/middie 플러그인을 등록하도록 되어있습니다. 이런 이유 때문에, fastify는 정확한 실행 순서를 지키지 않으면 FastifyAdapter에 미들웨어를 등록하지 못하고 에러가 발생하게 됩니다.

 

 

 

 

 

일관된 테스트를 위한 기여

처음에는, 단순하게 테스트 환경에서 자동으로 어댑터를 초기화 해보기로 했습니다.

// TestingModule.createNestApplication()
// 첫 번째 시도: TestingModule에 auto-init 로직 추가
if (typeof (httpAdapter as any)?.init === 'function') {
  const originalInit = (proxy as any).init;
  (proxy as any).init = async function (this: any) {
    await (httpAdapter as any).init(); // 어댑터 자동 초기화
    if (originalInit) {
      return originalInit.call(this);
    }
    return this;
  };
}

 

 

하지만 이 방식의 문제는 NestApplication 생성은 동기 함수이고, FastifyAdapter의 init은 비동기 함수이기 때문에, 근본적으로 돌아가지 않습니다. 미들웨어 등록 시점에는 여전히 플러그인 등록이 Pending 상태일 수도 있기 때문이죠. 이러한 프록시 래핑으로는 실제 미들웨어 타이밍을 제어할 수 없었습니다.

 

 

위 문제를 해결하기 위해 생각하다, 스스로 내린 결론은 애플리케이션 생성 함수를 비동기함수로 추가하는 방향이었습니다. 그러다보니, 우선 개발자에게 명확한 에러 메시지를 유도하고, PR을 마무리한 후 새로운 이슈를 제기하려고 했습니다. use를 오버로딩해서, 에러메시지만 제공하는 방향으로 다시 커밋을 올렸어요.

// FastifyAdapter
public use(...args: any[]) {
  if (!this.isMiddieRegistered) {
    Logger.warn(
      'Middleware registration requires the "@fastify/middie" plugin to be registered first. ' +
      'Make sure to call app.init() before registering middleware with the Fastify adapter. ' +
      'See https://github.com/nestjs/nest/issues/15310 for more details.',
      FastifyAdapter.name,
    );
    throw new TypeError('this.instance.use is not a function');
  }
  return super.use(...args);
}

 

 

 

하지만 우리의(?) 카밀 아저씨(?)가 좋은 아이디어를 제시해줬습니다. 메인테이너가 제안해준 의견 덕분에 미들웨어를 큐에 캐싱해두고 지연등록하는 FastifyAdapter의 미들웨어 등록 시스템 기능을 만들 수 있었습니다.

 

 

 

 

// FastifyAdapter
private pendingMiddlewares: Array<{ args: any[] }> = [];

public async init() {
  if (this.isMiddieRegistered) {
    return;
  }
  await this.registerMiddie();

  // Register any pending middlewares that were added before init
  if (this.pendingMiddlewares.length > 0) {
    for (const { args } of this.pendingMiddlewares) {
      (this.instance.use as any)(...args);
    }
    this.pendingMiddlewares = [];
  }
}

public use(...args: any[]) {
  // Fastify requires @fastify/middie plugin to be registered before middleware can be used.
  // If middie is not registered yet, we queue the middleware and register it later during init.
  if (!this.isMiddieRegistered) {
    this.pendingMiddlewares.push({ args });
    return this;
  }
  return (this.instance.use as any)(...args);
}

 

 

 

 

마치며...

글을 정리하다보니 저 혼자 오픈소스의 은사(?)라고 생각하는 김인제님께서 말씀하시던 게 생각납니다.

 

"오픈소스 기여로 수억명에게 임팩트 만들기"

 

실제로 몇 명이나 될 지는 모르겠지만, express와 fastify 모두 app.use() 호출 타이밍에 제약이 없어졌기 때문에 동일한 코드 패턴으로 미들웨어 등록이 가능해졌습니다. 서버 프레임워크의 변환 시 조금이나마 코드 변경을 줄일 수 있지 않을까요? DX 향상에 미약하나마 도움이 되는 기여였으면 좋겠습니다.

 

항상 기여하면서 느끼는거지만, 오픈소스 자체에 깊은 이해와 더불어 오픈소스 구현에 사용된 다른 프레임워크나 그 근간이 되는 개념들을 자연스레 학습할 수 있게 되고, 더 깊게 이해할 수 있는 것 같습니다. 오픈소스의 진정한 가치는 이런 데서 나오는 게 아닌가 싶습니다.

300x250
mag1c

mag1c

2년차 주니어 개발자.

방명록