Controllers

Controllers는 들어오는(incoming) requests와 clinent로 나가는(returning) responses에 대한 책임을 가지고 있다.

Controller는 application에 대한 특정한 requests를 받는다. **routing** 메커니즘은 어떤 controller가 어떤 request를 받는지를 제어한다. 각 controller는 1개 이상의 route를 가진다. 그리고 route 마다 다양한 action들을 취한다.

routing: 클라이언트의 요청 URL을 기반으로 요청을 적절한 컨트롤러와 핸들러 함수에 매핑하는 과정이다.

basic한 controller를 만들기 위해, Class들과, Decorator들을 사용한다. Decorator들은 클래스에 필요한 metadata를 연관시키고, Nest가 routing map을 생성하여 요청을 해당 컨트롤러와 연결할 수 있게 해준다.

metaData: 
    데이터에 대한 정보를 설명하는 데이터. 여기서는 데코레이터를 통해 클래스나 메서드에 추가되는 정보를 의미한다.
routing map:
    클라이언트의 요청 URL을 특정 컨트롤러 메서드에 매핑하는 구조이다.

<팁!>
유효성 검사가 내장된 CRUD 컨트롤러를 빠르게 생성하려면, CLI의 CRUD 생성기를 사용할 수 있다. nest g resource [name]

Routing

아래의 코드는 @Controller() decorator를 사용한다. 이는 가장 기본적인 controller이다. 여기서 route path로 cats인 prefix를 설정한다. @Cotroller() decorator에 path prefix를 기입함으로써 간단히 관련된 routes를 group화 할 수 있다. 그리고 반복되는 코드를 최소화 할 수 있다. 예를 들어, 우리는 cat entity와 관련된 /cats route group들을 선택할 수 있다. 이 경우, @Controller() 데코레이터에 'cats' 경로 접두사를 지정하면 파일 내의 각 경로에 대해 경로 부분을 반복해서 작성할 필요가 없다.

import { Controller, Get } from '@nestjs/common';

@Controller('cats')
export class CatsController {
  @Get()
  findAll(): string {
    return 'This action returns all cats';
  }
}

<팁!>
CLI를 사용하여 controller를 만들고자 한다면, 간단히 $ nest g controller [name] 커멘드를 사용하면 된다.

findAll()에 붙어있는 @Get() HTTP request method decorator는 Nest에게 HTTP requests의 endpoint에 대한 handler를 만들어라고 명령한다. endpoint는 HTTP request method(위의 경우는 GET)와 route path에 대응된다.

이 때 routh path는 컨트롤러에 선언된 (선택적) prefix와 method의 decorator에 지정된 경로를 연결하여 결정된다. 예를 들어 @Controller에 'cats' 그리고 @Get(':id')라면 전체 path는 '/cats/:id'가 된다. 만일 prefix에 'cats'를 선언한다면, 더 이상 decoraotor에 어떤 path 정보를 기입할 필요가없다. Nest가 자동으로 map 'GET /cat' request를 handler에 mapping 할 것이다. 앞서 언급한 대로, path는 controller의 정보(prefix)와 requests method decorator의 정보를 포함한다. 그렇기에 'cats'와 prefix와 @Get('breed')가 존재한다면 route의 path는 'GET /cats/breed'가 된다.

위의 코드에서 만일 이 endpoint에서 만들어진 GET request가 호출된다면, Nest는 개발자가 만든 findAll() method로 route할 것이다. 이때 findAll()과 같은 method 이름은 개발자가 임의로 지정할 수 있다. path를 binding하기 위해 method를 선언해야 하지만, Nest는 method의 이름에 아무런 의미를 부여하지 않는다.

이 method는 정상 작동을 한다면 200의 status code와 관련된 response를 반환할 것이다.(위의 경우 단순한 string) 이를 이해하려면 Nest가 응답을 조작하는 두 가지 다른 옵션을 알아야 한다.

