NestJS 공식 문서 Versioning

GGAE99·2023년 7월 9일
0

NestJS 공식 문서

목록 보기
17/33

Versioning

이 장은 HTTP 기반 응용 프로그램에만 해당됩니다.

Versioning은 동일한 애플리케이션 내에서 컨트롤러 또는 개별 경로의 다른 버전을 사용할 수 있도록 해줍니다. 애플리케이션은 자주 변경되며, 이전 버전의 애플리케이션을 계속 지원함과 동시에 많은 변경 사항을 변경해야 하는 상황이 오기도 합니다.

지원되는 버전 관리 유형은 다음 4가지 입니다:

URI Versioning : 버전은 요청의 URI 내에서 전달됩니다 (기본값)

Header Versioning : 사용자 지정 요청 헤더에서 버전을 지정합니다

Media Type Versioning : 요청의 Accept 헤더에서 버전을 지정합니다

Custom Versioning : 요청의 어떤 측면이든 버전을 지정할 수 있습니다. 사용자 정의 함수가 해당 버전을 추출하는 데 사용됩니다.

URI Versioning Type

https://example.com/v1/routehttps://example.com/v2/route 같이 uri에 version을 포함합니다.
URI 버전 관리를 사용하면 전역 경로 접두사 (global path prefix)(존재하는 경우) 다음과 컨트롤러 또는 route paths 앞에 버전이 URI에 자동으로 추가됩니다.

아래와 같이 작성하면 URI Versioning을 사용할 수 있습니다.

// main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { VersioningType } from '@nestjs/common';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  app.enableVersioning({
    type: VersioningType.URI,
  });
  await app.listen(3000);
}
bootstrap();

URI의 버전은 기본적으로 v로 자동 접두사가 붙지만 접두사 키를 원하는 접두사로 설정하거나 비활성화하려는 경우 false로 설정하여 접두사 값을 구성할 수 있습니다.

VersioningType 열거형은 type 속성에 사용할 수 있으며 @nestjs/common 패키지에서 가져옵니다.

Header Versioning Type

헤더 버전 관리는 사용자 지정 요청 헤더를 사용하여 헤더 값이 요청에 사용할 버전이 되는 버전을 지정합니다.

헤더 버전 관리를 위한 HTTP 요청의 예:
애플리케이션에 대한 헤더 버전 관리를 활성화하려면 다음을 수행하십시오.

//main.ts
const app = await NestFactory.create(AppModule);
app.enableVersioning({
  type: VersioningType.HEADER,
  header: 'Custom-Header',
});
await app.listen(3000);

header 속성은 요청의 버전을 포함할 헤더의 이름이어야 합니다.

// 요청 예시
GET /users HTTP/1.1
Host: example.com
Custom-Header: v=2

Media Type Versioning Type

미디어 유형 버전 관리(Media Type Versioning)는 요청의 Accept 헤더를 사용하여 버전을 지정합니다.

Accept 헤더 내에서 버전은 세미콜론(;)으로 미디어 유형과 분리됩니다. 버전은 Accept: application/json;v=2와 같은 형식으로 버전을 나타내는 키-값 쌍을 포함해야 합니다. 이 때, 키는 버전을 구성하는 데 사용되는 접두사로 취급되며, 키와 구분자를 포함하도록 구성됩니다.

const app = await NestFactory.create(AppModule);
app.enableVersioning({
  type: VersioningType.MEDIA_TYPE,
  key: 'v=',
});
await app.listen(3000);

key 속성은 버전을 포함하는 키-값 쌍의 키와 구분자를 지정해야 합니다.
예를 들어, Accept: application/json;v=2와 같은 형식의 헤더를 사용하여 버전을 전달하고자 할 경우, key 속성은 'v='로 설정됩니다.

// 요청 예시
GET /users HTTP/1.1
Host: example.com
Accept: application/json;v=2

Custom Versioning Type

