강의

멘토링

커뮤니티

Inflearn Community Q&A

taesun420's profile image
taesun420

asked

Learning Express.js Properly: Part2 Engine Internal Working Principles and Clone Project

[Middleware Chain Implementation] – Middleware Chain Processing Structure Demystified

미들웨어 체인 구현 보다가 생긴 궁금증에 대해 질문 드려요!

Written on

·

52

·

Edited

0

Node.js에서는 req와 res 사이에 인증 처리와 같은 로직을 넣기 위해 미들웨어를 직접 만들어야 할 것 입니다. Express에서의 미들웨어 구현과 동작 방식은 최선의 방식인지 궁금합니다! 즉 Express가 아닌 Node.js로 미들웨어를 직접 구현했을때 Express 방식보다 더 나은 설계가 될 수 있는지 궁금합니다. 또 Nest.js (nest 아니더라도 다른 프레임워크) 에서는 Express가 미들웨어를 구현하고 동작시키는 방식을 그대로 따르고 있는지 아니면 설계 상의 차이를 두었는지도 궁금합니다. 감사합니다!

javascriptnode.jsexpressbackend소프트웨어-설계

Answer 2

1

nhcodingstudio님의 프로필 이미지
nhcodingstudio
Instructor

안녕하세요, TAESUN님! 프레임워크의 표면적인 사용법을 넘어 그 이면에 숨겨진 엔진의 설계와 미들웨어 체인 구현 방식까지 깊이 있게 파고드시는 모습이 정말 인상 깊습니다. 저 역시 백엔드 아키텍처를 설계하고 다양한 프레임워크의 내부 코드를 뜯어보며 치열하게 고민했던 주제이기에, 단순한 사용법을 넘어 시니어 개발자의 관점에서 디자인 패턴과 프레임워크 설계 사상을 바탕으로 제 생각과 경험을 나누어 보겠습니다.

결론부터 말씀드리자면 익스프레스(Express)의 미들웨어 방식은 실용적이고 훌륭하지만 구조적으로 완벽한 최선의 설계라고 보기는 어렵겠습니다. 노드제이에스(Node.js) 생태계가 발전함에 따라 이를 보완하기 위한 더 나은 설계 패턴들이 등장했으며, 네스트제이에스(NestJS)와 같은 모던 프레임워크들은 익스프레스의 방식을 그대로 따르지 않고 아키텍처 레벨에서 훨씬 더 정교하게 역할을 분리하는 방향으로 진화해 왔기 때문입니다.

우선 익스프레스의 미들웨어 동작 방식은 객체지향 디자인 패턴 중 하나인 책임 연쇄 패턴(Chain of Responsibility Pattern)을 기반으로 하고 있습니다. 하나의 요청이 들어오면 여러 개의 미들웨어 함수들이 순차적으로 요청 객체와 응답 객체를 넘겨받으며 자신의 역할을 수행하고, 콜백 함수를 호출하여 다음 미들웨어로 제어권을 넘기는 구조를 취합니다. 이 방식은 진입 장벽이 낮고 직관적이라는 강력한 장점이 있지만 동시에 치명적인 단점들도 존재합니다.

가장 큰 문제는 미들웨어 간에 데이터를 전달할 때 요청 객체에 임의의 프로퍼티를 직접 추가하여 상태를 변형(Mutation)하는 방식을 강제한다는 점입니다. 이는 타입스크립트 환경에서 타입 추론을 어렵게 만들고, 애플리케이션의 규모가 커질수록 어떤 미들웨어가 객체를 어떻게 조작했는지 추적하기 매우 힘든 상태 관리의 복잡성을 유발하게 됩니다. 또한 2024년 말 익스프레스 5 버전이 정식 출시되면서 개선되긴 했으나, 오랫동안 비동기 에러 처리가 매끄럽지 못해 개발자가 직접 예외 처리를 감싸서 다음 체인으로 넘겨주어야만 하는 설계적 한계가 존재해 왔습니다.

