Execution context

이 글은 Nest 공식문서를 번역한 글입니다. 원문 (opens in a new tab)

Nest는 여러 애플리케이션 컨텍스트(예: Nest HTTP 서버 기반, 마이크로서비스 및 웹소켓 애플리케이션 컨텍스트)에서 작동하는 애플리케이션을 쉽게 작성할 수 있도록 도와주는 여러 유틸리티 클래스를 제공합니다. 이러한 유틸리티는 현재 실행 컨텍스트에 대한 정보를 제공하여 광범위한 컨트롤러, 메서드 및 실행 컨텍스트에서 작동할 수 있는 일반 guards (opens in a new tab)filters (opens in a new tab)interceptors (opens in a new tab)를 구축하는 데 사용할 수 있습니다.

이 장에서는 이러한 클래스 두 가지를 다룹니다: ArgumentsHostExecutionContext입니다.

ArgumentsHost class# (opens in a new tab)

ArgumentsHost 클래스는 핸들러에 전달되는 인수를 검색하는 메서드를 제공합니다. 이 클래스를 사용하면 인수를 검색할 적절한 컨텍스트(예: HTTP, RPC(마이크로서비스) 또는 WebSockets)를 선택할 수 있습니다. 프레임워크는 일반적으로 host 매개변수로 참조되는 ArgumentsHost의 인스턴스를 사용자가 액세스하려는 위치에 제공합니다. 예를 들어  exception filter (opens in a new tab)catch() 메서드는 ArgumentsHost 인스턴스와 함께 호출됩니다.

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

Current application context# (opens in a new tab)

여러 애플리케이션 컨텍스트에서 실행되는  guards (opens in a new tab)filters (opens in a new tab)interceptors (opens in a new tab)를 구축할 때는 메서드가 현재 실행 중인 애플리케이션 유형을 확인할 수 있는 방법이 필요합니다. 이 작업은 ArgumentsHostgetType() 메서드를 사용하여 수행합니다:

if (host.getType() === "http") {
  // 일반 HTTP 요청(REST)의 컨텍스트에서만 중요한 작업을 수행합니다.
} else if (host.getType() === "rpc") {
  // 마이크로서비스 요청의 컨텍스트에서만 중요한 작업을 수행합니다.
} else if (host.getType<GqlContextType>() === "graphql") {
  // 그래프QL 요청의 컨텍스트에서만 중요한 작업을 수행합니다.
}

사용 가능한 애플리케이션 유형을 사용하면 아래와 같이 보다 일반적인 컴포넌트를 작성할 수 있습니다.

Host handler arguments# (opens in a new tab)

핸들러에 전달되는 인수의 배열을 검색하려면 호스트 객체의 getArgs() 메서드를 사용하는 것이 한 가지 방법입니다.

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

색인별로 특정 인수를 추출하려면 getArgByIndex() 메서드를 사용하면 됩니다:

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

이 예제에서는 인덱스로 요청 및 응답 객체를 검색했는데, 이는 애플리케이션을 특정 실행 컨텍스트에 연결하기 때문에 일반적으로 권장되지 않습니다. 대신 host 객체의 유틸리티 메서드 중 하나를 사용하여 애플리케이션에 적합한 애플리케이션 컨텍스트로 전환함으로써 코드를 보다 강력하고 재사용 가능하게 만들 수 있습니다. 컨텍스트 전환 유틸리티 메서드는 다음과 같습니다.

/**
 * 컨텍스트를 RPC로 전환합니다.
 */
switchToRpc(): RpcArgumentsHost;
/**
 * 컨텍스트를 HTTP로 전환합니다.
 */
switchToHttp(): HttpArgumentsHost;
/**
 * 컨텍스트를 WebSockets로 전환합니다.
 */
switchToWs(): WsArgumentsHost;

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

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# (opens in a new tab)

ExecutionContext는 현재 실행 프로세스에 대한 추가 세부 정보를 제공하는 ArgumentsHost를 확장합니다. ArgumentsHost와 마찬가지로 Nest는  guard (opens in a new tab)canActivate() 메서드와  interceptor (opens in a new tab)intercept() 메서드와 같이 필요할 수 있는 곳에 ExecutionContext의 인스턴스를 제공합니다. 다음과 같은 메서드를 제공합니다:

export interface ExecutionContext extends ArgumentsHost {
  /**
   * 현재 핸들러가 속한 컨트롤러 클래스의 유형을 반환합니다.
   */
  getClass<T>(): Type<T>;
  /**
   * 요청 파이프라인에서 다음에 호출될 핸들러(메서드)에 대한 참조를 반환합니다.
   */
  getHandler(): Function;
}

getHandler() 메서드는 호출하려는 핸들러에 대한 참조를 반환합니다. getClass() 메서드는 이 특정 핸들러가 속한 Controller 클래스의 유형을 반환합니다. 예를 들어 HTTP 컨텍스트에서 현재 처리된 요청이 CatsControllercreate() 메서드에 바인딩된 POST 요청인 경우 getHandler()create() 메서드에 대한 참조를 반환하고 getClass()는 인스턴스가 아닌 CatsController 클래스를 반환합니다.

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

현재 클래스와 핸들러 메서드 모두에 대한 참조에 액세스할 수 있는 기능은 뛰어난 유연성을 제공합니다. 가장 중요한 것은 가드 또는 인터셉터 내에서 Reflector#createDecorator를 통해 생성된 데코레이터 또는 내장된 @SetMetadata() 데코레이터를 통해 메타데이터 세트에 액세스할 수 있다는 점입니다. 이 사용 사례는 아래에서 다룹니다.

Reflection and metadata# (opens in a new tab)