Custom Versioning은 요청의 어떤 측면을 사용하여 버전(또는 버전들)을 지정하는 데 사용됩니다. 들어오는 요청은 추출 함수를 사용하여 분석되며, 이 함수는 문자열 또는 문자열 배열을 반환합니다.

요청자가 여러 버전을 지정한 경우, 추출 함수는 가장 높은 버전에서 가장 낮은 버전까지의 순서로 정렬된 문자열 배열을 반환할 수 있습니다. 버전은 가장 높은 것부터 가장 낮은 것까지의 순서로 라우트에 매칭됩니다. 버전은 가장 높은 것부터 가장 낮은 것까지의 순서로 라우트에 매칭됩니다.

추출 함수가 빈 문자열 또는 빈 배열을 반환하면 어떠한 라우트도 매칭되지 않고 404 오류가 반환됩니다.

예를 들어, 들어오는 요청이 버전 1, 2, 3을 지원한다고 지정된 경우, 추출 함수는 [3, 2, 1]을 반환해야 합니다. 이렇게 함으로써 가장 높은 버전의 라우트가 먼저 선택되도록 보장됩니다.

만약 [3, 2, 1] 버전이 추출되었지만, 실제로 버전 2와 1에 대한 라우트만 존재하는 경우, 버전 2에 매칭되는 라우트가 선택됩니다 (버전 3은 자동으로 무시됩니다).

추출 함수로부터 반환된 배열을 기반으로 가장 높은 매칭 버전을 선택하는 것은 Express 어댑터의 설계 제한으로 인해 신뢰성이 떨어집니다. Express에서는 하나의 버전(문자열 또는 1개 요소의 배열)을 사용하는 것은 제대로 작동합니다. Fastify는 가장 높은 매칭 버전 선택과 단일 버전 선택을 모두 올바르게 지원합니다.

애플리케이션에 대해 Custom Versioning을 사용하려면 다음과 같이 추출기 함수를 생성하고 애플리케이션에 전달합니다.

// 사용자 지정 헤더에서 버전 목록을 가져와서 정렬된 배열로 변환하는 예제 추출기.
// 이 예에서는 Fastify를 사용하지만 Express 요청도 비슷한 방식으로 처리할 수 있습니다.
const extractor = (request: FastifyRequest): string | string[] =>
  [request.headers['custom-versioning-field'] ?? '']
     .flatMap(v => v.split(','))
     .filter(v => !!v)
     .sort()
     .reverse()

const app = await NestFactory.create(AppModule);
app.enableVersioning({
  type: VersioningType.CUSTOM,
  extractor,
});
await app.listen(3000);
// express 예시
import express from 'express';

const extractor = (req: express.Request): string | string[] => {
  const customVersioningHeader = req.headers['custom-versioning-field'] || '';
  const versions = customVersioningHeader.split(',').filter(Boolean).sort().reverse();
  return versions.length > 0 ? versions[0] : '';
};

async function bootstrap() {
  const app = await NestFactory.create(AppModule, new ExpressAdapter(express()));

  app.use((req, res, next) => {
    req.version = extractor(req);
    next();
  });

  await app.listen(3000);
}

bootstrap();
// 요청 예시
GET /users HTTP/1.1
Host: example.com
custom-versioning-field: 2,1
// 출력 예시
Handling /users request for version 2

Usage

버전 관리를 통해 컨트롤러, 개별 경로의 버전 지정 및 특정 리소스에서 버전 관리를 제외할 수 있는 방법을 제공합니다. 버전 관리의 사용법은 애플리케이션이 사용하는 버전 관리 유형에 관계없이 동일합니다.

주의사항
만약 애플리케이션에 버전 관리가 활성화되어 있지만 컨트롤러나 경로에서 버전을 지정하지 않은 경우, 해당 컨트롤러/경로로의 모든 요청은 404 응답 상태 코드를 받게 됩니다. 마찬가지로, 버전을 포함한 요청을 받았지만 해당 버전에 해당하는 컨트롤러나 경로가 없는 경우에도 404 응답 상태 코드를 반환합니다.

Controller versions