이를 현실의 상황에 비유해 보자면 마치 여러 부서를 거쳐야 하는 하나의 원본 결재 서류라는 요청 객체에 모든 담당자가 각자의 포스트잇이나 볼펜 메모를 직접 덕지덕지 적어 가며 다음 부서로 넘기는 상황과 같습니다. 나중에 서류가 최종 책임자에게 도착했을 때 도대체 누가 언제 어떤 메모를 추가했는지 그 이력을 추적하거나 문서를 깔끔하게 관리하기가 매우 까다로워지는 복잡성을 띠게 되는 것입니다. 익스프레스 방식의 전형적인 구조와 한계는 아래 코드와 같이 요청 객체를 직접 변형하여 데이터를 전달하는 사이드 이펙트가 발생하거나, 비동기 처리 중 발생하는 에러를 수동으로 잡아 넘겨주어야 했던 구조적 아쉬움에서 잘 드러납니다.

// 익스프레스의 전형적인 책임 연쇄 패턴 기반 미들웨어 예시
app.use((req, res, next) => {
  // req 객체를 직접 변형하여 데이터를 전달하는 사이드 이펙트 발생
  req.customUser = { id: 1, role: 'admin' };
  
  // 비동기 처리 중 발생하는 에러를 수동으로 잡아 next(err)로 넘겨주어야 했던 구조적 아쉬움
  someAsyncOperation()
    .then(() => next())
    .catch(err => next(err));
});

그렇다면 익스프레스가 아닌 노드제이에스 순수 환경에서 엔진을 직접 설계한다면 더 나은 아키텍처를 구축할 수 있을지에 대해 말씀드려 보겠습니다. 실제로 코아(Koa.js)라는 프레임워크가 익스프레스의 한계를 극복하기 위해 등장하며 보여준 어니언 모델(Onion Model) 패턴이 아주 좋은 해답이 됩니다. 어니언 모델은 최신 자바스크립트의 비동기 문법을 적극적으로 활용하여 요청이 들어올 때뿐만 아니라 응답이 나갈 때도 미들웨어가 제어권을 가질 수 있도록 설계된 패턴입니다.

콜백 기반이 아니라 프라미스(Promise)를 반환하는 다음 함수를 비동기적으로 기다리게 함으로써, 미들웨어가 체인의 끝까지 실행된 후 다시 역순으로 돌아오며 후처리를 할 수 있게 됩니다. 이는 데이터베이스 트랜잭션의 롤백이나 요청 응답 시간 측정 같은 로직을 하나의 미들웨어 안에서 매우 우아하게 통합하여 처리할 수 있게 해줍니다. 이해를 돕기 위해 이 어니언 모델을 현실의 고급 레스토랑에 비유해 보자면, 전담 서버라는 미들웨어가 손님의 주문이라는 요청을 받아 주방이라는 다음 로직에 전달한 뒤 요리가 완전히 완성될 때까지 묵묵히 기다리는 것과 같습니다. 이후 완성된 요리라는 응답을 받아 다시 손님 테이블로 가져다주면서 마지막에 치즈를 갈아 올려주거나 영수증을 준비하는 식의 최종 후처리까지 하나의 완벽한 흐름으로 깔끔하게 마무리하는 체계적인 서비스 방식이 되는 것입니다. 따라서 순수 노드제이에스로 미들웨어 체인을 구현한다면 아래와 같이 프라미스 체이닝을 활용한 어니언 모델로 설계하는 것이 훨씬 더 유연하고 강력한 엔진 설계가 됩니다.

// 순수 Node.js 환경에서 비동기 문법을 활용해 설계할 수 있는 어니언 모델 미들웨어 체인 예시
const middlewareChain = [
  async (context, next) => {
    const start = Date.now();
    console.log('요청 진입: 인증 처리 등 사전 로직 수행');
    
    await next(); // 다음 미들웨어가 완전히 끝날 때까지 대기하며 제어권을 넘김
    
    // 체인이 끝난 후 제어권이 다시 역순으로 돌아옴 (우아한 후처리 가능)
    const ms = Date.now() - start;
    console.log(`응답 완료: 총 소요 시간 ${ms}ms`);
  }
];

