NESTJS를 배워보자(19) - Execution context

yoon·2023년 10월 23일
0

NESTJS를 배워보자

목록 보기
19/21

Execution context

nest의 공식문서를 토대로 작성합니다.

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

이러한 클래스 두 가지를 다룹니다. ArgumentsHostExecutionContext입니다.

ArgumentsHost class

ArgumentsHost 클래스는 핸들러에 전달되는 인수를 검색하는 메소드를 제공합니다. 이 클래스를 사용하면 인수를 검색할 적절한 컨텍스트(예: HTTP, RPC, WebSockets)를 선택할 수 있습니다. 프레임워크는 일반적으로 호스트 매개변수로 참조되는 ArgumentsHost의 인스턴스를 사용자가 접근하려는 위치에 제공합니다. 예를 들어 필터의 catch() 메소드는 ArgumentsHost 인스턴스를 사용하여 호출됩니다.

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

Current application context

여러 애플리케이션 컨텍스트에서 실행되는 일반 가드, 필터 및 인터셉터를 빌드할 때는 메소드가 현재 실행 중인 애플리케이션 유형을 확인할 수 있는 방법이 필요합니다. 이 작업은 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
}

HINT
GqlContextType@nestjs/graphql 패키지에서 import.

애플리케이션의 유형을 확인할 수 있게 되면 아래와 같이 보다 일반적인 컴포넌트를 작성할 수 있습니다.

Host handler arguments

핸들러에 전달되는 인수의 배열을 검색하려면 호스트 객체의 getArgs() 메소드를 사용할 수 있습니다.

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

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

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

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

/**
 * 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 타입 assertions를 사용하여 기본 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

ExecutionContextArgumentsHost를 확장하여 현재 실행 프로세스에 대한 추가 세부 정보를 제공합니다. 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;
}

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

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

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

# roles.decorator.ts

import { Reflector } from '@nestjs/core';

export const Roles = Reflector.createDecorator<string[]>();

여기서 Roles 데코레이터는 string[] 타입의 단일 인수를 받는 함수입니다.

여기서 오류가 날 수 있는데 이는 nest의 버전 때문일 수 있습니다.

$ npm i -g npm-check-updates
$ ncu

해보시면

이렇게 뜹니다.
맨 아래에 나온 것처럼

$ ncu -u
$ npm i

명령어를 입력해주면 버전이 업데이트되어 오류가 없어질 것입니다.

이제 이 데코레이터를 사용하려면 핸들러에 주석을 달기만 하면 됩니다.

# cats.controller.ts

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

여기에서는 admin 역할이 있는 사용자만 이 경로에 접근할 수 있어야 함을 나타내는 Roles 데코레이터 메타데이터를 create() 메소드에 첨부했습니다.

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

# roles.guard.ts

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

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

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

여기서 갑자기 context가 어디서 나오는가 하면
https://github.com/nestjs/nest/blob/v10.2.7/sample/01-cats-app/src/common/guards/roles.guard.ts
여기 nest 깃허브 확인해보면 다르게 구현되어 있는 걸 확인하실 수 있습니다..

Reflector#get 메소드를 사용하면 데코레이터 참조와 메타데이터를 검색할 컨텍스트의 두 가지 인수를 전달하여 메타데이터에 쉽게 접근할 수 있습니다. 이 예에서 지정된 데코레이터는 Roles입니다. 컨텍스트는 context.getHandler()를 호출하여 제공되며 그 결과 현재 처리된 라우트 핸들러의 메타데이터를 추출합니다. getHandler()는 라우트 핸들러 함수에 대한 참조를 제공합니다.

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

# cats.controller.ts

@Roles(['admin'])
@Controller('cats')
export class CatsController {}

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

# roles.guard.ts

const roles = this.reflector.get(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);
  }
}

기본 role로 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']을 포함하는 rolse가 생성됩니다.

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

Low-level approach

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

# cats.controller.ts

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

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

# roles.decorator.ts

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

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

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

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

# cats.controller.ts

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

경로의 role(사용자 지정 메타데이터)에 접근하려면 Reflector 헬퍼 클래스를 다시 사용합니다:

# roles.guard.ts

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

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

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

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

고생하셨습니다!
다음 글에서 만나요~~😀


저도 아직 배우는 단계입니다. 지적 감사히 받겠습니다. 함께 열심히 공부해요!!

profile
백엔드 개발자 지망생

0개의 댓글