백앤드(Back-End)/Nest.js

[Nest.js] Nest.js API 만들기 (6) - TypeORM API서버 적용(CRUD)

RyanSin 2021. 11. 13. 19:23
반응형

- 지난 시간

안녕하세요. 지난 시간에는 TypeORM 설정 및 연결하는 방법에 대해 알아봤습니다.

 

혹시 놓치고 오신 분들은 아래 링크를 통해 학습하고 오시는 걸 추천드리겠습니다.

[Nest.js] Nest.js API 만들기 (5) - TypeORM 개념 및 설치

 

[Nest.js] Nest.js API 만들기 (5) - TypeORM 개념 및 설치

- 개요 안녕하세요. 이번 시간에는 TypeORM을 통해 실제 데이터베이스와 연동하고 CRUD를 해보는 시간을 가져보겠습니다. TypeORM은 ORM 기술 중 하나로 "객체와 관계형 데이터 베이스를 매핑(연결)을

any-ting.tistory.com

 

 

- 개요

이번 시간에는 이때까지 만들었던 RESTFul API 서버를 수정해보겠습니다.

 

작업 순서는 아래와 같습니다.

  1. DTO 수정
  2. UserController 수정
  3. UserService 수정
  4. Repository CRUD 메서드 만들기
  5. Middleware 코드 주석

- DTO 수정

기존 소스 코드
import { IsNotEmpty, IsNumber, IsString } from 'class-validator';
import { PartialType } from '@nestjs/mapped-types';

/**
 * @description SRP를 위반하는 구조이지만 테스트용으로 한 파일에 두 클래스를 선언했다.
 *
 * SRP란: 한 클래스는 하나의 책임만 가져야한다. (단일 책임의 원칙)
 */
export class CreateUserDto {
  @IsNumber()
  @IsNotEmpty()
  id: number; // 유저 고유 아이디

  @IsString()
  @IsNotEmpty()
  name: string; // 유저 이름
}

export class UpdateUserDto extends PartialType(CreateUserDto) {}

 

수정된 소스 코드
import {
  IsNotEmpty,
  IsNumber,
  IsString,
  Matches,
  MaxLength,
  MinLength,
} from 'class-validator';
import { PartialType } from '@nestjs/mapped-types';

/**
 * @description SRP를 위반하는 구조이지만 테스트용으로 한 파일에 두 클래스를 선언했다.
 *
 * SRP란: 한 클래스는 하나의 책임만 가져야한다. (단일 책임의 원칙)
 */
export class CreateUserDto {
  @IsString()
  @IsNotEmpty()
  user_id: string; // 유저 아이디

  @IsString()
  @IsNotEmpty()
  @MinLength(8)
  @MaxLength(16)
  // 최소 8자 및 최대 16자, 하나 이상의 대문자, 하나의 소문자, 하나의 숫자 및 하나의 특수 문자
  @Matches(
    /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,16}$/,
    {
      message: '비밀번호 양식에 맞게 작성하세요.',
    },
  )
  password: string; //유저 비밀번호

  @IsString()
  @IsNotEmpty()
  name: string; // 유저 이름

  @IsNumber()
  @IsNotEmpty()
  age: number; //유저 나이
}

export class UpdateUserDto extends PartialType(CreateUserDto) {
  @IsString()
  id: string;
}

속성이 많이 변경된 걸 알 수 있습니다.

 

Entity 기준으로 필요한 속성을 추가했으며 필요한 유효성 체크 기능을 추가했습니다.

 

그리고 Update시에는 유저 고유 아이디 값이 필요하기 때문에 id속성을 추가했습니다.

 

@Matches 옵션은 비밀번호 유효성 체크 기준을 만족시키지 못하면 메시지를 반환합니다.

 

비밀번호 유효성 검사 충족 X

 

- UserController 수정

기존 소스 코드
import {
  Body,
  Controller,
  Delete,
  Get,
  Param,
  Patch,
  Post,
  Put,
  Query,
  UsePipes,
  ParseIntPipe,
  ValidationPipe,
  DefaultValuePipe,
} from '@nestjs/common';
import { UserService } from './user.service';
import { CreateUserDto, UpdateUserDto } from './dto/user.dto';

@Controller('user')
export class UserController {
  constructor(private readonly userService: UserService) {}

  @Get() //경로를 설정하지 않으면 "user/" 경로로 설정이 된다.
  getHelloWorld(): string {
    return this.userService.getHelloWorld();
  }

