[NestJS] Execution context __docs(번역)

DatQueue·2022년 12월 24일
4

NestJS _TIL

목록 보기
4/12
post-thumbnail

시작하기에 앞서

NestJS를 학습하며 "가드(Guard)"를 구현하는 과정에서 "Execution context(실행 컨텍스트)" 라는 키워드를 통해 특정 라우터에 원하는 가드를 적용시켜줄 수 있다는 것을 알게 되었다.

내가 아는 "실행 컨텍스트(Execution context)"는 예전 자바스크립트를 학습하며 익혔던 "실행 컨텍스트" 뿐이다. 하지만 Nest에서 사용한 실행 컨텍스트는 이와는 별개이다. 컨텍스트라는 것은 현재 동작의 배경이라는 범용적 개념이다.
NestJS에서 특히 가드, 필터 등을 작성하는데 사용하게 되는 해당 "실행 컨텍스트"는 어떠한 의미를 지니고, 어떻게 쓰이는지에 대해 이번 포스팅에서 알아보고자 한다.

이번 포스팅은 Nest_docs (Execution context) 번역을 중심으로 진행한다.


공식문서 번역 및 추가 학습


Execution context

Nest provides several utility classes that help make it easy to write applications that function across multiple application contexts (e.g., Nest HTTP server-based, microservices and WebSockets application contexts). These utilities provide information about the current execution context which can be used to build generic guards, filters, and interceptors that can work across a broad set of controlleres, methods, and execution contexts.

( Nest는 여러 애플리케이션 컨텍스트 (예: Nest HTTP server-based, microservices 및 WebSockets 애플리케이션 컨텍스트)에서 작동하는 애플리케이션을 쉽게 작성할 수 있도록 도와주는 몇 가지 유틸리티 클래스를 제공합니다. 이러한 유틸리티 클래스는 광범위한 컨트롤러, 메서드 및 실행 컨텍스트에서 작동할 수 있는 제네릭 가드, 필터인터셉터를 구축하는데 사용할 수 있는 현재 실행 컨텍스트에 대한 정보를 제공합니다. )

We cover two such classes in this chapter: ArgumentsHost and ExecutionContext.

( 이번 챕터에서는 다음의 두 가지 클래스를 다룹니다. : ArgumentsHost & ExecutionContext )


ArgumentsHost class

The ArgumentsHost class provides methods for retrieving the arguments being passed to a handler. It allows choosing the appropriate context (e.g., HTTP, RPC (microservice), or WebSockets) to retrieve the arguments from. The framework provides an instance of ArgumentsHost, typically referenced as a host parameter, in places where you may want to access it. For example, the catch() method of an exception filter is called with an ArgumentsHost instance.

( ArgumentsHost 클래스는 핸들러에 전달되는 인수를 검색하는 메서드를 제공합니다. 이를 통해 적절한 컨텍스트(예: HTTP, RPC(microservice) 또는 WebSockets)를 선택하여 인수를 검색할 수 있습니다. 프레임워크는 일반적으로 호스트 매개변수로 참조되는 ArgumentsHost 인스턴스를 액세스하려는 위치에 제공합니다. 예를 들어, 예외 필터catch() 메서드는 ArgumentsHost 인스턴스와 함께 호출됩니다. )


ArgumentsHost simply acts as an abstraction over a handler's arguments. For example, for HTTP server applications (when @nestjs/platform-express is being used), the host object encapsulates Express's [request, response, next] array, where request is the request object, response is the response object, and next is a function that controls the application's request-response cycle. On the other hand, for GraphQL applications, the host object contains the [root, args, context, info] array.

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


위의 내용을 깊게 익히기 위해선 [Exception Filter(예외필터)]에 관해 깊게 알아볼 필요가 있다. 이 또한 nestJS 공식문서에서 알아볼 수 있다.
Exception Filter __Arguments host

그래도 간단히 집고 넘어가보자.

ArgumentsHost 인터페이스는 아래와 같이 구성되어있다.

export interface ArgumentsHost {
  getArgs<T extends Array<any> = any[]>(): T;
  getArgByIndex<T = any>(index: number): T;
  switchToRpc(): RpcArgumentsHost;
  switchToHttp(): HttpArgumentsHost;
  switchToWs(): WsArgumentsHost;
}

이처럼 ArgumentsHost는 유용한 메서드 세트를 제공하는 인자 배열에 지나지 않는다. 우리가 HTTP 서버에서 많이 사용하는 switchToHttp() 메서드또한 여기에 포함되어 있는 것을 확인할 수 있다.

조금 더 직접적으로 확인해보자.
실제로 우리가 만드는 프로젝트에서 어떻게 작성되는지 확인하는게 조금 더 와닿지 않을까.

우리는 "인증 / 인가 (Authentication / Authorization)" 를 구현하는 작업을 진행하면서 "가드(Guard)"를 작성하였다.

