Nest.js - 상품 삭제 API

Temporary·2024년 8월 8일
0

Nods.js

목록 보기
28/39

CRUD API 구현

지금까지 API에서 Create, Read, Update를 만들어 봤다.

이제는 Delete를 만들어보려 한다.

Product Delete

10-04-typeorm-crud-validation 폴더를 복사 붙여넣기하여

폴더 이름을 10-05-typeorm-crud-soft-delete 로 변경하고

터미널을 해당 폴더로 이동해서 yarn install 을 입력해 필요한 모듈을 설치한다.

10-05-typeorm-crud-soft-deletesrcapisproductsproducts.resolver.ts 에 상품을 삭제하는 API deleteProduct를 추가한다.

// products.resolver.ts  

// ... 생략

@Mutation(() => Boolean)
  deleteProduct(
    @Args('productId') productId: string, //
  ): Promise<boolean> {
    return this.productsService.delete({ productId });
  }

// ... 생략
  • 수정을 할 때는 수정 결과를 통해 어떤 상품에서 어떤 부분이 수정이 되었는지 알려줄 수 있다. 따라서, @Mutation 의 return 값이 Product 였다. 하지만, 삭제를 하게 되면 DB에서 삭제되어 없어지므로 return 값을 Product로 가지고 올 수 없기 때문에 그 결과는 ‘삭제가 완료되었습니다' 의 String 이나 true, false 의 Boolean 값을 통해 알려줘야 한다.
  • updateProduct API와 동일하게 @Args를 사용해 productId 를 받아서 해당 productId 를 가진 product 를 삭제 할 것이다. 따라서 productId 는 삭제 조건이 된다.
  • productId 를 받아서 productService에 있는 delete 함수에 넘겨준다.
  • return : 비즈니스 로직으로부터 받은 결과를 프론트 또는 사용자에게 돌려준다.

srcapisproductsproducts.service.ts 파일에 delete 함수를 만들어서 아래와 같이 로직을 작성해준다.

// products.service.ts

// ... 생략

async delete({ productId }: IProductsServiceDelete): Promise<boolean> {
    // 1. 실제 삭제
    const result = await this.productsRepository.delete({ id: productId });
    return result.affected ? true : false;
  }

// ... 생략

넘겨받은 productId와 DB에 존재하는 product의 Id가 일치하는 데이터를 삭제해준다.

  • 삭제가 완료될 때까지 기다려줘야하기에 async ~ await를 사용한다.
  • 삭제의 결과는 객체로 나오는데, result 변수에 담아서 삭제가 제대로 이루어졌는지 확인하기 위해 .affected 를 사용했다.
    • 삭제가 이루어진 게 있다면 true 를 프론트로 리턴
    • 삭제가 이루어진 게 없다면 false 를 프론트로 리턴

srcapisproductsinterfacesproducts-service.interface.ts 파일에 IProductsServiceDelete를 만들어줍니다.

// products-service.interface.ts

// ...생략

// 추가
export interface IProductsServiceDelete {
  productId: string;
} 

지금까지 만든 deleteProduct API 를 확인해보자

yarn start:dev 로 서버를 실행하고

Dbeaver를 실행해서 삭제할 productId 를 복사한다.

이 테이블에서 컴퓨터 상품을 삭제해 보겠다.

http://localhost:3000/graphql 에 접속해 삭제 API를 요청한다.

  • 받는 결과가 객체가 아닌 Boolean 타입이기에 return 중괄호가 필요없다.
  • 삭제가 제대로 이루어져 true 가 반환되는 것을 확인할 수 있다.

Dbeaver 에서 확인해보면 컴퓨터 상품이 아예 사라진것을 볼 수 있다.



물리 삭제 & 논리 삭제

실무에서는 위의 삭제와 같이 데이터 베이스에서 데이터가 완전히 삭제되는 방법은 잘 사용하지 않는다.(물리 삭제)

삭제를 하고 보니 데이터를 잘못 삭제해서 복구하고 싶을 수도 있고, 어떤 걸 삭제했는지 다시 보고 싶을 수 있기 때문이다.

그래서 실무에서는 아예 데이터를 날려버리는 방법 대신, 삭제 여부를 나타내는 속성을 추가해 가짜로 삭제하는 방법을 많이 사용한다.

이를 soft delete (논리 삭제) 라고한다.

이런 방식으로 삭제를 하기 위해 typeORM에서 제공해주는 soft deletesoft remove 함수가 있다. 직접 구현해보자

논리삭제 1. isDeleted 컬럼을 추가하기

첫번째 방법은 productisDeleted 라는 속성을 추가로 만들어서, 삭제 여부를 알 수 있게 하는 것이다.

삭제된 데이터는 true 로 바뀌게 설정하여 데이터를 조회할 때는 isDeleted가 false인 것만 가져오면 된다.

즉, 데이터베이스에서 해당 데이터는 실제로 삭제된 것이 아니기때문에 그 데이터는 존재한 상태에서 isDeleted가 false 인 것만 데이터 조회 시 가져오게 되는 것이다.

srcapisproductentitiesproduct.entity.ts 파일에 isDeleted 컬럼을 추가해준다.