  /**
   * @author Ryan
   * @description @Body 방식 - @Body 어노테이션 여러개를 통해 요청 객체를 접근할 수 있습니다.
   *
   * CreateUserDto를 사용해서 @Body 전달 방식을 변경합니다.
   *
   * @param id 유저 고유 아이디
   * @param name 유저 이름
   */
  @Post('/create_user')
  @UsePipes(ValidationPipe)
  onCreateUser(@Body() createUserDto: CreateUserDto): User[] {
    return this.userService.onCreateUser(createUserDto);
  }

  /**
   * @author Ryan
   * @description 전체 유저 조회
   */
  @Get('/user_all')
  getUserAll(): User[] {
    return this.userService.getUserAll();
  }

  /**
   * @author Ryan
   * @description @Query 방식 - 단일 유저 조회
   *
   * @param id 유저 고유 아이디
   */
  @Get('/user')
  findByUserOne1(
    @Query('id', new DefaultValuePipe(1), ParseIntPipe) id: number,
  ): User {
    return this.userService.findByUserOne(id);
  }

  /**
   * @author Ryan
   * @description @Param 방식 - 단일 유저 조회
   *
   * @param id 유저 고유 아이디
   */
  @Get('/user/:id')
  findByUserOne2(@Param('id', ParseIntPipe) id: number): User {
    return this.userService.findByUserOne(id);
  }

  /**
   * @author Ryan
   * @description @Param & @Body 혼합 방식 - 단일 유저 수정
   *
   * @param id 유저 고유 아이디
   * @param name 유저 이름
   */
  @Patch('/user/:id')
  @UsePipes(ValidationPipe)
  setUser(
    @Param('id', ParseIntPipe) id: number,
    @Body() updateUserDto: UpdateUserDto,
  ): User {
    return this.userService.setUser(id, updateUserDto);
  }

  /**
   * @author Ryan
   * @description @Body 방식 - 전체 유저 수정
   *
   * @param updateUserDto 유저 정보
   */
  @Put('/user/update')
  @UsePipes(ValidationPipe)
  setAllUser(@Body() updateUserDto: UpdateUserDto): User[] {
    return this.userService.setAllUser(updateUserDto);
  }

  /**
   * @author Ryan
   * @description @Query 방식 - 단일 유저 삭제
   *
   * @param id 유저 고유 아이디
   */
  @Delete('/user/delete')
  deleteUser(@Query('id', ParseIntPipe) id: number): User[] {
    return this.userService.deleteUser(id);
  }
}

 

수정된 소스 코드
import {
  Body,
  Controller,
  Delete,
  Get,
  Param,
  Patch,
  Post,
  Put,
  Query,
  UsePipes,
  ValidationPipe,
  ParseUUIDPipe,
} from '@nestjs/common';
import { UserService } from './user.service';
import { CreateUserDto, UpdateUserDto } from './dto/user.dto';
import { User } from 'src/entity/user.entity';

@Controller('user')
export class UserController {
  constructor(private readonly userService: UserService) {}

  @Get() //경로를 설정하지 않으면 "user/" 경로로 설정이 된다.
  getHelloWorld(): string {
    return this.userService.getHelloWorld();
  }

  /**
   * @author Ryan
   * @description @Body 방식 - @Body 어노테이션 여러개를 통해 요청 객체를 접근할 수 있습니다.
   *
   * CreateUserDto를 사용해서 @Body 전달 방식을 변경합니다.
   *
   * @param id 유저 고유 아이디
   * @param name 유저 이름
   */
  @Post('/create_user')
  @UsePipes(ValidationPipe)
  onCreateUser(@Body() createUserDto: CreateUserDto): Promise<boolean> {
    return this.userService.onCreateUser(createUserDto);
  }

  /**
   * @author Ryan
   * @description 전체 유저 조회
   */
  @Get('/user_all')
  getUserAll(): Promise<User[]> {
    return this.userService.getUserAll();
  }

  /**
   * @author Ryan
   * @description @Query 방식 - 단일 유저 조회
   *
   * @param id 유저 고유 아이디
   */
  @Get('/user')
  findByUserOne1(@Query('id', ParseUUIDPipe) id: string): Promise<User> {
    return this.userService.findByUserOne(id);
  }

  /**
   * @author Ryan
   * @description @Param 방식 - 단일 유저 조회
   *
   * @param id 유저 고유 아이디
   */
  @Get('/user/:id')
  findByUserOne2(@Param('id', ParseUUIDPipe) id: string): Promise<User> {
    return this.userService.findByUserOne(id);
  }

  /**
   * @author Ryan
   * @description @Param & @Body 혼합 방식 - 단일 유저 수정
   *
   * @param id 유저 고유 아이디
   * @param name 유저 이름
   */
  @Patch('/user/:id')
  @UsePipes(ValidationPipe)
  setUser(
    @Param('id', ParseUUIDPipe) id: string,
    @Body() updateUserDto: UpdateUserDto,
  ): Promise<boolean> {
    return this.userService.setUser(id, updateUserDto);
  }

