컨트롤러 1편

웹 어플리케이션에서 컨트롤러(Controller)란 외부의 요청을 처리하는 모듈을 의미합니다. 좀 더 정확하게 말하면 하나 이상의 클라이언트가 보내는 요청을 처리하고 요청을 보낸 클라이언트에게 응답을 반환하는 역할이죠.

컨트롤러

컨트롤러의 목적은 애플리케이션에 대한 특정 요청을 수신하는 것입니다. 라우팅 메커니즘은 어떤 컨트롤러가 해당 요청을 처리할지 조정합니다. 보통은 각각의 컨트롤러는 하나 이상의 경로가 있으며, 각기 다른 경로는 각기 다른 행동을 수행합니다.

라우팅(Routing)이란?

라우팅은 네트워크에서는 어떠한 패킷을 원하는 곳으로 보내는 행위를 가리킵니다.(여기를 참고하세요). 이 글에서 라우팅이란 들어온 HTTP 요청을 특정 컨트롤러로 보내는 행위를 말합니다.

Nest는 기본 컨트롤러를 생성하기 위해서 클래스와 데코레이터를 사용합니다. 데코레이터는 클래스를 필수 메타데이터와 연결하고 Nest가 라우팅 맵을 만들 수 있도록 합니다.

라우팅

다음 예제에서는 기본 컨트롤러를 정의하는 데 필요한 @Controller() 데코레이터를 사용합니다. 예제에서는 선택적 경로 경로 접두사로 cats를 명시했습니다.

// cats.controller.ts / 예제1

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

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

데코레이터에서 경로 접두사를 사용하면 관련 경로 집합을 쉽게 그룹화하고 반복 코드를 최소화 할 수 있습니다. 예를 들어 볼까요? 위 예제의 findAll()Get에도 경로를 직접 입력할 수 있습니다. Get은 이 메소드가 GET 메소드를 처리한다는 말인데 곧 이어 설명합니다. @Controller() 데코레이터의 선택적 경로 접두어를 안붙인다면 아래와 같이 모든 메소드에 경로를 붙여줘야 합니다.

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

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

  @Get('cats/:id')
  findOne(@Param('id') id: number): string {
    return 'This action returns one cats';
  }

  @Get('cats/musical')
  findMusical() {
    return 'This action returns musical CATs';
  }
}

cats/ 단어의 반복이 계속 됩니다. 위 코드를 아래와 같이 바꾸는게 @Controller()데코레이터의 경로 접두어입니다.

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

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

  @Get(':id')
  findOne(@Param('id') id: number): string {
    return 'This action returns one cats';
  }

  @Get('musical')
  findMusical() {
    return 'This action returns musical CATs';
  }
}

다시 예제1로 돌아와서 findAll() 메서드 앞의 @Get() 데코레이터가 있습니다. @Get() 데코레이터는 Nest 프레임워크에 해당 메서드가 HTTP 요청의 특정 엔드포인트를 처리한다고 알려줍니다. 엔드포인트는 HTTP 요청 메서드(여기서는 GET) 및 라우트 패스(Route path)에 해당합니다. 라우트도 경로고 패스도 경로라는 뜻이라서 일부러 원문으로 번안한 Nest에서 말하는 라우트 패스란 findAll()과 같은 메서드의 URL 경로는 컨트롤러가 선언한 접두사(선택사항)와 @Get()과 같은 요청 데코레이터에 지정된 경로를 연결하여 정해진다는 점입니다. 예를 들어 예제1에서는 컨트롤러의 패스가 cats이며, findAll()은 경로 정보가 없기 때문에 Nest는 GET /cats 요청을 findAll()메서드에 매핑합니다. 예제3에서는 /cats/musical URL 요청이 오면 findMusical()메서드가 실행됩니다.

API 엔드포인트(Endpoint)란?

웹서비스에서 API 엔드포인트란 클라이언트가 여러분의 API에 접근할 수 있는 URL을 뜻합니다.

위 예제에서는 클라이언트에서 /cats요청이 왔을때 findAll()메서드로 라우팅하는데, Nest 입장에서는 이 findAll이라는 메서드 이름은 별로 중요하지 않습니다. 반드시 요청 데코레이터 다음에는 메서드를 선언해야 하지만 이름은 아무거나 지어도 됩니다. 심지어 abcdef같은 메서드 이름을 지어도 됩니다. 왜냐하면 Nest는 이런 메서드 이름에 어떤 의미도 부여하지 않기 때문이죠.

