[NestJS] Controller 개념 및 실습

JeungJoo Lee
CrocusEnergy
Published in
10 min readSep 17, 2020

지난 시간에 [NestJS] starter-kit bolierplate 구조 살펴보기 & PostgreSQL Docker 준비 를 통해 NestJS 공식 프로젝트에서 제공하는 starter-kit boilerplate 구조와 CRUD 실습을 위해 PostgreSQL 을 docker-compose 를 통해 설치해보았다.

이번 시간에는 NestJS 의 Controller 의 개념과 사용법에 대해 알아보도록 하겠다. 아마 Java의 Spring Boot 를 사용해보았던 개발자라면 굉장히 유사한 점들을 많이 발견할 수 있을 것이다.

Controller

모든 MVC 패턴에 Controller 의 역할은 클라이언트에서 들어오는 Request 를 받는 역할과 동시에 클라이언트에게 Response 를 하는 역할을 한다. Nest 도 마찬가지이다. Controller 의 역할은 동일하고 도식화를 하면 아래와 같다.

Controller 는 하나의 Route 만 갖는 것이 아니다. 예를 들어, 유저 정보를 CRUD 하는 API 리소스가 있다고 생각해보면 아래와 같이 다양한 Route 정보와 Action 정보를 담고 있는 HTTP Method 가 하나의 기능을 담당하고 있는 Controller 에 가지고 있을것이다.

/user/:userId GET (특정 유저 정보 가져오기)
/user POST (유저 정보 저장하기)
/user/:userId PUT (특정 유저 정보 수정하기)
/user/:userId DELETE (특정 유저 정보 삭제하기)

그렇다면, 위와 같은 Routing 을 설정할 수 있는 Controller를 Nest.js 에서는 어떻게 만드는 지 확인해보자.

src/user/user.controller.ts

import { Controller, Get, Param } from '@nestjs/common';@Controller('user')
export class UserController {
@Get(':userId')
findOne(@Param('userId') id: string): string {
return Object.assign({id, userName: '이정주'});
}
}

위의 코드를 살펴보자 우선 /user/:userId 에 Route 정보를 토대로 특정 유저 정보를 가져오는 API 를 간단하게 Controller 로 구현한 것이다. ES6+ 부터 지원하게된 Decorator 기능을 이용해서 사용한 것인데 Java 에서는 이를 애노테이션( Annotation ) 이라고 한다.

@Controller 의 선언은 이 해당 클래스가 Controller 의 역할을 한다 라는 것이고 이 안에 메서드들은 @Get 또는 @Put , @Post , @Delete 등의 HTTP Method 형태대로 Decorator 를 선언할 수 있다. 의미는 직관적으로 해당 메서드는 서버에 상태를 바꾸는 것인지 정보를 가져오는 것인지 구분하여 API를 RestFul 하게 작성할 수 있다.

위의 코드를 해석하자면 prefix로 /user 라는 컨텍스트 URI 를 통해 Request 파라미터를 통해 받은 :userId 정보를 가지고 서버에서 찾아 Response 해주겠다는 코드이다. 현재는 DB가 연결되어 있지 않으므로 Object.assign 을 통해 오브젝트를 생성해서 Response 해주는데 String 타입으로 되어있는데 해당 코드는 아래와 같이 요청시 JSON String 타입으로 Content-Type 을 자동으로 설정해준다.

기본적으로 string 으로 던진 Response는 JSON 형태로 Serialize 해서 던지고 Http Status 도 기본적으로 200 OK 혹은 POST 의 경우 201를 사용한다. 하지만 만약 내가 이 부분도 지정하고 싶다면 “@HttpCode” Decorator를 사용해도 된다. 기본적인 Response 객체를 통해 Express 프레임워크처럼 핸들링하고 싶다면 @Res() Decorator를 통한 response 객체를 사용하면 된다 코드는 아래와 같다.

@Get(':userId')
findOne(@Param('userId') id: string, @Res() res): string {
return res.status(200).send({id, userName: '이정주', accountNum:123});
}

앞서 살펴본 @Param 이라는 Decorator를 가지고 userId 정보를 클라이언트에서 Request 정보에서 추출하여 편리하게 사용하였다. 만약 path variable 이 아닌 Query String 이나 Post 로 요청할 때 Body 에 있는 값을 쉽게 파싱할 수 있을까에 대한 물음도 있을 것이다. 그에 대한 Decorator도 이미 정의가 되어있다. 이러한 부분들은 Http Request 객체를 파싱하여 가져오는 부분으로 편리하게 Decorator를 통해 사용할 수 있다.

