TS/Nest.js

[Nest] : Scheduler ๋ฅผ ์‚ฌ์šฉํ•œ SoftDeleteData ์™„์ „ ์‚ญ์ œ

๊ถŒ์Šพํ–„ 2023. 8. 23. 17:48

 

Reference 1
Reference 2
Reference 3
Reference 4
Reference 5
Reference 6

 

๊ตฌํ˜„ํ•˜๊ณ ์ž ํ•˜๋Š” ๊ธฐ๋Šฅ

nest.js/typeORM ์˜ softRemove ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜๊ณ , ํ•ด๋‹น ๋ฐ์ดํ„ฐ์˜ deletedAt ์ปฌ๋Ÿผ์ด ํ˜„์žฌ ์‹œ๊ฐ„ ๊ธฐ์ค€ ํŠน์ • ๊ธฐ๊ฐ„์ด ์ง€๋‚œ ๋ฐ์ดํ„ฐ๋ผ๋ฉด ์„œ๋ฒ„์—์„œ ์ž๋™์œผ๋กœ ํ•ด๋‹น ๋ฐ์ดํ„ฐ๋ฅผ DB์—์„œ ์˜๊ตฌ์‚ญ์ œํ•ด์ฃผ๋Š” ๊ธฐ๋Šฅ

 

ํ•„์š” ํŒจํ‚ค์ง€

npm install --save @nestjs/schedule

๋„ค์ŠคํŠธ์˜ ์Šค์ผ€์ฅด๋Ÿฌ ํŒจํ‚ค์ง€๊ฐ€ ํ•„์š”ํ•˜๋‹ค.

 

ํ˜„์žฌ ์†Œํ”„ํŠธ ๋ฆฌ๋ฌด๋ธŒ ์‹œํ‚ค๋Š” ๋กœ์ง

  // ์ด๋ ฅ์„œ - ์‚ญ์ œ
  async removeResume(resumeId: number): Promise<object> {
    // ์‚ญ์ œํ•  ์ด๋ ฅ์„œ ํ™•์ธ
    const resume = await this.resumeRepository.findOne({
      where: { id: resumeId },
    });
    // ์˜ˆ์™ธ์ฒ˜๋ฆฌ
    if (!resume) {
      throw new HttpException('Not found resume', HttpStatus.NOT_FOUND);
    }
    // SOFT_REMOVED ์ด๋ ฅ์„œ
    const deletedResume = await this.resumeRepository.softRemove(resume);
    // ์˜ˆ์™ธ์ฒ˜๋ฆฌ
    if (!deletedResume) {
      throw new HttpException('์‚ญ์ œ์— ์‹คํŒจํ•˜์˜€์Šต๋‹ˆ๋‹ค.', HttpStatus.BAD_REQUEST);
    }

    // ๋ฐ˜ํ™˜๊ฐ’
    return { message: `${deletedResume.title} ์ด๋ ฅ์„œ๊ฐ€ ์‚ญ์ œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.` };
  }

์˜ˆ์™ธ์ฒ˜๋ฆฌ๋ฅผ ์ œ์™ธํ•œ๋‹ค๋ฉด, ์‚ญ์ œํ•  ์ด๋ ฅ์„œ๋ฅผ ๊ธ์–ด์™€์„œ softRemove ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•ด์„œ null ๊ฐ’์ด deletedAt ์— Date ํƒ€์ž…์„ ๋„ฃ์–ด์ค€๋‹ค.

 

import Cron

import { Cron } from '@nestjs/schedule';

๋”ฐ๋กœ ์ž…๋ ฅ์„ ํ•ด์ฃผ์–ด๋„ ๋˜๊ณ , ์ฝ”๋“œ ์ž‘์—…์„ ํ•˜๋ฉด์„œ mac ๊ธฐ์ค€ cmd+. ์„ ๋ˆ„๋ฅด๋ฉด TS ๊ฒฝ์šฐ ์ž๋™์œผ๋กœ ํ•ด๋‹น ํ‚ค์›Œ๋“œ๋ฅผ import ํ•ด์ค€๋‹ค.

 