클라이언트의 응답을 처리하는 메서드는 HTTP 상태코드와 응답을 반환합니다. 위의 findAll()은 상태코드 200과 단순한 문자열을 반환할 뿐이죠. 왜 그럴까요? 이를 설명하기 위해 먼저 Nest가 응답을 조작하기 위해 두 가지 다른 옵션을 사용한다는 개념을 소개하겠습니다.

표준 (권장)

내장 메서드를 사용하면 요청 핸들러(여기서는 findAll()과 같은 요청을 처리하는 메서드)가 JavaScript 객체 또는 배열을 반환 할 때 자동으로 JSON으로 직렬화(Serialize)됩니다. 만약 Javascript 원시형(Primitive Type. 예를 들어 string, number, boolean이 있습니다)을 반환할 경우 직렬화하지 않고 바로 그 값을 전송합니다. 이렇게하면 응답 처리가 간단해집니다. 값을 반환하기만 하면 Nest가 나머지를 처리합니다.

또한 응답의 상태 코드는 201을 사용하는 POST 요청을 제외하고는 항상 기본적으로 200입니다. 나는 특별히 200 외에 다른 값(예: 204)을 보내고 싶다면 @HttpCode(...) 데코레이터를 사용해서 핸들러 수준에서 동작을 쉽게 변경할 수 있습니다.

특정 라이브러리 전용

Express나 Fastify 별로 직접 응답을 조작할 수 있습니다. findAll(@Res() response))과 같이 @Res() 데코레이터를 메서드 시그니쳐에 추가하면 됩니다. 이렇게 접근하면 Express/Fastify의 기본 Response 메서드를 사용할 수 있습니다. 예를 들어 Express를 사용한다면 response.status(200).send() 이렇게 200을 보낼 수가 있다는 말이죠.

요청 객체(Request Object)

방금 Response를 설명했으니 Request 객체에 대해서 언급하고 2편을 마무리하겠습니다. 핸들러(예: findAll()는 종종 클라이언트가 보낸 요청(Request)의 세부 정보를 필요로 할 때가 있습니다. 예를 들어 세션 정보나 헤더 정보가 필요할때 말이죠. Nest는 Express나 Fastify의 요청 객체에 접근할 수 있도록 합니다. @Res()처럼 핸들러의 시그니쳐에 데코레이터를 추가하여 Nest에 주입하도록 지시하여 요청 객체에 액세스할 수 있습니다.

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 타이핑을 위해서는 @types/express 패키지를 설치하시기 바랍니다.

요청 객체는 HTTP 요청을 뜻합니다. 이 요청에는 쿼리 문자열, 매개 변수, HTTP 헤더 및 본문에 대한 속성을 가지고 있습니다 (자세한 내용은 여기). Nest에는 이 속성을 편리하게 가져올 수 있도록 편리하게 사용할 수있는 @Body()또는 @Query() 같은 전용 데코레이터를 제공합니다. 아래는 제공된 데코레이터와 데코레이터가 의미하는 원래 플랫폼(Express, Fastify)의 객체 목록입니다.

@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

* 기본 HTTP 플랫폼 (예 : Express 및 Fastify)에서 입력과의 호환성을 위해 Nest는 @Res()및 @Response()데코레이터를 제공합니다. @Res()는 단순히 @Response()의 별칭입니다. 둘 다 기본 HTTP 플랫폼 response 객체의 인터페이스를 직접 노출합니다. 하지만 개인적인 경험으로는 @Response()보다는 @Res()를 사용하시기 바랍니다. response객체를 완전하게 활용하기 위해서는 기본 HTTP 플랫폼의 response의 타이핑을 가져와야하는데 아래와 같이 @Response()와 기본 HTTP 플랫폼의 response와 이름이 겹치기 때문입니다.

import { Controller, Get, Response } from '@nestjs/common';
import { Response } from 'express'; // 이 부분에서 에러

@Controller('cats')
export class CatsController {
  @Get()
  findAll(@Response() res: Response) {}
}

// 아니면

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

@Controller('cats')
export class CatsController {
  @Get()
  findAll(@Response() res: any) {}
}

만약 메서드 핸들러에서 @Res()@Respoonse()를 사용했다면, 위에서 언급한대로 Nest를 특정 라이브러리 전용 모드로 사용한다는 것을 뜻하기 때문에 반드시 응답을 관리해야 합니다. 이 경우 res.json(...)res.send(...)로 직접적으로 응답을 주지 않으면 HTTP 서버가 중단되는 참극을 목격하실 수 있습니다.

원 글에서는 컨트롤러 설명을 1편의 글로 적었지만, 저는 두 편으로 쪼개어 설명하겠습니다.