OPTION DESCRIPTION
Standard (recommended) 만일 내장 함수(built-in method)를 사용하여, request handler가 Javascript Object나 Array를 반환할 때, 이는 자동적으로 JSON으로 직렬화(serialized)된다. 하지만 원시타입(Primitive, e.g. string, number, boolean)을 반환할 때는 별도의 직렬화를 하지 않는다. 이를 통해 응답 처리는 단순해진다. 값만 반환하면 나머지는 Nest가 처리한다. 게다가, 기본적으로 response의 POST는 status code 201을, 나머지는 200을 반환한다. 만일 이를 바꾸고 싶다면 단순히 @HttpCode(...) decorator를 추가하면 된다.
Library-specific 개발자는 library-specific(e.g. Express) response object를 사용할 수 있다. 이는 method handler signature안에 @Res() decorator를 사용하여 주입할 수 있다.(e.g. findAll(@Res() response)) 이러한 접근방식으로, 해당 객체가 드러내는 native response handling methods를 사용할 수 있다. 예를 들어 Express에서, 'response.status(200).send()'와 같은 코드를 사용하여 응답을 구성할 수 있다.
Method handler signature: 메서드의 매개변수와 반환 '타입'을 포함한 정의
native response handling method: Express나 Fastify와 같은 HTTP 서버 라이브러리가 제공하는 응답 처리 메서드로
    상태 코드 설정, 헤더 설정, 응답 본문 전송 등을 수행한다.
    e.g. 
    1. response.status(code): HTTP 응답 상태 코드를 설정
    2. response.send(body): 응답 본문을 전송한다.
    3. response.json(json): JSON 형식의 응답 본문을 전송한다.
    4. response.set(header, value): 응답 헤더를 설정한다.
// 상태코드의 변경
import { Controller, Get, HttpCode } from '@nestjs/common';

@Controller('cats')
export class CatsController {
  @Get()
  @HttpCode(204)
  findAll() {
    return 'No Content';
  }
}
// Express를 기반의 @Res() 활용
import { Controller, Get, Res } from '@nestjs/common';
import { Response } from 'express';

@Controller('cats')
export class CatsController {
  @Get()
  findAll(@Res() response: Response) {
    const cats = ['cat1', 'cat2', 'cat3'];
    response.status(200).json(cats); // 상태코드 조작 가능
  }
}

주의할점
Nest는 @Res()@Next()의 사용을 감지한다. 이는 개발자가 library-specific option을 사용함을 나타낸다. 허나 만일 Standard approach와 동시에 사용한다면 Standard approach 접근 방식이 자동으로 비활성화되어 정상적으로 동작하지 않는다.
만일 두 가지 접근 방식을 동시에 사용하려면(e.g. cookie/header를 설정하기 위해 response 객체에 주입을 하되 나머지는 프레임워크에 맡기기 위해, 즉 응답 객체를 직접 조작하면서 나머지 응답 처리는 프레임워크에 맡기려면), @Res({passthough:true}) 데코레이터에서 'passthrough' 옵션을 'true'로 설정해줘야 한다.

Request object

Handler는 client의 request에 접근할 필요가 있다. Nest는 request object에 접근을 가능케한다. @Req() decorator를 주입함으로써 request object에 조작할 수 있다.

import { Controller, Get, Req } from '@nestjs/common';
import { Request } from 'express';

@Controller('cats')
export class CatsController {
  @Get()
  findAll(@Req() request: Request): string {
    return 'This action returns all cats';
  }
}

express의 타입 지정을 위해 (위의 예에서는 request:Request), @types/express package를 설치하면 된다.

request object는 HTTP request를 나타낸다. 그리고 request query string, parameters, HTTP headers, body 등을 가진다. 대부분의 경우에 이러한 속성을 수동으로 가져올 필요는 없다. 대신 @Body(), @Query()와 같은 전용 decorator를 통해 접근할 수 있다.

decorators obejct list
@Request(), @Req() req
@Response(), @Res() * res
@Next() next
@Session() req.session
@Param(key?: string) req.params/ req.params[key]
@Body(key?:string) req.body/req.body[key]
@Query(key?:string) req.query/req.query[key]
@Headers(name?:string) req.headers/req.headers[name]
@Ip() req.ip
@HostParam() req.hosts

NestJS는 하위 HTTP 플랫폼(e.g. Express와 Fastify) 간의 타입 호환성을 위해, Nest는 @Res(), @Response() decorator를 제공한다. @Res()는 @Response()의 별칭이다. 둘 다 하위 native platform의 응답 객체 interface를 직접 노출한다. 만일 이 둘을 사용하면, 개발자는 하위 라이브러리(e.g. @types/express)에서 타입을 import 해야한다. 기억해야 할 것이 @Res()@Response()을 method handler에 주입한다는 것은, Nest가 response를 Library-specify mode로 처리함을 뜻한다. 곧, 개발자가 response의 관리에 책임이 있다는 것이다. 그렇기에 개발자는 반드시 response object를 사용하여 어떤 형태로든 응답을 반환해야 한다.(e.g. res.json(...), res.send(...)). 그렇지 않으면 HTTP 서버가 응답을 기다리며 멈추게 된다.

