NestJS 공식문서 Execution context

GGAE99·2023년 7월 24일
0

NestJS 공식 문서

목록 보기
11/33
post-thumbnail

Execution context

Nest는 여러 애플리케이션 컨텍스트들(예: Nest HTTP 서버 기반, 마이크로서비스 및 웹소켓 애플리케이션 컨텍스트)을 아울러서 작동하는 애플리케이션을 쉽게 작성할 수 있도록 몇 가지 유틸리티 클래스들을 제공합니다. 이 유틸리티들은 전반적인 컨트롤러, 메서드, 실행 컨텍스트를 아울러 동작하는 범용적인 guards, filters, interceptors를 만들 수 있도록 현재 실행 컨텍스트에 대한 정보를 제공합니다.

이 챕터에서는 ArgumentsHost, ExecutionContext 두 클래스를 다룹니다.

ArgumentsHost

ArgumentsHost 클래스는 핸들러에 전달 된 인자들을 찾을 수 있는 메서드들을 제공하며, 인자들을 찾을 수 있는 적절한 컨텍스트(예를 들면 HTTP, RPC(마이크로서비스), 웹소켓)를 선택할 수 있게 해줍니다. Nest는 ArgumentsHost의 인스턴스를 제공하며, 접근하려는 곳에서는 일반적으로 host라는 이름의 매개변수로 참조됩니다. 예를 들어, 예외 필터의 catch()메서드는 ArgumentsHost 인스턴스와 함께 호출됩니다.

ArgumentsHost는 단순히 핸들러의 인자들을 추상화한 것입니다. 예를 들어, HTTP 서버 애플리케이션(@nestjs/platform-express를 사용하는 경우)의 경우, host 객체는 Express[request, response, next] 배열을 캡슐화합니다. 여기서 request는 요청 객체를, response는 응답 객체를, next는 애플리케이션의 요청-응답 사이클을 제어하는 함수입니다. 반면에 GraphQL 애플리케이션에서의 host객체는 [root, args, context, info] 배열을 포함합니다.

Current application context

여러 애플리케이션 컨텍스트를 아울러 동작하는 일반적인 guards, filters, interceptors를 만들기 위해서는 현재 메서드가 동작하고 있는 애플리케이션 타입을 결정지어야 하며, 이는 ArgumentsHostgetType()메서드를 통해 가능합니다:

if (host.getType() === 'http') {
  // do something that is only important in the context of regular HTTP requests (REST)
} else if (host.getType() === 'rpc') {
  // do something that is only important in the context of Microservice requests
} else if (host.getType<GqlContextType>() === 'graphql') {
  // do something that is only important in the context of GraphQL requests
}

GqlContextType@nestjs/graphql패키지에서 import합니다.

이처럼, 사용 가능한 애플리케이션 타입을 활용하여 보다 범용적인 컴포넌트를 작성할 수 있습니다.

Host handler arguments

핸들러에 전달 된 인자를 찾기 위해 host 객체의 getArgs()메서드를 사용할 수 있습니다.

const [req, res, next] = host.getArgs();

getArgByIndex()메서드를 사용하여 특정 인자만 찾을 수 있습니다:

const request = host.getArgByIndex(0);
const response = host.getArgByIndex(1);

이 예제들에서 요청 객체 및 응답 객체를 인덱스를 통해 찾았는데, 일반적으로 이는 어플리케이션을 특정한 하나의 실행 컨텍스트에 국한시키는 것이므로 권장되지 않습니다. 대신에, host객체에는 적절한 어플리케이션 컨텍스트로 전환할 수 있는 유틸리티 메서드가 있으므로 이를 사용하여 더욱 안정되고 재사용가능한 코드를 작성할 수 있습니다. 아래는 컨텍스트를 전환하는 유틸리티 메서드들입니다.

/**
 * Switch context to RPC.
 */
switchToRpc(): RpcArgumentsHost;
/**
 * Switch context to HTTP.
 */
switchToHttp(): HttpArgumentsHost;
/**
 * Switch context to WebSockets.
 */
switchToWs(): WsArgumentsHost;

이전 예제를 switchToHttp() 메서드를 사용하여 다시 작성해보겠습니다. host.switchToHttp() 호출은 HTTP 애플리케이션 컨텍스트에 적합한 HttpArgumentsHost 객체를 반환합니다. HttpArgumentsHost 객체에는 원하는 객체를 추출하는 데 사용할 수 있는 두 가지 유용한 메서드가 있습니다. 또한 이 경우에는 네이티브 Express 형식의 객체를 반환하기 위해 Express 타입 어설션(type assertions)을 사용합니다:

const ctx = host.switchToHttp();
const request = ctx.getRequest<Request>();
const response = ctx.getResponse<Response>();

