ํ์ดํ
ํ์ดํ๋ @Injectable() ๋ฐ์ฝ๋ ์ดํฐ๊ฐ ์ ์ฉ๋ ํด๋์ค์ด๋ฉฐ, PipeTransform ์ธํฐํ์ด์ค๋ฅผ ๊ตฌํํฉ๋๋ค.
ํ์ดํ๋ ์ผ๋ฐ์ ์ผ๋ก ๋ ๊ฐ์ง ์ฉ๋๋ก ์ฌ์ฉ๋ฉ๋๋ค:
- ๋ณํ (Transformation): ์ ๋ ฅ ๋ฐ์ดํฐ๋ฅผ ์ํ๋ ํํ๋ก ๋ณํ (์: ๋ฌธ์์ด์ ์ ์๋ก ๋ณํ)
- ๊ฒ์ฆ (Validation): ์ ๋ ฅ ๋ฐ์ดํฐ๋ฅผ ํ๊ฐํ๊ณ , ์ ํจํ๋ฉด ๊ทธ๋๋ก ํต๊ณผ์ํค๊ณ , ๊ทธ๋ ์ง ์์ผ๋ฉด ์์ธ๋ฅผ ๋ฐ์์ํด
์ด ๋ ๊ฒฝ์ฐ ๋ชจ๋, ํ์ดํ๋ ์ปจํธ๋กค๋ฌ ๋ผ์ฐํธ ํธ๋ค๋ฌ์์ ์ฒ๋ฆฌ๋๋ ์ธ์์ ์ ์ฉํฉ๋๋ค. Nest๋ ๋ฉ์๋๊ฐ ํธ์ถ๋๊ธฐ ์ง์ ์ ํ์ดํ๋ฅผ ๊ฐ์ ์ํค๋ฉฐ, ํ์ดํ๋ ๋ฉ์๋์ ์ ๋ฌ๋ ์ธ์๋ฅผ ๋ฐ์ ์ด๋ฅผ ์ฒ๋ฆฌํฉ๋๋ค. ๋ณํ ๋๋ ๊ฒ์ฆ ์์ ์ ์ด ์์ ์์ ์ํ๋๋ฉฐ, ๊ทธ ํ ๋ผ์ฐํธ ํธ๋ค๋ฌ๋ (ํ์ํ ๊ฒฝ์ฐ ๋ณํ๋) ์ธ์๋ฅผ ๊ฐ์ง๊ณ ํธ์ถ๋ฉ๋๋ค.
Nest๋ ์ฆ์ ์ฌ์ฉํ ์ ์๋ ์ฌ๋ฌ ๊ฐ์ง ๋ด์ฅ ํ์ดํ๋ฅผ ์ ๊ณตํฉ๋๋ค. ๋ํ, ์ฌ์ฉ์ ์ ์ ํ์ดํ๋ ์ง์ ์์ฑํ ์ ์์ต๋๋ค. ์ด ์ฅ์์๋ ๋ด์ฅ ํ์ดํ๋ฅผ ์๊ฐํ๊ณ ์ด๋ฅผ ๋ผ์ฐํธ ํธ๋ค๋ฌ์ ๋ฐ์ธ๋ฉํ๋ ๋ฐฉ๋ฒ์ ์ดํด๋ณธ ํ, ๋ช ๊ฐ์ง ์ฌ์ฉ์ ์ ์ ํ์ดํ๋ฅผ ๊ตฌํํด ๋ณด๋ฉฐ ์ฒ์๋ถํฐ ๋ง๋๋ ๋ฐฉ๋ฒ์ ์ค๋ช ํฉ๋๋ค.
ํํธ
ํ์ดํ๋ ์์ธ ์ฒ๋ฆฌ ์์ญ (Exceptions Zone) ์์์ ์คํ๋ฉ๋๋ค. ์ด๋ ํ์ดํ๊ฐ ์์ธ๋ฅผ ๋ฐ์์ํฌ ๊ฒฝ์ฐ, ํด๋น ์์ธ๊ฐ ์ ์ญ ์์ธ ํํฐ๋ ํ์ฌ ์ปจํ ์คํธ์ ์ ์ฉ๋ ์์ธ ํํฐ์ ์ํด ์ฒ๋ฆฌ๋๋ค๋ ๋ป์ ๋๋ค. ์ ๋ด์ฉ์ ๊ณ ๋ คํ๋ฉด, ํ์ดํ์์ ์์ธ๊ฐ ๋ฐ์ํ๋ฉด ์ปจํธ๋กค๋ฌ ๋ฉ์๋๋ ์ดํ ์คํ๋์ง ์๋๋ค๋ ์ ์ด ๋ถ๋ช ํด์ง๋๋ค. ์ด๋ฌํ ๊ตฌ์กฐ๋ ์ธ๋ถ์์ ์ ํ๋ฆฌ์ผ์ด์ ์ผ๋ก ๋ค์ด์ค๋ ๋ฐ์ดํฐ๋ฅผ ์์คํ ๊ฒฝ๊ณ์์ ๊ฒ์ฆํ๊ธฐ ์ํ ๋ชจ๋ฒ ์ฌ๋ก (Best Practice)๋ฅผ ์ ๊ณตํฉ๋๋ค.
๋ด์ฅ ํ์ดํ
Nest์๋ ๊ธฐ๋ณธ์ ์ผ๋ก ์ ๊ณต๋๋ ์ฌ๋ฌ ํ์ดํ๊ฐ ์์ต๋๋ค:
- ValidationPipe
- ParseIntPipe
- ParseFloatPipe
- ParseBoolPipe
- ParseArrayPipe
- ParseUUIDPipe
- ParseEnumPipe
- DefaultValuePipe
- ParseFilePipe
- ParseDatePipe
์ด๋ค์ ๋ชจ๋ @nestjs/common ํจํค์ง์์ export ๋ฉ๋๋ค.
๊ฐ๋จํ ์๋ก ParseIntPipe๋ฅผ ์ฌ์ฉํ๋ ๋ฐฉ๋ฒ์ ์ดํด๋ณด๊ฒ ์ต๋๋ค. ์ด ํ์ดํ๋ ๋ณํ (transformation) ์ฉ๋๋ก ์ฌ์ฉ๋๋ฉฐ, ๋ฉ์๋ ํธ๋ค๋ฌ์ ํ๋ผ๋ฏธํฐ๊ฐ ์๋ฐ์คํฌ๋ฆฝํธ ์ ์๋ก ๋ณํ๋๋๋ก ๋ณด์ฅํฉ๋๋ค. ๋ณํ์ ์คํจํ๋ฉด ์์ธ๊ฐ ๋ฐ์ํฉ๋๋ค. ์ด ์ฅ์ ํ๋ฐ๋ถ์์๋ ParseIntPipe์ ๊ฐ๋จํ ์ปค์คํ ๊ตฌํ ์์ ๋ ๋ณด์ฌ์ค ์์ ์ ๋๋ค. ์๋ ์์ ๊ธฐ์ ์ Parse* ํ์ดํ๋ค (ParseBoolPipe, ParseFloatPipe, ParseEnumPipe, ParseArrayPipe, ParseDatePipe, ParseUUIDPipe) ์๋ ๋์ผํ๊ฒ ์ ์ฉ๋ฉ๋๋ค.
ํ์ดํ ์ฐ๊ฒฐํ๊ธฐ
ํ์ดํ๋ฅผ ์ฌ์ฉํ๋ ค๋ฉด, ํด๋น ํ์ดํ ํด๋์ค์ ์ธ์คํด์ค๋ฅผ ์ ์ ํ ์ปจํ ์คํธ์ ๋ฐ์ธ๋ฉํด์ผ ํฉ๋๋ค. ParseIntPipe ์์ ์์๋ ํน์ ๋ผ์ฐํธ ํธ๋ค๋ฌ ๋ฉ์๋์ ํ์ดํ๋ฅผ ์ฐ๊ฒฐํ๊ณ , ๋ฉ์๋๊ฐ ํธ์ถ๋๊ธฐ ์ ์ ํ์ดํ๊ฐ ์คํ๋๋๋ก ํด์ผ ํฉ๋๋ค. ์ด ์์ ์ ๋ค์๊ณผ ๊ฐ์ ๊ตฌ๋ฌธ์ผ๋ก ์ํ๋๋ฉฐ, ์ด๋ฅผ "๋ฉ์๋ ๋งค๊ฐ๋ณ์ ์์ค์์ ํ์ดํ๋ฅผ ๋ฐ์ธ๋ฉํ๋ค"๋ผ๊ณ ๋ถ๋ฆ ๋๋ค:
@Get(':id')
async findOne(@Param('id', ParseIntPipe) id: number) {
return this.catsService.findOne(id);
}
์ด๊ฒ์ ๋ค์ ๋ ๊ฐ์ง ์กฐ๊ฑด ์ค ํ๋๊ฐ ๋ฐ๋์ ์ฐธ์ด ๋๋๋ก ๋ณด์ฅํฉ๋๋ค: ์ฒซ์งธ, findOne() ๋ฉ์๋์์ ๋ฐ์ ํ๋ผ๋ฏธํฐ๊ฐ this.catsService.findOne() ํธ์ถ์์ ๊ธฐ๋ํ๋ ๋๋ก ์ซ์์ด๊ฑฐ๋, ๋์งธ, ๊ฒฝ๋ก ํธ๋ค๋ฌ๊ฐ ํธ์ถ๋๊ธฐ ์ ์ ์์ธ๊ฐ ๋ฐ์ํ๋ ๊ฒฝ์ฐ์ ๋๋ค.
์๋ฅผ ๋ค์ด, ๊ฒฝ๋ก๊ฐ ๋ค์๊ณผ ๊ฐ์ด ํธ์ถ๋๋ค๊ณ ๊ฐ์ ํด ๋ณด๊ฒ ์ต๋๋ค:
GET localhost:3000/abc
Nest๋ ๋ค์๊ณผ ๊ฐ์ ์์ธ๋ฅผ ๋ฐ์์ํต๋๋ค:
{
"statusCode": 400,
"message": "Validation failed (numeric string is expected)",
"error": "Bad Request"
}
์์ธ๊ฐ ๋ฐ์ํ๋ฉด fineOne() ๋ฉ์๋์ ๋ณธ๋ฌธ์ ์คํ๋์ง ์์ต๋๋ค.
์์ ์์ ์์๋ ํด๋์ค (ParseIntPipe)๋ฅผ ์ ๋ฌํ๊ณ ์์ผ๋ฉฐ, ์ด๋ ์ธ์คํด์ค๊ฐ ์๋๋ผ ํด๋์ค๋ฅผ ์ ๋ฌํจ์ผ๋ก์จ ์ธ์คํด์คํ ์ฑ ์์ ํ๋ ์์ํฌ์ ๋งก๊ธฐ๊ณ ์์กด์ฑ ์ฃผ์ (Dependency Injection)์ ๊ฐ๋ฅํ๊ฒ ํฉ๋๋ค. ํ์ดํ๋ ๊ฐ๋์ ๋ง์ฐฌ๊ฐ์ง๋ก, ์ง์ ์ธ์คํด์ค๋ฅผ ๋ง๋ค์ด ์ ๋ฌํ ์๋ ์์ต๋๋ค. ์ง์ ์ธ์คํด์ค๋ฅผ ์ ๋ฌํ๋ ๋ฐฉ์์ ๊ธฐ๋ณธ ๋ด์ฅ ํ์ดํ์ ๋์์ ์ต์ ์ ํตํด ์ปค์คํฐ๋ง์ด์ง ํ๊ณ ์ ํ ๋ ์ ์ฉํฉ๋๋ค.
@Get(':id')
async findOne(
@Param('id', new ParseIntPipe({ errorHttpStatusCode: HttpStatus.NOT_ACCEPTABLE }))
id: number,
) {
return this.catsService.findOne(id);
}
๋ค๋ฅธ ๋ณํ ํ์ดํ๋ค (๋ชจ๋ Parse* ํ์ดํ๋ค)์ ๋ฐ์ธ๋ฉํ๋ ๋ฐฉ์๋ ์ ์ฌํฉ๋๋ค. ์ด๋ฌํ ํ์ดํ๋ค์ ๋ชจ๋ ๋ผ์ฐํธ ํ๋ผ๋ฏธํฐ, ์ฟผ๋ฆฌ ๋ฌธ์์ด ํ๋ผ๋ฏธํฐ, ์์ฒญ ๋ณธ๋ฌธ ๊ฐ์ ์ ํจ์ฑ์ ๊ฒ์ฌํ๋ ๋ฌธ๋งฅ์์ ๋์ํฉ๋๋ค.
์๋ฅผ ๋ค์ด, ์ฟผ๋ฆฌ ๋ฌธ์์ด ํ๋ผ๋ฏธํฐ์ ๊ฒฝ์ฐ ๋ค์๊ณผ ๊ฐ์ด ์ฌ์ฉํ ์ ์์ต๋๋ค:
@Get()
async findOne(@Query('id', ParseIntPipe) id: number) {
return this.catsService.findOne(id);
}
๋ค์์ ParseUUIDPipe๋ฅผ ์ฌ์ฉํ์ฌ ๋ฌธ์์ด ํ๋ผ๋ฏธํฐ๋ฅผ ํ์ฑํ๊ณ UUID์ธ์ง ๊ฒ์ฆํ๋ ์์์ ๋๋ค.
@Get(':uuid')
async findOne(@Param('uuid', new ParseUUIDPipe()) uuid: string) {
return this.catsService.findOne(uuid);
}
ํํธ
ParseUUIDPipe()๋ฅผ ์ฌ์ฉํ ๋ UUID ๋ฒ์ 3, 4 ๋๋ 5๋ฅผ ํ์ฑ ํ๊ฒ ๋ฉ๋๋ค. ํน์ ๋ฒ์ ์ UUID๋ง ํ์ฉํ๊ณ ์ถ๋ค๋ฉด, ํด๋น ๋ฒ์ ์ ํ์ดํ ์ต์ ์ ์ ๋ฌํ ์ ์์ต๋๋ค.
์์์๋ ๋ค์ํ Parse* ๊ณ์ด์ ๋ด์ฅ ํ์ดํ๋ฅผ ๋ฐ์ธ๋ฉํ๋ ์์ ๋ฅผ ์ดํด๋ณด์์ต๋๋ค. ์ ํจ์ฑ ๊ฒ์ฌ (Validation) ํ์ดํ๋ ์ด์๋ ์กฐ๊ธ ๋ค๋ฅด๊ฒ ๋ฐ์ธ๋ฉ๋ฉ๋๋ค. ์ด์ ๋ํด์๋ ๋ค์ ์น์ ์์ ์ค๋ช ํฉ๋๋ค.
ํํธ
์ ํจ์ฑ ๊ฒ์ฌ ํ์ดํ์ ๋ํ ๋ค์ํ ์์ ๋ Validation technique ์น์ ์ ์ฐธ๊ณ ํ์ธ์.
์ปค์คํ ํ์ดํ
์์ ์ธ๊ธํ๋ฏ์ด, ์์ ๋ง์ ์ปค์คํ ํ์ดํ๋ฅผ ๋ง๋ค ์ ์์ต๋๋ค. Nest๋ ๊ฐ๋ ฅํ ๋ด์ฅ ParseIntPipe์ ValidationPipe๋ฅผ ์ ๊ณตํ์ง๋ง, ์ปค์คํ ํ์ดํ๊ฐ ์ด๋ป๊ฒ ๊ตฌ์ฑ๋๋์ง ์ดํดํ๊ธฐ ์ํด ๊ฐ๊ฐ์ ๊ฐ๋จํ ๋ฒ์ ์ ์ฒ์๋ถํฐ ๋ง๋ค์ด ๋ณด๊ฒ ์ต๋๋ค.
์ฐ์ ๊ฐ๋จํ ValidationPipe๋ถํฐ ์์ํ๊ฒ ์ต๋๋ค. ์ฒ์์๋ ์ ๋ ฅ ๊ฐ์ ๋ฐ์ ๊ทธ๋๋ก ๋ฐํํ๋, ์ฆ ํญ๋ ํจ์ (identity function)์ฒ๋ผ ๋์ํ๊ฒ ๋ง๋ค ๊ฒ์ ๋๋ค.
import { PipeTransform, Injectable, ArgumentMetadata } from '@nestjs/common';
@Injectable()
export class ValidationPipe implements PipeTransform {
transform(value: any, metadata: ArgumentMetadata) {
return value;
}
}
ํํธ
PipeTransform<T, R>๋ ๋ชจ๋ ํ์ดํ๊ฐ ๊ตฌํํด์ผ ํ๋ ์ ๋ค๋ฆญ ์ธ์คํด์ค์ ๋๋ค. ์ด ์ธํฐํ์ด์ค์์ T๋ ์ ๋ ฅ ๊ฐ์ ํ์ ์, R์ transform() ๋ฉ์๋์ ๋ฐํ ํ์ ์ ๋ํ๋ ๋๋ค.
๋ชจ๋ ํ์ดํ๋ PipeTransform ์ธํฐํ์ด์ค๋ฅผ ๋ง์กฑ์ํค๊ธฐ ์ํด transform() ๋ฉ์๋๋ฅผ ๊ตฌํํด์ผ ํฉ๋๋ค. ์ด ๋ฉ์๋๋ ๋ ๊ฐ์ ๋งค๊ฐ๋ณ์๋ฅผ ๊ฐ์ง๋๋ค:
- value: ํ์ฌ ์ฒ๋ฆฌ ์ค์ธ ๋ฉ์๋ ์ธ์ (ํด๋น ์ธ์๊ฐ ๋ผ์ฐํธ ํธ๋ค๋ง ๋ฉ์๋์ ์ ๋ฌ๋๊ธฐ ์ ์ ๊ฐ)
- metadata: ํ์ฌ ์ฒ๋ฆฌ ์ค์ธ ๋ฉ์๋ ์ธ์์ ๋ฉํ๋ฐ์ดํฐ
metadata ๊ฐ์ฒด๋ ๋ค์๊ณผ ๊ฐ์ ์์ฑ๋ค์ ๊ฐ์ง๊ณ ์์ต๋๋ค:
export interface ArgumentMetadata {
type: 'body' | 'query' | 'param' | 'custom';
metatype?: Type<unknown>;
data?: string;
}
์ด ์์ฑ๋ค์ ํ์ฌ ์ฒ๋ฆฌ ์ค์ธ ์ธ์์ ๋ํ ์ ๋ณด๋ฅผ ์ค๋ช ํฉ๋๋ค:
type | ์ธ์์ ์์น๋ฅผ ๋ํ๋ ๋๋ค. ์: @Body(), @Query(), @Param(), ๋๋ ์ฌ์ฉ์ ์ ์ ํ๋ผ๋ฏธํฐ (์์ธํ ๋ด์ฉ์ ์ฌ๊ธฐ ์ฐธ๊ณ ) |
metatype | ์ธ์์ ๋ฉํํ์ ์ ๋ํ๋ ๋๋ค. ์: String. ์ฐธ๊ณ ๋ก, ๋ผ์ฐํธ ํธ๋ค๋ฌ ๋ฉ์๋ ์๊ทธ๋์ฒ์์ ํ์ ์ ์ธ์ ์๋ตํ๊ฑฐ๋ ์ผ๋ฐ JavaScript๋ฅผ ์ฌ์ฉํ ๊ฒฝ์ฐ undefined๊ฐ ๋ฉ๋๋ค. |
data | ๋ฐ์ฝ๋ ์ดํฐ์ ์ ๋ฌ๋ ๋ฌธ์์ด์ ๋๋ค. ์: @Body('string') ์์ 'string'. ๋ฐ์ฝ๋ ์ดํฐ ๊ดํธ๋ฅผ ๋น์๋๋ฉด undefined๊ฐ ๋ฉ๋๋ค. |
๊ฒฝ๊ณ
TypeScript์ ์ธํฐํ์ด์ค๋ ํธ๋์คํ์ผ ๊ณผ์ ์์ ์ฌ๋ผ์ง๊ธฐ ๋๋ฌธ์, ๋ฉ์๋ ํ๋ผ๋ฏธํฐ์ ํ์ ์ด ํด๋์ค๊ฐ ์๋๋ผ ์ธํฐํ์ด์ค๋ก ์ ์ธ๋๋ฉด metatype ๊ฐ์ Object๊ฐ ๋ฉ๋๋ค.
์คํค๋ง ๊ธฐ๋ฐ ๊ฒ์ฆ
์ฐ๋ฆฌ์ Validation ํ์ดํ๋ฅผ ์ข ๋ ์ ์ฉํ๊ฒ ๋ง๋ค์ด ๋ด ์๋ค. ์๋ฅผ ๋ค์ด, CatsController์ create() ๋ฉ์๋๋ฅผ ์ดํด๋ณด๋ฉด, ์๋น์ค ๋ฉ์๋๋ฅผ ์คํํ๊ธฐ ์ ์ POST ๋ณธ๋ฌธ ๊ฐ์ฒด๊ฐ ์ ํจํ์ง ํ์ธํ๊ณ ์ถ์ ๊ฒ์ ๋๋ค.
@Post()
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
createCatDto ๋ณธ๋ฌธ ํ๋ผ๋ฏธํฐ์ ์ง์คํด ๋ด ์๋ค. ์ด ํ๋ผ๋ฏธํฐ์ ํ์ ์ CreateCatDto ์ ๋๋ค:
export class CreateCatDto {
name: string;
age: number;
breed: string;
}
์ฐ๋ฆฌ๋ create ๋ฉ์๋์ ๋ค์ด์ค๋ ๋ชจ๋ ์์ฒญ์ด ์ ํจํ ๋ณธ๋ฌธ (body)์ ํฌํจํ๊ณ ์๋์ง๋ฅผ ๋ณด์ฅํ๊ณ ์ ํฉ๋๋ค. ์ด๋ฅผ ์ํด createCatDto ๊ฐ์ฒด์ ์ธ ๋ฉค๋ฒ๋ฅผ ๊ฒ์ฆํด์ผ ํฉ๋๋ค. ์ด ์์ ์ ๋ผ์ฐํธ ํธ๋ค๋ฌ ๋ฉ์๋ ์์์ ์ํํ ์๋ ์์ง๋ง, ๊ทธ๋ ๊ฒ ํ๋ฉด ๋จ์ผ ์ฑ ์ ์์น (SRP)์ ์๋ฐํ๊ฒ ๋ฉ๋๋ค.
๋ค๋ฅธ ์ ๊ทผ ๋ฐฉ์์ผ๋ก๋ ๋ณ๋์ ์ ํจ์ฑ ๊ฒ์ฌ๊ธฐ ํด๋์ค (validator class)๋ฅผ ๋ง๋ค์ด ํด๋น ์์ ์ ์์ํ๋ ๋ฐฉ๋ฒ์ด ์์ต๋๋ค. ํ์ง๋ง ์ด ๋ฐฉ๋ฒ์ ๊ฐ ๋ฉ์๋์ ์์ ๋ถ๋ถ์์ ํด๋น ์ ํจ์ฑ ๊ฒ์ฌ๊ธฐ๋ฅผ ํธ์ถํ๋ ๊ฒ์ ๋งค๋ฒ ๊ธฐ์ตํด์ผ ํ๋ค๋ ๋จ์ ์ด ์์ต๋๋ค.
๊ทธ๋ฌ๋ฉด ์ ํจ์ฑ ๊ฒ์ฌ ๋ฏธ๋ค์จ์ด (validation middleware)๋ฅผ ๋ง๋ค์ด๋ณด๋ ๊ฑด ์ด๋จ๊น์? ๊ฐ๋ฅํ๊ธด ํ์ง๋ง, ์์ฝ๊ฒ๋ NestJS์์๋ ์ ํ๋ฆฌ์ผ์ด์ ์ ์ญ์ ๋ชจ๋ ์ปจํ ์คํธ์์ ์ฌ์ฉํ ์ ์๋ ์ ๋ค๋ฆญ ํ ๋ฏธ๋ค์จ์ด๋ฅผ ๋ง๋๋ ๊ฒ์ด ๋ถ๊ฐ๋ฅํฉ๋๋ค. ์๋ํ๋ฉด ๋ฏธ๋ค์จ์ด๋ ์ด๋ค ํธ๋ค๋ฌ๊ฐ ํธ์ถ๋ ์ง๋ ํด๋น ํธ๋ค๋ฌ์ ํ๋ผ๋ฏธํฐ์ ๋ํ ์ ๋ณด๋ฅผ ์ ์ ์๊ธฐ ๋๋ฌธ์ ๋๋ค.
๋ฐ๋ก ์ด๊ฒ์ด ํ์ดํ (pipes)๊ฐ ์ค๊ณ๋ ์ ํํ ์ฉ๋ก์ ๋๋ค. ๋ฐ๋ผ์ ์ด์ ์ ํจ์ฑ ๊ฒ์ฌ ํ์ดํ๋ฅผ ์ ๊ตํ๊ฒ ๊ฐ์ ํด ๋ณด๊ฒ ์ต๋๋ค.
๊ฐ์ฒด ์คํค๋ง ๊ฒ์ฆ
๊ฐ์ฒด ์ ํจ์ฑ ๊ฒ์ฌ๋ฅผ ๊น๋ํ๊ณ ์ค๋ณต ์์ด (DRY) ์ํํ ์ ์๋ ์ฌ๋ฌ ๊ฐ์ง ์ ๊ทผ ๋ฐฉ์์ด ์์ต๋๋ค. ๊ทธ์ค ํ๋๋ ์คํค๋ง ๊ธฐ๋ฐ ์ ํจ์ฑ ๊ฒ์ฌ (schema-based validation)์ ๋๋ค. ์ง๊ธ๋ถํฐ ์ด ๋ฐฉ์์ ์ฌ์ฉํด ๋ณด๊ฒ ์ต๋๋ค.
Zod ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ํ์ฉํ๋ฉด ๊ฐ๋จํ๊ณ ์ฝ๊ธฐ ์ฌ์ด API๋ก ์คํค๋ง๋ฅผ ์ ์ํ ์ ์์ต๋๋ค. ์ด์ Zod ๊ธฐ๋ฐ ์คํค๋ง๋ฅผ ์ฌ์ฉํ๋ ์ปค์คํ Validation Pipe๋ฅผ ๋ง๋ค์ด ๋ณด๊ฒ ์ต๋๋ค.
๋จผ์ ์๋ ๋ช ๋ น์ด๋ก ํ์ํ ํจํค์ง๋ฅผ ์ค์นํ์ธ์:
$ npm install --save zod
์๋ ์ฝ๋ ์์ ์์๋ ์คํค๋ง๋ฅผ ์์ฑ์ ์ธ์๋ก ๋ฐ๋ ๊ฐ๋จํ ํด๋์ค๋ฅผ ์์ฑํฉ๋๋ค. ๊ทธ๋ฐ ๋ค์ schema.parse() ๋ฉ์๋๋ฅผ ์ฌ์ฉํ์ฌ ์ ๋ฌ๋ ์คํค๋ง์ ๋ํด ๋ค์ด์จ ์ธ์๋ฅผ ๊ฒ์ฆํฉ๋๋ค.
์์ ์ธ๊ธํ๋ฏ์ด, ๊ฒ์ฆ ํ์ดํ๋ ์ธ์๊ฐ ์ ํจํ๋ฉด ๊ฐ์ ๊ทธ๋๋ก ๋ฐํํ๊ณ , ์ ํจํ์ง ์์ผ๋ฉด ์์ธ๋ฅผ ๋์ง๋๋ค.
๋ค์ ์น์ ์์๋, @UsePipes() ๋ฐ์ฝ๋ ์ดํฐ๋ฅผ ์ฌ์ฉํ์ฌ ํน์ ์ปจํธ๋กค๋ฌ ๋ฉ์๋์ ์ ์ ํ ์คํค๋ง๋ฅผ ๊ณต๊ธํ๋ ๋ฐฉ๋ฒ์ ์ดํด๋ด ๋๋ค. ์ด๋ฅผ ํตํด ์ฐ๋ฆฌ๊ฐ ์๋ํ๋ ๋๋ก ๊ฒ์ฆ ํ์ดํ๋ฅผ ๋ค์ํ ์ปจํ ์คํธ์์ ์ฌ์ฌ์ฉํ ์ ์๊ฒ ๋ฉ๋๋ค.
import { PipeTransform, ArgumentMetadata, BadRequestException } from '@nestjs/common';
import { ZodSchema } from 'zod';
export class ZodValidationPipe implements PipeTransform {
constructor(private schema: ZodSchema) {}
transform(value: unknown, metadata: ArgumentMetadata) {
try {
const parsedValue = this.schema.parse(value);
return parsedValue;
} catch (error) {
throw new BadRequestException('Validation failed');
}
}
}
๊ฒ์ฆ ํ์ดํ ๋ฐ์ธ๋ฉํ๊ธฐ
์์ ์ฐ๋ฆฌ๋ ParseIntPipe์ ๊ฐ์ ๋ณํ ํ์ดํ (๊ทธ๋ฆฌ๊ณ ๋ค๋ฅธ Parse* ํ์ดํ๋ค)๋ฅผ ๋ฐ์ธ๋ฉํ๋ ๋ฐฉ๋ฒ์ ์ดํด๋ณด์์ต๋๋ค.
๊ฒ์ฆ ํ์ดํ๋ฅผ ๋ฐ์ธ๋ฉํ๋ ๋ฐฉ๋ฒ๋ ๋งค์ฐ ๊ฐ๋จํฉ๋๋ค.
์ด๋ฒ ์์ ์์๋ ๋ฉ์๋ ํธ์ถ ์์ค์์ ํ์ดํ๋ฅผ ๋ฐ์ธ๋ฉํ๋ ค๊ณ ํฉ๋๋ค. ํ์ฌ ์์ ์์๋ ZodValidationPipe๋ฅผ ์ฌ์ฉํ๊ธฐ ์ํด ๋ค์๊ณผ ๊ฐ์ ์์ ์ด ํ์ํฉ๋๋ค:
- ZodValidationPipe ์ธ์คํด์ค๋ฅผ ์์ฑํฉ๋๋ค.
- ํ์ดํ ์์ฑ์์ ์ปจํ ์คํธ์ ๋ง๋ Zod ์คํค๋ง๋ฅผ ์ ๋ฌํฉ๋๋ค.
- ํด๋น ํ์ดํ๋ฅผ ๋ฉ์๋์ ๋ฐ์ธ๋ฉํฉ๋๋ค.
Zod ์คํค๋ง ์์ :
import { z } from 'zod';
export const createCatSchema = z
.object({
name: z.string(),
age: z.number(),
breed: z.string(),
})
.required();
export type CreateCatDto = z.infer<typeof createCatSchema>;
์ฐ๋ฆฌ๋ ์๋์ ๊ฐ์ด @UsePipes() ๋ฐ์ฝ๋ ์ดํฐ๋ฅผ ์ฌ์ฉํ์ฌ ์ด๋ฅผ ์ํํฉ๋๋ค:
@Post()
@UsePipes(new ZodValidationPipe(createCatSchema))
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
ํํธ
@UsePipes() ๋ฐ์ฝ๋ ์ดํฐ๋ @nestjs/common ํจํค์ง์์ ๊ฐ์ ธ์ต๋๋ค.
๊ฒฝ๊ณ
Zod ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฌ์ฉํ๋ ค๋ฉด tsconfig.json ํ์ผ์์ strictNullChecks ์ค์ ์ ํ์ฑํํด์ผ ํฉ๋๋ค.
ํด๋์ค ๊ฒ์ฆ๊ธฐ
๊ฒฝ๊ณ
์ด ์น์ ์ ๊ธฐ์ ๋ค์ TypeScript๋ฅผ ํ์๋ก ํ๋ฉฐ, ์ ํ๋ฆฌ์ผ์ด์ ์ด ์์ JavaScript๋ก ์์ฑ๋ ๊ฒฝ์ฐ์๋ ์ฌ์ฉํ ์ ์์ต๋๋ค.
์ฐ๋ฆฌ์ ์ ํจ์ฑ ๊ฒ์ฌ ๊ธฐ๋ฒ์ ๋ํ ๋์ฒด ๊ตฌํ ๋ฐฉ๋ฒ์ ์ดํด๋ณด๊ฒ ์ต๋๋ค.
Nest๋ class-validator ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ ์ ์ฐ๋๋ฉ๋๋ค. ์ด ๊ฐ๋ ฅํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ ๋ฐ์ฝ๋ ์ดํฐ ๊ธฐ๋ฐ์ ์ ํจ์ฑ ๊ฒ์ฌ๋ฅผ ์ฌ์ฉํ ์ ์๊ฒ ํด ์ค๋๋ค. ๋ฐ์ฝ๋ ์ดํฐ ๊ธฐ๋ฐ์ ์ ํจ์ฑ ๊ฒ์ฌ๋ ํนํ Nest์ ํ์ดํ ๊ธฐ๋ฅ๊ณผ ๊ฒฐํฉ๋์์ ๋ ๋งค์ฐ ๊ฐ๋ ฅํ๋ฐ, ์ด๋ ์ฒ๋ฆฌ ์ค์ธ ์์ฑ์ ๋ฉํํ์ ์ ์ ๊ทผํ ์ ์๊ธฐ ๋๋ฌธ์ ๋๋ค. ์์ํ๊ธฐ์ ์์, ๋ค์๊ณผ ๊ฐ์ ํจํค์ง๋ค์ ์ค์นํด์ผ ํฉ๋๋ค:
$ npm i --save class-validator class-transformer
์ด ํจํค์ง๋ค์ด ์ค์น๋๋ฉด, CreateCatDto ํด๋์ค์ ๋ช ๊ฐ์ง ๋ฐ์ฝ๋ ์ดํฐ๋ฅผ ์ถ๊ฐํ ์ ์์ต๋๋ค. ์ด ๊ธฐ๋ฒ์ ํฐ ์ฅ์ ์ค ํ๋๋ ๋ฐ๋ก ์ฌ๊ธฐ์์ ๋๋ฌ๋ฉ๋๋ค: CreateCatDto ํด๋์ค๊ฐ ์ฐ๋ฆฌ์ POST body ๊ฐ์ฒด์ ๋ํ ๋จ์ผ ์ง์ค์ ์์ฒ (single source of truth)์ผ๋ก ๋จ๋๋ค๋ ์ ์ ๋๋ค (๋ฐ๋ก ์ ํจ์ฑ ๊ฒ์ฌ์ฉ ํด๋์ค๋ฅผ ๋ง๋ค ํ์๊ฐ ์์ต๋๋ค).
import { IsString, IsInt } from 'class-validator';
export class CreateCatDto {
@IsString()
name: string;
@IsInt()
age: number;
@IsString()
breed: string;
}
ํํธ
class-validator ๋ฐ์ฝ๋ ์ดํฐ์ ๋ํด ๋ ์์๋ณด๋ ค๋ฉด ์ฌ๊ธฐ์์ ํ์ธํ์ธ์.
์ด์ ์ด๋ฌํ ์ด๋ ธํ ์ด์ ์ ์ฌ์ฉํ๋ ValidationPipe ํด๋์ค๋ฅผ ์์ฑํ ์ ์์ต๋๋ค.
import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common';
import { validate } from 'class-validator';
import { plainToInstance } from 'class-transformer';
@Injectable()
export class ValidationPipe implements PipeTransform<any> {
async transform(value: any, { metatype }: ArgumentMetadata) {
if (!metatype || !this.toValidate(metatype)) {
return value;
}
const object = plainToInstance(metatype, value);
const errors = await validate(object);
if (errors.length > 0) {
throw new BadRequestException('Validation failed');
}
return value;
}
private toValidate(metatype: Function): boolean {
const types: Function[] = [String, Boolean, Number, Array, Object];
return !types.includes(metatype);
}
}
ํํธ
๋ค์ ํ๋ฒ ๋ง์๋๋ฆฌ๋ฉด, ์ด ์ฅ์์ ๋ณด์ฌ์ค ๊ธฐ๋ณธ์ ์ธ ์์ ์ฒ๋ผ ์ง์ ์ผ๋ฐ์ ์ธ validation pipe๋ฅผ ๋ง๋ค ํ์๋ ์์ต๋๋ค. Nest์์๋ ValidationPipe๋ฅผ ๊ธฐ๋ณธ์ ์ผ๋ก ์ ๊ณตํฉ๋๋ค. ์ด ๋ด์ฅ ValidationPipe๋ ๋ณธ ์ฅ์์ ์์ ๋ก ๋ง๋ ๊ฒ๋ณด๋ค ๋ ๋ค์ํ ์ต์ ์ ์ ๊ณตํ๋ฉฐ, ์ฌ์ฉ์ ์ ์ pipe์ ๋์ ๋ฐฉ์์ ์ค๋ช ํ๊ธฐ ์ํด ๋จ์ํ๊ฒ ๊ตฌ์ฑ๋ ๊ฒ์ ๋๋ค. ์ ์ฒด ์ธ๋ถ์ฌํญ๊ณผ ๋ค์ํ ์์ ๋ ์ฌ๊ธฐ์์ ํ์ธํ ์ ์์ต๋๋ค.
์๋ฆผ
์ ์์ ์์๋ class-transformer ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฌ์ฉํ์ผ๋ฉฐ, ์ด๋ class-validator ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ ๋์ผํ ์ ์์ ์ํด ๊ฐ๋ฐ๋์ด ์๋ก ์ ํธํ๋ฉ๋๋ค.
์ด ์ฝ๋๋ฅผ ์ดํด๋ด ์๋ค. ๋จผ์ , transform() ๋ฉ์๋๊ฐ async๋ก ํ์๋์ด ์๋ค๋ ์ ์ ์ฃผ๋ชฉํ์ธ์. Nest๋ ๋๊ธฐ์ ๋น๋๊ธฐ ํ์ดํ๋ฅผ ๋ชจ๋ ์ง์ํ๊ธฐ ๋๋ฌธ์ ์ด๊ฒ์ด ๊ฐ๋ฅํฉ๋๋ค. ์ด ๋ฉ์๋๋ฅผ async๋ก ๋ง๋ ์ด์ ๋, class-validator์ ์ผ๋ถ ๊ฒ์ฆ ์์ ์ด ๋น๋๊ธฐ์ ์ผ๋ก ์ํ๋ ์ ์๊ธฐ ๋๋ฌธ์ ๋๋ค. (์: Promise๋ฅผ ์ฌ์ฉํ๋ ๊ฒ์ฆ)
๊ทธ๋ค์์ผ๋ก, ArgumentMetadata ๊ฐ์ฒด์์ metatype ํ๋๋ง ์ถ์ถํ๊ธฐ ์ํด ๊ตฌ์กฐ ๋ถํด ํ ๋น์ ์ฌ์ฉํฉ๋๋ค. ์ด๋ ArgumentMetadata ์ ์ฒด๋ฅผ ๋ฐ์ ํ ๋ณ๋์ ๋ฌธ์ฅ์ผ๋ก metatype์ ํ ๋นํ๋ ๋ฐฉ์๋ณด๋ค ๊ฐ๊ฒฐํ ํํ์ ๋๋ค.
์ด์ด์ toValidate()๋ผ๋ ํฌํผ ํจ์์ ์ฃผ๋ชฉํ์ธ์. ์ด ํจ์๋ ํ์ฌ ์ฒ๋ฆฌ ์ค์ธ ์ธ์๊ฐ ๊ธฐ๋ณธ JavaScript ํ์ ์ธ ๊ฒฝ์ฐ ์ ํจ์ฑ ๊ฒ์ฌ๋ฅผ ์๋ตํ๋๋ก ํฉ๋๋ค. ๊ธฐ๋ณธ ํ์ ์๋ ์ ํจ์ฑ ๊ฒ์ฌ ๋ฐ์ฝ๋ ์ดํฐ๋ฅผ ๋ถ์ผ ์ ์๊ธฐ ๋๋ฌธ์ ๊ฒ์ฌํ ์ด์ ๊ฐ ์์ต๋๋ค.
๊ทธ๋ค์, class-transformer์ plainToInstance() ํจ์๋ฅผ ์ฌ์ฉํ์ฌ ์ผ๋ฐ JavaScript ๊ฐ์ฒด๋ฅผ ํ์ ์ด ์ง์ ๋ ๊ฐ์ฒด๋ก ๋ณํํฉ๋๋ค. ๋คํธ์ํฌ ์์ฒญ์ผ๋ก ์ญ์ง๋ ฌํ๋ POST ๋ฐ๋ ๊ฐ์ฒด๋ ํ์ ์ ๋ณด๊ฐ ์๊ธฐ ๋๋ฌธ์ (Express์ ํ๋ซํผ ๋์ ๋ฐฉ์์ ๊ทธ๋ ์ต๋๋ค), ์ฐ๋ฆฌ๊ฐ ์ ์ํ DTO ํด๋์ค์ ์ ์ธ๋ ์ ํจ์ฑ ๊ฒ์ฌ ๋ฐ์ฝ๋ ์ดํฐ๋ฅผ ์ ์ฉํ๋ ค๋ฉด ์ด ๋ณํ์ด ๋ฐ๋์ ํ์ํฉ๋๋ค. ๊ทธ๋์ผ ํด๋น ๊ฐ์ฒด๋ฅผ ๋จ์ํ ์ผ๋ฐ ๊ฐ์ฒด๊ฐ ์๋ ๋ฐ์ฝ๋ ์ดํฐ๊ฐ ๋ถ์ฐฉ๋ ํด๋์ค ์ธ์คํด์ค๋ก ๊ฐ์ฃผ๋์ด ์ ํจ์ฑ ๊ฒ์ฌ๋ฅผ ์ํํ ์ ์์ต๋๋ค.
๋ง์ง๋ง์ผ๋ก ๋ค์ ๊ฐ์กฐํ์๋ฉด, validation pipe๋ ์ ํจํ ๊ฒฝ์ฐ ๊ฐ์ ๊ทธ๋๋ก ๋ฐํํ๊ฑฐ๋, ์ ํจํ์ง ์์ผ๋ฉด ์์ธ๋ฅผ ๋์ง๋ ๊ฒ์ด ์ ๋ถ์ ๋๋ค.
์ด์ ๋จ์ ๋จ๊ณ๋ ValidationPipe๋ฅผ ๋ฐ์ธ๋ฉํ๋ ๊ฒ์ ๋๋ค. ํ์ดํ๋ ๋งค๊ฐ๋ณ์ ์ค์ฝํ, ๋ฉ์๋ ์ค์ฝํ, ์ปจํธ๋กค๋ฌ ์ค์ฝํ ๋๋ ์ ์ญ ์ค์ฝํ์์ ๋ฐ์ธ๋ฉํ ์ ์์ต๋๋ค. ์์ Zod ๊ธฐ๋ฐ ํ์ดํ์์๋ ๋ฉ์๋ ์์ค ๋ฐ์ธ๋ฉ ์์๋ฅผ ๋ณด์์ต๋๋ค. ์๋ ์์ ์์๋ ํ์ดํ ์ธ์คํด์ค๋ฅผ @Body() ๋ฐ์ฝ๋ ์ดํฐ์ ๋ฐ์ธ๋ฉํ์ฌ, ์์ฒญ ๋ณธ๋ฌธ (post body)์ ์ ํจ์ฑ ๊ฒ์ฌ๋ฅผ ์ํํฉ๋๋ค.
@Post()
async create(
@Body(new ValidationPipe()) createCatDto: CreateCatDto,
) {
this.catsService.create(createCatDto);
}
๋งค๊ฐ๋ณ์ ์ค์ฝํ์ ํ์ดํ๋ ๊ฒ์ฆ ๋ก์ง์ด ํน์ ๋งค๊ฐ๋ณ์ ํ๋์๋ง ๊ด๋ จ๋ ๋ ์ ์ฉํฉ๋๋ค.
์ ์ญ ์ค์ฝํ ํ์ดํ
์ ์ญ ์ค์ฝํ ํ์ดํ๋ ValidationPipe์ฒ๋ผ ์ต๋ํ ๋ฒ์ฉ์ ์ผ๋ก ๋ง๋ค์ด์ง ํ์ดํ์ ์ ์ฉ์ฑ์ ๊ทน๋ํํ๊ธฐ ์ํด ์ฌ์ฉํ ์ ์์ผ๋ฉฐ, ์ด๋ฅผ ์ค์ ํ๋ฉด ์ ํ๋ฆฌ์ผ์ด์ ์ ์ญ์ ๋ชจ๋ ๋ผ์ฐํธ ํธ๋ค๋ฌ์ ์๋์ผ๋ก ์ ์ฉ๋ฉ๋๋ค.
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe());
await app.listen(process.env.PORT ?? 3000);
}
bootstrap();
์๋ฆผ
ํ์ด๋ธ๋ฆฌ๋ ์ ํ๋ฆฌ์ผ์ด์ ์ ๊ฒฝ์ฐ useGlobalPipes() ๋ฉ์๋๋ ๊ฒ์ดํธ์จ์ด์ ๋ง์ดํฌ๋ก์๋น์ค์๋ ํ์ดํ๋ฅผ ์ค์ ํ์ง ์์ต๋๋ค. "ํ์ค" (ํ์ด๋ธ๋ฆฌ๋๊ฐ ์๋) ๋ง์ดํฌ๋ก์๋น์ค ์ ํ๋ฆฌ์ผ์ด์ ์์๋ useGlobalPipes()๊ฐ ์ ์ญ ํ์ดํ๋ฅผ ์ ์์ ์ผ๋ก ๋ฑ๋กํฉ๋๋ค.
์ ์ญ ํ์ดํ๋ ์ ํ๋ฆฌ์ผ์ด์ ์ ์ฒด์ ๊ฑธ์ณ ๋ชจ๋ ์ปจํธ๋กค๋ฌ์ ๋ผ์ฐํธ ํธ๋ค๋ฌ์ ์ฌ์ฉ๋ฉ๋๋ค.
์์กด์ฑ ์ฃผ์ ๊ด์ ์์ ๋ณด๋ฉด, ์ด๋ค ๋ชจ๋ ์ธ๋ถ์์ (useGlobalPipes()๋ฅผ ์ฌ์ฉํ์ฌ) ๋ฑ๋ก๋ ์ ์ญ ํ์ดํ๋ ๋ชจ๋ ์ปจํ ์คํธ ๋ฐ์์ ๋ฐ์ธ๋ฉ๋์๊ธฐ ๋๋ฌธ์ ์์กด์ฑ์ ์ฃผ์ ๋ฐ์ ์ ์์ต๋๋ค. ์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด, ๋ค์๊ณผ ๊ฐ์ ๋ฐฉ์์ผ๋ก ๋ชจ๋ ๋ด๋ถ์์ ์ง์ ์ ์ญ ํ์ดํ๋ฅผ ์ค์ ํ ์ ์์ต๋๋ค:
import { Module } from '@nestjs/common';
import { APP_PIPE } from '@nestjs/core';
@Module({
providers: [
{
provide: APP_PIPE,
useClass: ValidationPipe,
},
],
})
export class AppModule {}
ํํธ
์ด ๋ฐฉ์์ผ๋ก ํ์ดํ์ ๋ํด ์์กด์ฑ ์ฃผ์ ์ ์ํํ ๊ฒฝ์ฐ, ์ด ๊ตฌ์ฑ์ด ์ด๋ค ๋ชจ๋์์ ์ฌ์ฉ๋๋ ์๊ด์์ด ํด๋น ํ์ดํ๋ ์ค์ ๋ก๋ ์ ์ญ (global)์ ๋๋ค. ๊ทธ๋ ๋ค๋ฉด ์ด๋์์ ์ด ์์ ์ ์ํํด์ผ ํ ๊น์? ์ ์์ ์์์ฒ๋ผ ํ์ดํ (ValidationPipe)๊ฐ ์ ์๋ ๋ชจ๋์์ ์ํํ๋ ๊ฒ์ด ์ข์ต๋๋ค. ๋ํ useClass๋ ์ปค์คํ ํ๋ก๋ฐ์ด๋๋ฅผ ๋ฑ๋กํ๋ ์ ์ผํ ๋ฐฉ๋ฒ์ด ์๋๋ฉฐ, ์ด์ ๋ํ ๋ ๋ง์ ์ ๋ณด๋ฅผ ์ฌ๊ธฐ์์ ํ์ธํ ์ ์์ต๋๋ค.
๋ด์ฅ ValidationPipe
๋ค์ ํ๋ฒ ๋ง์๋๋ฆฌ์ง๋ง, ๋ฒ์ฉ์ ์ธ ์ ํจ์ฑ ๊ฒ์ฌ ํ์ดํ๋ฅผ ์ง์ ๋ง๋ค ํ์๋ ์์ต๋๋ค. Nest๋ ๊ธฐ๋ณธ์ ์ผ๋ก ValidationPipe๋ฅผ ๋ด์ฅํ์ฌ ์ ๊ณตํฉ๋๋ค. ๋ด์ฅ๋ ValidationPipe๋ ์ด ์ฅ์์ ์์๋ก ๋ง๋ ๊ธฐ๋ณธ ๊ตฌํ๋ณด๋ค ํจ์ฌ ๋ ๋ง์ ์ต์ ์ ์ ๊ณตํฉ๋๋ค. ์ด ์ฅ์์๋ ์ปค์คํ ํ์ดํ์ ๋์ ๋ฐฉ์์ ์ค๋ช ํ๊ธฐ ์ํด ๋จ์ํ ์ํ์ ๋ง๋ ๊ฒ์ ๋๋ค. ์ ์ฒด ์ธ๋ถ์ฌํญ๊ณผ ๋ค์ํ ์์ ๋ ์ฌ๊ธฐ์์ ํ์ธํ ์ ์์ต๋๋ค.
๋ณํ ์ฌ์ฉ ์ฌ๋ก
์ ํจ์ฑ ๊ฒ์ฌ๋ ์ปค์คํ ํ์ดํ์ ์ ์ผํ ์ฉ๋๊ฐ ์๋๋๋ค. ์ด ์ฅ์ ์์์์ ์ธ๊ธํ๋ฏ์ด, ํ์ดํ๋ ์ ๋ ฅ ๋ฐ์ดํฐ๋ฅผ ์ํ๋ ํ์์ผ๋ก ๋ณํ (transform) ํ๋ ์ญํ ๋ ํ ์ ์์ต๋๋ค. ์ด๋ transform() ํจ์์์ ๋ฐํ๋ ๊ฐ์ด ํด๋น ์ธ์์ ๊ธฐ์กด ๊ฐ์ ์์ ํ ๋ฎ์ด์ฐ๊ธฐ ๋๋ฌธ์ ๋๋ค.
์ด ๊ธฐ๋ฅ์ ์ธ์ ์ ์ฉํ ๊น์? ๋๋ก๋ ํด๋ผ์ด์ธํธ๋ก๋ถํฐ ์ ๋ฌ๋ ๋ฐ์ดํฐ๋ฅผ ๋ผ์ฐํธ ํธ๋ค๋ฌ์์ ์ ์ ํ ์ฒ๋ฆฌํ๊ธฐ ์ ์ ๋ณํํด์ผ ํ๋ ๊ฒฝ์ฐ๊ฐ ์์ต๋๋ค. ์๋ฅผ ๋ค์ด, ๋ฌธ์์ด์ ์ ์๋ก ๋ณํํด์ผ ํ ์๋ ์๊ณ , ์ผ๋ถ ํ์ ๋ฐ์ดํฐ ํ๋๊ฐ ๋๋ฝ๋์์ ๊ฒฝ์ฐ ๊ธฐ๋ณธ๊ฐ์ ์ ์ฉํ๊ณ ์ถ์ ์๋ ์์ต๋๋ค. ๋ณํ ํ์ดํ๋ ์ด๋ฌํ ๊ธฐ๋ฅ์ ์ํํ๊ธฐ ์ํด ํด๋ผ์ด์ธํธ ์์ฒญ๊ณผ ๋ผ์ฐํธ ํธ๋ค๋ฌ ์ฌ์ด์ ์ฒ๋ฆฌ ํจ์๋ฅผ ์ฝ์ ํ๋ ๋ฐฉ์์ผ๋ก ๋์ํฉ๋๋ค.
๋ค์์ ๋ฌธ์์ด์ ์ ์๋ก ํ์ฑ ํ๋ ๊ฐ๋จํ ParseIntPipe์ ๋๋ค. (์์์ ์ธ๊ธํ๋ฏ์ด, Nest๋ ๋ ์ ๊ตํ ๋ด์ฅ ParseIntPipe๋ฅผ ์ ๊ณตํฉ๋๋ค. ์ด ์์๋ ๋จ์ํ ์ปค์คํ ๋ณํ ํ์ดํ์ ์์ ๋๋ค.)
import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common';
@Injectable()
export class ParseIntPipe implements PipeTransform<string, number> {
transform(value: string, metadata: ArgumentMetadata): number {
const val = parseInt(value, 10);
if (isNaN(val)) {
throw new BadRequestException('Validation failed');
}
return val;
}
}
๊ทธ๋ฐ ๋ค์ ์๋์ ๊ฐ์ด ์ด ํ์ดํ๋ฅผ ํน์ ๋งค๊ฐ๋ณ์์ ๋ฐ์ธ๋ฉํ ์ ์์ต๋๋ค:
@Get(':id')
async findOne(@Param('id', new ParseIntPipe()) id) {
return this.catsService.findOne(id);
}
๋ ๋ค๋ฅธ ์ ์ฉํ ๋ณํ ์ฌ๋ก๋ ์์ฒญ์ ํฌํจ๋ ID๋ฅผ ์ฌ์ฉํ์ฌ ๋ฐ์ดํฐ๋ฒ ์ด์ค์์ ๊ธฐ์กด ์ฌ์ฉ์ ์ํฐํฐ๋ฅผ ์กฐํํ๋ ๊ฒ์ ๋๋ค:
@Get(':id')
findOne(@Param('id', UserByIdPipe) userEntity: UserEntity) {
return userEntity;
}
์ด ํ์ดํ์ ๊ตฌํ์ ๋ ์์๊ฒ ๋งก๊ธฐ์ง๋ง, ๋ค๋ฅธ ๋ชจ๋ ๋ณํ ํ์ดํ์ ๋ง์ฐฌ๊ฐ์ง๋ก ์ ๋ ฅ ๊ฐ (ID)์ ๋ฐ์ ์ถ๋ ฅ ๊ฐ (UserEntity ๊ฐ์ฒด)์ ๋ฐํํ๋ค๋ ์ ์ ์ฃผ๋ชฉํ์ธ์. ์ด๋ฌํ ๋ฐฉ์์ ํธ๋ค๋ฌ์์ ๋ฐ๋ณต๋๋ ๋ณด์ผ๋ฌํ๋ ์ดํธ ์ฝ๋๋ฅผ ๊ณตํต ํ์ดํ๋ก ์ถ์ถํจ์ผ๋ก์จ ์ฝ๋์ ์ ์ธ์ ์ฑ๊ฒฉ์ ๋์ด๊ณ DRY ์์น์ ์งํค๋ ๋ฐ ๋์์ด ๋ฉ๋๋ค.
๊ธฐ๋ณธ๊ฐ ์ ๊ณตํ๊ธฐ
Parse* ํ์ดํ๋ ๋งค๊ฐ๋ณ์ ๊ฐ์ด ์ ์๋์ด ์๊ธฐ๋ฅผ ๊ธฐ๋ํฉ๋๋ค. null ๋๋ undefined ๊ฐ์ ๋ฐ์ผ๋ฉด ์์ธ๋ฅผ ๋ฐ์์ํต๋๋ค. ์ฟผ๋ฆฌ ๋ฌธ์์ด ๋งค๊ฐ๋ณ์ ๊ฐ์ด ๋๋ฝ๋ ๊ฒฝ์ฐ์๋ ์๋ํฌ์ธํธ๊ฐ ์ด๋ฅผ ์ฒ๋ฆฌํ ์ ์๋๋ก ํ๋ ค๋ฉด, Parse* ํ์ดํ๊ฐ ๋์ํ๊ธฐ ์ ์ ์ฃผ์ ํ ๊ธฐ๋ณธ๊ฐ์ ์ ๊ณตํด์ผ ํฉ๋๋ค. ์ด๋ฅผ ์ํด DefaultValuePipe๋ฅผ ์ฌ์ฉํฉ๋๋ค. ์๋ ์์์ ๊ฐ์ด, @Query() ๋ฐ์ฝ๋ ์ดํฐ ์์์ ๊ด๋ จ๋ Parse* ํ์ดํ ์์ DefaultValuePipe๋ฅผ ์ธ์คํด์คํํ๋ฉด ๋ฉ๋๋ค.
@Get()
async findAll(
@Query('activeOnly', new DefaultValuePipe(false), ParseBoolPipe) activeOnly: boolean,
@Query('page', new DefaultValuePipe(0), ParseIntPipe) page: number,
) {
return this.catsService.findAll({ activeOnly, page });
}
๊ฐ๋
๊ฐ๋๋ @Injectable() ๋ฐ์ฝ๋ ์ดํฐ๊ฐ ๋ถ์ ํด๋์ค์ด๋ฉฐ, CanActivate ์ธํฐํ์ด์ค๋ฅผ ๊ตฌํํฉ๋๋ค.
๊ฐ๋๋ ํ๋์ ์ฑ ์๋ง์ ๊ฐ์ง๋๋ค. ๋ฐํ์์ ํน์ ์กฐ๊ฑด (์: ๊ถํ, ์ญํ , ACL ๋ฑ)์ ๋ฐ๋ผ ์์ฒญ์ ํด๋น ๋ผ์ฐํธ ํธ๋ค๋ฌ๊ฐ ์ฒ๋ฆฌํ ์ ์๋์ง ์ฌ๋ถ๋ฅผ ๊ฒฐ์ ํฉ๋๋ค. ์ด๋ ํํ ์ธ๊ฐ (authorization)๋ผ๊ณ ๋ถ๋ฆฝ๋๋ค. ์ธ๊ฐ (authorization)์ ์์ฃผ ํจ๊ป ์ฌ์ฉ๋๋ ์ธ์ฆ (authentication)์ ์ ํต์ ์ธ Express ์ ํ๋ฆฌ์ผ์ด์ ์์๋ ๋ณดํต ๋ฏธ๋ค์จ์ด๋ฅผ ํตํด ์ฒ๋ฆฌํด ์์ต๋๋ค. ์ธ์ฆ์ ํ ํฐ ๊ฒ์ฆ์ด๋ ์์ฒญ ๊ฐ์ฒด์ ์์ฑ ์ถ๊ฐ ๋ฑ ํน์ ๋ผ์ฐํธ์ ๊ฐํ๊ฒ ์ฐ๊ด๋์ง ์์ ์์ ์ด๊ธฐ ๋๋ฌธ์ ๋ฏธ๋ค์จ์ด๋ก ์ฒ๋ฆฌํ๊ธฐ์ ์ ์ ํฉ๋๋ค.
ํ์ง๋ง ๋ฏธ๋ค์จ์ด๋ ๋ณธ์ง์ ์ผ๋ก ์ด๋ค ํธ๋ค๋ฌ๊ฐ ์คํ๋ ์ง ๋ชจ๋ฅด๋ ์ํ์์ next()๋ฅผ ํธ์ถํ๊ธฐ ๋๋ฌธ์ "๋ฌด์งํ (dumb)" ์กด์ฌ์ ๋๋ค. ๋ฐ๋ฉด, ๊ฐ๋๋ ExecutionContext ์ธ์คํด์ค์ ์ ๊ทผํ ์ ์๊ธฐ ๋๋ฌธ์ ๋ค์์ ์ด๋ค ํธ๋ค๋ฌ๊ฐ ์คํ๋ ์ง๋ฅผ ์ ํํ ์๊ณ ์์ต๋๋ค. ๊ฐ๋๋ ์์ธ ํํฐ, ํ์ดํ, ์ธํฐ์ ํฐ์ฒ๋ผ ์์ฒญ/์๋ต ์ฌ์ดํด์ ์ ํํ ์ง์ ์ ์ฒ๋ฆฌ ๋ก์ง์ ์ฝ์ ํ ์ ์๋๋ก ์ค๊ณ๋์์ผ๋ฉฐ, ์ ์ธ์ ์ผ๋ก ์ฌ์ฉํ ์ ์์ต๋๋ค. ์ด๋ฅผ ํตํด ์ฝ๋์ ์ค๋ณต์ ์ค์ด๊ณ ์ ์ธ์ ์ธ ์คํ์ผ์ ์ ์งํ ์ ์์ต๋๋ค.
ํํธ
๊ฐ๋๋ ๋ชจ๋ ๋ฏธ๋ค์จ์ด๊ฐ ์คํ๋ ์ดํ, ์ธํฐ์ ํฐ๋ ํ์ดํ๊ฐ ์คํ๋๊ธฐ ์ ์ ์คํ๋ฉ๋๋ค.
์ธ๊ฐ ๊ฐ๋
์์ ์ธ๊ธํ๋ฏ์ด, ์ธ๊ฐ (authorization)๋ ๊ฐ๋์ ๋ํ์ ์ธ ์ฌ์ฉ ์ฌ๋ก์ ๋๋ค. ํน์ ๋ผ์ฐํธ๋ ์ค์ง ํธ์ถ์ (์ผ๋ฐ์ ์ผ๋ก ์ธ์ฆ๋ ์ฌ์ฉ์)๊ฐ ์ถฉ๋ถํ ๊ถํ์ ๊ฐ์ง๊ณ ์์ ๋๋ง ์ ๊ทผ ๊ฐ๋ฅํด์ผ ํฉ๋๋ค. ์ง๊ธ๋ถํฐ ๋ง๋ค AuthGuard๋ ์ฌ์ฉ์๊ฐ ์ด๋ฏธ ์ธ์ฆ๋์์์ ์ ์ ๋ก ํ๋ฉฐ, ์์ฒญ ํค๋์ ํ ํฐ์ด ํฌํจ๋์ด ์๋ค๊ณ ๊ฐ์ ํฉ๋๋ค. ์ด ๊ฐ๋๋ ์์ฒญ์์ ํ ํฐ์ ์ถ์ถํ๊ณ ๊ฒ์ฆํ ํ, ์ถ์ถ๋ ์ ๋ณด๋ฅผ ๋ฐํ์ผ๋ก ํด๋น ์์ฒญ์ ์งํํ ์ ์์์ง๋ฅผ ๊ฒฐ์ ํฉ๋๋ค.
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Observable } from 'rxjs';
@Injectable()
export class AuthGuard implements CanActivate {
canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
const request = context.switchToHttp().getRequest();
return validateRequest(request);
}
}
ํํธ
์ ํ๋ฆฌ์ผ์ด์ ์ ์ค์ ์ธ์ฆ (authentication) ๋ฉ์ปค๋์ฆ์ ๊ตฌํํ๋ ๋ฐฉ๋ฒ์ด ๊ถ๊ธํ๋ค๋ฉด, ์ด ์ฅ (chapter)์ ์ฐธ๊ณ ํ์ธ์. ๋ณด๋ค ์ ๊ตํ ์ธ๊ฐ (authorization) ์์ ๋ฅผ ๋ณด๊ณ ์ถ๋ค๋ฉด, ์ด ํ์ด์ง๋ฅผ ํ์ธํด ๋ณด์ธ์.
validateRequest() ํจ์ ์์ ๋ก์ง์ ํ์์ ๋ฐ๋ผ ๊ฐ๋จํ๊ฑฐ๋ ์ ๊ตํ ์ ์์ต๋๋ค. ์ด ์์ ์ ํต์ฌ์ ๊ฐ๋ (guard)๊ฐ ์์ฒญ/์๋ต ์ฌ์ดํด์ ์ด๋ป๊ฒ ๋ค์ด๋ง๋์ง๋ฅผ ๋ณด์ฌ์ฃผ๋ ๊ฒ์ ๋๋ค.
๋ชจ๋ ๊ฐ๋๋ canActivate() ํจ์๋ฅผ ๊ตฌํํด์ผ ํฉ๋๋ค. ์ด ํจ์๋ ํ์ฌ ์์ฒญ์ด ํ์ฉ๋๋์ง๋ฅผ ๋ํ๋ด๋ ๋ถ๋ฆฌ์ธ ๊ฐ์ ๋ฐํํด์ผ ํฉ๋๋ค. ์ด ๋ฐํ์ ๋๊ธฐ์ ์ผ ์๋ ์๊ณ , ๋น๋๊ธฐ (Promise๋ Observable) ์ผ ์๋ ์์ต๋๋ค. Nest๋ ์ด ๋ฐํ๊ฐ์ ์ฌ์ฉํด ๋ค์ ๋์์ ์ ์ดํฉ๋๋ค:
- true๋ฅผ ๋ฐํํ๋ฉด ์์ฒญ์ ์ฒ๋ฆฌ๋ฉ๋๋ค.
- false๋ฅผ ๋ฐํํ๋ฉด Nest๋ ์์ฒญ์ ๊ฑฐ๋ถํฉ๋๋ค.
์คํ ์ปจํ ์คํธ
canActivate() ํจ์๋ ํ๋์ ์ธ์, ์ฆ ExecutionContext ์ธ์คํด์ค๋ฅผ ๋ฐ์ต๋๋ค. ExecutionContext๋ ArgumentsHost๋ฅผ ์์ํฉ๋๋ค. ArgumentsHost๋ ์์ธ ํํฐ ์ฅ์์ ์ด๋ฏธ ์ดํด๋ณธ ๋ฐ ์์ต๋๋ค. ์์ ์์ ์์๋ Request ๊ฐ์ฒด์ ๋ํ ์ฐธ์กฐ๋ฅผ ์ป๊ธฐ ์ํด, ์์ ์ฌ์ฉํ๋ ArgumentsHost์ ๋์ผํ ํฌํผ ๋ฉ์๋๋ค์ ๊ทธ๋๋ก ์ฌ์ฉํ๊ณ ์์ต๋๋ค. ์์ธํ ๋ด์ฉ์ ์์ธ ํํฐ ์ฅ์ Arguments host ์น์ ์ ์ฐธ์กฐํ์ธ์.
ArgumentsHost๋ฅผ ์์ํจ์ผ๋ก์จ ExecutionContext๋ ํ์ฌ ์คํ ํ๋ก์ธ์ค์ ๋ํ ์ถ๊ฐ์ ์ธ ์ธ๋ถ ์ ๋ณด๋ฅผ ์ ๊ณตํ๋ ์๋ก์ด ํฌํผ ๋ฉ์๋๋ค๋ ํฌํจํ๊ฒ ๋ฉ๋๋ค. ์ด๋ฌํ ์ ๋ณด๋ค์ ๋ค์ํ ์ปจํธ๋กค๋ฌ, ๋ฉ์๋, ์คํ ์ปจํ ์คํธ์์ ๋ชจ๋ ๋์ํ ์ ์๋ ๋ณด๋ค ๋ฒ์ฉ์ ์ธ ๊ฐ๋๋ฅผ ๋ง๋ค ๋ ์ ์ฉํ๊ฒ ์ฌ์ฉ๋ ์ ์์ต๋๋ค. ExecutionContext์ ๋ํด ๋ ์์๋ณด๋ ค๋ฉด ExecutionContext ๋ฌธ์๋ฅผ ์ฐธ๊ณ ํ์ธ์.
์ญํ ๊ธฐ๋ฐ ์ธ์ฆ
ํน์ ์ญํ (role)์ ๊ฐ์ง ์ฌ์ฉ์๋ง ์ ๊ทผํ ์ ์๋๋ก ํ์ฉํ๋ ์ข ๋ ๊ธฐ๋ฅ์ ์ธ ๊ฐ๋ (guard)๋ฅผ ๋ง๋ค์ด๋ด ์๋ค. ์ฐ์ ์ ๊ธฐ๋ณธ์ ์ธ ๊ฐ๋ ํ ํ๋ฆฟ์ผ๋ก ์์ํด์, ์ดํ ์น์ ์์ ์ ์ฐจ ๊ธฐ๋ฅ์ ์ถ๊ฐํด ๋๊ฐ๊ฒ ์ต๋๋ค. ์ง๊ธ ๋จ๊ณ์์๋ ๋ชจ๋ ์์ฒญ์ ํ์ฉํ๋ ํํ์ ๋๋ค.
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Observable } from 'rxjs';
@Injectable()
export class RolesGuard implements CanActivate {
canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
return true;
}
}
๊ฐ๋ ๋ฐ์ธ๋ฉ
ํ์ดํ (pipes)๋ ์์ธ ํํฐ (exception filters)์ ๋ง์ฐฌ๊ฐ์ง๋ก, ๊ฐ๋๋ ์ปจํธ๋กค๋ฌ ๋ฒ์ (controller-scoped), ๋ฉ์๋ ๋ฒ์ (method-scoped) ๋๋ ์ ์ญ ๋ฒ์ (global-scoped)๋ก ์ค์ ํ ์ ์์ต๋๋ค. ์๋ ์์๋ @UseGuards() ๋ฐ์ฝ๋ ์ดํฐ๋ฅผ ์ฌ์ฉํ์ฌ ์ปจํธ๋กค๋ฌ ๋ฒ์์ ๊ฐ๋๋ฅผ ์ค์ ํ ๊ฒ์ ๋๋ค. ์ด ๋ฐ์ฝ๋ ์ดํฐ๋ ํ๋์ ์ธ์ ๋๋ ์ผํ๋ก ๊ตฌ๋ถ๋ ์ฌ๋ฌ ๊ฐ์ ์ธ์๋ฅผ ๋ฐ์ ์ ์์ด, ํ์ํ ๊ฐ๋๋ค์ ํ ๋ฒ์ ์ ์ฉํ ์ ์์ต๋๋ค.
@Controller('cats')
@UseGuards(RolesGuard)
export class CatsController {}
ํํธ
@UseGuards() ๋ฐ์ฝ๋ ์ดํฐ๋ @nestjs/common ํจํค์ง์์ import ๋ฉ๋๋ค.
์ ์์์์ ์ฐ๋ฆฌ๋ RolesGuard ํด๋์ค๋ฅผ (์ธ์คํด์ค๊ฐ ์๋) ์ ๋ฌํ์ต๋๋ค. ์ด๋ ์ธ์คํด์คํ๋ฅผ Nest ํ๋ ์์ํฌ์ ๋งก๊ธฐ๊ณ ์์กด์ฑ ์ฃผ์ ์ ๊ฐ๋ฅํ๊ฒ ํฉ๋๋ค. ํ์ดํ๋ ์์ธ ํํฐ์ ๋ง์ฐฌ๊ฐ์ง๋ก, ์ง์ ์์ฑํ ์ธ์คํด์ค๋ฅผ ์ ๋ฌํ ์๋ ์์ต๋๋ค.
@Controller('cats')
@UseGuards(new RolesGuard())
export class CatsController {}
์์ ๊ตฌ์กฐ๋ ํด๋น ์ปจํธ๋กค๋ฌ์ ์ ์ธ๋ ๋ชจ๋ ํธ๋ค๋ฌ์ ๊ฐ๋๋ฅผ ์ ์ฉํฉ๋๋ค. ๊ฐ๋๋ฅผ ํน์ ๋ฉ์๋์๋ง ์ ์ฉํ๊ณ ์ถ๋ค๋ฉด @UseGuards() ๋ฐ์ฝ๋ ์ดํฐ๋ฅผ ๋ฉ์๋ ์์ค์ ์ง์ ํ๋ฉด ๋ฉ๋๋ค.
์ ์ญ ๊ฐ๋๋ฅผ ์ค์ ํ๋ ค๋ฉด Nest ์ ํ๋ฆฌ์ผ์ด์ ์ธ์คํด์ค์ useGlobalGuards() ๋ฉ์๋๋ฅผ ์ฌ์ฉํฉ๋๋ค:
const app = await NestFactory.create(AppModule);
app.useGlobalGuards(new RolesGuard());
์๋ฆผ
ํ์ด๋ธ๋ฆฌ๋ ์ฑ์ ๊ฒฝ์ฐ, useGlobalGuards() ๋ฉ์๋๋ ๊ธฐ๋ณธ์ ์ผ๋ก ๊ฒ์ดํธ์จ์ด๋ ๋ง์ดํฌ๋ก์๋น์ค์ ๋ํด ๊ฐ๋๋ฅผ ์ค์ ํ์ง ์์ต๋๋ค (์ด ๋์์ ๋ณ๊ฒฝํ๋ ๋ฐฉ๋ฒ์ ํ์ด๋ธ๋ฆฌ๋ ์ ํ๋ฆฌ์ผ์ด์ ๋ฌธ์๋ฅผ ์ฐธ์กฐํ์ธ์). "ํ์ค" (๋นํ์ด๋ธ๋ฆฌ๋) ๋ง์ดํฌ๋ก์๋น์ค ์ฑ์ ๊ฒฝ์ฐ์๋ useGlobalGuards()๊ฐ ๊ฐ๋๋ฅผ ์ ์ญ์ผ๋ก ์ค์ ํฉ๋๋ค.
์ ์ญ ๊ฐ๋๋ ์ ํ๋ฆฌ์ผ์ด์ ์ ์ฒด์ ๊ฑธ์ณ ๋ชจ๋ ์ปจํธ๋กค๋ฌ์ ๋ชจ๋ ๋ผ์ฐํธ ํธ๋ค๋ฌ์ ์ ์ฉ๋ฉ๋๋ค. ์์กด์ฑ ์ฃผ์ (Dependency Injection) ์ธก๋ฉด์์ ๋ณด๋ฉด, ์ด๋ค ๋ชจ๋ ์ธ๋ถ์์ (์์ ์์ ์ฒ๋ผ useGlobalGuards()๋ฅผ ์ฌ์ฉํ์ฌ) ์ ์ญ ๊ฐ๋๋ฅผ ๋ฑ๋กํ๋ฉด, ๋ชจ๋์ ์ปจํ ์คํธ ์ธ๋ถ์์ ๋ฐ์ธ๋ฉ์ด ์ด๋ฃจ์ด์ง๊ธฐ ๋๋ฌธ์ ์์กด์ฑ์ ์ฃผ์ ํ ์ ์์ต๋๋ค. ์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๋ ค๋ฉด, ์๋์ ๊ฐ์ ๋ฐฉ์์ผ๋ก ๊ฐ๋๋ฅผ ๋ชจ๋ ๋ด๋ถ์์ ์ง์ ์ค์ ํ ์ ์์ต๋๋ค:
import { Module } from '@nestjs/common';
import { APP_GUARD } from '@nestjs/core';
@Module({
providers: [
{
provide: APP_GUARD,
useClass: RolesGuard,
},
],
})
export class AppModule {}
ํํธ
์ด ๋ฐฉ์์ผ๋ก ๊ฐ๋์ ๋ํด ์์กด์ฑ ์ฃผ์ ์ ์ํํ ๋, ์ด ๊ตฌ์ฑ์ด ์ด๋ค ๋ชจ๋์์ ์ฌ์ฉ๋์๋์ง์ ๊ด๊ณ์์ด ํด๋น ๊ฐ๋๋ ์ ์ญ (Global) ๊ฐ๋๊ฐ ๋๋ค๋ ์ ์ ์ ์ํ์ธ์. ์ด ์์ ์ ๊ฐ๋ (์ ์์ ์์๋ RolesGuard)๊ฐ ์ ์๋ ๋ชจ๋์์ ์ํํ๋ ๊ฒ์ด ์ข์ต๋๋ค. ๋ํ, useClass๋ ์ปค์คํ ํ๋ก๋ฐ์ด๋ ๋ฑ๋ก์ ์ฒ๋ฆฌํ๋ ์ ์ผํ ๋ฐฉ๋ฒ์ด ์๋๋๋ค. ์ด์ ๋ํ ๋ ๋ง์ ์ ๋ณด๋ ์ฌ๊ธฐ์์ ํ์ธํ ์ ์์ต๋๋ค.
ํธ๋ค๋ฌ๋ณ ์ญํ ์ค์ ํ๊ธฐ
RolesGuard๋ ํ์ฌ ์๋ ์ค์ด์ง๋ง, ์์ง ๊ทธ๋ ๊ฒ ๋๋ํ์ง ์์ต๋๋ค. ๊ฐ์ฅ ์ค์ํ ๊ฐ๋ ๊ธฐ๋ฅ์ธ ์คํ ์ปจํ ์คํธ (Execution Context)๋ฅผ ์์ง ์ ๋๋ก ํ์ฉํ์ง ๋ชปํ๊ณ ์์ฃ . ํ์ฌ ์ด ๊ฐ๋๋ ์ญํ (Role)์ ๋ํด ์๋ฌด๊ฒ๋ ๋ชจ๋ฅด๊ณ , ๊ฐ ํธ๋ค๋ฌ๋ง๋ค ํ์ฉ๋ ์ญํ ์ด ๋ฌด์์ธ์ง๋ ์ธ์ํ์ง ๋ชปํฉ๋๋ค. ์๋ฅผ ๋ค์ด CatsController๋ ๊ฒฝ๋ก๋ง๋ค ๋ค๋ฅธ ๊ถํ ์ฒด๊ณ๋ฅผ ๊ฐ์ง ์ ์์ต๋๋ค. ์ผ๋ถ๋ ๊ด๋ฆฌ์ (admin)๋ง ์ ๊ทผํ ์ ์๊ณ , ๋ค๋ฅธ ์ผ๋ถ๋ ๋ชจ๋์๊ฒ ์ด๋ ค ์์ ์๋ ์์ฃ . ๊ทธ๋ ๋ค๋ฉด ์ด๋ป๊ฒ ๊ฒฝ๋ก๋ง๋ค ์ญํ ์ ์ ์ฐํ๊ณ ์ฌ์ฌ์ฉ ๊ฐ๋ฅํ ๋ฐฉ์์ผ๋ก ๋งค์นญํ ์ ์์๊น์?
์ฌ๊ธฐ์ ์ปค์คํ ๋ฉํ๋ฐ์ดํฐ (Custom Metadata)๊ฐ ๋ฑ์ฅํฉ๋๋ค (์์ธํ ์์๋ณด๊ธฐ) Nest๋ Reflector.createDecorator ์ ์ ๋ฉ์๋๋ฅผ ์ฌ์ฉํด ์์ฑ๋ ๋ฐ์ฝ๋ ์ดํฐ ๋๋ ๋ด์ฅ๋ @SetMetadata() ๋ฐ์ฝ๋ ์ดํฐ๋ฅผ ํตํด ๋ผ์ฐํธ ํธ๋ค๋ฌ์ ์ปค์คํ ๋ฉํ๋ฐ์ดํฐ๋ฅผ ๋ถ์ฐฉํ ์ ์๋ ๊ธฐ๋ฅ์ ์ ๊ณตํฉ๋๋ค.
์๋ฅผ ๋ค์ด, Reflector.createDecorator ๋ฉ์๋๋ฅผ ์ฌ์ฉํด์ ํธ๋ค๋ฌ์ ๋ฉํ๋ฐ์ดํฐ๋ฅผ ๋ถ์ฐฉํ๋ @Roles() ๋ฐ์ฝ๋ ์ดํฐ๋ฅผ ๋ค์๊ณผ ๊ฐ์ด ๋ง๋ค ์ ์์ต๋๋ค. Reflector๋ Nest ํ๋ ์์ํฌ์์ ๊ธฐ๋ณธ์ ์ผ๋ก ์ ๊ณต๋๋ฉฐ @nestjs/core ํจํค์ง์์ ๋ ธ์ถ๋ฉ๋๋ค.
import { Reflector } from '@nestjs/core';
export const Roles = Reflector.createDecorator<string[]>();
์ฌ๊ธฐ์ Roles ๋ฐ์ฝ๋ ์ดํฐ๋ string[] ํ์ ์ ์ธ์๋ฅผ ๋ฐ๋ ํจ์์ ๋๋ค.
์ด์ ์ด ๋ฐ์ฝ๋ ์ดํฐ๋ฅผ ์ฌ์ฉํ๋ฉด, ๋จ์ํ ํด๋น ํธ๋ค๋ฌ ์์ ๋ค์๊ณผ ๊ฐ์ด ์ฃผ์์ ๋ฌ์์ฃผ๋ฉด ๋ฉ๋๋ค:
@Post()
@Roles(['admin'])
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
์ฌ๊ธฐ์๋ create() ๋ฉ์๋ ์์ Roles ๋ฐ์ฝ๋ ์ดํฐ์ ๋ฉํ๋ฐ์ดํฐ๋ฅผ ๋ถ์ฐฉํ์ฌ, admin ์ญํ ์ ๊ฐ์ง ์ฌ์ฉ์๋ง ์ด ๊ฒฝ๋ก์ ์ ๊ทผํ ์ ์๋๋ก ์ง์ ํ ๊ฒ์ ๋๋ค.
๋ํ, Reflector.createDecorator ๋ฉ์๋๋ฅผ ์ฌ์ฉํ๋ ๋์ , Nest์์ ์ ๊ณตํ๋ ๋ด์ฅ ๋ฐ์ฝ๋ ์ดํฐ์ธ @SetMetadata()๋ฅผ ์ฌ์ฉํ ์๋ ์์ต๋๋ค. ์ด์ ๋ํ ์์ธํ ๋ด์ฉ์ ์ฌ๊ธฐ์์ ํ์ธํ ์ ์์ต๋๋ค.
๋ชจ๋ ๊ฒฐํฉํ๊ธฐ
์ด์ ๋ค์ RolesGuard๋ก ๋์๊ฐ์ ์์ ์ค๋ช ํ ๋ด์ฉ์ ๊ฒฐํฉํด ๋ณด๊ฒ ์ต๋๋ค. ํ์ฌ๋ ๋ชจ๋ ์์ฒญ์ ํ์ฉํ๋๋ก ํญ์ true๋ฅผ ๋ฐํํ๊ณ ์์ง๋ง, ์ฐ๋ฆฌ๋ ํ์ฌ ์ฌ์ฉ์์๊ฒ ํ ๋น๋ ์ญํ (roles)๊ณผ ํ์ฌ ๋ผ์ฐํธ์ ์๊ตฌ๋๋ ์ญํ (roles)์ ๋น๊ตํด์ ์กฐ๊ฑด์ ์ผ๋ก ์ฒ๋ฆฌ๋๋๋ก ๋ง๋ค๊ณ ์ ํฉ๋๋ค.
์ด๋ฅผ ์ํด, ํ์ฌ ์ฒ๋ฆฌ ์ค์ธ ๋ผ์ฐํธ์ ์ค์ ๋ ์ญํ (์ปค์คํ ๋ฉํ๋ฐ์ดํฐ)์ ์ฝ์ด์ผ ํ๋ฉฐ, ์ด๋ฅผ ์ํด ๋ค์ Reflector ํฌํผ ํด๋์ค๋ฅผ ์ฌ์ฉํ๊ฒ ๋ฉ๋๋ค.
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Roles } from './roles.decorator';
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const roles = this.reflector.get(Roles, context.getHandler());
if (!roles) {
return true;
}
const request = context.switchToHttp().getRequest();
const user = request.user;
return matchRoles(roles, user.roles);
}
}
ํํธ
Node.js ์ธ๊ณ์์๋ ์ธ์ฆ๋ ์ฌ์ฉ์ (user)๋ฅผ request ๊ฐ์ฒด์ ์ฒจ๋ถํ๋ ๊ฒ์ด ์ผ๋ฐ์ ์ธ ๊ด๋ก์ ๋๋ค. ๋ฐ๋ผ์ ์์ ์์ ์ฝ๋์์๋ request.user๊ฐ ์ฌ์ฉ์ ์ธ์คํด์ค์ ํ์ฉ๋ ์ญํ ์ ๋ณด๋ฅผ ๋ด๊ณ ์๋ค๊ณ ๊ฐ์ ํฉ๋๋ค. ์ค์ ์ ํ๋ฆฌ์ผ์ด์ ์์๋ ์ฌ์ฉ์ ์ธ์ฆ ๊ฐ๋๋ ๋ฏธ๋ค์จ์ด ๋ด์์ ์ด ์ฐ๊ฒฐ์ ์ง์ ๊ตฌํํด์ผ ํฉ๋๋ค. ์ด ์ฃผ์ ์ ๋ํ ์์ธํ ๋ด์ฉ์ ํด๋น ์ฑํฐ๋ฅผ ์ฐธ๊ณ ํ์ธ์.
๊ฒฝ๊ณ
matchRoles() ํจ์ ๋ด๋ถ์ ๋ก์ง์ ํ์์ ๋ฐ๋ผ ๋จ์ํ๊ฑฐ๋ ๋ณต์กํ๊ฒ ๊ตฌ์ฑํ ์ ์์ต๋๋ค. ์ด ์์ ์ ํต์ฌ ๋ชฉ์ ์ ๊ฐ๋ (Guard)๊ฐ ์์ฒญ/์๋ต ์ฌ์ดํด์ ์ด๋ป๊ฒ ์๋ํ๋์ง๋ฅผ ๋ณด์ฌ์ฃผ๋ ๊ฒ์ ๋๋ค.
Execution context ์ฅ์ Reflection and metadata ์น์ ์ ์ฐธ๊ณ ํ๋ฉด, Reflector๋ฅผ ์ปจํ ์คํธ์ ๋ฐ๋ผ ์ด๋ป๊ฒ ํ์ฉํ ์ ์๋์ง์ ๋ํ ์์ธํ ๋ด์ฉ์ ํ์ธํ ์ ์์ต๋๋ค.
๊ถํ์ด ๋ถ์กฑํ ์ฌ์ฉ์๊ฐ ์๋ํฌ์ธํธ๋ฅผ ์์ฒญํ๋ฉด, Nest๋ ์๋์ผ๋ก ๋ค์๊ณผ ๊ฐ์ ์๋ต์ ๋ฐํํฉ๋๋ค:
{
"statusCode": 403,
"message": "Forbidden resource",
"error": "Forbidden"
}
์ฃผ์ํ ์ ์, ๋ด๋ถ์ ์ผ๋ก ๊ฐ๋๊ฐ false๋ฅผ ๋ฐํํ๋ฉด ํ๋ ์์ํฌ๋ ์๋์ผ๋ก ForbiddenException์ ๋์ง๋๋ค. ๋ค๋ฅธ ์ค๋ฅ ์๋ต์ ๋ฐํํ๊ณ ์ถ๋ค๋ฉด, ์ง์ ํน์ ์์ธ๋ฅผ ๋์ ธ์ผ ํฉ๋๋ค. ์๋ฅผ ๋ค์ด:
throw new UnauthorizedException();
๊ฐ๋์์ ๋ฐ์ํ ๋ชจ๋ ์์ธ๋ ์์ธ ์ฒ๋ฆฌ ๊ณ์ธต (์ ์ญ ์์ธ ํํฐ ๋ฐ ํ์ฌ ์ปจํ ์คํธ์ ์ ์ฉ๋ ์์ธ ํํฐ)์ ์ํด ์ฒ๋ฆฌ๋ฉ๋๋ค.
ํํธ
์ค์ ์ ํ๋ฆฌ์ผ์ด์ ์์ ๊ถํ ๋ถ์ฌ๋ฅผ ๊ตฌํํ๋ ์์ ๋ฅผ ์ฐพ๊ณ ์๋ค๋ฉด, ์ด ์ฅ์ ํ์ธํ์ธ์.
Reference
'๐ ๊ณต์ ๋ฌธ์ ๋ฒ์ญ > Nest.js' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
[Nest.js] Fundamentals - Custom providers, Asynchronous providers (0) | 2025.07.15 |
---|---|
[Nest.js] Overview - Interceptors, Custom decorators (0) | 2025.07.13 |
[Nest.js] Overview - Middleware, Exception filters (2) | 2025.07.07 |
[Nest.js] Overview - Providers, Modules (0) | 2025.07.05 |
[Nest.js] Overview - First Steps, Controllers (0) | 2025.07.05 |