native platform: NestJS와 같은 프레임워크가 작동하는 기본 HTTP 서버 라이브러리를 의미한다. NestJS는 Express와 Fastify와 같은 HTTP 서버 라이브러리 위에서 동작할 수 있다. 이러한 라이브러리를 "native platform"이라고 부르며. 각각의 platform은 고유의 요청 및 응답 객체와 메서드를 제공한다.

method: 클래스 내에 정의된 함수, 객체 지향 프로그래밍에서 객체의 동작을 정의
method handler: 특정 HTTP 요청을 처리하는 함수, 주로 컨트롤러 클래스 내에서 정의된다.

Resources

앞에서 GET route, resoucres를 가져오는 endpoint를 정의내렸다. 이번에는 POST route, 새 record를 만드는 endpoint를 만들 것이다.

import { Controller, Get, Post } from '@nestjs/common';

@Controller('cats')
export class CatsController {
  @Post()
  create() {
    return 'This action adds a new cat';
  }

  @Get()
  findAll() {
    return 'This action returns all cats';
  }
}

Nest는 standard한 HTTP method을 위한 decorator들을 제공한다.
Get, Post, Put, Delete, Patch, Options, Head 그리고 All, 이것은 모든 것들 다루는 endpoint이다.

Route wildcards

Pattern을 기반으로한 route도 지원한다. 예를 들어 *(asterisk)는 와일드 카드로 사용되며, 임의의 문자 조합과 일치한다.

import { Controller, Get } from '@nestjs/common';

@Controller('files')
export class FilesController {
  @Get('download/*')
  downloadFile() {
    return 'This action handles downloading files';
  }

  @Get('images/*')
  serveImage() {
    return 'This action serves images';
  }
}

이 밖에 ?, +, * 그리고 () 또한 route path에 쓰인다. 이는 정규 표현식(regular expression)과 유사하다. 하이픈(-), 점(.)은 문자열 기반 경로에서 문자 그대로 해석된다.

주의사항
route의 중간에서의 wildcard는 express에 의해서만 지원된다.

Status code

앞서 언급한 대로 201번인 POST를 제외한 나머지의 상태코드는 200이다. 이는 @HttpCode(...) 를 통해 handler level에서 조작할 수 있다. @nestjs/common package에서 import한다.

@Post()
@HttpCode(204)
create() {
  return 'This action adds a new cat';
}

만일 상태 코드가 static하지 않고 다양한 요소에 의존한다면, library-specific response object(inject using @Res())를 사용하면 된다.

Headers

custom response header을 사용하기 위해서는 @Header() decorator이나, library-specific response object를 사용한다.( res.header() 을 통해 직접적으로) Header는 @nestjs/common package를 통해 import한다.

@Post()
@Header('Cache-Control', 'none')
create() {
  return 'This action adds a new cat';
}

Redirection

redirection은 클라이언트가 요청한 URL을 다른 URL로 자동으로 전환시키는 웹 기술이다. 웹 서버 또는 어플리케이션은 클라이언트에게 HTTP 응답으로 특정 상태 코드와 새로운 URL을 보내어 클라이언트가 해당 URL로 이동하도록 유도한다.

response를 다른 URL로 redirection하기 위해서는, @Redirect() decorator를 사용하거나 library-specific response object의 res.redirect() 를 사용한다.
@Redirect() 는 두 가지 argument를 필요로 한다. urlstatusCode이다.(둘 다 optional) status의 default value는 302이다.

@Get()
@Redirect('https://nestjs.com', 301)

만일 개발자가 HTTP 상태 코드와 redirect URL을 동적으로 하고 싶다면, HttpRedirectResponse interface(@nest/common)을 사용한다.

method handler의 반환값이 존재한다면 @Redirect()의 값을 덮는다.

import { Controller, Get, Redirect } from '@nestjs/common';

@Controller('example')
export class ExampleController {

  // 기본적으로 'https://default-url.com'로 302 상태 코드로 리다이렉트
  @Get('default-redirect')
  @Redirect('https://default-url.com', 302)
  defaultRedirect() {
    // 이 메서드가 반환하는 값이 없으므로, 데코레이터의 URL과 상태 코드가 사용됨
  }