// product.entity.ts

// ...

@Entity()
@ObjectType()
export class Product {
  @PrimaryGeneratedColumn('uuid')
  @Field(() => String)
  id: string;

  // ... 생략

  // 추가된 부분 isDeleted
  @Column({ default: false })
  @Field(() => Boolean)
  isDeleted: boolean;

// ... 생략

}

처음에는 삭제가 되지 않은 상태이기에 {default : fasle} 를 사용하여 기본값을 isDeleted 가 false 상태로 둔다.

삭제 요청시, 데이터를 완전히 삭제하는 것이 아닌 isDeleted 가 true 상태로 update 되게 해당 API를 만들어준다.

srcapisproductproducts.service.ts 파일로 들어와서 직접 구현해보자

// products.service.ts

// ... 생략

async delete({ productId }: IProductsServiceDelete): Promise<boolean> {
    // 1. 실제 삭제
    // const result = await this.productsRepository.delete({ id: productId });
    // return result.affected ? true : false;

    // 2. 소프트 삭제(직접 구현) - isDeleted 추가한 부분
    this.productsRepository.update({ id: productId }, { isDeleted: true })

  }

delete 함수 실행 시 삭제할 productId를 찾아서 isDeletedtrue업데이트해준다.

이 때, update({ 조건 }, { 변경할 값 }) 로 작성해야한다.

  • .update 대신에 .save도 가능하다.
    • .update : 수정된 객체 자체를 받아오는 것이 아니라 affected와 row를 사용하여 수정된 정보를 받아올 수 있다.
      const updateInfo = await this.productsRepository.update({ id: productId }, { isDeleted: true });
      			updateInfo.affected
      			updateInfo.row
    • .save : 저장되는 객체 자체를 반환해 줄 수 있다.

yarn start:dev 로 서버를 실행하여 확인해보자

새로 키보드 상품을 2개 추가한 상태에서 삭제를 해보겠다.

기본값으로는 false가 들어가는데 mysql에서는 0으로 보인다.

여기서 키보드1 상품을 삭제하면 isDeleted 값이 0에서 1로 변경된다. (false → true)

따라서 데이터를 조회할 때는 isDeletedfalse인 것만 가져오면 된다.

논리삭제 2. deletedAt 컬럼 추가

1) 방법처럼 삭제를 하게되면 조회할 때 삭제가 되지 않은 데이터들만 가지고 올 수는 있다.

하지만, 데이터가 언제 삭제되었는지는 알 수 없는 상태다.

데이터는 삭제되었는지와 함께 언제 삭제 됐는지도 중요하기 때문에

삭제됐는지와 삭제된 시간을 동시에 알 수 있도록 deletedAt 컬럼을 만들어 삭제될 때의 시간이 기록되는 방법이다.

product.entity.ts 파일에 deletedAt 컬럼을 추가해준다.

// product.entity.ts

@Entity()
@ObjectType()
export class Product {
  @PrimaryGeneratedColumn('uuid')
  @Field(() => String)
  id: string;

  // ... 생략

  // @Column({ default: false })
  // @Field(() => Boolean)
  // isDeleted: boolean;

  // 추가된 부분 - deletedAt 
  @Column({ nullable: true })
  @Field(() => Date)
  deletedAt: Date;

  // ... 생략

}

isDeleted 컬럼과 동일하게 ture & false 로 데이터를 구분할 수 있다.

  • true 일 경우 : deletedAt의 데이터가 있는 경우(삭제된 날짜 데이터)

  • false 일 경우 : deletedAt의 데이터인 날짜가 없는 null 인 상태

기본값은 따로 정하지 않았기 때문에 초기값은 null 이다.

따라서 조회할 때는 deletedAtnull이 아닌 것만 찾아와야 한다.

products.service.ts 파일에서 delete 함수 실행 시 deletedAt 을 현재 시간으로 업데이트하도록 고쳐준다.

// products.service.ts

async delete({ productId }: IProductsServiceDelete): Promise<boolean> {
    // 1. 실제 삭제
    // const result = await this.productsRepository.delete({ id: productId });
    // return result.affected ? true : false;

    // 2. 소프트 삭제(직접 구현) - isDeleted
    // this.productsRepository.update({ id: productId }, { isDeleted: true })

    // 3. 소프트 삭제(직접 구현) - deletedAt
    this.productsRepository.update({ id: productId }, { deletedAt: new Date() });              
  }

삭제가 되면 현재 날짜가 DB에 저장된다.

  • deleteAt 컬럼이 비어있을 경우 : 삭제가 되지 않은 상태

  • deleteAt 컬럼에 날짜가 있을 경우 : 해당 날짜에 삭제가 된 상태

yarn start:dev 로 서버를 실행하여 확인해 보자

플레이그라운드에서 키보드 상품을 추가하면, deletedAt이 null 인 상태로 데이터가 추가된다.

플레이그라운드에서 deleteProduct를 요청한 후에 다시 확인해보면,

삭제했을 때의 시간이 저장되어 있다.

논리삭제 3. softRemove 함수 사용