마지막으로 네스트제이에스와 같은 모던 프레임워크의 설계 사상에 대해 이어가 보겠습니다. 네스트제이에스는 내부적으로 익스프레스를 기본 엔진으로 사용하고 있기 때문에 익스프레스 미들웨어를 그대로 호환하여 사용할 수는 있지만, 아키텍처 설계 상으로는 익스프레스의 단순한 일차원적 미들웨어 체인 방식을 강하게 지양합니다. 대신 역할을 아주 세밀하게 쪼개는 관점 지향 프로그래밍(AOP)과 의존성 주입(DI) 패턴을 전면적으로 도입하였습니다.

네스트제이에스에서는 하나의 평면적인 미들웨어 대신 요청이 최종 목적지에 도달하기까지 가드(Guards), 인터셉터(Interceptors), 파이프(Pipes)라는 명확하고 체계적인 라이프사이클을 거치게 됩니다. 예를 들어 인증 로직은 미들웨어가 아니라 오직 접근 허용 여부만을 판단하여 반환하는 가드에서 처리하여 책임을 엄격하게 분리하고, 데이터의 변형이나 로깅은 반응형 프로그래밍(RxJS)을 활용하는 인터셉터에서 독립적으로 처리하는 식입니다.

이렇게 설계하면 요청 객체를 함부로 오염시키지 않고도 각 계층이 독립적으로 완벽하게 동작하며, 시스템의 예측 가능성이 높아지고 테스트 코드를 작성하기도 훨씬 수월해지는 압도적인 장점을 누릴 수 있습니다. 이를 현실의 거대한 공항 출입국 시스템에 빗대어 본다면 단순히 모든 일을 한 명의 직원이 순서대로 처리하는 것이 아니라, 탑승구 앞 입구에서 여권과 티켓만 전문적으로 검사하는 보안 요원인 가드, 수화물의 무게와 규격을 정확히 검사하고 변환해주는 전용 스캐너인 파이프, 그리고 승객의 동선과 보안 로그를 시스템에 조용히 기록하는 관제탑인 인터셉터처럼 각자의 역할이 완벽하고 독립적으로 분업화된 현대적인 아키텍처를 구축하는 것과 같습니다. 이러한 역할을 명확히 분리한 네스트제이에스의 설계 철학은 아래의 가드 활용 예시처럼 요청 객체에 데이터를 주입하는 대신 순수하게 불리언 형태의 인가 여부만 판별하여 전달하는 구조로 표현됩니다.

// NestJS의 가드(Guard)를 활용한 단일 책임 분리 아키텍처 예시
@Injectable()
export class AuthGuard implements CanActivate {
  canActivate(context: ExecutionContext): boolean {
    const request = context.switchToHttp().getRequest();
    // 요청 객체에 데이터를 주입하는 대신, 순수하게 불리언(boolean) 형태의 인가 여부만 판별하여 전달함
    return validateUserToken(request.headers.authorization); 
  }
}

엔진의 핵심 동작 원리와 여러 프레임워크의 아키텍처를 비교하며 깊게 파고드시는 TAESUN님의 학습 방식은 훌륭한 엔지니어로 도약하는 가장 확실하고 바른 길이라고 생각합니다. 이러한 아키텍처 레벨의 치열한 고민들이 차곡차곡 모여 TAESUN님만의 견고하고 통찰력 있는 소프트웨어 설계 철학을 완성해 줄 것이라 굳게 믿으며, 언제든 질문이 있으시면 편하게 말씀 부탁드리겠습니다.

TAESUN님처럼 본질을 꿰뚫는 좋은 질문은 제가 더 나은 강의를 만드는 데 정말 큰 귀감이 됩니다. 늘 TAESUN님의 성장을 진심으로 응원하며, 혹시 추가로 필요한 강의가 있으시면 jeony0535@naver.com으로 이메일 주세요. 확인 후 감사의 마음을 담아 할인 쿠폰을 함께 전달해 드리도록 하겠습니다.

감사합니다 😃😃

TAESUN님의 프로필 이미지
TAESUN
Questioner

늘 좋은 답변 주심에 감사드립니다! 질문 더 드려도 될까요? 🙃

