[NestJS] ArgumentHost and Excecution Context, Reflectioin and MetaData

Gon Kim·2022년 11월 29일
0

이걸 알아서 어디다 쓰지?라고 생각할 수 있다. 하지만 이는 NestJS에서 제공하는 utility 함수/데코레이터들(Guards, Interceptors, Exception filters...)을 사용할 때 필수적으로 알아야하는 개념이다. 개념을 이해하기에 그렇게 대단하고 어려운건 아니지만, 똑똑한 사람들은 AOP를 위해 이런 도구를 만드는 구나 싶었다.

ArgumentsHost is a powerful utility object that we'll examine further in the execution context chapter*. In this code sample, we use it to obtain a reference to the Request and Response
objects that are being passed to the original request handler (in the controller where the exception originates).

NestJS 공식 문서의 Exception filter 부분에서 긁어온 설명이다. 뭔지 정확히는 안알려주는데, exception이 터진 로직을 호출한 컨트롤러가 받은 request, response 객체를 가져올 수 있는 도구로 사용할 수 있는 친구라고 한다. 보고 띠용?해서 바로 찾으러 가봤다.

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 guardsfilters, and interceptors
that can work across a broad set of controllers, methods, and execution contexts.

NestJS 공식 문서 - Execution context
http, websocket, microservice 등을 context라고 칭한다. ArgumentHost는 어떤 context에서도 사용할 수 있는 유틸리티 클래스이다. guards, filters, interceptors를 만드는데 핵심으로 사용되는 친구이고, 현재 로직이 실행되는 context에 대한 정보를 제공한다고 한다. 정보가 뭔지 조금 더 자세히 알아보자

1) ArgumentHost class

핸들러에게 넘겨진 인자들을 제공해주기 위한 클래스이다.

인자(arguments)를 뽑아올 적절한 context(http, RPC(microservice), websockets)를 선정하면, 거기서 인자들을 뽑아올 수 있다.

http 요청을 핸들링하는 컨트롤러를 기준으로 생각해보면, AgrumentHost 객체는 [request, response, next]를 추상화한다.

http context인 경우에는 request, response, next가 정보가 되는 것이다. websocket의 경우 client, data가 있다.

조금 이해가 되려나?!@

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
}

따라서 제네럴하게 사용할 수 있는 guards, filters, interceptors를 만들 때 context에 따라 분기처리할 수 있도록 getType이라는 메소드도 제공한다.

getArgs

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

당연히 핸들러가 받았던 인자들을 반환하는 메소드도 제공한다.

switchTo…

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

getType, getArgs를 조합해도 되지만, switTo.. 메소드도 제공한다.

해당 메소드들은 각각 context에 맞춰 커스터마이즈된 클래스의 구현체를 반환한다.

무슨 뜻이냐면,

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

switchToHttp()는 HttpArgumentsHost 인스턴스를 반환하고, 이 친구는 http에 관한 argument host이기 때문에 getRequest, getResponse같은 메서드를 제공한다. 더 코드가 명확해진다.

2) ExecutionContext class

ExecutionContext는 ArgumentHost를 상속한다. 현재 context에 관한 더 구체적인 정보를 제공하는 친구이다.

추가적인 정보가 뭘까?

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

결론부터 말하면 http context기준 추가적인 정보는, 해당 요청을 다루는 handler와 그 handler가 속한 controller class이다.

getHandler 메소드는 요청에 의해 호출된 handler를, getClass는 handler가 속한 class를 반환한다.

그런데 이런 정보를 대체 어디다가 쓸 수 있을까?

Reflection and metadata

nestJS는 @SetMetadata 데코레이터로 handler들에게 custom metadata를 붙여주는 기능을 제공한다.

@Post()
@SetMetadata('roles', ['admin'])
async create(@Body() createCatDto: CreateCatDto) {
  this.catsService.create(createCatDto);
}
  • roles는 metadata의 key, [’admin’]은 value이다.

SetMetadata 데코레이터를 저렇게 쓰는 것 보다, 데코레이터를 커스터마이즈해서 쓰기를 추천한다.

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

export const Roles = (...roles: string[]) => SetMetadata('roles', roles);
@Post()
@Roles('admin')
async create(@Body() createCatDto: CreateCatDto) {
  this.catsService.create(createCatDto);
}

어쨋든 이렇게 metadata를 달아놓으면, 우린 이 metadata에 접근 가능하다. 접근은 @nestjs/core에서 제공하는 Reflector class로 할 수 있다.

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

이런식으로 class에 주입 가능하다.

Reflection - get

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

reflector는 metadata key와 handler를 인자로 받는다. get함수를 통해 metadata를 가져온다.

현재 실행되는 handler에서부 metadata를 추출해오는 것

@Roles('admin')
@Controller('cats')
export class CatsController {}
// roles.guard.ts
const roles = this.reflector
						.get<string[]>('roles', context.getClass());

controller scope으로 metadata를 세팅하면, reflector의 get 메소드의 두번째 인자로 controller class를 넘겨주면 된다.

어찌됐든, 해당 metadata가 주입된 가장 큰 단위의 것을 넘겨주면 되는 것

Reflection - getAllAndOverride

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

컨트롤러에 roles 데코레이터를 이렇게 달아놓았다면 어떻게 해야할까?

기본적으로 roles에 user를 부여하고, 특정 메소드에서 admin을 metadata로 override한 상황이다.

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

getAllAndOverride 메소드를 통해 handler와 controller class를 모두 넘겨줘 처리해주면 된다.

이 경우 create함수의 경우 roles에는 admin이 담길 것.

Reflection - getAllAndMerge

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

이렇게 합칠 수도 있다. roles에는 ['user', 'admin']이 담긴다.

Ref

https://docs.nestjs.com/fundamentals/execution-context#execution-context

profile
응애

0개의 댓글