RolesGuard 를 한 번 살펴보자.
유저가 가지고 있는 속성으로 리소스의 허용을 판별하는 인가를 구현하는데에 좋은 사례이다.

// RolesGuard

import { CanActivate, ExecutionContext, Injectable } from "@nestjs/common";
import { Reflector } from "@nestjs/core";
import { Observable } from "rxjs";
import { User } from "src/user/entities/user.entity";


@Injectable()
export class RolesGuard implements CanActivate {
  constructor(private readonly reflector: Reflector) {}

  canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> {
      const roles = this.reflector.get<string[]>('roles', context.getHandler());

      if(!roles) {
        return true;
      }
      console.log(roles);

      const request = context.switchToHttp().getRequest();
      const user = request.user as User;
      
      return user && user.roles && user.roles.some(role => roles.includes(role));
  }
}

위에서 작성된 RolesGuard 뿐만 아니라 일반적 nest의 가드는 CanActivate 인터페이스를 구현해야한다.

그렇다면 CanActivate 인터페이스의 구현체는 어떤 모습일까?

//CanActivate

export interface CanActivate {
    /**
     * @param context Current execution context. Provides access to details about
     * the current request pipeline.
     *
     * @returns Value indicating whether or not the current request is allowed to
     * proceed.
     */
    canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean>;
}

canActivate의 파라미터인 context는 현재 실행 컨텍스트이다. 이는 현재 요청 파이프라인의 세부 정보에 대한 액세스를 제공한다.

그렇다면 context 파라미터가 참조하고 있는 (실행 컨텍스트에 접근하기 위한) ExecutionContext인터페이스를 확인해보자.

//ExecutionContext

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

ExecutionContext 인터페이스는 ArgumentsHost 인터페이스를 상위로 두고 있는 것을 확인할 수 있다.

Hierarchy

  • ArgumentsHost
    - ExecutionContext

즉, contextExecutionContext에서 제공하는 메서드와 ArgumentsHost에서 제공하는 (위 ArgumentsHost 인터페이스 참조) 메서드를 사용할 수 있게 된다.

진행중인 코드는 http 서버 요청 중이므로 switchToHttp() 메서드를 사용한다.


Current application context

When building generic guards, filters, and interceptors which are meant to run across multiple application contexts, we need a way to determine the type of application that our method is currently running in. Do this with the getType() method of ArgumentsHost:

( 여러 애플리케이션 컨텍스트에서 실행되는 일반적 가드, 필터, 인터셉터를 빌드할 때, 메서드가 현재 실행중인 애플리케이션의 타입을 결정하는 방법이 필요합니다. 우리는 이 방법을 ArgumentsHost인터페이스의 getType() 메서드를 통해 구현합니다. )

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
}

With the application type available, we can write more generic components, as shown below.

( 애플리케이션의 타입이 사용 가능하다면, 아래와 같이 더 일반적인 요소들을 작성할 수 있습니다. )


Host handler arguments

To retrieve the array of arguments being passed to the handler, one approach is to use the host object's getArgs() method.

( 핸들러에 전달되는 인수 배열을 검색하기 위한 한 가지 방법으로, host 객체의 getArgs() 메서드를 사용하는 것입니다. )

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

우리가 작성중인 코드에서 실행 컨텍스트를 참조하는 context가 위 코드의 host가 될 것이고, 직접 req, res, next를 콘솔 창에 출력해보면

이와 같이 다양한 정보들을 얻을 수 있을 것이다. 직접 확인해보길 보란다.


You can pluck a particular argument by index using the getArgByIndex() method.

( getArgsByIndex() 메서드를 사용하게 되면 특정 인수를 인덱스별로 뽑아낼 수 있습니다. )

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

앞전의 getArgs()로 추출했을때의 경우와 동일한 값을 얻게 된다.


In these examples we retrieved the request and response objects by index, which is not typically recommended as it couples the application to a particular execution context. Instead, you can make your code more robust and reusable by using one of the host object's utility methods to switch to the appropriate application context for your application. The context switch utility methods are shown below.

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

