[NestJS | Docs] Execution context 알아보기 (feat. ArgumentHost)
2024.02.27- -
1. Execution context
Nest는 여러 컨텍스트(예: Nest HTTP 서버 기반, 마이크로서비스 및 웹소켓 애플리케이션 컨텍스트)에서 작동하는 애플리케이션을 쉽게 작성할 수 있도록 도와주는 여러 유틸리티 클래스를 제공합니다. 이러한 유틸리티는 현재 실행 맥락(execution context)에 대한 정보를 제공하여 더 광범위한 컨트롤러, 메서드 및 실행 컨텍스트에서 동작하는 범용 guard, filter 및 interceptor를 구축하는 데 사용할 수 있습니다.
이번 포스팅에서는 두 개의 클래스를 다룰 것입니다: ArgumentHost & ExecutionContext
1-1. ArgumentHost class
ArgumentsHost 클래스는 핸들러에 전달되는 인자를 가져오는 메서드를 제공합니다. 이 클래스를 사용하면 인자를 가져올 적절한 컨텍스트(예: HTTP, RPC(마이크로서비스) 또는 WebSockets)를 선택할 수 있습니다.
프레임워크는 일반적으로 host 매개변수로 참조되는 ArgumentsHost의 인스턴스를 사용자가 액세스하려는 위치에 제공합니다. 예를 들어 exception filter의 catch() 메서드는 ArgumentsHost 인스턴스와 함께 호출됩니다.
ArgumentsHost는 간단히 말해 핸들러의 인자를 추상화하는 역할을 합니다. 예를 들어, HTTP 서버 애플리케이션(@nestjs/platform-express가 사용되는 경우)의 경우 host 객체는 Express의 [request, response, next] 배열을 캡슐화하며, 여기서 각각 request는 요청 객체, response는 응답 객체, next는 애플리케이션의 요청-응답 사이클을 제어하는 함수입니다.
(반면 GraphQL 애플리케이션의 경우 호스트 객체에는 [root, args, context, info] 배열이 포함됩니다.)
1-2. Current application context (현재 애플리케이션 컨텍스트)
여러 애플리케이션 컨텍스트에서 실행되는 범용 guard, filter 및 interceptor를 구축할 때는 메서드가 현재 실행 중인 애플리케이션의 유형을 확인할 수 있는 메서드가 필요합니다. 이러한 작업은 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
}
사용 가능한 애플리케이션 유형들을 활용하여 아래와 같이 보다 범용적인 컴포넌트를 작성할 수 있습니다.
1-3. Host handler arguments (host 핸들러 인자)
핸들러에 전달되는 인수로 이루어진 배열을 가져오려면 host 객체의 getArgs() 메서드를 사용하는 것이 한 가지 방법입니다.
const [req, res, next] = host.getArgs();
index별로 특정 인자를 추출하려면 getArgByIndex() 메서드를 사용하면 됩니다:
const request = host.getArgByIndex(0);
const response = host.getArgByIndex(1);
이 예제에서는 인덱스로 요청 및 응답 객체를 가져왔는데, 이는 애플리케이션을 특정 execution context와 하나로 묶어버리기 때문에 일반적으로 권장되지 않습니다.
대신 host 객체의 유틸리티 메서드 중 하나를 사용하여 애플리케이션에 적합한 애플리케이션 컨텍스트로 전환함으로써 코드를 보다 강력하고 재사용 가능하게 만들 수 있습니다. context switch 유틸리티 메서드는 다음과 같습니다.
/**
* 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 type assertion을 사용하여 네이티브 Express type 객체를 반환하도록 할 수 있습니다:
const ctx = host.switchToHttp();
const request = ctx.getRequest<Request>();
const response = ctx.getResponse<Response>();
마찬가지로 WsArgumentsHost와 RpcArgumentsHost에는 마이크로서비스 및 웹소켓 컨텍스트에서 적절한 객체를 반환하는 메서드가 있습니다. 다음은 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;
}
1-4. ExecutionContext class
ExecutionContext는 현재 실행 프로세스에 대한 추가 세부 정보를 제공하는 ArgumentsHost를 확장한 것입니다. ArgumentsHost와 마찬가지로 Nest는 guard의 canActivate() 메서드와 interceptor의 intercept() 메서드에서처럼 ExecutionContext의 인스턴스를 필요로 하는 곳에 제공합니다. ExecutionContext 인스턴스는 다음과 같은 메서드를 제공합니다:
export interface ExecutionContext extends ArgumentsHost {
/**
* 현재 핸들러가 속한 controller class의 타입을 반환합니다.
*/
getClass<T>(): Type<T>;
/**
* 요청 파이프라인에서 다음에 호출될 핸들러에 대한 참조를 반환합니다.
*/
getHandler(): Function;
}
getHandler() 메서드는 호출하려는 핸들러에 대한 참조를 반환합니다. getClass() 메서드는 이 특정 핸들러가 속한 컨트롤러 클래스의 유형을 반환합니다.
예를 들어 HTTP 컨텍스트에서 현재 처리되고 있는 요청이 CatsController의 create() 메서드에 바인딩된 POST 요청인 경우, getHandler()는 create() 메서드에 대한 참조를 반환하고 getClass()는 인스턴스가 아닌 CatsController클래스를 반환합니다.
const methodKey = ctx.getHandler().name; // "create"
const className = ctx.getClass().name; // "CatsController"
현재 클래스와 핸들러 메서드 모두에 대한 참조를 바탕으로 이것들에 액세스할 수 있는 기능은 뛰어난 유연성을 제공합니다. 여기서 가장 중요한 것은 guard 또는 interceptor 내에서 Reflector#createDecorator를 통해 생성된 데코레이터 또는 내장된 @SetMetadata() 데코레이터를 통해 metatdata 세트에 액세스할 수 있다는 점입니다. 이 사용 사례는 바로 아래에서 다루도록 하겠습니다.
1-5. Reflection and metadata
Nest는 Reflector#createDecorator 메서드를 통해 생성된 데코레이터와 내장된 @SetMetadata() 데코레이터를 통해 라우트 핸들러에 custom metatdata를 첨부할 수 있는 기능을 제공합니다. 이 장에서는 두 가지 접근 방식을 비교하고 guard 또는 interceptor 내에서 이러한 metadata에 액세스하는 방법에 대해서 살펴보겠습니다.
Reflector#createDecorator를 사용하여 강력하게 타입이 지정된 데코레이터를 생성하려면 type 인자를 지정해야 합니다. 예를 들어 문자열 배열을 인자로 받는 Roles 데코레이터를 만들어 보겠습니다.
import { Reflector } from '@nestjs/core';
export const Roles = Reflector.createDecorator<string[]>();
여기서 Roles 데코레이터는 string[] 타입의 단일 인자를 받는 함수입니다.
이제 이 데코레이터를 사용하려면 사용사고 싶은 핸들러에 해당 데코레이터를 달기만 하면 됩니다:
// cats.controller.ts
@Post()
@Roles(['admin'])
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
여기에서는 관리자(admin) 역할이 있는 사용자만 이 경로에 액세스할 수 있도록 하기 위해 create() 메서드에 @Roles() 데코레이터 메타데이터를 붙여주었습니다.
라우트의 role(s)(custom metadata)에 액세스하려면 Reflector 헬퍼 클래스를 다시 사용해야 합니다. Reflector는 일반적인 방법으로 클래스에 주입할 수 있습니다:
// role.guard.ts
@Injectable()
export class RolesGuard {
constructor(private reflector: Reflector) {}
}
이제 핸들러 metadata를 읽기 위해 get() 메서드를 사용합니다.
const roles = this.reflector.get(Roles, context.getHandler());
Reflector#get 메서드를 사용하면 데코레이터 참조와 메타데이터를 가져올 context(데코레이터 대상), 이 두 가지 인자를 전달하여 메타데이터에 쉽게 액세스할 수 있습니다. 이 예제에서 지정된 데코레이터는 Roles입니다.
context는 context.getHandler()를 호출하여 제공되며, 그 결과 현재 처리되는 라우트 핸들러의 메타데이터가 추출됩니다. getHandler()는 라우트 핸들러 함수에 대한 참조를 제공한다는 점을 기억합시다.
또는 컨트롤러 수준에서 메타데이터를 적용하여 컨트롤러 클래스의 모든 경로에 적용하여 컨트롤러를 구성할 수도 있습니다.
@Roles(['admin'])
@Controller('cats')
export class CatsController {}
이 경우 컨트롤러 메타데이터를 추출하기 위해서는 context.getHandler() 대신 context.getClass()를 두 번째 인자로 전달합니다(메타데이터 추출을 위한 컨텍스트로 컨트롤러 클래스를 제공하기 위해):
const roles = this.reflector.get(Roles, context.getClass());
다양한 level에서 메타데이터를 제공할 수 있음을 고려해야 할 때, 여러 컨텍스트에서 메타데이터를 추출하고 병합해야 하는 경우가 있을 수도 있습니다.
Reflector 클래스는 그러한 경우 역시 지원하는데, 이곳에 사용되는 두 가지 유틸리티 메서드를 제공합니다. 해당 메서드는 컨트롤러와 메서드 메타데이터를 한 번에 추출하고 서로 다른 방식으로 결합합니다.
두 level 모두에서 Roles 메타데이터를 제공하는 다음 시나리오를 고려해 봅시다.
@Roles(['user'])
@Controller('cats')
export class CatsController {
@Post()
@Roles(['admin'])
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
}
위와 같이 'user'를 기본 역할로 지정하고 특정 메서드에 대해 선택적으로 재정의하려는 경우, getAllAndOverride() 메서드를 사용할 수 있습니다.
const roles = this.reflector.getAllAndOverride(Roles, [context.getHandler(), context.getClass()]);
위의 메타데이터를 사용하여 create() 메서드의 컨텍스트에서 실행되는 이 코드가 포함된 guard에서는 ['admin']을 포함하는 roles가 생성됩니다.
만약 둘 다에 대한 메타데이터를 가져와 병합하고 싶다면(이 메서드는 배열과 객체를 모두 병합합니다) getAllAndMerge() 메서드를 사용합니다:
const roles = this.reflector.getAllAndMerge(Roles, [context.getHandler(), context.getClass()]);
이렇게 하면 ['user', 'admin']을 포함하는 roles를 가져오게 됩니다.
이 2개의 병합 메서드는 모두 첫 번째 인자로 metatdata의 key를 전달하고 두 번째 인자로는 metadata의 target context 배열(즉, getHandler() 및/또는 getClass() 메서드에 대한 호출)을 전달합니다.
1-6. Low-level approach
앞서 언급했듯이 Reflector#createDecorator를 사용하는 대신, 내장된 @SetMetadata() 데코레이터를 사용하여 핸들러에 메타데이터를 첨부할 수도 있습니다.
@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를 사용하면 메타데이터의 키와 값을 더 많이 제어할 수 있고, 둘 이상의 인자를 받는 데코레이터를 만들 수도 있다는 점입니다.
이제 custom @Roles() 데코레이터가 생겼으므로 이를 사용하여 create() 메서드를 데코레이션할 수 있습니다.
@Post()
@Roles('admin')
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
라우트의 role(s)에 액세스하기위해 Reflector 헬퍼 클래스를 다시 사용합시다.
// roles.guard.ts
@Injectable()
export class RolesGuard {
constructor(private reflector: Reflector) {}
}
이제 핸들러의 metadata를 읽기 위해 get() 메서드를 사용합니다.
const roles = this.reflector.get<string[]>('roles', context.getHandler());
여기서는 데코레이터 참조를 전달하는 대신 메타데이터 키를 첫 번째 인자로 전달합니다(이 경우 'roles'). 그 외에 다른 모든 것들은 Reflector#createDecorator 예시와 동일하게 유지됩니다.
'Back-end > NestJS' 카테고리의 다른 글
[NestJS | Docs] Exception filters (예외 필터) (1) | 2024.03.04 |
---|---|
[NestJS | Docs] Middleware 알아보기 (0) | 2024.02.28 |
[NestJS | Docs] Configuration 사용법 (0) | 2024.02.23 |
[NestJS | Docs] Custom Decorators 알아보기 (커스텀 데코레이터) (0) | 2024.02.21 |
[NestJS | Docs] Passport 알아보기 (feat. authentication) (0) | 2024.02.20 |
소중한 공감 감사합니다