컨트롤러는 들어오는 요청(request)를 처리하고 응답(response)를 클라이언트에 돌려주는 역할을 맡는 중심적인 모듈이다. (API).controllers.ts
와 같은 파일명으로 작성하게되고, API 디렉토리에 저장하게 된다.
컨트롤러의 목적은 애플리케이션에 대한 특정 요청을 수신하는 것
입니다. 라우팅 메커니즘은 어떤 컨트롤러가 어떤 요청을 수신하는지 제어
합니다. 종종 각 컨트롤러에는 둘 이상의 라우트가 있으며 다른 라우트는 다른 작업을 수행
할 수 있다.
기본 컨트롤러를 만들기 위해 클래스
와 데코레이터
를 사용한다. 데코레이터는 클래스를 필수 메타데이터와 연결
하고 Nest가 라우팅 맵을 만들 수 있도록 한다(요청
을 해당 컨트롤러에 연결
).
Controller.ts
에는 비즈니스 로직은 포함되어있지 않고, 실질적인 로직은 Service에 담긴다. 따라서 express
에서의 route설정과 동일한 역할을 하게된다. 그렇기 때문에 해당되는 route에 적절한 함수(비즈니스 로직)을 call하는 역할만을 수행한다.
nest g resource [name]
다음과 같은 명령어를 통해서 Controller의 템플릿 파일을 생성할 수 있다. 위 명령어를 사용해서 생성하게 되면
name.controller.ts
과 같은 형식으로 파일이 새로 생성된다. 내부는 클래스로 선언된 기본 파일이 만들어진다.
예제를 통해서 라우트를 생성하는 방법을 알아보도록 하자. 기본 컨트롤러를 정의하기 위해서는 @Controller()
과 같은 데코레이터를 사용해야한다. 이전에 공부했던 내용처럼 @Controller()
과 같이 경로(path)를 지정하지 않으면 기본적으로 /(루트 경로)
를 통하는 요청을 수신하게 된다. 따라서,컨트롤러 데코레이터에 지정한 경로를 새로 지정하지 않고, API 계층화를 성립시킬 수 있다.
코드를 통해 살펴보자.
import { Controller, Get } from '@nestjs/common';
import { UsersService } from './user.service';
@Controller('user')
export class UsersController {
constructor(private readonly usersService:UsersService){ }
@Get()
findAll(): Promise<UserEntity[]> {
return this.usersService.findAll();
}
위 코드를 살펴보면 Controller
라는 데코레이터로 user
라는 라우트를 설정하고 있으며, 거기에 오는 GET HTTP Verb
에 대한 응답을 지정하고 있다. contructor
를 이용해서 UsersService
를 초기화 시켜주고, 내부에서 해당 로직을 사용할 수 있도록 하고 있다. findAll() 내장 메서드
를 사용하게 되면, 요청 핸들러가 자바스크립트 객체 또는 배열을 반환할 때 자동으로 JSON
으로 직렬화된다. 그러나 자바스크립트 기본 타입
(string, number, boolean)을 반환하면 Nest는 직렬화를 시도하지 않고 값만 되돌려보낸다. 이렇게하면 응답처리가 가볍게 처리된다.
응답의 상태코드를 별도로 지정해두지 않으면 성공 코드로, POST 요청의 경우에는 201
을 되돌려주고 나머지는 기본적으로 200
만을 되돌려준다. 핸들러 수준에서 @HttpCode(...)
데코레이터를 활용하면 이 동작을 변경해줄 수 있다. @HttpCode()
나 기타 데코레이터들을 아래에서 다루도록 하자.
핸들러는 종종 클라이언트 요청 세부정보에 액세스할 수 있어야 한다. Get 요청이든, Post 요청이든 서버에 요청이 올 때 내부에 딸려오는 토큰이나 데이터를 가지고 자격권한을 판별해야하니까. Nest는 Express 기반
이기 때문에 기본적으로 요청객체 접근이 가능하다. 핸들러의 시그니처에 @Req()
데코레이터를 추가하면 Nest에 주입하도록 지시하고 이를 통해서 요청객체에 접근할 수 있다. 단 사용하기 이전에 @types/express
패키지를 미리 설치해줘야 한다.
함수 시그니쳐란?
함수 시그니처 혹은 메서드 시그니처는 컴파일러가 함수를 구분하기 위한 구성요소라고 한다. 쉽게 말하면 두 가지 다른 함수가 있을 때 이 함수의 구분점이 되는 요소들을 말한다.
- 함수의 이름(함수명)
- 매개변수(Parameter)의 개수
- 매개변수의 타입(Types)
- 반환 타입(Return Type)
import { Controller, Get, Req } from '@nestjs/common';
import { Request } from 'express'; // 요청객체 express에서 가져오기
@Controller('user')// 경로 접두사
export class CatsController {
@Get() // 요청 라우트
findAll(@Req() request: Request): string {
return 'This action returns all users';
}
}
위의 코드를 살펴보면, express
의 기본 타입 패키지에서 Request
라는 요청객체를 가져온 것을 확인할 수 있다. 핸들러 내부의 매개변수로 Request타입
을 전달함으로서, 컨트롤러 내부에서 요청객체에 접근해서 데이터를 조작할 수 있다. 조작이라기보다는 내부 데이터를 가공해서 어떻게 사용할지를 정하는 과정이겠지만.. 이것을 다시 Service 비즈니스 로직에 필요한 내용들을 분리해서 매개변수로 전달하면 요청객체 내부에 딸려온 데이터나 헤더같은 것을 통해서 다양한 기능을 구현할 수 있다.
요청객체(Request)
요청객체는 HTTP 요청 자체의 메타데이터를 나타낸다. 쿼리 문자열(?=&), 매개변수, HTTP 헤더, 본문에 대한 속성 값을 포함단다. 대부분 이러한 속성을 직접 잘라낼 필요는 없다.
@Body
나@Query()
같은 데코레이터가 미리 준비되어있다! 요청객체에 대한 부분만 좀더 세부적으로 나타내면 좋을 듯 하다.
요청객체 전체
를 가져오는 데코레이터, 이를 통해서 express의 req
인자의 방식으로 요청객체에 접근할 수 있다.응답객체 전체
를 가져오는 데코레이터, 이를 통해서 express의 res
인자의 방식으로 응답객체에 접근할 수 있다.미들웨어로 사용하기 위해서 다음 콜백을 지정하는 데코레이터
다. express의 커스텀 미들웨어의 맛만 봤지만, nestjs에서 미들웨어
를 작성하는 방법은 생각해본적이 없지만 공부해보면 좋을 듯하다.session
을 가져오는 데코레이터, 이를 통해 express의 req.session
에 접근하는 방식과 동일하게 접근이 가능하다.req.params
, req.params[key]
와 같이 전달인자에 접근하는 방식이다. req.body
, req,body[key]
와 같이 바디값에 접근하는 방식이다.req.query
, req.query[key]
와 같이 쿼리문자열에 접근하는 방식이다.req.headers
, req.headers[name]
와 같이 헤더에 접근하는 방식이다.이전에는 GET 라우트를 통해서 users 리소스를 가져오는 엔드포인트를 정의했다. 일반적으로 서버 측에 새 레코드를 생성하는 엔드포인트도 제공하려고 한다. 이를 위해 POST 핸들러도 정의해보도록 하겠다.
import { Controller, Get, Post } from '@nestjs/common';
import { UserEntity } from './entity/user.entity';
import { UsersService } from './user.service';
@Controller('user')
export class UsersController {
constructor(private readonly usersService:UsersService){ }
@Get()
findAll(): Promise<UserEntity[]> {
return this.usersService.findAll();
}
@Post()
createUser(): Promise<UserEntity[]> {
return this.usersService.createNewUser();
}
위와 같이 간단하게 작성해볼 수 있을 것 같다. Nest는 모든 표준 HTTP 메소드에 대해 데코레이터를 제공한다. @Get()
, @Post()
, @Delete()
, @Patch()
, @Options()
, @Head()
, @All()
이를 통해서 express에서 구성했던 엔드포인트를 간단하게 경로 중복코드없이 작성할 수 있다.
패턴 기반 라우트도 지원된다. 별표(*)는 와일드 카드로 사용되며 모든 문자조합과 일치한다.
@Get('ab*cd')
findAll() {
return 'This route uses as wildcard';
}
'ab*cd'
라우트 경로는 abcd
, ab_cd
, abecd
등의 라우트 경로와 일치한다. ?
, +
, *
및 ()
문자는 라우트 경로에 사용될 수 있으며 해당 정규표현식 대응 부분의 하위집합이다. 하이픈(-
)과 점(.
)은 문자열 기반 경로로 문자 그대로 해석된다.
위에서 나왔던 내용인데, 201
로 POST 요청에 대한 응답하는 것을 제외하곤 모든 응답 상태코드는 기본적으로 200
이다. 핸들러 레벨에서 @HttpCode(${status code})
데코레이터를 추가해서 이러한 동작을 쉽게 변경할 수 있다.
import { HttpCode } from '@nestjs/common';
...코드 생략...
@Post()
@HttpCode(204)
create() {
return 'This action adds a new user';
}
종종 상태코드는 정적이 아니지만 다양한 요인에 따라 달라진다. 응답(@Res()
를 사용하여 주입)객체를 사용할 수 있다.
커스텀 응답헤더를 지정하려면 @Headers()
데코레이터 또는 라이브러리별 응답객체를 사용할 수 있다. (그리고 비즈니스 로직에서는 res.header()
을 직접 호출해서 사용한다.)
@Post()
@Header('Cache-Control', 'none')
create() {
return this.usersService.createUser()
}
응답을 특정 URL로 리디렉션 하려면 @Redirct()
데코레이터 또는 라이브러리별 응답객체를 사용할 수 있다. (그리고 비즈니스 로직에서는 res.redirect()
를 직접 호출해서 사용한다.) @Redirect()
는 url
과 statusCode
라는 두개의 인수를 취하며 둘 다 선택사항이다. 생략된 경우는 기본적으로 statusCode의 기본값은 302(Found)
이 된다.
@Get()
@Redirect('https://nestjs.kr', 301)
가끔 HTTP 상태코드
또는 리디렉션 URL
을 동적으로 확인해야 할 수 있다. 다음과 같은 형태로 라우트 핸들러 메서드에서 객체를 반환하면 된다.
{
"url": string,
"statusCode": number,
}
반환된 값은 @Redirect()
데코레이터에 전달된 모든 인수를 재정의한다. 예를 들면
@Get('docs')
@Redirct('https://docs.nestjs.com', 302)
getDocs(@Query('version') version) {
if(version && version === '5') {
return { url: 'https://docs.nestjs.com/v5/' };
}
}
요청의 일부로 동적데이터를 수락해야 하는 경우 정적 경로가 있는 라우트가 동작하지 않는다. (예: GET /users/1
은 ID가 1d인 user data를 가져온다.). 매개변수가 있는 라우트를 정의하기위해서 라우트 경로에 라우트 매개변수 토큰 tokens을 추가하여 요청 URL의 해당 위치에서 동적값을 캡처할 수 있다. 아래 @Get()
데코레이터 예제의 경로 매개변수 토큰은 이 사용법을 보여준다. 이렇게 선언된 라우트 매개변수는 @Param()
데코레이터를 사용하여 액세스할 수 있으며, 이는 메소드 시그니처에 추가되어야 한다.
@Get(':id')
findOne(@Param() params):string {
console.log(params.id);
return `This action returns a #${params.id} user`
}
@Param()
은 메소드 매개변수(Params)를 데코레이팅한다. 라우트 매개변수를 메소드 본문내에서 장식된 메소드의 매개변수의 속성으로 사용할 수 있도록 한다. 코드에서 보듯이 params.id
를 참조해서 id
매개변수에 액세스할 수 있다. 특정 매개변수 토큰을 데코레이터에 전달한 다음에 직접 라우트 매개변수를 참조할 수 있다.
@Get(':id')
findOne(@Param('id') id : string): string {
return `This action returns a #${id} user`;
}
쉽게 말하면 라우트 매개변수에서,
@Param()
을 전달해서 전체 매개변수를 접근하게 하거나,@Param('id')
처럼 특정 매개변수에 직접 접근할 수 있도록 하는 방식이다. 이 방식을 이용하면 parameter를 접근해서, 동적으로 요청마다 다른 값들에 대응해서 핸들러가 작동하도록 할 수 있다.
@Controller()
데코레이터는 들어오는 요청의 HTTP 요청의 HTTP 호스트가 특정값과 일치하도록 host
옵션을 사용할 수 있다.
@Controller({host : 'admin.example.com'})
export class AdminController {
@Get()
index(): string {
return 'Admin Page';
}
}
이 부분의 용도가 잘 이해가 가지 않는다.
다른 프로그래밍 언어 배경을 가진 사람들의 경우 Nest에서 거의 모든 것이 들어오는 요청에서 공유된다는 사실을 배우는 것은 예상치 못한 일이다. 데이터베이스에 대한 연결 풀, 전역상태의 싱글톤 서비스 등이 있다. Node.js
는 모든 요청이 별도의 스레드에서 처리되는 요청/응답 다중스레드 상태 비저장(Multi-Threaded Stateless)모델을 따르지 않는다. 따라서 싱글톤 인스턴스를 사용하는 것은 애플리케이션에 완전히 안전하다.
그러나 컨트롤러 요청 기반 수명이 바람직한 동작일 수 있는 경우가 있다. 예를 들어 GraphQL 애플리케이션의 요청별 캐싱, 요청 추적 또는 멀티테넌시가 있다.
우리는 최신 자바스크립트를 좋아하며 데이터 추출이 대부분 비동기적이라는 것을 알고 있다. 이것이 Nest가 비동기(async)
기능을 지원하고 잘 작동되는 이유다.
모든 비동기 함수는 Promise
객체를 반환해야 한다. 즉, Nest가 자체적으로 해결할 수 있는 지연된 값을 반환할 수 있다. 이것의 예를 보자.
Promise 처리법
@Get()
async findAll(): Promise<any[]> {
return [];
}
위의 코드는 실제로 동작한다. 또한 Nest 라우트 핸들러는 RxJS 관찰가능한 스트림
을 반환할 수 있으므로 훨씬 더 강력하다. Nest는 자동으로 아래의 소스를 구독하고 마지막으로 내보낸 값을 가져온다.
RxJS의 Observable과 Promise의 비교에 대한 내용을 글로 따로 남기자.
https://stackoverflow.com/questions/37364973/what-is-the-difference-between-promises-and-observables
Observable 처리법
@Get()
findAll(): Observable<any[]> {
return of([]);
}
POST 라우트 핸들러의 이전 예제엔 클라이언트 매개변수를 허용하지 않았다. 여기에 @Body()
데코레이터를 추가하여 이 문제를 해결하자.
그러나 TypeScript
를 사용하는 경우 DTO(Data Transfer Object : 데이터 전송객체)
의 스키마를 결정해야 한다. DTO는 데이터가 네트워크를 통해 전송되는 방식을 정의하는 객체
다. TypeScript 인터페이스를 사용하거나 간단한 클래스를 사용하여 DTO 스키마를 확인할 수 있다. 흥미롭게도 여기서 클래스를 사용하는 것이 좋다. 왜냐하면 클래스는 자바스크립트 ES6 표준 기법의 일부이므로 컴파일된 자바스크립트에서 실제 엔티티로 유지된다. 반면 TypeScript 인터페이스는 트랜스 팡리중에 제거되기 때문에 Nest는 런타임에 이를 참조할 수 없다. 파이프와 같은 기능은 런타임에 메타타입에 엑세스 할 수 있을때 추가 가능성을 제공하기 때문에 중요하다.
export class CreateUserDto {
name: string;
age: number;
email: string;
}
기본 속성은 세가지로 설정했다. 이름과, 나이, 그리고 이메일 정보만 가지고 있다. 그런 다음 이를 Controller
내부에서 import시키면 새로 만든 DTO를 사용할 수 있다.
import createUserDto from './interfaces/createUser.dto';
@Post()
async create(@Body() createUserDto: CreateUserDto) {
return 'This action adds a new user';
}
오류 처리에 대한 내용은 따로 글을 작성하자!