Cron ์‚ฌ์šฉํ•˜๊ธฐ

  // ์†Œํ”„ํŠธ๋ฆฌ๋ฌด๋ธŒ ์‹œํ‚จ ์ด๋ ฅ์„œ๊ฐ€ ์‹œ๊ฐ„์ด ์ง€๋‚˜๋ฉด ์ž๋™์œผ๋กœ ์‚ญ์ œ ํ•˜๋Š” ๋กœ์ง
  @Cron('0 0 * * *') // ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์—์„œ๋Š” ํ•ด๋‹น ์ฝ”๋“œ
  // @Cron('*/10 * * * * *') // ๊ฐœ๋ฐœํ™˜๊ฒฝ์—์„œ๋Š” 10์ดˆ๋งˆ๋‹ค ํ•ด๋‹น ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ ์‹คํ–‰์œผ๋กœ ์„ค์ •ํ•ด์„œ ์ฝ”๋“œ ์ž‘์—… ํ•˜์‹œ์ฅฌ
  async cleanupResumes() {
    // Date ํƒ€์ž…์˜ ๋ฐ์ดํ„ฐ๋ฅผ ๋‹ด๊ณ 
    const oneDayAgo = new Date();
    // ๋‹ด์€ ๋ฐ์ดํ„ฐ์—์„œ 1์ผ์„ ๋บ€ ๋ฐ์ดํ„ฐ๋ฅผ ์„ธํŒ…
    oneDayAgo.setDate(oneDayAgo.getDate() - 1);
    // 1์ผ ์ด์ „์— ์†Œํ”„ํŠธ๋ฆฌ๋ฌด๋ธŒ ์‹œํ‚จ ์ด๋ ฅ์„œ ์‚ญ์ œ
    const cleanupTarget = await this.resumeRepository.find({
      withDeleted: true,
    });
    // ์™„์ „ ์‚ญ์ œ์‹œํ‚ฌ cleanupTarget list์—์„œ ํ•˜๋‚˜ํ•˜๋‚˜๋ฅผ DB์—์„œ ์‚ญ์ œํ•ด์ค€๋‹ค.
    for (const resume of cleanupTarget) {
      // ๋ฝ‘์•„์˜จ ๋ฐ์ดํ„ฐ์—์„œ ์‚ญ์ œํ•  ๋ฐ์ดํ„ฐ๋“ค ์กฐ๊ฑด ๋งŒ๋“ค๊ธฐ
      if (resume.deletedAt <= oneDayAgo && resume.deletedAt !== null) {
        // db์—์„œ ์‚ญ์ œ ==> delete ์‚ฌ์šฉ์‹œ ์‚ญ์ œ ์•ˆ๋จ !!
        await this.resumeRepository.remove(resume);
      }
    }
  }

์ฒ˜์Œ ์‚ฌ์šฉํ•ด๋ณด๋Š” ๋กœ์ง์ด๋ผ์„œ ํ•˜๋‚˜ํ•˜๋‚˜ ์ฃผ์„์„ ๋‹ฌ์•˜๋‹ค.

 

์ฝ”๋“œ ํ•ด์„

Cron์„ ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ๋ฅผ ์‚ฌ์šฉํ•ด์„œ ๊ฐ€์ ธ์˜ค๊ณ  ์˜ต์…˜์„ ์žก์•„์ค€๋‹ค.

์ด 6๊ฐœ์˜ ์˜ต์…˜์ž๋ฆฌ๋ฅผ ์„ค์ •ํ•  ์ˆ˜ ์žˆ๊ณ , ์™ผ์ชฝ๋ถ€ํ„ฐ "์ดˆ ๋ถ„ ์‹œ ์ผ ์›” ์š”์ผ" ์ด๋ผ๊ณ  ์•Œ๊ณ  ์žˆ๋‹ค.