알아본 바로는 req 객체를 mutable하게 바꿀 수 있는 것은 Koa.js도 마찬가지이지만 차이점은 Koa.js는 아예 ctx.state라는 별도의 네임스페이스를 제공하여 커스텀 데이터는 여기에만 담는것 같아요, 또 AI한테 물어보니 어니언 모델이 Mutation의 부작용을 줄일 수 있다고 말하더라구요!

req 객체를 mutable하게 변경하면 상태 추적이 어렵다는 거 까지는 이해가 됩니다. 하지만, 어니언 모델이 어떻게 상태 추적의 어려움을 해소시키는 건지 코드 예시를 떠올리기 힘드네요 ㅜ AI는 오염구역을 격리하고 회수 경로를 열어준다는데 이게 어떤 문제를 해결할 수 있는지는 와닿지가 않네요

그리고 혹시 immutable 라이브러리를 도입하는건 어떤 상황에서 좋은 효과가 날 수 있을지도 궁금합니다. immutable하게 변경한다고 해도 변경 사항을 추적하는 코드는 만들어야 될 것 같은데 이것도 어떤 방식으로 하는게 좋을까요?

0

안녕하세요, 인프런 AI 인턴입니다. TAESUN님.

Node.js와 Express.js의 미들웨어 구현 및 작동 방식에 대한 질문을 주셨군요. 이 주제는 백엔드 개발에 있어 매우 중요한 부분입니다. 아래에 질문하신 내용에 대한 답변과 설명을 드리겠습니다.

1. Express의 미들웨어 구현 및 동작 방식

Express.js에서는 미들웨어를 사용하여 HTTP 요청과 응답 객체 사이에 인증 처리, 로깅, 데이터 파싱 등의 작업을 손쉽게 수행할 수 있습니다. 이런 방식을 통한 미들웨어의 사용은 개발자가 공통적인 작업을 손쉽게 모듈화할 수 있도록 도와줍니다.

Express에서 미들웨어의 동작 방식은 라우트 핸들러가 요청을 받기 전에 특정 로직을 실행할 수 있도록 합니다. 미들웨어는 순차적으로 호출되며, 각 미들웨어는 next() 함수를 호출하여 다음 미들웨어로 제어를 전달합니다. 이는 미들웨어 체인을 형성하고, 특정 조건에 따라 미들웨어 체인의 끝에서 응답을 종료할 수도 있습니다.

2. Express가 아닌 Node.js의 미들웨어 설계

Node.js로 직접 미들웨어를 구현할 수도 있지만, 이는 Express의 강력하고 직관적인 미들웨어 시스템에 비해 복잡하고 시간이 많이 걸릴 수 있습니다. Express의 미들웨어 체인 구조는 개발자의 코드 유지보수 및 재사용성을 증진시켜주기 때문에 매우 효율적입니다. Node.js로 구현하는 방식은 서버의 요구사항이 매우 특정적이고 Express의 기능이 지나치게 일반적일 때만 고려될 수 있습니다.

3. Nest.js 및 다른 프레임워크의 미들웨어 설계

Nest.js는 Express.js를 기반으로 한 프레임워크로, 내부적으로 Express의 미들웨어 기능을 그대로 사용합니다. 그러나 NestJS는 더 구조화된 모듈러 아키텍처를 제공하여 코드의 조직화와 확장을 쉽게 만들어 줍니다.

다른 프레임워크들도 대부분 Express의 미들웨어 개념을 따르거나, 이와 유사한 형태로 구현됩니다. Node.js 환경에서 미들웨어라는 개념은 매우 유용하고, 이를 차용하거나 개선한 다양한 방법이 프레임워크 내부에서 활용되고 있습니다.

관련해서 더 깊이 이해하고 싶으시다면, Express와 다른 프레임워크의 미들웨어 사용 비교 를 참고해보시면 좋습니다.

저는 질문자님의 이해를 돕고자 지식공유자님의 기존 답변을 참고하여 유사한 답변 링크를 추천해드리고 있습니다.

현재 베타 기능으로 답변이 만족스럽지 않을 수 있는 점 양해 부탁드립니다. 🙏

추가적으로 궁금한 점이 있으시면, 이어서 질문해 주세요. 곧 지식공유자께서 답변해 주실 것입니다.

taesun420's profile image
taesun420

asked

Ask a question