import { Controller, Get } from "@nestjs/common";

@Controller({
    version: '1',
    path: 'cats',
})
export class CatsControllerVersion {
  @Get()
  findAll(): string {
    return 'This action returns all cats for version 1';
  }
}

위와 같은 방식으로 컨트롤러를 설정하면
http://localhost:3000/v1/cats 요청으로 'This action returns all cats for version 1' 의 반환값을 받을 수 있다.

Route versions

@Controller('cats')
export class CatsController {
    constructor(){
        console.log('CatsController constructor');
    }

    @Version('3')
    @Get()
    findAll(): string {
        return 'This action returns all cats v3';
    }
}

http://localhost:3000/v3/cats 요청으로 'This action returns all cats v3' 의 반환값을 받을 수 있다.

Multiple versions

import { Controller, Get } from "@nestjs/common";

@Controller({
    version: ['1','2'],
    path: 'cats',
})
export class CatsControllerVersion {
  @Get()
  findAll(): string {
    return 'This action returns all cats 1 or 2';
  }
}

http://localhost:3000/v1/cats 요청 또는 http://localhost:3000/v2/cats 요청으로 'This action returns all cats 1 or 2' 의 반환값을 받을 수 있다.

Version "Neutral"

버전과는 관계없이 일부 컨트롤러나 경로는 동일한 기능을 가질 수 있습니다.
이를 수용하기 위해 버전을 VERSION_NEUTRAL 심볼로 설정할 수 있습니다.

버전이 포함되지 않은 요청이나 어떤 버전이든 상관없는 요청은 VERSION_NEUTRAL 컨트롤러나 경로에 매핑됩니다.

주의사항: URI 버전 관리에서는 VERSION_NEUTRAL 리소스에는 URI에 버전이 표시되지 않습니다.

import { Controller, Get, VERSION_NEUTRAL } from '@nestjs/common';

@Controller({
  version: VERSION_NEUTRAL,
})
export class CatsControllerWithNeutral {
  @Get('cats')
  findAll(): string {
    return 'This action returns all cats regardless of version';
  }
}

Global default version

각 컨트롤러 또는 개별 경로마다 버전을 지정하고 싶지 않거나, 버전이 지정되지 않은 모든 컨트롤러/경로에 대해 특정 버전을 기본 버전으로 설정하고자 한다면, defaultVersion을 다음과 같이 설정할 수 있습니다.

// main.ts
app.enableVersioning({
  // ...
  defaultVersion: '1'
  // or
  defaultVersion: ['1', '2']
  // or
  defaultVersion: VERSION_NEUTRAL
});

Middleware versioning

미들웨어도 버전 관련 메타데이터를 사용하여 특정 경로의 버전에 대해 미들웨어를 구성할 수 있습니다. 이를 위해 MiddlewareConsumer.forRoutes() 메서드의 매개변수 중 하나로 버전 번호를 제공하면 됩니다.

//app.module.ts
import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
import { LoggerMiddleware } from './common/middleware/logger.middleware';
import { CatsModule } from './cats/cats.module';
import { CatsController } from './cats/cats.controller';

@Module({
  imports: [CatsModule],
})
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(LoggerMiddleware)
      .forRoutes({ path: 'cats', method: RequestMethod.GET, version: '2' });
  }
}

위의 코드를 사용하면 LoggerMiddleware/cats 엔드포인트의 버전 '2'에만 적용됩니다.

미들웨어는 URI, 헤더, 미디어 유형 또는 사용자 정의와 같이 이 섹션에 설명된 모든 버전 지정 유형과 함께 작동합니다.

질문 및 생각

  • URI Versioning이 가장 많이 쓰이는 것으로 조사
  • Global prefix
  • 어떤 순서로 version을 먼저 찾는가? => 이미 CatController에 구현되어있던 /cats 경로에 메소드를 먼저 실행하고 주석 처리시 CatsControllerWithNeutral의 메소드를 실행하는 결과가 나왔지만, 왜 그런지 이해하지 못함

0개의 댓글