마찬가지로, WsArgumentsHostRpcArgumentsHost에는 각각 마이크로서비스와 웹소켓 컨텍스트에서 적절한 객체를 반환하는 메서드가 있습니다. WsArgumentsHost의 메서드는 다음과 같습니다:

export interface WsArgumentsHost {
  /**
   * Returns the data object.
   */
  getData<T>(): T;
  /**
   * Returns the client object.
   */
  getClient<T>(): T;
}

이어서 RpcArgumentsHost의 메서드들입니다:

export interface RpcArgumentsHost {
  /**
   * Returns the data object.
   */
  getData<T>(): T;

  /**
   * Returns the context object.
   */
  getContext<T>(): T;
}

ExecutionContext class

ExecutionContextArgumentsHost를 상속 받으며, 현재 실행 프로세스에 대한 추가적인 세부사항을 제공합니다. ArgumentsHost와 같이, Nest는 우리가 원하는 곳에 ExecutionContext 인스턴스를 제공하며, 가드의 canActive()메서드나 인터셉터의 intercept()메서드가 그 예시입니다. 인스턴스는 아래 메서드들을 제공합니다:

export interface ExecutionContext extends ArgumentsHost {
  /**
   * Returns the type of the controller class which the current handler belongs to.
   */
  getClass<T>(): Type<T>;
  /**
   * Returns a reference to the handler (method) that will be invoked next in the
   * request pipeline.
   */
  getHandler(): Function;
}

getHandler()메서드는 실행 될 핸들러의 참조를 반환합니다. getClass()메서드는 그 핸들러가 속한 Controller클래스의 타입을 반환합니다. HTTP 컨텍스트에서의 예시로, 현재 처리되는 요청이 POST요청이고 CatsControllercreate()메서드가 실행된다면 getHandler()create()메서드의 참조를 반환하고 getClass()는 CatsController의 인스턴스가 아닌 타입을 반환합니다.

const methodKey = ctx.getHandler().name; // "create"
const className = ctx.getClass().name; // "CatsController"

현재 클래스와 핸들러 메서드 둘의 참조 모두에 접근하는 기능은 굉장한 유연성을 제공합니다. 가장 중요한 건, 연관된 가드나 인터셉터에서 @SetMetadata()데코레이터를 통해 지정한 메타데이터에 접근할 수 있는 기회가 된다는 점입니다. 이 사례에 대해서는 아래에서 다루어 보겠습니다.

Reflection and metadata

Nest는 사용자 정의 메타데이터 붙일 수 있는 기능을 제공하며 이는 라우트 핸들러에서 @SetMetadata()데코레이터를 통해 가능합니다. 이후 클래스 내에서 이 메타데이터에 접근하여 특정 결정을 내릴 수 있습니다.

// cats.controller.ts
@Post()
@SetMetadata('roles', ['admin'])
async create(@Body() createCatDto: CreateCatDto) {
  this.catsService.create(createCatDto);
}

@SetMetadata() 데코레이터는 @nestjs/common 패키지에서 import합니다.

위의 예제에서는 roles라는 메타데이터를 create()메서드에 붙였습니다(roles는 메타데이터 키이며 ['admin']은 그 값입니다). 라우트에서 @SetMetadata()데코레이터를 직접 쓰는 것은 그다지 좋은 방법은 아닙니다. 대신에 아래와 같이 사용자 정의 데코레이터를 작성합니다:

// roles.decorator.ts
import { SetMetadata } from '@nestjs/common';

export const Roles = (...roles: string[]) => SetMetadata('roles', roles);

이러한 접근 방식이 훨씬 깔끔하고 가독성 있으며, 타입을 더욱 엄격하게 관리할 수 있습니다. 이제 @Roles()데코레이터를 create()메서드에 사용합니다.

// cats.controller.ts
@Post()
@Roles('admin')
async create(@Body() createCatDto: CreateCatDto) {
  this.catsService.create(createCatDto);
}

라우트에서 role이라는 사용자 정의 메타데이터에 접근하기 위해서는 Reflector 헬퍼 클래스를 사용하면 됩니다. 이 클래스는 별도의 작업 없이 그저 @nestjs/core에서 가져다 사용하기만 하면 됩니다. Reflector 일반적인 방법으로 클래스에 주입시킬 수 있습니다:

// roles.guard.ts
@Injectable()
export class RolesGuard {
  constructor(private reflector: Reflector) {}
}

Reflector 클래스는 @nestjs/core 패키지에서 import합니다.

이제 핸들러 메타데이터를 읽기 위해 get()메서드를 사용합니다.

const roles = this.reflector.get<string[]>('roles', context.getHandler());