코드의 간결성을 위해 typeORM에서 제공해주는 함수 중 softRemove를 사용해 보자

softRemove는 id를 가지고만 삭제가 가능하다.

products.service.ts 파일에서 softRemove로 삭제해 준다.

// products.service.ts

async delete({ productId }: IProductsServiceDelete): Promise<boolean> {
    // 1. 실제 삭제
    // const result = await this.productsRepository.delete({ id: productId });
    // return result.affected ? true : false;

    // 2. 소프트 삭제(직접 구현) - isDeleted
    // this.productsRepository.update({ id: productId }, { isDeleted: true })

    // 3. 소프트 삭제(직접 구현) - deletedAt
    // this.productsRepository.update({ id: productId }, { deletedAt: new Date() });

    // 4. 소프트 삭제(TypeORM 제공) - softRemove
    this.productsRepository.softRemove({ id: productId }); 
    // 단점: id로만 삭제 가능
    // 장점: 여러ID 한번에 지우기도 가능 => .softRemove([{id: qqq}, {id: aaa}])                        
  
}

product.entity.ts 파일에 DeleteDateColumn을 추가해 준다.

// product.entity.ts

@Entity()
@ObjectType()
export class Product {
  @PrimaryGeneratedColumn('uuid')
  @Field(() => String)
  id: string;

  // ...생략

  // @Column({ default: false })
  // @Field(() => Boolean)
  // isDeleted: boolean;

  // @Column({ nullable: true })
  // @Field(() => Date)
  // deletedAt: Date;
	
  // 추가된 부분 어노테이션이 @DeleteDateColumn()으로 설정되어 있다.
  @DeleteDateColumn()
  deletedAt: Date;

  // ...생략

}

@DeleteDateColumn 에서 옵션을 따로 적어주지 않으면 기본값으로 삭제된 시간을 datetime 타입으로 저장한다.

직접 구현했을 때와 다르게, 데이터를 조회할때 조건을 주지 않아도 삭제 되지 않은 데이터만 조회된다.

typeorm에는 DeleteDateColumn 외에도 자동으로 시간을 추가해주는 방법이 있다.

@CreateDateColumn() // 데이터 등록시 등록 시간 자동으로 추가
 createdAt: Date;

@UpdateDateColumn() // 데이터 수정시 수정 시간 자동으로 추가
 updatedAt: Date;

@DeleteDateColumn() // 소프트삭제 시간 기록을 위함
 deletedAt: Date;

직접 API로 테스트를 해보면,
상품을 createProduct로 추가하고, deleteProduct로 삭제하면 deletedAt에 삭제한 시간이 저장된다.

그리고 find로 별다른 조건을 주지 않고 조회해도, 삭제된 상품은 나오지 않는다.

findAll(): Promise<Product[]> {
    return this.productsRepository.find();
  }

논리삭제 4. softDelete 함수 사용

마찬가지로 typeORM에서 지원해주는 또다른 함수 softDelete를 사용해보자

softDelete는 id 뿐 아니라, 다른 해당 컬럼명을 가지고도 삭제가 가능하다.

entity 부분은 그대로 두고

products.service.ts 파일을 수정해준다.

// products.service.ts

async delete({ productId }: IProductsServiceDelete): Promise<boolean> {
    // 1. 실제 삭제
    // const result = await this.productsRepository.delete({ id: productId });
    // return result.affected ? true : false;

    // 2. 소프트 삭제(직접 구현) - isDeleted
    // this.productsRepository.update({ id: productId }, { isDeleted: true })

    // 3. 소프트 삭제(직접 구현) - deletedAt
    // this.productsRepository.update({ id: productId }, { deletedAt: new Date() });

    // 4. 소프트 삭제(TypeORM 제공) - softRemove
    // this.productsRepository.softRemove({ id: productId }); // 단점: id로만 삭제 가능
    //                                                        // 장점: 여러ID 한번에 지우기도 가능 => .softRemove([{id: qqq}, {id: aaa}])

    // 5. 소프트 삭제(TypeORM 제공) - softDelete
    const result2 = await this.productsRepository.softDelete({ id: productId }); // 장점: 다른 컬럼으로도 삭제 가능
    return result2.affected ? true : false; //       
    // 장점: 다른 컬럼으로도 삭제 가능
	// 단점: 여러ID 한번에 지우기 불가능
  }

softDeletesoftRemove와 다르게 여러 행(Row)을 삭제할 수 있다.

그래서 result에 affected 값이 들어있는데 이는 삭제된 행의 갯수가 숫자로 담겨있다.

yarn start:dev 로 서버를 실행하여 확인해 보자

셔츠를 삭제한다고 하고

http://localhost:3000/graphql 에 접속해 삭제 API를 요청해보자

Dbeaver 에서 확인해보면 셔츠 상품이 삭제되면서 deletedAt 컬럼에 삭제된 데이터가 생겼다.

따라서, 언제든지 삭제가 가능하고 복구도 가능하게 되었다.

상품을 조회해보면 아래와 같이 삭제된 셔츠 상품은 조회되지 않는것을 확인할 수 있다.

profile
Temporary Acoount

0개의 댓글