배경

클라이언트 ←→ 서버 뿐만 아니라 Gateway ←→ Microservice 간 즉 서버간 통신에도 정상 응답이든 아니든 일관된 형태로 데이터를 주고 받고 싶었다.

REST API vs MessagePattern

Gateway와 Microservice간 통신을 진행함에 있어 해당 MessagePattern 같은 경우 REST API처럼 middleware와 같은 API 라이프사이클을 거치지 않는다. 그래서 이러한 과정을 거칠 수 있게 HTTP의 속성을 가져와 해당 흐름을 가져갈 수 있도록 하였다. 그리고 에러를 뱉는 부분도 REST API는 HTTP Exception이지만 MessagePattern은 RPC Exception이라는 것을 사용한다. (RPC에 대해서 나중에 더 공부해보자.) 따라서 해당 에러를 각각 대처할 수 있도록 RPC Exception을 만들었다. 하지만 그 과정 속에서 발생한 문제가 있었다.

  1. MessagePattern이 정상 응답을 내보낼 때에도 HTTP와 달리 status가 존재하지 않아 해당 응답의 형태를 바꿔주는 요소가 필요했다.
  2. REST API를 거치면 미들웨어의 Request와 Response를 받아 해당 로직을 로그로 찍을 수 있었다. 하지만 MessagePattern의 형태는 미들웨어로 잡을 수가 없었다.
  3. 하이브리드 앱에서 글로벌로 Exception Filter를 사용하면 RPC Exception을 거치지 못했다.
  4. RPC Exception에는 status가 존재하지 않아 status, data, message 3가지 속성을 가지는 일관된 요소의 형태로 응답을 내보낼 수가 없었다.
  5. 마지막으로 어떤 형태로 통신을 하든 응답을 받을 때 형식의 일관성을 가져 개발의 효율성을 높여야 하였다.

위 5가지 문제를 해결하면서 미들웨어, 인터셉터를 적절하게 사용할 필요가 있었다. Nest.js를 접한지 몇주 되지도 않았는데 해당 API 라이프사이클을 이해하기는 많이 어려웠다. 하지만 해당 기능들을 다 구현하고 나니 필요한 부분에 적재적소로잘 사용했다는 생각이 들어 뿌듯하였다.

해결 방법

https://cdn-images-1.medium.com/max/1600/1*HWuL31utXN0DOLB9TC6D5w.png

API Lifecycle made by jinsung_guri

전체적인 흐름은 게이트웨이에서 해당 요청을 거치고 여기서 마이크로서비스를 거쳐 요청을 처리하는 로직으로 구현하였다. 주로 게이트웨이에서는 로그를 찍는 미들웨어를 적용하였고 마이크로서비스를 호출하는 과정에서는 미들웨어 → 인터셉터 → 파이프 → 컨트롤러 → 인터셉터 → 필터를 거치는 전방향의 흐름에 대하여 대응하게 되었다.

Middleware의 역할

Gateway와 Microservice 앞단에 존재하는 Middleware는 요청과 응답을 로그로 찍어 서버에서 어떤 요청이 있었는지 확인할 수 있는 용도로 사용하였다. 그래서 프론트엔드와 연동을 할 때 어떤 요청이 있었는지 로그를 확인하면서 디버깅을 할 수 있도록 하였다. 그러나 MessagePattern의 요청 로그는 확인할 수가 없다는 단점이 있다.

Logging Middleware

// MessagePattern을 제외한 HTTP로 들어온 요소에 대한 로그를 보여주는 미들웨어
import { Injectable, Logger, NestMiddleware } from '@nestjs/common';
import { NextFunction, Request, Response } from 'express';

@Injectable()
export class LoggerMiddleware implements NestMiddleware {
  private logger: Logger = new Logger('HTTP');

  use(req: Request, res: Response, next: NextFunction) {
    const { body, headers, originalUrl } = req;
    const { statusCode } = res;

    // Request 출력
    const requestMessage = { headers, body, originalUrl };
    this.logger.log(`Request: ${JSON.stringify(requestMessage)}`);

    // Response 출력
    const response = res.write;
    const responseEnd = res.end;

    const chunkBuffers = [];
    res.write = (...chunks) => {
      const resArgs = chunks.map((chunk) => {
        if (!chunk) {
          res.once('drain', res.write);
        }
        return chunk;
      });

      if (resArgs[0]) {
        chunkBuffers.push(Buffer.from(resArgs[0]));
      }

      return response.apply(res, resArgs);
    };

    res.end = (...chunks) => {
      const resArgs = chunks.map((chunk) => {
        return chunk;
      });

      if (resArgs[0]) {
        chunkBuffers.push(Buffer.from(resArgs[0]));
      }

      const body = Buffer.concat(chunkBuffers).toString('utf8');

      const responseMessage = {
        statusCode,
        body: body || {},
        headers: res.getHeaders(),
      };

      this.logger.log(`Response: ${JSON.stringify(responseMessage)}`);
      return responseEnd.apply(res, resArgs);
    };

    if (next) {
      next();
    }
  }
}

Interceptor의 역할

Response로 내뱉는 Interceptor와 Request로 들어오는 Interceptor의 역할에는 차이가 있다. 먼저 Response로 내뱉는 Interceptor는 주로 컨트롤러가 내뱉는 결과를 status, data, message 이 형태로 바꿔주는 역할을 한다. 그리고 Microservice 와 Gateway 사이에 존재하는 Request Interceptor는 Middleware로 로그를 찍을 수 없는 MessagePattern에서의 Request, Response로그를 출력할 수 있도록 하는 역할을 한다.