Logo

NestJS에서 API 버전 관리하기(Versioning)

이번 글에서는 NestJS에서 API의 버전을 체계적으로 관리하는 방법에 대해서 배워보도록 하겠습니다.

API Versioning이란?

REST API와 같은 서버 애플리케이션을 운영하다 보면, 부득이하게 클라이언트에 큰 영향을 줄 수 있는 위험한 변경을 해야 할 때가 있는데요. API Versioning, 즉 버전 관리를 통해서, 우리는 서버 측 API 변경에 따른 클라이언트의 영향을 최소화하고, API의 호환성과 안정성을 높일 수 있습니다.

버전 관리가 이루어지는 API는 보통 클라이언트에게 v1, v2, v3… 이런 식으로 여러 버전의 API를 제공하는데요. 그리고 클라이언트 애플리케이션에 문제를 일으킬 소지가 있는 변경이 발생하면 버전을 올리게 됩니다. 따라서, 클라이언트는 기존 버전을 사용하다가, 준비가 되었을 때 신규 버전으로 넘어갈 수 있죠.

다시 말해서, API Versioning을 통해서 서버 측에서는 보다 유연하게 API 변경 사항을 배포할 수 있고, 클라이언트 측에서는 위험한 변경 사항을 인지한 상태에서 충분히 테스트 후에 버전 업그레이드를 할 수 있게 됩니다.

API 버전 관리 전략

API 버전 관리하는데는 다양한 전략이 사용되는데요. 대표적으로 URL 경로를 사용하는 방법과, HTTP 요청 헤더를 사용하는 방법을 들 수 있습니다.

URL 경로 안에 버전을 명시하는 것은 외부에 공개된 API에서 가장 흔하게 취하는 전략입니다. URL만 보고도 바로 버전을 파악할 수 있는 장점이 있죠.

GET /api/v1/users

Accept 요청 헤더나 클라이언트와 약속한 다른 HTTP 요청 헤더에 버전을 명시하는 전략도 많이 볼 수 있는데요. 버전을 일종의 메타데이터(metadata)라고 보면 HTTP 요청 헤더를 쓰는 것이 자연스럽습니다.

GET /api/users

Accept: application/json;v=1

본 포스팅에서는 둘 중에 좀 더 흔하게 볼 수 있는 전략인 URL 경로에 버전을 명시하는 방법으로 한번 실습을 진행해보도록 하겠습니다.

실습 프로젝트 구성

먼저 간단한 실습을 위해서 NestJS 프로젝트가 하나 필요할 것 같은데요. 터미널에서 NestJS CLI 도구의 nest new 명령어를 실행하여 새로운 프로젝트를 구성하도록 하겠습니다.

$ nest new our-nestjs
⚡  We will scaffold your app in a few seconds..

? Which package manager would you ❤️  to use? (Use arrow keys)npm
  yarn
  pnpm

NestJS CLI를 설치하고 NestJS 프로젝트를 구성하는 기본적인 방법은 관련 포스팅을 참고 바랍니다.

Versioning 활성화

NestJS에서 버전 관리를 하려면 우선 해당 기능을 활성화시켜야 합니다.

@nestjs/common 모듈에서 제공하는 VersioningType이라는 이넘(Enum)에는 위에서 설명드린 여러가지 버전 관리 전략이 들어있는데요. 실습에서는 URL 경로에 버전을 명시하는 VersioningType.URI를 사용하도록 하겠습니다.

버전 관리 활성화는 main.ts 파일에서 애플리케이션이 생성되자마자 해주면 되는데요. 애플리케이션 객체의 enableVersioning 메서드를 호출할 때, type 옵션을 VersioningType.URI 설정해주면 됩니다.

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

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

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

컨트롤러에 버전 적용

@Controller 데코레이터(Decorator)를 통해서 컨트롤러 수준에서 손쉽게 버전을 적용할 수 있습니다.

nest new 명령어가 생성해준 AppController에 경로를 "hello"로, 그리고 버전을 "1"로 설정해주겠습니다. (NestJS가 자동으로 앞에 v를 붙여주기 때문에 v1으로 설정하시면 안 됩니다.)

app.controller.ts
import { Controller, Get } from "@nestjs/common";
import { AppService } from "./app.service";

@Controller({ path: "hello", version: "1" })export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get()
  getHello(): string {
    return this.appService.getHello();
  }
}

이제 터미널에서 curl 명령어로 http://localhost:3000/v1/hello을 찔러보면 Hello World!가 응답되는 것을 확인할 수 있으실 겁니다.

터미널
$ curl http://localhost:3000/v1/hello
Hello World!

라우트에 버전 적용

버전은 개별 컨트롤러 메서드, 즉 라우트(route)에도 @Version 데코레이터를 사용하여 적용해줄 수 있는데요. 이 경우, 컨트롤러 클래스 수준에서 설정해준 버전보다 컨트롤러 메서드 수준에서 설정해준 버전이 우선하게 됩니다.

"Hello v2!"를 반환하는 getHelloV2() 메서드를 추가한 후에, 버전을 "2"로 설정해주겠습니다.

app.controller.ts
import { Controller, Get, Version } from "@nestjs/common";import { AppService } from "./app.service";

