dev_writer 2025. 7. 8. 23:41

ํŒŒ์ดํ”„

ํŒŒ์ดํ”„๋Š” @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๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์ž‘์—…์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค:

  1. ZodValidationPipe ์ธ์Šคํ„ด์Šค๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
  2. ํŒŒ์ดํ”„ ์ƒ์„ฑ์ž์— ์ปจํ…์ŠคํŠธ์— ๋งž๋Š” Zod ์Šคํ‚ค๋งˆ๋ฅผ ์ „๋‹ฌํ•ฉ๋‹ˆ๋‹ค.
  3. ํ•ด๋‹น ํŒŒ์ดํ”„๋ฅผ ๋ฉ”์„œ๋“œ์— ๋ฐ”์ธ๋”ฉํ•ฉ๋‹ˆ๋‹ค.

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