  // 리다이렉션 URL과 상태 코드를 동적으로 변경
  @Get('dynamic-redirect')
  @Redirect('https://default-url.com', 302)
  dynamicRedirect() {
    return { url: 'https://dynamic-url.com', statusCode: 301 };
    // 반환 값이 있으면, 이 값이 데코레이터 인수를 덮어씀
  }
}

Route parameters

static path로 인한 Route는 만일 개발자가 request의 부분으로서 dynamic data를 받기에 부적절하다.
예를 들어 GET /cats/1에서의 id값 1이 있다. NestJs에서는 경로에 동적 매개변수 토큰을 추가하여 요청 URL의 해당 위치에서 동적 값을 얻을 수 있다. 이러한 경로 매개변수는 콜론(':')을 사용하여 정의 된다.
아래의 @Get()에 있는 route parameter token의 사용 예시이다. route parameter는 @Param() 을 통하여 접근할 수 있다. param()은 method signature을 통해 추가 된다.

method signature: 메서드의 이름, 매개변수 리스트, 반환 타입을 포함한 메서드의 정의를 나타낸다.
//TS
@Get(':id')
findOne(@Param() params: any): string {
  console.log(params.id);
  return `This action returns a #${params.id} cat`;
}


//TS-타입 지정
@Get(':id')
findOne(@Param() params: {id:string}): string {
  console.log(params.id);
  return `This action returns a #${params.id} cat`;
}
@Get(':id')
@Bind(Param())
findOne(params) {
  console.log(params.id);
  return `This action returns a #${params.id} cat`;
}
//Bind() 데코레이터는 여러 매개변수 데코레이터를 결합하여 하나의 매개변수 객체로 전달할 수 있게 해준다.

NestJS에서 경로를 선언할 때, 매개변수가 있는 경로는 반드시 정적 경로 뒤에 선언해야 한다. 이렇게 해야 매개변수가 있는 경로가 정적 경로를 향하는 트래픽을 가로채지 않기 때문이다. path mapping은 선언된 순서대로 이루어진다. 만약 parameter가 있는 path를 static path보다 먼저 선언하면, 모든 해당 path가 매개변수 경로로 매칭되어 static 경로가 동작하지 않게 된다.

import { Controller, Get, Param } from '@nestjs/common';

@Controller('items')
export class ItemsController {
  @Get(':id')
  getItem(@Param('id') id: string): string {
    return `Item ID: ${id}`;
  }

  @Get('static')
  getStatic(): string {
    return 'This is a static route';
  }
}

잘못된 경우 : 매개변수가 있는 경로를 정적 경로보다 먼저 선언한 경우
'/items/static' 경로로 요청이 들어오면, 이 요청은 매개변수 경로 '@Get(':id')'로 매칭되어 id 파라미터로 인식된다.
따라서 getStatic() method는 호출되지 않는다.

     - 요청 URL: /items/static'
        - 결과: Item ID: static
import { Controller, Get, Param } from '@nestjs/common';

@Controller('items')
export class ItemsController {
  @Get('static')
  getStatic(): string {
    return 'This is a static route';
  }

  @Get(':id')
  getItem(@Param('id') id: string): string {
    return `Item ID: ${id}`;
  }
}

올바른 경우 : 정적 경로를 매개변수가 있는 경로보다 먼저 선언한 경우
'/items/static' 경로로 요청이 들어오면, 정적 경로 '@Get('static')'가 먼저 매칭되므로, 'getStatic()' method 가 호출된다.
매개변수 경로 '@Get(':id')'는 다른 모든 경로를 처리한다.

    - 요청 URL: '/items/static'
        - 결과: 'This is a static route'
    - 요청 URL: '/items/123'
        - 결과: 'Item ID: 123'

@Param() 은 method parameter을 decorate되어, path parameter를 method 내부에서 해당 매개 변수의 속성으로 사용할 수 있게 한다. 이를 통해 요청 경로의 특정 매개변수를 쉽게 접근하고 사용할 수 있다. Param@nestjs/common package에서 import 한다.

//TS
@Get(':id')
findOne(@Param('id') id: string): string {
  return `This action returns a #${id} cat`;
}
//JS
@Get(':id')
@Bind(Param('id'))
findOne(id) {
  return `This action returns a #${id} cat`;
}

Sub-Domain Routing

@Controllerhost 옵션을 가질 수 있다. 이를 통해 들어오는 요청의 HTTP 호스트가 특정 값과 일치해야 함을 요구할 수 있다. 이를 통해 특정 호스트에 대해 경로를 바인딩할 수 있다. 즉 특정 도메인에서만 api를 요청할 수 있도록 하는 것이다.