@Controller({ path: "hello", version: "1" })
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get()
  getHello(): string {
    return this.appService.getHello();
  }

  @Version("2")  @Get()  getHelloV2(): string {    return "Hello Version 2!";  }}

NestJS 앱을 재구동한 후에, 서버에 http://localhost:3000/v2/hello로 요청을 보내보면 Hello v2!가 응답되는 것을 확인하실 수 있을 거에요.

터미널
curl http://localhost:3000/v2/hello
Hello Version 2!

버전이 누락된 요청 처리

만약에 클라이언트가 요청에 버전을 명시해주지 않았을 때는 서버에서 어떻게 처리해야 할까요? 404 Not Found를 응답해줄 수도 있겠지만, 최신 버전에 대한 응답을 제공할 수도 있을 것입니다.

요청에 버전이 누락되었을 때, 특정 컨트롤러나 라우트에게 처리를 맡기고 싶다면, 버전을 VERSION_NEUTRAL으로 설정해주면 됩니다.

getHelloV2() 메서드가 v2 버전뿐만 아니라 버전이 누락된 요청도 처리할 수 있도록 수정해보겠습니다.

app.controller.ts
import { Controller, Get, Version, VERSION_NEUTRAL } from "@nestjs/common";import { AppService } from "./app.service";

@Controller({ path: "hello", version: "1" })
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get()
  getHello(): string {
    return this.appService.getHello();
  }

  @Version(["2", VERSION_NEUTRAL])  @Get()  getHelloV2(): string {    return "Hello Version 2!";  }}

@Version 데코레이터를 사용할 때, 여러 버전을 배열로 설정할 수 있는 부분을 눈여겨 보세요.

NestJS 앱을 재구동한 후에, http://localhost:3000/hello를 찔러보면 http://localhost:3000/v2/hello을 찔렀을 때와 동일한 응답이 돌아올 것입니다.

터미널
$ curl http://localhost:3000/hello
Hello Version 2!

디폴트 버전 설정

애플리케이션 수준에서 기본으로 사용될 버전을 설정해줄 수도 있는데요. 이렇게 설정해준 버전은 컨트롤러나 라우트 수준에서 버전을 설정해주지 않았을 때 적용되게 됩니다.

main.ts 파일을 열고, 버전 관리를 활성화할 때, 디폴트 버전을 "3"으로 설정해보겠습니다.

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

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

  app.enableVersioning({
    type: VersioningType.URI,
    defaultVersion: '3',  });

  await app.listen(3000);
}
bootstrap();

그 다음, 컨트롤러 파일을 열고, @Controller 데코레이터로 부터 버전 설정을 제거합니다.

app.controller.ts
import { Controller, Get, Version, VERSION_NEUTRAL } from "@nestjs/common";
import { AppService } from "./app.service";

@Controller({ path: "hello" })export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get()
  getHello(): string {
    return this.appService.getHello();
  }

  @Version(["2", VERSION_NEUTRAL])
  @Get()
  getHelloV2(): string {
    return "Hello Version 2!";
  }
}

이제 http://localhost:3000/v3/hello를 찔러보면, 기존에 v1 버전에서 보았던 응답과 동일한 응답을 보실 수 있으실 것입니다.

$ curl http://localhost:3000/v3/hello
Hello World!

반면에 기존에 잘 작동하던 http://localhost:3000/v1/hello은 404 응답 코드를 응답하게 됩니다.

$ curl http://localhost:3000/v1/hello
{"message":"Cannot GET /v1/hello","error":"Not Found","statusCode":404}

다시 @Controller 데코레이터에 버전 설정을 추가해주면 해당 버전을 잘 처리해주겠죠?

전역 경로 접두어와 함께 사용

전역 경로 접두어(global path prefix)와 버전 관리를 같이 하는 경우 약간의 주의가 필요한데요. 컨트롤러나 라우트 수준에서 설정해준 경로는 버전 다음에 나오지만, 전역 경로 접두어는 버전 앞에 나옵니다.

예를 들어, 애플리케이션의 setGlobalPrefix() 메서드를 통해서 전역 경로 접두어로 "api"를 설정해주겠습니다.

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

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

  app.setGlobalPrefix('api');
  app.enableVersioning({
    type: VersioningType.URI,
    defaultVersion: '3',
  });

  await app.listen(3000);
}
bootstrap();

그러면 API를 호출할 때 http://localhost:3000/api/v2/hello처럼 URL 전체 경로를 구성하셔야 합니다. 버전 부분이 전역 경로 접두어와 라우트 경로 사이에 있는 게 보이시죠? 👀

$ curl http://localhost:3000/api/v2/hello
Hello Version 2!

전체 코드

실습 프로젝트의 코드는 아래에서 직접 확인하고 실행해볼 수 있습니다.

마치면서

지금까지 실습을 통해서 NestJS에서 어떻게 버전 관리를 할 수 있는지 알아보았습니다. 개발하시는 API의 버전을 잘 관리하셔서 클라이언트에게 좋은 경험을 제공하실 수 있으셨으면 좋겠습니다.

NestJS에 관련된 다른 포스팅은 관련 태그를 참고 바라겠습니다.