본문 바로가기
Research/Nest.js

08. Nest.js_RestAPI_DTO

by RIEM 2023. 2. 7.

DTO란

배경지식

백엔드에서 자원을 효율적으로 사용하기 위해 요청 횟수를 최소화하고, 요청 시 최대한 많은 데이터를 보내는 것이 중요합니다. 어떻게 하면 데이터를 모아서 한번에 전달할 수 있을까라는 고민 끝에 고안된 것이 바로 DTO, 데이터 전송 객체입니다.

DTO란

DTO는 Data Transfer Obejcts의 약자입니다. DTO를 사용하면 input 데이터의 1)프로퍼티와 2)타입을 미리 설정할 수 있습니다. DTO를 사용하면 데이터 타입을 통제하기 때문에 장기적으로 협업에 도움이 됩니다.

DTO를 사용하는 이유

  1. 유닛 테스트가 쉬워진다. Service 레이어에서 유닛 테스트를 진행 시, Entity를 바로 적용하면 Service 테스트가 가능합니다. 굳이 DB와 연동해서 테스트할 필요가 없어집니다
  2. 유지보수가 쉬워집니다. 예를 들어 userDto에 name, age 타입이 있고, userService 로직 params에 userDto를 적용해줍니다. 그런데 추후 gender 필드가 추가될 경우, userDto에 gender 타입 추가해주면 끝이 납니다. 만약, Dto가 없다면 필드가 추가될 때마다 userService에 이에 상응하는 params를 추가해주어야 합니다

dto - request payload

dto 만들기

// src/create-event.dto.ts

export class CreateEventDto {
  name: string;
  description: string;
  when: string;
  address: string;
}

컨트롤러에서 dto 사용하기

// src/events.controllers.ts
...
import { CreateEventDto } from './create-event.dto';
...
  @Post()
  create(@Body() input: CreateEventDto) {

    // O
    return input.address;

    // X
    return input.love // 'CreateEventDto' 형식에 'love' 속성이 없습니다.ts(2339)

  }
  ...

Screen Shot 2023-02-07 at 4 15 05 PM|400

dto를 사용하면 input 데이터의 형식을 통제할 수 있습니다. 좀 더 풀어보자면, CreateEventDto에서 우리는 name, description, when, address 프로퍼티만 사용하도록 강제할 수 있습니다. 만약 정의하지 않은 love 프로퍼티를 입력하면 에러가 발생합니다. 이렇게 하면 협업 시에 팀원들에게 프로퍼티만 쓰도록 선택권을 줄 수 있으니 서로 편해집니다.

dto - update payload

만약 update 요청한 데이터의 양식을 체크하고 싶다면 어떻게 해야할까요?

크게 2가지 방식이 있는데, 1)TS optional로 타입 체크를 하거나, 2)nestjs/mapped-types 패키지를 사용하면 됩니다. 저희는 패키지를 사용하여 양식을 체크해보겠습니다.

nestjs/mapped-types 패키지 설치

https://www.npmjs.com/package/@nestjs/mapped-types

npm i --save @nestjs/mapped-types

update-event.dto 생성

update-event.dto.ts라는 dto를 만들어서 update 요청 시 양식을 체크할 것입니다. 이때 TS의 옵셔널 대신 mapped-types 패키지의 PartialType 옵션을 사용할 것입니다. 이 옵션을 사용하면 input의 모든 프로퍼티를 옵셔널로 취급하게 해줍니다.

// src/update-event.dto.ts

import { PartialType } from '@nestjs/mapped-types';
import { CreateEventDto } from './create-event.dto';

// CreateEventDto 원본의 일부 타입만 가져오도록 = 옵셔널이랑 같은 효과
export class UpdateEventDto extends PartialType(CreateEventDto) { }

컨트롤러에 dto 적용

// src/events.controllers.ts
...
import { UpdateEventDto } from './update-event.dto';
...

@Patch(':id')
  update(@Param('id') identity, @Body() input: UpdateEventDto) { 
    return input.address;
  }

Entity

Entity란?

이벤트 엔티티를 만들어서 적용하기 전에 Entity의 개념에 대해 짚고 넘어갑시다.

Entity는 실체 또는 객체를 말합니다. 개념, 저장되어야 하는 무엇을 지칭합니다. 예를 들어, 학교에는 과목, 교실이라는 엔티티가 있습니다. 엔티티는 인스턴스의 집합입니다. 과목이란 엔티티 안에는 물리, 미술, 수학이라는 인스턴스들이 포함됩니다.

학교
- 엔티티 : 과목
    - 인스턴스 : 물리
    - 인스턴스 : 미술
    - 인스턴스 : 수학

Entity 작성

// src/event.entity.ts
export class Event {
  id: number;
  number: string;
  description: string;
  when: Date;
  address: string;
};

Entity 적용

// src/events.controllers.ts
...
import { Event } from './event.entity';

@Controller('/events')
export class EventsController {

  // 이벤트 엔티티 적용
  private events: Event[] = [];

...

이벤트 엔티티 추가해주었다. 임시 저장소라고 생각하면 된다. 이후 각 CRUD 코드도 엔티티에 맞춰 수정해주었다.

최종 코드 스니펫

// src/events.controllers.ts
import { Body, Controller, Delete, Get, Param, Patch, Post, HttpCode } from '@nestjs/common';
import { CreateEventDto } from './create-event.dto';
import { UpdateEventDto } from './update-event.dto';
import { Event } from './event.entity';

@Controller('/events')
export class EventsController {

  // 이벤트 엔티티 적용
  private events: Event[] = [];

  // 5개 정도 가볍게 유지하는 것이 좋다
  @Get()
  findAll() {
    // 배열을 반환하여도 클라이언트는 JSON 데이터로 받는다
    return this.events;
  }

  // Param 데코레이터 사용하여 파라미터 가져오기
  @Get(':id')
  findOne(@Param('id') id) {
    const event = this.events.find((event) => event.id === parseInt(id));
    return event;
  }

  // Body 데코레이터로 body 값 가져오기
  @Post()
  create(@Body() input: CreateEventDto) {
    // Dto에서 타입 규정했기에 프로퍼티, 타입이 통제
    const event = {
      ...input,
      when: new Date(input.when), // dto에는 규정되었지만 input에 없는 데이터 추가
      id: this.events.length + 1, // auto-increment
    };
    this.events.push(event);
    return event;
  }

  @Patch(':id')
  update(@Param('id') id, @Body() input: UpdateEventDto) { 
    const index = this.events.findIndex((event) => event.id === parseInt(id));

    this.events[index] = {
      ...this.events[index],
      ...input,
      when: input.when ? new Date(input.when) : this.events[index].when,
    };
    return this.events[index]; // 수정 아이템 반환
  }

  @Delete(':id')
  @HttpCode(204)
  remove(@Param('id') id) {
    // id 가지지 않는 event들만 필터링. 즉, id 가진 이벤트를 제외하는 것
    this.events = this.events.filter((event) => event.id !== parseInt(id));   }
}

추가 참조

댓글