@Controller({ host: 'admin.example.com' })
export class AdminController {
  @Get()
  index(): string {
    return 'Admin page';
  }
}
  • 위의 경우 'admin.example.com' 호스트에 대해서만 'AdminController'의 path를 활성화 한다.
  • '/' path로 들어오는 요청은 호스트가 'admin.example.com'인 경우에만 처리된다.
HTTP host: 웹 요청을 처리할 서버의 도메인 이름 또는 IP주소를 의미한다.
    웹 브라우저나 클라이언트는 서버에 요청을 보낼 때 요청 헤더의 호스트 정보를 포함시킨다.
    이 호스트 정보는 서버가 어떤 도메인에 대한 요청인지를 식별하는 데 사용된다.

주의사항
Fastify는 nested routers에 대한 지원의 부족으로 만일 sub-domain routing을 해야한다면, Express adapter을 대신 사용해야 한다.

route path와 유사하게, hosts option을 사용하면 host name에서의 dynamic value, host paramter token을 캡쳐할 수 있다. @HostParam() decorator을 통해 host parameter에 접근할 수 있다. method는 method signature로 사용되어야 한다.

import { Controller, Get, HostParam, Param } from '@nestjs/common';

@Controller({ host: ':subdomain.example.com' })
export class SubdomainController {
  @Get('user/:id')
  getUser(@HostParam('subdomain') subdomain: string, @Param('id') id: string) {
    return `This action returns user with id ${id} from the ${subdomain}.example.com subdomain`;
  }
}

Scopes

NestJS에서는 대부분의 리소스가 들어오는 모든 요청에 대해 공유된다. 이러한 점은 다른 프로그래밍 언어 배경을 가진 사람에게는 놀라울 것이다. DB에 대한 연결 풀, 전역 상태를 가진 singleton service 등이 이러한 예이다. 이는 Node.js가 request/response multi Threaded Stateless Model을 따르지 않기 때문이다. 따라서 singleton instance를 사용하는 것이 application에서 완전히 안전하다.

NestJS는 Node.js 기반으로 동작한다. 특히, NestJS는 다수의 요청을 처리할 때 대부분의 componenet를 공유한다.

