메인 콘텐츠로 이동하기
  1. Posts/

기존 Express API에 routing-controllers를 선택한 이유

·414 자·2 분
목차
전체 코드는 GitHub 저장소에서 확인할 수 있습니다.

비교하게 된 이유 #

이미 서버 구성, middleware와 controller가 있는 Express API에 type-safe routing과 OpenAPI 문서를 추가하려고 했습니다. 이때 비교한 선택지는 routing-controllerstsoa였습니다.

Nest.js는 이번 비교에서 제외했습니다. 기존 Express 애플리케이션을 유지하려는 상황에서 더 큰 application framework로 옮기는 선택은 범위가 달랐기 때문입니다.

두 라이브러리의 차이 #

두 라이브러리 모두 TypeScript decorator를 사용하지만, route를 연결하는 위치가 다릅니다.

routing-controllers tsoa
Route 등록 decorator가 붙은 controller class를 runtime에 등록 decorator가 붙은 controller에서 route code 생성
기존 Express 앱 useExpressServer로 기존 app에 연결 생성된 route를 RegisterRoutes로 등록
Middleware 기존 Express middleware와 class middleware를 함께 사용 생성된 route와 tsoa runtime 흐름에 맞춰 구성
OpenAPI routing-controllers-openapi 같은 integration 사용 tsoa CLI로 spec 생성
이 프로젝트에서의 설정 비용 decorator와 routing-controllers 옵션 추가 tsoa config, 생성 명령과 생성 결과 관리 추가

어느 라이브러리가 항상 더 낫다고 판단한 것은 아닙니다. 이미 만들어진 application을 얼마나 바꿔야 하는지를 기준으로 선택했습니다.

Routing과 middleware #

routing-controllers는 기존 Express application과 controller를 가까운 경계에서 연결합니다. 이 프로젝트에서는 이미 생성한 app을 useExpressServer에 전달하고, route prefix와 controller, middleware, interceptor, authorization checker를 함께 설정했습니다.

useExpressServer(app, {
  routePrefix: '/api',
  controllers: [/* controller paths */],
  middlewares: [HttpErrorHandler, LoggingHandler],
  interceptors: [/* interceptor paths */],
  authorizationChecker
});

이 구조에서는 필요한 곳에서 RequestResponse를 계속 사용할 수 있었습니다. route 선언 방식을 바꾼다는 이유만으로 middleware까지 다시 작성할 필요도 없었습니다.

반면 tsoa는 route 생성 단계가 필요합니다. 보통 TypeScript compile 전이나 development 과정에서 CLI를 실행합니다.

{
  "scripts": {
    "build": "tsoa spec-and-routes -c=tsoa.production.json && tsc -p ./",
    "dev": "nodemon -x tsoa spec-and-routes -c=tsoa.json"
  }
}

생성된 route file을 기준으로 운영하려는 project라면 자연스러운 방식입니다. 다만 기존 Express server에서는 별도의 결과물과 실행 단계를 함께 관리해야 했습니다.

Documentation #

두 선택지 모두 OpenAPI 문서를 만들 수 있지만, 연결하는 방식은 달랐습니다.

routing-controllers에서는 controller와 DTO에 이미 쌓인 metadata를 활용하기 위해 routing-controllers-openapiclass-validator-jsonschema를 사용했습니다.

const spec = routingControllersToSpec(
  getMetadataArgsStorage(),
  routingControllerOptions,
  additionalProperties
);

tsoa는 config file을 기준으로 spec과 route file을 생성합니다. entry file, controller 경로, output directory, base path와 compiler option 등을 설정해야 합니다.

{
  "entryFile": "src/server.ts",
  "controllerPathGlobs": ["src/**/*.controller.ts"],
  "spec": {
    "outputDirectory": "src",
    "basePath": "/api",
    "specVersion": 3
  },
  "routes": {
    "routesDir": "src",
    "basePath": "/api"
  }
}

설정이 끝난 뒤에는 생성 방식이 편리합니다. 다만 이 project에서는 이미 route와 문서화 설정이 있었기 때문에, 기존 구조에 바로 연결할 수 있는 쪽이 더 적합했습니다.

내가 선택한 방식 #

이 project에서는 기존 Express 경계를 유지할 수 있다는 이유로 routing-controllers를 선택했습니다. route를 decorator 기반 controller로 옮기면서도 middleware 흐름은 유지했고, 같은 controller metadata로 OpenAPI 문서도 만들 수 있었습니다.

그렇다고 tsoa가 나쁜 선택이라는 뜻은 아닙니다. 생성된 route file과 명확한 compile-time 생성 단계를 중심으로 project를 구성한다면 판단은 달라질 수 있습니다.

참고 #