본문 바로가기

Backend/디자인 패턴

Middleware Pattern (미들웨어 패턴)

저는 주로 Node.JS 를 사용해서 미들웨어라는 게 익숙합니다만, 다른 언어 혹은 프레임워크에서도 미들웨어가 이렇게 적극적으로 쓰이는지는 잘 모르겠습니다. 아무튼 미들웨어는 Node.JS 에서 가장 특징적인 패턴입니다.

 

미들웨어는 아무래도 Express 에서 용어가 대중화되었고 이 패턴을 적극적으로 사용하고 있지 않나 싶습니다.

 

function(req, res, next) { ... }

 

Express 에서 미들웨어는 위와 같이 생겼습니다. 저기서 req 는 Http request, res 는 Http response 이며 next 는 이 미들웨어가 작업을 완료하고 다음 차례의 미들웨어를 트리거할 때 호출되는 콜백 함수입니다. 미들웨어 함수를 정의할 때 저 next 가 빠져있으면 미들웨어로 간주되지 않아 에러가 발생하기도 합니다.

 

일반적으로 Express 의 미들웨어는 아래와 같은 일들을 합니다.

 

  • 요청 본문의 구문 분석
  • 로그 생성
  • 세션 관리
  • 요청 및 응답 압축 or 해제
  • CSRF

딱히 코어 로직과는 상관없는 작업들이라서 일반적으로는 비즈니스 로직에만 집중할 수 있게 하는 서포트의 개념이 강합니다만, 어떻게 구성하느냐에 따라 비즈니스 로직 급으로 꽤 중요한 부분을 맡기도 하는 것 같습니다. 팀에서 사용하는 Express 미들웨어의 예시를 간단히

들어보겠습니다.

 

// parse_candidate.ts
export async function parseCandidate(req: Request, res: Response, next: NextFunction): Promise<void> {
  const candidates = req.body.candidates || [];
  const items: Array<{ item: Nullable<Item> }> = 
    candidates.map(({ itemId }: { itemId: string }) => {
      const item = getItem(itemId);
      return { item };
    });

  res.locals.items = items;

  next();
}

// route.ts
...
app.post('/items', parseCandidate, send);
...

 

요청 본문 중 candidates 를 받아서 그 안에 있는 itemId 를 통해 item 을 파싱해 res.locals 에 세팅하고 넘겨주는 미들웨어입니다.

이후의 비즈니스 로직에선 res.locals.items 를 받아서 어떻게 처리할지에 대해서만 집중하고, 그 전처리 과정은 미들웨어에서 처리하는

구조이죠.

 

위 예시에선 route 에서 /items 에 대한 POST 요청이 들어왔을 때 parseCandidate 만 미들웨어로 넣었습니다만, 실제 코드에선 더 많은 미들웨어를 전처리 과정으로 넣었고 Express 에선 이 부분이 (parseCandidate, parseA, parseB, ..) 와 같이 넣으면 next() 에 의해
파이프라인의 다음 단계가 호출되는 구조로 간단하게 되어 있어 쉽게 사용하고 있습니다.

 

위에 예시는 Express 에 한정적이긴 하지만, 잠깐 적은 것처럼 미들웨어는 파이프라인에서의 모든 종류 데이터에 대한 전처리 및 후처리를 나타내기도 합니다. 파이프라인에서 데이터가 처리되고 전파되는 방식에 strict 한 규칙은 없고, 아래와 같은 것들을 포함합니다.

 

  • 추가 속성 또는 기능을 사용한 데이터 추가
  • 데이터 불변성을 유지하고 처리 결과를 사본으로 반환

Express 라고 다른건 아니지만, 팀에서 파이프라인의 미들웨어로서 사용하고 있는 예시 코드도 잠깐 보도록 하겠습니다.

 

import fs from 'fs';
import { createGzip } from 'zlib';
import { pipeline } from 'stream';

class A {
  
  ...
  
  private gzip(originPath: string, gzPath: string): Promise<void> {
    return new Promise<void>((resolve, reject) => {
      const gzip = createGzip();
      const source = fs.createReadStream(originPath);
      const destination = fs.createWriteStream(gzPath);
      pipeline(source, gzip, destination, err => {
        err ? reject(err) : resolve();
      });
    });
  }
  
  ...
}

 

너무 일부 메서드만 뽑아와서 좀 뭔가 휑하지만.. Node.JS 에서 제공하는 stream 의 pipeline 을 사용한 예시입니다.

작성한 gzip 메서드의 기능은 원래 있는 파일을 gz 으로 압축한 파일을 떨구는 로직인데, Node.JS 에선 이런 pipeline 을 쉽게 사용할 수 있도록 모듈을 제공합니다. readStream 과 writeStream 만 열어서 가운데에 로직만 끼워넣으면 되는 형태죠.

파이프라인 내에서 readStream -> gzip -> writeStream 을 처리하는 형태로 gzip 이 미들웨어의 역할을 하는 것입니다.

 

Node.JS 에 너무 맞춰진 이번 포스팅이긴하지만, Node.JS 개발자라면 이러한 미들웨어 패턴을 활용할만한 포인트는 많을 것입니다.