자 그럼 POST Method 를 통해 Body를 어떻게 Decorator 를 선언하고 Body의 데이터를 받아 올 수 있는 지 코드를 살펴보도록 하겠습니다.

@Post()
saveUser(@Body() payload): string {
return Object.assign({
statusCode: 201,
data: payload,
statusMsg: 'created successfully',
});
}

굉장히 간단한 코드입니다. 아직 DB를 통해 저장을 하거나 하는 부분이 누락 되었기 때문에 Body 로 부터 받은 json 데이터를 다시 data 프로퍼티로 Response 해주고 있습니다.

위의 POST 메서드로 /user api 를 호출 했을 때 결과이다. 코드에서 의도하였던 데로 Response 하는 것을 확인할 수 있다.

Status Code와 Header, Redirection 모두 데코레이션으로도 설정할 수 있다.

@Post()
@HttpCode(201)
@Header('Cache-Control', 'none')
@Redirect('<https://www.naver.com>', 301)
.... 생략 ....

Javascript 는 우선 기본적으로 Asynchronous 랭귀지라고 해도 무방하다. 모든 요청이 비동기식으로 처리 되진 않지만 대부분의 데이터 처리에 있어서는 비동기적 처리를 하고 있다. 아래 예제를 보자 /user/list GET API를 통해 데이터를 Async 하게 처리한다고 생각해보자 우선 Promise 를 리턴한다고 했을 때 Response Type 을 Promise 타입으로 지정하면 resolve 시 Response 에 대한 값을 동기적으로 보장받을 수 있다.

@Get('list')
findAll(): Promise<any[]> {
return new Promise((resolve) =>
setTimeout(() => resolve([{userName:'이정주'},{userName: '김명일'}]), 100)
);
}

결과는 아래와 같이 출력된다.

DTO 사용 (Data Transfer Object)

종전에 살펴 보았던 POST 에서 DTO를 정의하지 않고 JSON 으로 Body 에 넣어 보내기만 했는데 명확하게 어떠한 데이터 오브젝트를 넘길 것인지 사전에 Class 타입으로 DTO를 설정한다. DTO 객체를 아래와 같이 Class 타입의 오브젝트를 만든 후 위에서 만든 /user POST API를 수정하도록 하겠다.

src/user/dto/user.dto.ts

export class UserDto {  private _userId:string;
private _userName:string;
constructor(userId: string, userName: string) {
this._userId = userId;
this._userName = userName;
}
get userId(): string {
return this._userId;
}
set userId(value: string) {
this._userId = value;
}
get userName(): string {
return this._userName;
}
set userName(value: string) {
this._userName = value;
}
}

DTO 의 선언은 위와 같고 아래와 같이 기존 API를 조금 변경하였다.

@Post()
saveUser(@Body() userDto: **UserDto**): string {
return Object.assign({
data: { ...userDto },
statusCode: 201,
statusMsg: `saved successfully`,
});
}

위와 같이 모호했던 DataType 의 payload를 명확하게 어떠한 DTO로 받을 것인지 정의하였다. 그리고 API 테스트를 해보자

위와 같이 DTO 에 userName, userId 프로퍼티를 JSON 형태로 Request 시 정의 하면 결과가 의도된 대로 출력되는 것을 볼 수 있다. 물론 실패 케이스도 있을 텐데 Error Handler 과 연관되는 부분이라 이 부분은 나중에 살펴보도록 하겠다.

마지막으로 이렇게 만든 UserController 의 경우 Module 에 등록되어야 한다. Module 에 등록되지 않은 경우 Nest Core는 이 Controller 가 Runtime 시 등록되지 않았기 때문에 정의한 Route 정보들은 찾지를 못한다. 본인의 경우 아래와 같이 user.module.ts 를 만들고 위에서 추가한 UserController 클래스를 등록하였다.

src/user/user.module.ts

import { Module } from '@nestjs/common';
import { UserController } from './user.controller';
@Module({
imports: [],
controllers: [UserController],
providers: [],
})
export class UserModule {}

위와 같이 @Module 데코레이터를 선언하고 옵션으로 controllers 에 UserController를 추가한다.

다음 챕터에서 살펴볼 부분은 services, repositories, factories, helpers 등의 디펜던시를 만들어 사용하게 될 Provider를 살펴보도록 하겠다.

다음글. [NestJS] Providers 개념 및 실습

--

--