2๋ฒˆ ์ฝ”๋“œ์˜ ์ฒซ๋ฒˆ์งธ ์˜ต์…˜์ž๋ฆฌ์— */10์€ "์ดˆ"์ž๋ฆฌ์— ๋“ค์–ด๊ฐ€๋Š” ์˜ต์…˜์ด๊ณ  10์ดˆ ๊ฐ„๊ฒฉ์œผ๋กœ ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ ์ดํ•˜์˜ ๋กœ์ง์ด ์‹คํ–‰๋œ๋‹ค๋Š” ๋œป์ด๋‹ค.

  @Cron('0 0 * * *') // ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์—์„œ๋Š” ํ•ด๋‹น ์ฝ”๋“œ
  // @Cron('*/10 * * * * *') // ๊ฐœ๋ฐœํ™˜๊ฒฝ์—์„œ๋Š” 10์ดˆ๋งˆ๋‹ค ํ•ด๋‹น ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ ์‹คํ–‰์œผ๋กœ ์„ค์ •ํ•ด์„œ ์ฝ”๋“œ ์ž‘์—… ํ•˜์‹œ์ฅฌ

 

์ด ์ฝ”๋“œ๋Š” ํ˜„์žฌ์˜ ์‹œ๊ฐ„์„ ๋‹ด์€ ์‹๋ณ„์ž์ธ oneDayAgo์— ์ƒˆ๋กœ์šด ๋ฐ์ดํ„ฐ๋ฅผ setํ•ด์ฃผ๋Š” ๊ณผ์ •์ด๊ณ  setํ•˜๋Š” ๋ฐ์ดํ„ฐ๋Š” ํ˜„์žฌ ๋‚ ์งœ๋ฐ์ดํ„ฐ์—์„œ 1์ผ์„ ๋นผ์ฃผ๋Š” ๋กœ์ง์ด๋‹ค.

 oneDayAgo.setDate(oneDayAgo.getDate() - 1);

 

๊ฒฐ๊ณผ์ ์œผ๋กœ ํ˜„์žฌ ๋‚ ์งœ๋ณด๋‹ค 1์ผ์ด ์ง€๋‚œ ๋‚ ์งœ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ง„ oneDayAgo๋ฅผ ๊ฐ€์ง€๊ณ  DB์—์„œ softRemove๋œ ๋ฐ์ดํ„ฐ๋ฅผ ํ•˜๋‚˜ํ•˜๋‚˜ ๋ฐ˜๋ณตํ•˜๋ฉด์„œ deletedAt ๊ฐ’์ด oneDayAgo ๋ณด๋‹ค ์ด์ „์˜ ๋‚ ์งœ๋ผ๋ฉด ์‚ญ์ œ๋ฅผ ํ•˜๋Š” ๋กœ์ง์„ ๊ตฌํ˜„ํ•œ๋‹ค.

    for (const resume of cleanupTarget) {
      // ๋ฝ‘์•„์˜จ ๋ฐ์ดํ„ฐ์—์„œ ์‚ญ์ œํ•  ๋ฐ์ดํ„ฐ๋“ค ์กฐ๊ฑด ๋งŒ๋“ค๊ธฐ
      if (resume.deletedAt <= oneDayAgo && resume.deletedAt !== null) {
        // db์—์„œ ์‚ญ์ œ ==> delete ์‚ฌ์šฉ์‹œ ์‚ญ์ œ ์•ˆ๋จ !!
        await this.resumeRepository.remove(resume);
      }
    }

 

 

๊ฐ€์žฅ ์• ๋จน์€ ๋ถ€๋ถ„

๋ฐ”๋กœ ์ด ๋ถ€๋ถ„์ด๋‹ค.

์ด์œ ๋Š” deletedAt์— null์ด ์•„๋‹Œ Dateํƒ€์ž…์ด ๋‹ด๊ฒจ์žˆ๋Š” ์• ๋“ค์€ ์ด๋ฏธ ์‚ญ์ œ๊ฐ€ ๋˜์—ˆ๋‹ค๋Š” ๋ช…๋ชฉ์•„๋ž˜ ์žˆ๋Š” ์นœ๊ตฌ๋“ค์ด๊ณ , ๋”ฐ๋ผ์„œ ๊ธฐ์กด์˜ ๋ฐฉ์‹๋Œ€๋กœ DB์—์„œ ์ด ์นœ๊ตฌ๋“ค์„ ์กฐํšŒํ•˜๋ ค๊ณ  ํ•˜๋ฉด ์กฐํšŒ๊ฐ€ ๋˜์ง€ ์•Š๋Š”๋‹ค.