Reflectorget 메서드를 사용하면 두 개의 인자를 전달하여 메타데이터에 쉽게 접근할 수 있습니다. 첫 번째 인자는 메타데이터 key이며, 두 번째 인자는 메타데이터를 검색할 context(데코레이터 대상)입니다. 이 예제에서 지정된 key는 'roles'입니다(위의 roles.decorator.ts 파일과 해당 파일에서 SetMetadata()를 호출하는 부분을 참조하세요). 컨텍스트는 context.getHandler()를 호출함으로써 제공되며, 이는 현재 처리 중인 라우트 핸들러의 메타데이터를 추출하는 데 사용됩니다. 기억하세요, getHandler()는 라우트 핸들러 함수에 대한 참조를 반환합니다.

또한, 컨트롤러 수준에서 메타데이터를 적용하여 컨트롤러 클래스의 모든 라우트에 적용할 수 있습니다.

// cats.controller.ts
@Roles('admin')
@Controller('cats')
export class CatsController {}

이 경우, 컨트롤러 메타데이터를 추출하기 위해 context.getClass()를 두 번째 인자로 전달합니다 (메타데이터 추출을 위해 컨트롤러 클래스를 컨텍스트로 제공하기 위함):

//roles.guard.ts
const roles = this.reflector.get<string[]>('roles', context.getClass());

하나의 메타데이터를 다양한 레벨에서 동시에 지정한다면, 다수의 컨텍스트로부터 메타데이터를 추출하고 합쳐야 할 것입니다. Reflector 클래스는 이를 위해 두 가지의 유틸리티 메서드를 제공합니다. 이 메서드들은 컨트롤러와 메서드 두 곳 모두에서 메타데이터를 추출하고, 각기 다른 방법으로 조합합니다.

'roles'메타데이터를 두 가지 레벨에서 동시에 공급한다고 가정해 봅시다.

// cats.controller.ts
@Roles('user')
@Controller('cats')
export class CatsController {
  @Post()
  @Roles('admin')
  async create(@Body() createCatDto: CreateCatDto) {
    this.catsService.create(createCatDto);
  }
}

위 코드의 의도가 'user'를 role을 기본으로 취급하되 몇몇 메서드에서만 role을 덮어씌우고자 하는 것이라면, getAllAndOverride() 메서드를 사용하면 됩니다.

// roles.guard.ts
const roles = this.reflector.getAllAndOverride<string[]>('roles', [
  context.getHandler(),
  context.getClass(),
]);

created() 메서드의 컨텍스트 속에서 동작하는 가드에서 위와 같이 코드를 작성하면 roles의 값은 ['admin']가 됩니다.

두 메타데이터를 병합하려면 getAllAndMerge() 메서드를 사용합니다. 이 메서드는 두 배열을 하나로 병합합니다.

// roles.guard.ts
const roles = this.reflector.getAllAndMerge<string[]>('roles', [
  context.getHandler(),
  context.getClass(),
]);

위와 같이 작성하면 roles의 값은 ['user', 'admin']가 됩니다.

이 두 가지 메서드를 사용할 때에는 첫 번째 인자로 메타데이터 키를, 두 번재 인자로는 메타데이터 대상 컨텍스트(getHandler()getClass() 메서드를 호출한 결과)가 담긴 배열을 건네야 합니다.

질문 및 생각

  • 실행 컨텍스트란게 뭐야?
    코드가 실행되고 연산되는 범위를 나타내는 추상적인 개념.
    우리가 코드를 작성하고 실행한다면 실행 컨텍스트(Execution Context) 내부에서 실행되고 있는 것입니다. 즉 코드들이 실행되기 위한 환경이자 하나의 박스이자 컨테이너라 볼 수 있습니다.

  • 핸들러는 뭐야?
    컨트롤러에서 getCats(), getCatById(), createCat()과 같은 메서드들이 핸들러

  • host 객체는 바로 쓸 수 있는 거야?
    얍얍 쓸 수 있습니다.

import { ArgumentsHost } from '@nestjs/common';
host: ArgumentsHost
  • rpc는 뭐야?
    RPC (Remote Procedure Call)는 분산 시스템에서 다른 컴퓨터나 프로세스에 있는 함수 또는 프로시저를 호출하는 프로그래밍 모델을 말합니다. 이 모델을 사용하면 원격 시스템의 함수를 마치 로컬 함수처럼 호출할 수 있으며, 데이터를 주고 받을 수 있습니다.
    NestJS에서 RPC 컨텍스트는 마이크로서비스 아키텍처를 구현하는 데 사용됩니다.

나의 정리

자바 스크립트 에서의 실행 컨텍스트와 여기서 설명하는 Nest의 애플리케이션 컨텍스트는 다른 것 같다.

지금 설명하는 것은 실행하는 애플리케이션의 종류에 따라 인자를 쉽게 받을 수 있도록 nest에서 코드를 제공해주는 것 이라고 생각한다.

요청에 대한 req, res를 전역으로 사용하기 위함

실행 컨텍스트 넘 어렵다..

1개의 댓글

comment-user-thumbnail
2023년 7월 24일

좋은 글 감사합니다.

답글 달기