componenet: 주로 모듈, 컨트롤러, 서비스, 미들웨어, 파이프, 필터, 가드 등을 의미한다. 
    이러한 컴포넌트는 NestJS 어플리케이션의 주요 빌딩 블록으로, 각 컴포넌트는 특정한 역할과 책임을 가지고 있다.
    1. module: 관련된 컴포넌트들을 그룹화하여 구성 요소를 조직화하는 단위. 
        모든 NestJS 어플리케이션은 최소 하나의 루트 모듈을 가지며, 어플리케이션의 구조를 정의
    2. controller: 컨트롤러는 들어오는 요청을 처리하고, 클라이언트에게 응답을 반환하는 역할을 한다. 
        주로 라우팅을 처리하며, 서비스 계층과 상호작용한다.
    3. service: 서비스는 비즈니스 로직과 데이터 접근을 담당한다. 일반적으로 싱글턴으로 동작하며, 
        컨트롤러나 다른 서비스에서 주입받아 사용
    4. middle: request가 controller에 도달하기 전에 처리되는 함수. loggin, authentication, request 변환 등 다양한 목적에 사용
    5. pipe: transformation과 validation을 담당한다. 요청 데이터가 컨트롤러 핸들러로 전달되기 전에 처리된다.
    6. filter: 필터는 예외를 처리하고, 사용자 정의 예외 응답을 생성할 수 있다.
    7. guard: 요청이 컨트롤러 라우트 핸들러에 도달하기 전에 실행되며, 주로 인증 및 권한 부여 로직을 포함한다.
  • NestJS에서의 공유 컴포넌트

    1. database 연결 풀: 여러 요청이 동일한 데이터베이스 연결 풀을 사용한다. 이는 효율성을 높이고 자원 낭비를 줄인다.
      • DataBase Connection Pool: 다수의 DB 연결을 미리 생성해 두고, 요청이 있을 때 재사용할 수 있도록 관리하는 메커니즘. 이는 각 요청마다 새로운 DB 연결을 생성하는 대신, 미리 생성된 연결을 재사용하여 성능을 최적화하고 자원을 절약한다.
      • import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { AppService } from './app.service'; @Module({ imports: [ TypeOrmModule.forRoot({ type: 'mysql', host: 'localhost', port: 3306, username: 'test', password: 'test', database: 'test', entities: [__dirname + '/../**/*.entity{.ts,.js}'], synchronize: true, // 연결 풀 설정 extra: { connectionLimit: 10, // 연결 풀 크기 설정 }, }), ], providers: [AppService], }) export class AppModule {}
    2. 싱글턴 서비스: singleton 서비스는 어플리케이션 전역에서 단 하나의 instance로 존재하는 서비스이다. 대부분의 서비스는 싱글턴으로 동작하여 글로벌 상태를 유지할 수 있다. 즉 여러 요청 간에 동일한 인스턴스를 공유할 수 있음을 의미한다. 메모리 사용을 최적화하고, 반복적인 초기화 비용을 줄인다.
    3. 글로벌 상태: application 전역에서 공유되는 데이터나 설정을 의미한다. NestJS에서는 싱글턴 서비스를 통해 이러한 글로벌 상태를 관리할 수 있다. 즉 application 전역에서 상태를 공유하여 전역 설정이나 데이터를 중앙에서 관리할 수 있다.
    4. // 아래의 ConfigService는 어플리케이션 전역에서 설정 값을 관리한다. // 다른 서비스나 모듈에서 ConfigService를 주입받아 설정 값을 읽거나 쓸 수 있다. import { Injectable } from '@nestjs/common'; @Injectable() export class ConfigService { private config = { appName: 'NestJS Application', version: '1.0.0', }; getConfig(key: string) { return this.config[key]; } setConfig(key: string, value: any) { this.config[key] = value; } } @Module({ providers: [ConfigService], exports: [ConfigService], }) export class ConfigModule {}
  • Node.js와 멀티스레딩:

    • Node.js는 요청/응답 모델에서 멀티스레드 상태 비저장 모델을 따르지 않는다. 대신, 단일 스레드 이벤트 루프를 사용하여 비동기적으로 요청을 처리한다. 이러한 특성으로 싱글턴 인스턴스를 사용하는 것이 안전하다.
  • NestJS의 범위(Scopes):

    • NestJS에서는 서비스의 인스턴스 생성 방식을 제어하기 위해 다양한 범위를 제공한다. 각 범위는 서비스 인스턴스의 수명 주기와 범위를 정의한다.
    1. 기본 범위(Default Scope): 기본적으로 NestJS의 모든 서비스는 싱글턴으로 동작한다. 즉, 어플리케이션이 시작될 때 한 번 인스턴스화 되고, 모든 요청에서 동일한 인스턴스를 사용한다.
    2. @Injectable() export class MySingletonService { private count = 0; increment() { this.count++; } getCount() { return this.count; } }
    3. 요청 범위(Request Scope): 요청 범위의 서비스는 각 요청마다 새로운 인스턴스를 생성한다. 이렇게 하면 요청 간에 상태를 공유하지 않는다.
    4. import { Injectable, Scope } from '@nestjs/common'; @Injectable({ scope: Scope.REQUEST }) export class MyRequestScopedService { private count = 0; increment() { this.count++; } getCount() { return this.count; } }
    5. 트랜지언트 범위(Transient Scope): 트랜지언트 범위의 서비스는 주입될 때마다 새로운 인스턴스를 생성한다. 이를 주입받는 각 컴포넌트가 고유의 인스턴스를 가지게 함을 의미한다.
    6. import { Injectable, Scope } from '@nestjs/common'; @Injectable({ scope: Scope.TRANSIENT }) export class MyTransientService { private count = 0; increment() { this.count++; } getCount() { return this.count; } }

그러나 일부 경우에는 요청 기반의 라이프사이클이 바람직할 수 있다. 예를 들어, GraphQL application에서의 per-request caching. request tracking, multi-tenancy가 있다.

Multi Threaded Stateless Model: 각 요청이 별도의 스레드에 의해 처리되는 것
SingleTon: 특정 클래스의 인스턴스를 1개만 생성되는 것을 보장하는 디자인 패턴

request-cahcing: 요청별로 캐시를 관리하여 DB 요청을 줄일 수 있다.
request tracking: 각 요청마다 고유한 추적 ID를 생성하여 요청을 추적할 수 있다.
multi-tenancy: 하나의 소프트웨어 인스턴스가 여러 사용자를 동시에 서비스하는 아키텍쳐를 말함
    각 요청마다 다른 tenancy의 데이터를 처리할 수 있다. 
tenancy: 소프트웨어 application을 사용하는 개별 고객 또는 조직을 말한다.

Asynchronicity(비동기성)

Asynchronicity는 작업이 완료될 때까지 기다리지 않고 다음 작업을 수행하는 것을 말한다.
대부분의 data extraction의 경우 asynchronous하게 된다. 그렇기에 Nest에서는 async function을 제공한다.자세히

모든 async function은 Promise를 반환한다. 즉 비동기 작업을 수행할 때 연기된(deferred) 값을 반환할 수 있으며, nest는 이를 스스로 해결할 수 있다. 이는 Promise, async/await를 사용하여 비동적으로 처리되는 값을 반환할 수 있음을 의미한다.

@Get()
async findAll(): Promise<any[]> {
  return [];
}

위의 코드는 유효하다. 게다가 Nest route handler는 RxJS observable streams를 사용함으로써 더 강력하다. 이를 통해 비동기 데이터 흐름을 쉽게 관리할 수 있다. Nest는 내부적으로 observable에 자동으로 구독하고, 스트림이 완료되면 마지막으로 방출된 값을 클라이언트에게 반환한다.

RxJS: Reactive Extensions for JavaScript의 약자로, 비동기 및 이벤트 기반 프로그램을 쉽게 작성할 수 있도록 도와주는 라이브러리다. 

Observable: data stream을 나타내며, 시간이 지남에 따라 여러 값을 방출할 수 있다. 
    - 데이터가 도착했을 때 수행할 작업을 정의하는 구독자(subscriber)를 가질 수 있다.
Observer: Observable에서 방출된 데이터를 처리하는 콜백 함수의 모음. 보통 next, error, complete method를 가진다.
Subscription: Observable에 대해 구독을 시작하거나 중지하는 것을 관리한다.
Operators: Observable의 데이터 스트림을 변형하거나 필터링할 수 있는 함수들이다.

Observable은 RxJS(Reactive Extensions for JavaScript) 라이브러리에서 제공하는 핵심 개념 중 하나로, 비동기 데이터 스트림을 나타낸다. 'Observable'은 데이터의 흐름을 관리하고, 이를 구독(subscribe)하여 데이터가 발생할 때마다 반응할 수 있게 한다. 이를 통해 이벤트 기반의 비동기 프로그래밍을 쉽게 구현할 수 있다.

@Get()
findAll(): Observable<any[]> {
  return of([]);
}

Request Payloads

이전의 POST Example은 any client params를 받지 못한다. 이를 @Body() decorator을 추가함으로써 고칠 수 있다.

먼저 DTO(Data Transfer Object) schema를 추가한다. DTO는 network를 거쳐 어떻게 보내는지를 정의한다. Typescript의 interface를 통해, 혹은 class로 DTO schema를 결정할 수 있다. 신기하게도 class 사용을 권장한다. 왜냐하면, Class들은 JavaScript ES6 standard의 부분이기에 compiled JavaScript의 안에서 real entities로 보존된다. 반면에 Typescript의 interface들은 transpilation 과정에서 삭제된다. 그리고 Nest는 runtime 과정에서 이를 언급하지 않는다. 이는 NestJs에서 중요한 의미를 가지는데, 특히 파이프(Pipe) 와 같은 기능이 런타임에 변수의 메타타입(metatype)에 접근할 수 있을 때 추가적인 가능성을 제공하기 때문이다.

compile: 한 언어로 작성된 소스 코드를 다른 언어로 변환하는 것을 의미한다.
    e.g. Java -> bytecode
         c -> assembly
transpile: 한 언어로 작성된 소스 코드를 비슷한 수준의 추상화를 가진 다른 언어로 변환하는 것
    e.g. es6 -> es5
         c++ -> c
         coffescript -> javascript
export class CreateCatDto {
  name: string;
  age: number;
  breed: string;
}

이는 3개의 간단한 속성을 가진다. 이후에 우리는 새로히 만들어진 DTO를 사용할 수 있다.

@Post()
async create(@Body() createCatDto: CreateCatDto) {
  return 'This action adds a new cat';
}
// TS
@Post()
@Bind(Body())
async create(createCatDto) {
  return 'This action adds a new cat';
}

ValidationPipe는 method handler로 전달되면 안되는 속성들을 filter해준다. 개발자는 acceptable한 properties들을 whitelist할 수 있다. 그리고 whilelist되지 못한 속성들을 자동으로 stripped 된다.자세히

Full resource error

아래의 경우 몇 개의 decorator를 이용해 만든 basic controller의 예이다. internal data에 접근하고 조작하기 위한 몇개의 method들이 담겨져 있다.

import { Controller, Get, Query, Post, Body, Put, Param, Delete } from '@nestjs/common';
import { CreateCatDto, UpdateCatDto, ListAllEntities } from './dto';

@Controller('cats')
export class CatsController {
  @Post()
  create(@Body() createCatDto: CreateCatDto) {
    return 'This action adds a new cat';
  }

  @Get() // HTTP 요청의 쿼리 매개변수를 추출하는 데 사용
  findAll(@Query() query: ListAllEntities) {
    return `This action returns all cats (limit: ${query.limit} items)`;
  }

  @Get(':id')
  findOne(@Param('id') id: string) {
    return `This action returns a #${id} cat`;
  }

  @Put(':id')
  update(@Param('id') id: string, @Body() updateCatDto: UpdateCatDto) {
    return `This action updates a #${id} cat`;
  }

  @Delete(':id')
  remove(@Param('id') id: string) {
    return `This action removes a #${id} cat`;
  }
}

Nest CLI는 위의 작업을 손쉽게 만들어주는, all the boilerplate code를 만들어주는 generator를 제공한다.자세히

Getting up and Running

위의 controller가 정의되었음에도 Nest는 CatsController의 존재를 알지못한다. 그렇기에 이 class의 instance를 만들지 못한다.

Controller는 항상 module에 속한다. @Module() decorator안의 controllers array에 class가 속해야 한다. 아직 예시에서는 AppModule을 제외한 나머지 module을 만들지 않았기에, 우리는 여기서 CatsController를 넣는다.

import { Module } from '@nestjs/common';
import { CatsController } from './cats/cats.controller';

@Module({
  controllers: [CatsController],
})
export class AppModule {}

@Module() Decorator를 사용하여 MetaData를 module class에 첨부하였으며, 이제 Nest는 어떤 controller들이 mount되어야 하는지 쉽게 알 수 있다.

Library-Specific Approach

지금까지 Nest에서 responses들을 조작하는 기본적인 way들을 알아봤다. 두 번째 방법은 library-specific response object이다. response object를 inject하기 위해서, @Res() decorator를 사용해야한다.

//JS
import { Controller, Get, Post, Bind, Res, Body, HttpStatus } from '@nestjs/common';

@Controller('cats')
export class CatsController {
  @Post()
  @Bind(Res(), Body())
  create(res, createCatDto) {
    res.status(HttpStatus.CREATED).send();
  }

  @Get()
  @Bind(Res())
  findAll(res) {
     res.status(HttpStatus.OK).json([]);
  }
}
import { Controller, Get, Post, Res, HttpStatus } from '@nestjs/common';
import { Response } from 'express';

@Controller('cats')
export class CatsController {
  @Post()
  create(@Res() res: Response) {
    res.status(HttpStatus.CREATED).send();
  }

  @Get()
  findAll(@Res() res: Response) {
     res.status(HttpStatus.OK).json([]);
  }
}

이러한 방식은 response object를 직접적으로 제어함(Header manipulation, library-specific features, ...)으로써 좀더 flexibility 하지만 이는 좀더 조심해서 사용해야한다. 일반적으로 상대적으로 덜 clear하고, 일부 단점이 존재한다. 무엇보다도 코드들이 platform-dependent 해진다. 예를 들어 Express에서 Fastify로 변경할 경우, 응답 객체의 API가 달라 코드 변경이 필요하다. 테스트가 복잡해진다. 응답 객체를 직접 mocking 해야하므로 테스트가 더 어려워 진다.

또한 compatibility를 잃을 것이다. 플랫폼 종속적인 접근 방식을 사용하면, interceptors와 @HttpCode(), @Header() decorator 등 Nest의 표준 응답 처리에 의존하는 기능들과의 호환성을 잃게 된다. 이럴 해결하기 위해, passthrough 옵션을 true로 설정할 수 있다.

// TS
@Get()
findAll(@Res({ passthrough: true }) res: Response) {
  res.status(HttpStatus.OK);
  return [];
}

이제 native response object와 interact를 할 수 있다. (e.g. set cookies, or headers).

'NestJS > Docs_OVERVIEW' 카테고리의 다른 글

Exception filters  (0) 2024.06.05
Middleware  (1) 2024.06.04
Modules  (1) 2024.06.04
Providers  (0) 2024.06.04
First Steps  (0) 2024.06.03

+ Recent posts