/** Switch Method

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

Let's rewrite the previous example using the switchToHttp() method. The host.switchToHttp() helper call returns an HttpArgumentsHost object that is appropriate for the HTTP application context. The HttpArgumentsHost object has two useful methods we can use to extract the desired objects. We also use the Express type assertions in this case to return native Express typed objects:

( switchToHttp() 메서드를 사용하여 이전 예제를 다시 작성해 보겠습니다. host.switchToHttp() 헬퍼는 HTTP 애플리케이션 컨텍스트에 적합한 HttpArgumentsHost 객체를 반환합니다. HttpArgumnetsHost 객체에는 원하는 객체를 추출하는데 사용할 수 있는 두 가지 유용한 메서드가 있습니다. 또한 우리는 Express 타입 단언을 사용하여 네이티브 Express 타입 객체를 리턴할 수 있습니다. )

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

해당 코드에서 getRequest<>()getResponse<>()의 제네릭 타입으로 받아온 RequestResponse가 Express의 타입 단언으로써 불러온 것이라 할 수 있다.

import { Request, Response } from "express";

위와 같이 불러올 수 있다.


Similarly WsArgumentHost and RpcArgumentsHost have methods to return appropriate objects in the microservices and WebSockets contexts.

( 마찬가지로 WsArgumentHostRpcArgumentsHost에 microservices와 WebSockets 컨텍스트에 적절한 객체를 리턴하는 메서드가 있습니다. )

---- 예시는 생략 ----


ExecutionContext class

ExecutionContext extends ArgumentsHost, providing additional details about the current execution process. Like ArgumentsHost, Nest provides an instance of ExecutionContext in places you may need it, such as in the canActivate() method of a guard and the intercept() method of a interceptor. It provides the following methods:

( ArgumentsHost를 확장한ExecutionContext는 현재 실행 프로세스에 대한 추가적인 세부사항을 제공합니다. ArgumentsHost와 같이, Nest는 당신이 필요로 하는 곳에 가드의 canActivate() 메서드, 인터셉터의 intercept() 메서드를 ExecutionContext로써 제공한다. 그것은 다음과 같은 메서드를 제공합니다 : )

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;
}

The getHandler() method returns a reference to the handler about to be invoked. The getClass() method returns the type of the Controller class which this particular handler belongs to. For example, in an HTTP context, if the currently processed request is a POST request, bound to the create() method on the CatsController, getHandler() returns a reference to the create() method and getClass() returns the CatsController type (not instance).

( getHandler() 메서드는 호출할 핸들러에 대한 참조를 반환합니다. getClass() 메서드는 이 특정 핸들러가 속한 컨트롤러 클래스의 타입을 반환합니다. 예를 들어, HTTP 컨텍스트에서 현재 처리된 요청이 CatsControllercreate() 메서드에 바인딩된 Post 요청이라면, getHandler()create()메서드에 대한 참조를 반환하고, getClass()CatsController 타입(인스턴스가 아님)을 반환합니다. )

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

The ability to access references to both the current class and handler method provides great flexibility. Most importantly, it gives us the opportunity to access the metadata set through the @SetMetadata() decorator from within guards or interceptors. We cover this use case below.

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


Reflection and metadata

Nest provides the ability to attach custom metadata to route handlers through the @SetMetadata() decorator. We can then access this metadata from within our class to make certain decisions.

( Nest는 @SetMetadata() 데코레이터를 통해 라우터 핸들러에 사용자 정의 메타데이터를 첨부하는 기능을 제공해줍니다. 그런 다음 우리는 클래스 내에서 이 메타데이터에 접근하여 특정한 결정을 내릴 수 있습니다. )

여기서 "특정한 결정"이라 함은, 예를 들어 RolesGuard를 구현한다고 하면, 해당 가드를 주입하고자하는 컨트롤러의 메서드에 주입한 후 유저에 따라 서로 다른 권한에 대한 판별을 검증하는 기능을 의미할 것이다.

아래에서부터, 해당 공식문서에서도 RolesGuard를 통해 Custom Metadata를 설명 및 진행한다.


해당 RolesGuard에 대한 더 자세한 내용을 알고 싶다면
NestJS_ docs [ Role Guard ],
NestJS_ docs [ Role Guard ] -- 번역 (velog)
위 링크를 참조 바랍니다.


// cats.controller.ts

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

HINT
The @SetMetadata() decorator is imported from the @nestjs/common package.


With the construction above, we attached the roles metadata (roles is a metadata key and ['admin'] is the associated value) to the create() method. While this works, it's not good practice to use @SetMetadata() directly in your routes. Instead, create your own decorators, as shown belows:

( RolesGuard 번역 과정에서 (위 링크 참조) 진행하였으므로 생략 )

// role.decorator.ts

import { SetMetadata } from '@nestjs/common';

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

This approach is much cleaner and more readable, and is strongly typed. Now that we have a custom @Roles() decorator, we can use it decorate the create() method.

// cats.controller.ts

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

To access the route's role(s) (custom metadata), we'll use the Reflector helper class, which is provided out of the box by the framework and exposed from the @nestjs/core package. Reflector can be injected into a class in the normal way:

( 라우터의 role(s)에 접근하기 위해, 프레임워크에 의해 즉시 사용 가능하고 nestjs/core 패키지로부터 노출되는 Reflector 헬퍼 클래스를 사용할 것입니다. Reflector는 일반적 방법으로 클래스에 주입될 수 있습니다. )

// roles.gurad,ts

@Injectable()
export class RolesGuard {
  constructor(private reflector: Reflector) {}
}

HINT
The Reflector class is imported from the @nestjs/core package.


Now, to read the handler metadata, use the get() method.

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

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

The Reflector#get method allows us to easily access the metadata by passing in two arguments: a metadata key and a context(decorator target) to retrieve the metadata from. In this example, the specified key is roles (refer back to the roles.decorator.ts file above and the SetMetadata() call made there). The context is provided by the call to context.gethandler(), which results in extracting the metadata for the currently processed route handler. Remember, getHandler() gives us a reference to the route handler function.

( Reflector#get 메서드는 두 가지 인수인 메타데이터 와 메타데이터를 검색할 컨텍스트를 전달하여 메타데이터에 쉽게 접근할 수 있게끔 합니다. 이 예제에서 지정된 roles ( 위의 roles.decorator.ts 파일 및 해당 파일에서 호출한 SetMetadata() 참조 바람 ) 입니다. 컨텍스트는 현재 처리된 라우트 핸들러에 대한 메타데이터를 추출하는 context.getHandler()에 대한 호출로써 제공됩니다. getHandler() 는 라우트 핸들러 함수에 대한 참조를 제공합니다. )


Alternatively, we may organize our controller by applying metadata at the controller level, applying to all routes in the controller class.

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

// cats.controller.ts

@Roles('admin')   // --> applying metadata at the controller level
@Controller('cats')
export class CatsController {}

In this case, to extract controller metadata, we pass context.getClass() as the second argument (to provide the controller class as the context for metadata extraction) instead of context.getHandler() :

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

// roles.guard.ts

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

Give the ability to provide metadata at multiple levels, you may need to extract and merge metadata from several contexts. The Reflector class provides two utility methods used to help with this.

Consider the following scenario, where you've supplied 'roles' metadata at both levels.

( 여러(복수의) 레벨에서 메타데이터를 제공하는 능력을 부여하기 위해, 여러 컨텍스트에서 메타데이터를 추출하고 병합할 필요가 있습니다. Reflector 클래스는 이것을 도와주는데 사용되는 두 가지 유틸리티 메서드를 제공합니다.
두 레벨 모두에서 roles 메타데이터를 제공하게 된 다음 시나리오를 고려해보세요. )

// cats.controller.ts

@Roles('user')   // -- applying metadata at the controller level
@Controller('cats')
export class CatsController {
  @Post()
  @Roles('admin')  // - at the router level
  async create(@Body() createCatDto: CreateCatDto) {
    this.catsService.create(createCatDto);
  }
}

If your intent is to specify 'user' as the default role, and override it selectively for certain methods, you would probably use the getAllAndOverride() method.

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

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

A guard with this code, running in the context of the create() method, would result in roles containing [admin].

To get metadata for both and merge it (this method merges both arrays and objects), use the getAllAndMerge() method:

( create() 메서드의 컨텍스트에서 실행되는 위 코드가 포함된 가드는 [admin] 역할을 포함하는 역할이 될 것입니다. 두 가지 모두(roles)에 대한 메타데이터를 가져오고 병합하기 위해선(이 메서드는 배열과 객체를 병합), getAllAndMerge() 메서드를 사용할 수 있습니다. )

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

This would result in roles containing ['user', 'admin'].

For both of these merge methods, you pass the metadata key as the first argument, and an array of metadata target contexts (i.e., calls to the getHandler() and/or getClass() methods) as the second argument.

( 이것은 ['user', 'admin']를 포함하는 역할을 불러온다. 이러한 모든 메서드 병합과정에서, 첫 번째 인자로 메타데이터 키를, 두 번째 인자로써 메타데이터의 타겟이 되는 컨텍스트의 배열을(ex: getHandler() 및/또는 getClass() 메서드 호출) 전달한다. )


생각정리

이번 포스팅에선 인증 인가에서 적용하게 되는 "가드"를 구현하는 과정에서 쓰이게 되는 개념인 "Execution Context(실행 컨텍스트)"에 대해 Nest 공식문서 번역 과정을 통해 알아보았다. 실행 컨텍스트를 참조하는 객체는 어떠한 인터페이스를 통해 사용되게 되고, 어떠한 메서드로써 우리가 원하는 기능을 구현할 수 있을지에 대해 알 수 있었다.
지난번 RolesGuard 공식 문서 번역 이후, 두 번째 번역 작업이었다. 앞으로 nest 공식 문서 번역은 계속될 것입니다...


( 혹시 이 글을 읽고, 필요하신 번역 파트가 있다면 말씀해주세요... 최대한 빨리 번역들어갑니다. )

profile
You better cool it off before you burn it out / 티스토리(Kotlin, Android): https://nemoo-dev.tistory.com

0개의 댓글