만족

[Express] expressjs 는 왜 느릴까? 본문

[Express] expressjs 는 왜 느릴까?

Backend/Express Satisfaction 2025. 8. 8. 17:32

최근 fastify 라는 nodejs 를 알게 되었는데, 이 framework는 "express 보다 4배 빠르다" 고 한다.


https://fastify.dev/benchmarks/

 

위 사이트에 들어가보면 다양한 framework 들과 express의 벤치마크를 분석하는데, express 는 그 중에서도 압도적인 최하위를 기록하고 있다.

벤치마크

'use strict'

const fastify = require('fastify')()

const schema = {
  schema: {
    response: {
      200: {
        type: 'object',
        properties: {
          hello: {
            type: 'string'
          }
        }
      }
    }
  }
}

fastify.get('/', schema, function (_req, reply) {
  reply.send({ hello: 'world' })
})

fastify.listen({ port: 3000, host: '127.0.0.1' })

 

가장 빠르다는 fastify 를 사용하여 요청 시 간단한 json을 응답하도록 작성하면, 초당 45000건의 요청을 처리할 수 있다고 한다.

 

 

 

 

 

'use strict'

const express = require('express')

const app = express()

app.disable('etag')
app.disable('x-powered-by')

app.get('/', function (_req, res) {
  res.json({ hello: 'world' })
})

app.listen(3000)


반면에 express로 동일한 기능을 하는 코드를 작성했을 경우 초당 10000건밖에 처리하지 못한다.

 

왜 그럴까?

일단 expressjs 는 등록한 미들웨어를 순차적으로 전부 실행한다.

 

app.use(md1);
app.use(md2);
app.use(md3);
app.use(md4);

app.get('/', ()=>{
  //...
});

 

이런 식으로 되어있다면 / 로 접근했을 때 md1, md2, md3, md4 를 모두 실행한 다음 / 이 실행되는 것이다.

 

app.get('/a', ()=>{
  //...
});
app.get('/b', ()=>{
  //...
});
app.get('/c', ()=>{
  //...
});

 

또한 이런 구조에서 /c 에 접근한다면 등록된 순서대로 라우터를 검색한다.

 

1. /a 에 해당하는가? -> 아니오

2. /b 에 해당하는가? -> 아니오

3. /c 에 해당하는가? -> get 요청이 맞는가? -> 맞으므로 실행

 

따라서 등록된 미들웨어의 갯수가 많을수록, 실행/검사해야 하는 함수가 증가하므로 성능이 낮아진다.

 

반면 fastify 는 tree 구조로 라우터를 등록하기 때문에 즉시 라우트를 찾을 수 있으므로,

express처럼 모든 라우팅을 순회하여 검색할 필요가 없다.

 

이러한 구조적 차이 때문에 예제처럼 간단한 코드에서도 성능 차이가 크게 벌어지게 되고,

많은 라우팅이 등록된 실제 프로덕션에서는 더 큰 차이가 벌어지게 된다.

 

변경되는 req, res 객체의 구조

express의 장점이자 단점이라고 할 수 있다.

 

미들웨어에 전달되는 req, res객체를 마음대로 추가/제거할 수 있다는 점은 개발자에게 유연성을 제공하지만, nodejs 엔진에는 친화적이지 않다.

 

app.use((req, res, next)=>{
  req.props= {}; //... req 객체에 새로운 프로퍼티가 추가됨
  //...
  next();
});

//다음 미들웨어에서도 req.props에 접근 가능하다

 

이렇게 되면 작동에는 전혀 문제가 없지만, v8엔진은 req 객체를 최적화할 수 없게 된다.

 

v8 엔진의 히든 클래스(Hidden Class)

 

  • V8(Chromium, Node.js의 JS 엔진)은 성능을 위해 JS 객체를 C++ 클래스처럼 고정된 구조(Shape)로 인식한다
  • 이 구조를 히든 클래스라고 부르고, 프로퍼티 이름과 추가 순서가 동일하면 같은 히든 클래스를 재활용한다
  • 히든 클래스가 재활용되면 인라인 캐싱(IC)이 먹혀서 메서드/프로퍼티 접근이 CPU 한두 번에 끝난다
  • 문제: 프로퍼티를 동적으로 추가·삭제·타입 변경하면 히든 클래스가 깨져서 **디옵트(deopt)**가 발생하고, 최적화된 코드가 다 버려져 속도가 저하된다.
// 느린 예시 (Express-style)
function slow() {
  const obj = {};
  obj.a = 1;       // shape 1
  obj.b = 2;       // shape 2
  delete obj.a;    // shape 3 → 디옵트 발생
  return obj.b;
}

// 빠른 예시 (Fastify-style)
function fast() {
  const obj = { a: 1, b: 2 }; // shape 1 고정
  return obj.b;
}

 

그래도 express를 조금 더 빠르게 사용하려면...

가장 중요한 것은 불필요한 middleware는 제거하는 것이다.

 

express-generator 를 사용해 프로젝트를 초기화했다면

logger나 cookie-parser 같은 미들웨어가 자동으로 설정되는데

필요한 middleware 만 남기고 모두 제거해야 한다.

 

모든 요청마다 실행되는 함수이기 때문에, 모든 요청에 필요한 함수인지 판단하고 적절히 추가/제거한다.

 

특히 nextjs 에서 customServer를 사용하는 경우 express를 사용하는 경우가 많을텐데,

이 경우에도 불필요한 middleware가 있는지 확인하고 제거해야 한다.

 

회사에서도 확인해보니 실제로 사용하고 있지는 않지만 등록되어있는 미들웨어들이 있어서 

제거하고 벤치마크해보니 throughput 이 약 28%나 증가했다.

 

전: 30초동안 331k개 처리

 

후: 30초 동안 425k개 처리

 

또, express는 순차적으로 미들웨어를 실행하기 때문에 빠르게 응답해야 하는 라우트를 먼저 등록해주면 응답 속도에 도움이 된다.

 

app.get('/a', ()=>{
  // /a로 접근하면 가장 먼저 도달하므로 비교적 빠름
});

//...


app.get('/c', ()=>{
  // /c로 접근하더라도 앞에서 등록된 라우터를 전부 검사하고 도달하므로 오래 걸림
});


사실 이정도까지는 정말 엄청난 트래픽이 쏟아지는게 아니라면 크게 유의미하지는 않지만 

expressjs 의 동작 방식을 이해하고 있으면 추후 최적화 시 도움이 될 것이다.



Comments