Nest는 여러 애플리케이션 컨텍스트들(예: Nest HTTP 서버 기반, 마이크로서비스 및 웹소켓 애플리케이션 컨텍스트)을 아울러서 작동하는 애플리케이션을 쉽게 작성할 수 있도록 몇 가지 유틸리티 클래스들을 제공합니다. 이 유틸리티들은 전반적인 컨트롤러, 메서드, 실행 컨텍스트를 아울러 동작하는 범용적인 guards, filters, interceptors를 만들 수 있도록 현재 실행 컨텍스트에 대한 정보를 제공합니다.
이 챕터에서는 ArgumentsHost
, ExecutionContext
두 클래스를 다룹니다.
ArgumentsHost
클래스는 핸들러에 전달 된 인자들을 찾을 수 있는 메서드들을 제공하며, 인자들을 찾을 수 있는 적절한 컨텍스트(예를 들면 HTTP, RPC(마이크로서비스), 웹소켓)를 선택할 수 있게 해줍니다. Nest는 ArgumentsHost
의 인스턴스를 제공하며, 접근하려는 곳에서는 일반적으로 host
라는 이름의 매개변수로 참조됩니다. 예를 들어, 예외 필터의 catch()
메서드는 ArgumentsHost
인스턴스와 함께 호출됩니다.
ArgumentsHost
는 단순히 핸들러의 인자들을 추상화한 것입니다. 예를 들어, HTTP 서버 애플리케이션(@nestjs/platform-express
를 사용하는 경우)의 경우, host
객체는 Express
의 [request, response, next]
배열을 캡슐화합니다. 여기서 request
는 요청 객체를, response
는 응답 객체를, next
는 애플리케이션의 요청-응답 사이클을 제어하는 함수입니다. 반면에 GraphQL
애플리케이션에서의 host
객체는 [root, args, context, info]
배열을 포함합니다.
여러 애플리케이션 컨텍스트를 아울러 동작하는 일반적인 guards, filters, interceptors를 만들기 위해서는 현재 메서드가 동작하고 있는 애플리케이션 타입을 결정지어야 하며, 이는 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
}
GqlContextType
은@nestjs/graphql
패키지에서 import합니다.
이처럼, 사용 가능한 애플리케이션 타입을 활용하여 보다 범용적인 컴포넌트를 작성할 수 있습니다.
핸들러에 전달 된 인자를 찾기 위해 host
객체의 getArgs()
메서드를 사용할 수 있습니다.
const [req, res, next] = host.getArgs();
getArgByIndex()
메서드를 사용하여 특정 인자만 찾을 수 있습니다:
const request = host.getArgByIndex(0);
const response = host.getArgByIndex(1);
이 예제들에서 요청 객체 및 응답 객체를 인덱스를 통해 찾았는데, 일반적으로 이는 어플리케이션을 특정한 하나의 실행 컨텍스트에 국한시키는 것이므로 권장되지 않습니다. 대신에, host
객체에는 적절한 어플리케이션 컨텍스트로 전환할 수 있는 유틸리티 메서드가 있으므로 이를 사용하여 더욱 안정되고 재사용가능한 코드를 작성할 수 있습니다. 아래는 컨텍스트를 전환하는 유틸리티 메서드들입니다.
/**
* 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 형식의 객체를 반환하기 위해 Express 타입 어설션(type assertions)을 사용합니다:
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;
}
ExecutionContext
는 ArgumentsHost
를 상속 받으며, 현재 실행 프로세스에 대한 추가적인 세부사항을 제공합니다. ArgumentsHost
와 같이, Nest는 우리가 원하는 곳에 ExecutionContext
인스턴스를 제공하며, 가드의 canActive()
메서드나 인터셉터의 intercept()
메서드가 그 예시입니다. 인스턴스는 아래 메서드들을 제공합니다:
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 컨텍스트에서의 예시로, 현재 처리되는 요청이 POST
요청이고 CatsController
의 create()
메서드가 실행된다면 getHandler()
는 create()
메서드의 참조를 반환하고 getClass()
는 CatsController의 인스턴스가 아닌 타입을 반환합니다.
const methodKey = ctx.getHandler().name; // "create"
const className = ctx.getClass().name; // "CatsController"
현재 클래스와 핸들러 메서드 둘의 참조 모두에 접근하는 기능은 굉장한 유연성을 제공합니다. 가장 중요한 건, 연관된 가드나 인터셉터에서 @SetMetadata()
데코레이터를 통해 지정한 메타데이터에 접근할 수 있는 기회가 된다는 점입니다. 이 사례에 대해서는 아래에서 다루어 보겠습니다.
Nest는 사용자 정의 메타데이터 붙일 수 있는 기능을 제공하며 이는 라우트 핸들러에서 @SetMetadata()
데코레이터를 통해 가능합니다. 이후 클래스 내에서 이 메타데이터에 접근하여 특정 결정을 내릴 수 있습니다.
// cats.controller.ts
@Post()
@SetMetadata('roles', ['admin'])
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
@SetMetadata()
데코레이터는@nestjs/common
패키지에서 import합니다.
위의 예제에서는 roles
라는 메타데이터를 create()
메서드에 붙였습니다(roles
는 메타데이터 키이며 ['admin']
은 그 값입니다). 라우트에서 @SetMetadata()
데코레이터를 직접 쓰는 것은 그다지 좋은 방법은 아닙니다. 대신에 아래와 같이 사용자 정의 데코레이터를 작성합니다:
// roles.decorator.ts
import { SetMetadata } from '@nestjs/common';
export const Roles = (...roles: string[]) => SetMetadata('roles', roles);
이러한 접근 방식이 훨씬 깔끔하고 가독성 있으며, 타입을 더욱 엄격하게 관리할 수 있습니다. 이제 @Roles()
데코레이터를 create()
메서드에 사용합니다.
// cats.controller.ts
@Post()
@Roles('admin')
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
라우트에서 role이라는 사용자 정의 메타데이터에 접근하기 위해서는 Reflector
헬퍼 클래스를 사용하면 됩니다. 이 클래스는 별도의 작업 없이 그저 @nestjs/core
에서 가져다 사용하기만 하면 됩니다. Reflector
일반적인 방법으로 클래스에 주입시킬 수 있습니다:
// roles.guard.ts
@Injectable()
export class RolesGuard {
constructor(private reflector: Reflector) {}
}
Reflector
클래스는@nestjs/core
패키지에서 import합니다.
이제 핸들러 메타데이터를 읽기 위해 get()
메서드를 사용합니다.
const roles = this.reflector.get<string[]>('roles', context.getHandler());
Reflector
의 get
메서드를 사용하면 두 개의 인자를 전달하여 메타데이터에 쉽게 접근할 수 있습니다. 첫 번째 인자는 메타데이터 key이며, 두 번째 인자는 메타데이터를 검색할 context(데코레이터 대상)입니다. 이 예제에서 지정된 key는 'roles'
입니다(위의 roles.decorator.ts
파일과 해당 파일에서 SetMetadata()
를 호출하는 부분을 참조하세요). 컨텍스트는 context.getHandler()
를 호출함으로써 제공되며, 이는 현재 처리 중인 라우트 핸들러의 메타데이터를 추출하는 데 사용됩니다. 기억하세요, getHandler()
는 라우트 핸들러 함수에 대한 참조를 반환합니다.
또한, 컨트롤러 수준에서 메타데이터를 적용하여 컨트롤러 클래스의 모든 라우트에 적용할 수 있습니다.
// cats.controller.ts
@Roles('admin')
@Controller('cats')
export class CatsController {}
이 경우, 컨트롤러 메타데이터를 추출하기 위해 context.getClass()
를 두 번째 인자로 전달합니다 (메타데이터 추출을 위해 컨트롤러 클래스를 컨텍스트로 제공하기 위함):
//roles.guard.ts
const roles = this.reflector.get<string[]>('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);
}
}
위 코드의 의도가 'user'
를 role을 기본으로 취급하되 몇몇 메서드에서만 role을 덮어씌우고자 하는 것이라면, getAllAndOverride()
메서드를 사용하면 됩니다.
// roles.guard.ts
const roles = this.reflector.getAllAndOverride<string[]>('roles', [
context.getHandler(),
context.getClass(),
]);
created()
메서드의 컨텍스트 속에서 동작하는 가드에서 위와 같이 코드를 작성하면 roles
의 값은 ['admin']
가 됩니다.
두 메타데이터를 병합하려면 getAllAndMerge()
메서드를 사용합니다. 이 메서드는 두 배열을 하나로 병합합니다.
// roles.guard.ts
const roles = this.reflector.getAllAndMerge<string[]>('roles', [
context.getHandler(),
context.getClass(),
]);
위와 같이 작성하면 roles
의 값은 ['user', 'admin']
가 됩니다.
이 두 가지 메서드를 사용할 때에는 첫 번째 인자로 메타데이터 키를, 두 번재 인자로는 메타데이터 대상 컨텍스트(getHandler()
나 getClass()
메서드를 호출한 결과)가 담긴 배열을 건네야 합니다.
실행 컨텍스트란게 뭐야?
코드가 실행되고 연산되는 범위를 나타내는 추상적인 개념.
우리가 코드를 작성하고 실행한다면 실행 컨텍스트(Execution Context) 내부에서 실행되고 있는 것입니다. 즉 코드들이 실행되기 위한 환경이자 하나의 박스이자 컨테이너라 볼 수 있습니다.
핸들러는 뭐야?
컨트롤러에서 getCats()
, getCatById()
, createCat()
과 같은 메서드들이 핸들러
host 객체는 바로 쓸 수 있는 거야?
얍얍 쓸 수 있습니다.
import { ArgumentsHost } from '@nestjs/common';
host: ArgumentsHost
자바 스크립트 에서의 실행 컨텍스트와 여기서 설명하는 Nest의 애플리케이션 컨텍스트는 다른 것 같다.
지금 설명하는 것은 실행하는 애플리케이션의 종류에 따라 인자를 쉽게 받을 수 있도록 nest에서 코드를 제공해주는 것 이라고 생각한다.
요청에 대한 req, res를 전역으로 사용하기 위함
실행 컨텍스트 넘 어렵다..
좋은 글 감사합니다.