  /**
   * @author Ryan
   * @description @Body 방식 - 전체 유저 수정
   *
   * @param updateUserDto 유저 정보
   */
  @Put('/user/update')
  @UsePipes(ValidationPipe)
  setAllUser(@Body() updateUserDto: UpdateUserDto[]): Promise<boolean> {
    return this.userService.setAllUser(updateUserDto);
  }

  /**
   * @author Ryan
   * @description @Query 방식 - 단일 유저 삭제
   *
   * @param id 유저 고유 아이디
   */
  @Delete('/user/delete')
  deleteUser(@Query('id', ParseUUIDPipe) id: string): Promise<boolean> {
    return this.userService.deleteUser(id);
  }
}

크게 바뀐 부분은 ParseUUIDPipe와 return 타입을 비동기(Promise <T>) 형식으로 변경했습니다.

 

TypeORM 메서드는 기본적으로 Promise 객체입니다. 그렇기 때문에 async, await를 사용할 수 있습니다.

 

 

- UserService 수정

기존 소스 코드
import { Injectable } from '@nestjs/common';
import { CreateUserDto, UpdateUserDto } from './dto/user.dto';

const users: User[] = [
  { id: 1, name: '유저1' },
  { id: 2, name: '유저2' },
  { id: 3, name: '유저3' },
];

@Injectable()
export class UserService {
  /**
   * @author Ryan
   * @description 유저 생성
   *
   * @param createUserDto 유저 데이터
   *
   * @returns {User[]} users
   */
  onCreateUser(createUserDto: CreateUserDto): User[] {
    return users.concat({ id: createUserDto.id, name: createUserDto.name });
  }

  /**
   * @author Ryan
   * @description 모든 유저 조회
   *
   * @returns {User[]} users
   */
  getUserAll(): User[] {
    return users;
  }

  /**
   * @author Ryan
   * @description 단일 유저 조회
   *
   * @param id 유저 고유 아이디
   * @returns {User} users
   */
  findByUserOne(id: number): User {
    return users.find((data) => data.id == id);
  }

  /**
   * @author Ryan
   * @description 단일 유저 수정
   *
   * @returns {User} users
   */
  setUser(id: number, updateUserDto: UpdateUserDto): User {
    return users.find((data) => {
      if (data.id == id) return (data.name = updateUserDto.name);
    });
  }

  /**
   * @author Ryan
   * @description 전체 유저 수정
   *
   * @param updateUserDto 유저 정보
   *
   * @returns {User[]} users
   */
  setAllUser(updateUserDto: UpdateUserDto): User[] {
    return users.map((data) => {
      if (data.id == updateUserDto.id) {
        data.name = updateUserDto.name;
      }

      return {
        id: data.id,
        name: data.name,
      };
    });
  }

  /**
   * @author Ryan
   * @description 유저 삭제
   *
   * @param id
   * @returns {User[]} users
   */
  deleteUser(id: number): User[] {
    return users.filter((data) => data.id != id);
  }

  getHelloWorld(): string {
    return 'Hello World!!';
  }
}

 

수정된 소스 코드
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { UserRepository } from 'src/repository/user.repository';
import { CreateUserDto, UpdateUserDto } from './dto/user.dto';
import { User } from 'src/entity/user.entity';

@Injectable()
export class UserService {
  constructor(
    @InjectRepository(UserRepository) private userRepository: UserRepository,
  ) {}

  /**
   * @author Ryan
   * @description 유저 생성
   *
   * @param createUserDto 유저 데이터
   *
   * @returns {User[]} users
   */
  onCreateUser(createUserDto: CreateUserDto): Promise<boolean> {
    return this.userRepository.onCreate(createUserDto);
  }

  /**
   * @author Ryan
   * @description 모든 유저 조회
   *
   * @returns {User[]} users
   */
  getUserAll(): Promise<User[]> {
    return this.userRepository.findAll();
  }

  /**
   * @author Ryan
   * @description 단일 유저 조회
   *
   * @param id 유저 고유 아이디
   * @returns {User} users
   */
  findByUserOne(id: string): Promise<User> {
    return this.userRepository.findById(id);
  }

  /**
   * @author Ryan
   * @description 단일 유저 수정
   *
   * @param id 유저 고유 아이디
   * @param updateUserDto 유저 정보
   *
   * @returns {Promise<boolean>} true
   */
  setUser(id: string, updateUserDto: UpdateUserDto): Promise<boolean> {
    return this.userRepository.onChnageUser(id, updateUserDto);
  }