Nest는 Reflector#createDecorator 메서드를 통해 생성된 데코레이터와 내장된 @SetMetadata() 데코레이터를 통해 라우트 핸들러에 custom metadata를 첨부할 수 있는 기능을 제공합니다. 이 섹션에서는 두 가지 접근 방식을 비교하고 가드 또는 인터셉터 내에서 메타데이터에 액세스하는 방법을 살펴보겠습니다.

Reflector#createDecorator를 사용하여 강력한 타입의 데코레이터를 만들려면 타입 인수를 지정해야 합니다. 예를 들어 문자열 배열을 인수로 받는 Roles 데코레이터를 만들어 보겠습니다.

import { Reflector } from "@nestjs/core";
 
export const Roles = Reflector.createDecorator<string[]>();

여기서 Roles 데코레이터는 string[] 타입의 단일 인수를 받는 함수입니다. 이제 이 데코레이터를 사용하려면 핸들러에 주석을 달기만 하면 됩니다:

cats.controller.ts

여기에서는 admin 역할이 있는 사용자만 이 경로에 액세스할 수 있도록 하기 위해 create() 메서드에 Roles 데코레이터 메타데이터를 첨부했습니다.

경로의 역할(사용자 정의 메타데이터)에 액세스하려면 Reflector 헬퍼 클래스를 다시 사용하겠습니다. Reflector 일반적인 방법으로 클래스에 삽입할 수 있습니다:

roles.guard.ts

이제 핸들러 메타데이터를 읽으려면 get() 메서드를 사용합니다:

const roles = this.reflector.get(Roles, context.getHandler());

Reflector#get 메서드를 사용하면 데코레이터 참조와 메타데이터를 검색할 컨텍스트(데코레이터 대상)의 두 가지 인수를 전달하여 메타데이터에 쉽게 액세스할 수 있습니다. 이 예에서 지정된 데코레이터Roles입니다(위의 roles.decorator.ts 파일을 다시 참조하세요). 컨텍스트는 context.getHandler()를 호출하여 제공되며, 그 결과 현재 처리된 라우트 핸들러의 메타데이터가 추출됩니다. getHandler()는 라우트 핸들러 함수에 대한 참조를 제공한다는 점을 기억하세요.

또는 컨트롤러 수준에서 메타데이터를 적용하여 컨트롤러 클래스의 모든 경로에 적용하여 컨트롤러를 구성할 수도 있습니다.

cats.controller.ts

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

roles.guard.ts

여러 수준에서 메타데이터를 제공하는 기능을 고려할 때 여러 컨텍스트에서 메타데이터를 추출하고 병합해야 할 수도 있습니다. Reflector 클래스는 이를 지원하는 데 사용되는 두 가지 유틸리티 메서드를 제공합니다. 이 메서드는 컨트롤러와 메서드 메타데이터를 한 번에 추출하고 서로 다른 방식으로 결합합니다.

두 수준 모두에서 Roles 메타데이터를 제공한 다음 시나리오를 생각해 보세요.

cats.controller.ts

'user'를 기본 역할로 지정하고 특정 메서드에 대해 선택적으로 재정의하려는 경우, getAllAndOverride() 메서드를 사용할 수 있습니다.

const roles = this.reflector.getAllAndOverride(Roles, [context.getHandler(), context.getClass()]);

위의 메타데이터를 사용하여 create() 메서드의 컨텍스트에서 실행되는 이 코드가 포함된 가드는 ['admin']을 포함하는 역할을 생성합니다.

둘 다에 대한 메타데이터를 가져와 병합하려면(이 메서드는 배열과 객체를 모두 병합합니다) getAllAndMerge() 메서드를 사용합니다:

const roles = this.reflector.getAllAndMerge(Roles, [context.getHandler(), context.getClass()]);

이렇게 하면 ['user', 'admin']을 포함하는 roles가 됩니다.

이 두 병합 메서드 모두에서 메타데이터 키를 첫 번째 인수로 전달하고 메타데이터 대상 컨텍스트 배열(즉, getHandler() 및/또는 getClass() 메서드에 대한 호출)을 두 번째 인수로 전달합니다.

Low-level approach# (opens in a new tab)

앞서 언급했듯이 Reflector#createDecorator를 사용하는 대신 내장된 @SetMetadata() 데코레이터를 사용하여 핸들러에 메타데이터를 첨부할 수도 있습니다.

cats.controller.ts

위의 구성에서 roles 메타데이터(roles는 메타데이터 키이고 ['admin']은 연관된 값)를 create() 메서드에 첨부했습니다. 이렇게 해도 작동하지만 @SetMetadata()를 경로에 직접 사용하는 것은 좋지 않습니다. 대신 아래와 같이 자체 데코레이터를 만들 수 있습니다:

roles.decorator.ts

이 접근 방식은 훨씬 더 깔끔하고 가독성이 높으며 Reflector#createDecorator 접근 방식과 어느 정도 유사합니다. 차이점은 @SetMetadata를 사용하면 메타데이터 키와 값을 더 많이 제어할 수 있고 둘 이상의 인수를 받는 데코레이터를 만들 수도 있다는 것입니다.

이제 사용자 정의 @Roles() 데코레이터가 있으므로 이를 사용하여 create() 메서드를 데코레이션할 수 있습니다.

cats.controller.ts

경로의 역할(사용자 지정 메타데이터)에 액세스하려면 Reflector 헬퍼 클래스를 다시 사용하겠습니다:

roles.guard.ts

이제 핸들러 메타데이터를 읽으려면 get() 메서드를 사용합니다.

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

여기서는 데코레이터 참조를 전달하는 대신 메타데이터 키를 첫 번째 인수로 전달합니다(이 경우 'roles'). 다른 모든 것은 Reflector#createDecorator 예시와 동일하게 유지됩니다.