๋‚˜๋Š” new Date() ์ƒ์„ฑ์ž๋ฅผ ์‚ฌ์šฉํ•ด์„œ ํ• ๋‹นํ•œ Date๊ฐ’(= ISO 8601) ๊ณผ ํ˜„์žฌ deletedAt์— ๋“ค์–ด์žˆ๋Š” ๋ฐ์ดํ„ฐ ํƒ€์ž…(GMT)์ด ๋‹ฌ๋ผ์„œ ์กฐํšŒ๊ฐ€ ์•ˆ๋˜๋Š” ์ค„ ์•Œ์•˜๋‹ค.

๊ทธ๋ž˜์„œ ์ด ๊ธ€ ์ œ์ผ ์œ—์ค„์— ์ฐธ๊ณ ๋ฌธ์„œ์˜ ์ ˆ๋ฐ˜ ์ด์ƒ์ด Date ํƒ€์ž… ๋ณ€ํ™˜์‹œํ‚ค๋Š” ๋ฌธ์„œ์ธ ์ด์œ ๊ฐ€ ๊ทธ ๋•Œ๋ฌธ์ด๋‹ค.

๊ทธ๋ž˜์„œ LessThan / Not(IsNull()) ์•„๋ฌด๋ฆฌ ์ด๋Ÿฐ ๋ฉ”์„œ๋“œ๋ฅผ ๊ฐ–๋‹ค ๋ถ™ํ˜€๋„ ์กฐํšŒ๊ฐ€ ์•ˆ๋˜๋Š” ์ค„ ์•Œ์•˜์œผ๋‚˜,

์œ„ ๋ฉ”์„œ๋“œ๋ฅผ createdAt์— ์ ์šฉ์„ ํ•ด์„œ ๋ฐ์ดํ„ฐ๋ฅผ ์กฐํšŒํ•ด๋ณด๋‹ˆ ํ˜„์žฌ ์‚ด์•„์žˆ๋Š” ์ด๋ ฅ์„œ๋Š” ์กฐํšŒ๊ฐ€ ๋˜๋Š” ๊ฒƒ์ด๋‹ค.

    const cleanupTarget = await this.resumeRepository.find({
      withDeleted: true,
    });

 

๊ทธ๋ž˜์„œ ์ด๋ฏธ ํ•œ ๋ฒˆ softRemove๊ฐ€ ๋œ ๋…€์„๋“ค์„ ์กฐํšŒํ•˜๋ ค๋ฉด ๋‹ค๋ฅธ ํ‚ค์›Œ๋“œ๊ฐ€ ํ•„์š”ํ•˜๋‹ค๋Š” ๊ฒƒ์„ ์•Œ๊ฒŒ ๋˜์—ˆ๊ณ , ํ•ด๋‹น ์˜ต์…˜์„ ์ฐพ์•„๋‚ด๋ฉด์„œ ์ •์ƒ์ ์œผ๋กœ ๋‚ด๊ฐ€ ์ƒ๊ฐํ–ˆ๋˜ ๋Œ€๋กœ ๋กœ์ง์„ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์—ˆ๋‹ค.

 

๋งˆ์ง€๋ง‰ ์ž‘์„ฑ ์ฝ”๋“œ ์ตœ์ƒ์œ„ ๋ชจ๋“ˆ ์ ์šฉํ•˜๊ธฐ

// app.module.ts

import { ScheduleModule } from '@nestjs/schedule';

@Module({
  imports: [
    ScheduleModule.forRoot(), // ์š” ๋…€์„์ด ๊ผญ ํ•„์š”
...
  ],
  controllers: [AppController],
  providers: [AppService, ChatGateway],
})

export class AppModule {}

์ด๋ ‡๊ฒŒ ๋˜๋ฉด ์ •์ƒ์ ์œผ๋กœ ์Šค์ผ€์ฅด๋Ÿฌ๊ฐ€ ์ž‘๋™๋  ๊ฒƒ์ด๋‹ค.

 

#nest #TS #typeORM #schedule #Cron #๋ฐ์ฝ”๋ ˆ์ดํ„ฐ