  /**
   * @author Ryan
   * @description 전체 유저 수정
   *
   * @param updateUserDto 유저 정보
   *
   * @returns {Promise<boolean>} true
   */
  setAllUser(updateUserDto: UpdateUserDto[]): Promise<boolean> {
    return this.userRepository.onChnageUsers(updateUserDto);
  }

  /**
   * @author Ryan
   * @description 유저 삭제
   *
   * @param id
   * @returns {Promise<boolean>} true
   */
  deleteUser(id: string): Promise<boolean> {
    return this.userRepository.onDelete(id);
  }

  getHelloWorld(): string {
    return 'Hello World!!';
  }
}

@Service 부분은 특별한 로직이 없기 때문에 해석하기 쉬우실 꺼라 생각합니다.

 

- Repository CRUD 생성

TypeORM Repository API는 기본적인 CRUD 할 수 있는 메서드를 지원합니다.

 

Repository API에 대한 자세한 내용은 아래 공식 홈페이지 내용을 참고하시는 걸 추천드리겠습니다. (내용이... 너무 많아요 ㅋㅋ)

https://typeorm.io/#/repository-api

 

TypeORM - Amazing ORM for TypeScript and JavaScript (ES7, ES6, ES5). Supports MySQL, PostgreSQL, MariaDB, SQLite, MS SQL Server,

 

typeorm.io

 

소스 코드
import { NotFoundException } from '@nestjs/common';
import { CreateUserDto, UpdateUserDto } from 'src/user/dto/user.dto';
import { EntityRepository, Repository } from 'typeorm';
import { User } from '../entity/user.entity';

@EntityRepository(User)
export class UserRepository extends Repository<User> {
  //유저 생성
  async onCreate(createUserDto: CreateUserDto): Promise<boolean> {
    const { user_id, password, name, age } = createUserDto;

    const user = await this.save({
      user_id,
      password,
      salt: '임시',
      name,
      age,
    });

    return user ? true : false;
  }

  //모든 유저 조회
  async findAll(): Promise<User[]> {
    return await this.find();
  }

  //단일 유저 조회
  async findById(id: string): Promise<User> {
    const user = await this.findOne(id);

    if (!user) {
      throw new NotFoundException('유저를 찾을 수 없습니다.');
    }

    return user;
  }

  //단일 유저 수정
  async onChnageUser(
    id: string,
    updateUserDto: UpdateUserDto,
  ): Promise<boolean> {
    const { name, age } = updateUserDto;

    const chnageUser = await this.update({ id }, { name, age });

    if (chnageUser.affected !== 1) {
      throw new NotFoundException('유저가 존재하지 않습니다.');
    }

    return true;
  }

  //전체 유저 수정
  async onChnageUsers(updateUserDto: UpdateUserDto[]): Promise<boolean> {
    const user = updateUserDto.map((data) => {
      return this.update(data.id, { name: data.name, age: data.age });
    });

    await Promise.all(user);

    return true;
  }

  //유저 삭제
  async onDelete(id: string): Promise<boolean> {
    /**
     * remove() & delete()
     * - remove: 존재하지 않는 아이템을 삭제하면 404 Error가 발생합니다.
     * - delete: 해당 아이템이 존재 유무를 파악하고 존재하면 삭제하고, 없다면 아무 에러도 발생하지 않는다.
     */
    const deleteUser = await this.delete(id);

    if (deleteUser.affected === 0) {
      throw new NotFoundException('유저가 존재하지 않습니다.');
    }

    return true;
  }
}

 

TypeORM Repository는 Promise 객체입니다. 이 부분을 잊지 않으시고 사용하시면 어렵지 않으십니다.

 

Middleware 수정

마지막으로 기존에 사용했던 미들웨어 코드를 주석해서 모두 접근이 가능하게 설정합니다.

import {
  Injectable,
  NestMiddleware,
  UnauthorizedException,
} from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';

@Injectable()
export class AuthMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: NextFunction) {
    // // 논리합 연산자 -> 왼쪽 피연산자가 false라면 오른쪽 피연산자가 실행
    // const name: string = req.query.name || req.body.name;

    // if (name == 'Ryan') {
    //   next();
    // } else {
    //   // Ryan 유저가 아니라면 허가 받지 않은 유저이기 때문에 401 Error를 반환
    //   throw new UnauthorizedException();
    // }

    next();
  }
}

 

천천히 차근차근 실습을 해보시면 쉽게 이해할 수 있습니다. 혹시 이해가 안되는 부분이 있다면 댓글을 꼭 남겨주시면 감사하겠습니다.

 

소스 저장소

github: https://github.com/Ryan-Sin/Node_Nest/tree/v5

 

GitHub - Ryan-Sin/Node_Nest

Contribute to Ryan-Sin/Node_Nest development by creating an account on GitHub.

github.com