티스토리 뷰

728x90

안녕하세요,

이번 포스팅에서는 NestJS 개발 중 알게 된 스냅샷과 히스토리 개념에 대해 이야기해보려고 합니다.

NestJS 개발 커뮤니티에서 유명한 '삼촌님'의 깃허브 레포지토리를 참고하면서 스냅샷 방식을 도입하게 되었습니다.

이전에는 히스토리성 로그 방식의 테이블을 사용했는데요,

이번 글에서는 두 방식의 차이점과 각각의 특징, 그리고 도입 예제에 대해 알아보겠습니다.

 

스냅샷과 히스토리성 테이블의 차이점


데이터 저장 방식

  • 히스토리 테이블:  변경된 내용만 순차적으로 기록합니다.
               "어제 뭐 했더라..." (매일매일 일기 쓰는 느낌)
  • 스냅샷: 특정 시점의 전체 상태를 저장합니다.
               "자, 여기 내 전재산!" (인생 통장 사진 찍기)

조회 성능

  • 히스토리 테이블: 최신 상태를 얻기 위해 여러 레코드를 조회하고 계산해야 합니다.
  • 스냅샷: 최신 스냅샷만 조회하면 되어 빠른 조회가 가능합니다.

저장 공간

  • 히스토리 테이블: 변경 사항만 저장하여 공간 효율적입니다.
  • 스냅샷: 전체 상태를 저장하므로 더 많은 공간이 필요합니다.

복원 용이성

  • 히스토리 테이블: 특정 시점으로의 복원이 복잡할 수 있습니다.
  • 스냅샷: 원하는 시점의 스냅샷을 선택하여 쉽게 복원이 가능합니다.

구현 복잡도

  • 히스토리 테이블: 변경 추적 로직이 복잡할 수 있습니다.
  • 스냅샷: 상대적으로 단순한 구현입니다.

 

다음은 삼촌님의 ERD 설계도 입니다.

다음 링크를 클릭하시면 상세히 보실 수 있습니다.

https://github.com/samchon/bbs-backend/blob/master/docs/ERD.md

 

bbs-backend/docs/ERD.md at master · samchon/bbs-backend

Simple Bullet-in Board System Backend. Contribute to samchon/bbs-backend development by creating an account on GitHub.

github.com

 

 

위 스냅샷 예제를 기반으로 prisma와 nestjs Repository는 어떻게 만들어야하는지 알아보겠습니다.

Prisma 예시 코드


model OnboardingGoal {
  id         String                   @id @default(uuid())
  user       User                     @relation(fields: [userId], references: [id])
  userId     String                   @unique @map("user_id")
  createdAt  DateTime                 @default(now()) @map("created_at") @db.Timestamptz(3)
  updatedAt  DateTime                 @updatedAt @map("updated_at") @db.Timestamptz(3)
  snapshots  OnboardingGoalSnapShot[] @relation("snapshots")
  snapshot   OnboardingGoalSnapShot?  @relation(fields: [snapshotId], references: [id])
  snapshotId String?

  @@map("onboarding_goal")
}

model OnboardingGoalSnapShot {
  id               String           @id @default(uuid())
  onBoardingGoal   OnboardingGoal   @relation("snapshots", fields: [onBoardingGoalId], references: [id])
  onBoardingGoalId String           @map("onboarding_goal_id")
  goalStartDate    DateTime         @map("goal_start_date") @db.Timestamptz(3)
  goalEndDate      DateTime         @map("goal_end_date") @db.Timestamptz(3)
  goalHours        Int              @map("goal_hours")
  goalSteps        Int              @map("goal_steps")
  createdAt        DateTime         @default(now()) @map("created_at") @db.Timestamptz(3)
  updatedAt        DateTime         @updatedAt @map("updated_at") @db.Timestamptz(3)
  OnboardingGoal   OnboardingGoal[]

  @@map("onboarding_goal_snapshot")
}

Repository 생성 예시 코드


export const createGoal = async ({
    goalEndDate,
    goalSteps,
    goalStartDate,
    goalHours,
    userId,
    tx,
  }: {
    goalEndDate: Date;
    goalSteps: number;
    goalStartDate: Date;
    goalHours: number;
  } & GlobalTypes.PrismaTransactionParams &
    GlobalTypes.UserIdParam) => {
    const goal = await getPrismaDB(tx).onboardingGoal.create({
      data: {
        userId,
        snapshots: {
          create: {
            goalEndDate,
            goalSteps,
            goalStartDate,
            goalHours,
          },
        },
      },
    });

    const snapshot = await getPrismaDB(tx).onboardingGoalSnapShot.findFirst({
      where: {
        onBoardingGoalId: goal.id,
      },
    });

    await getPrismaDB(tx).onboardingGoal.update({
      where: {
        id: goal.id,
      },
      data: {
        snapshotId: snapshot?.id,
      },
    });
    return goal;
  };

스냅샷 방식 도입 후 경험

스냅샷을 도입하면서 초기에는 원본 테이블과 스냅샷 테이블의 ID를 연결하는 과정에서 어려움을 겪었습니다.

그러나 Prisma Schema에서 snapshotId를 nullable하게 설정하여 문제를 해결했습니다.

서버 비용이 DB 비용보다 저렴합니다.

서버에서 UUID를 생성하여 주입하면 DB에서 UUID를 생성하는 비용을 줄이고 로직을 더 간단하게 처리할 수 있습니다.

수정된 Repository 예시 코드


import { v4 as uuidv4 } from 'uuid';

.
.
.
<생략>

    const uuid = uuidv4();
    const goal = await getPrismaDB(tx).onboardingGoal.create({
      data: {
        id: uuid,
        userId,
        snapshots: {
          create: {
            goalEndDate,
            goalSteps,
            goalStartDate,
            goalHours,
          },
        },
      },
    });

    await getPrismaDB(tx).onboardingGoal.update({
      where: {
        id: goal.id,
      },
      data: {
        snapshotId: uuid,
      },
    });
    return goal;
  };​
 
 
 

스냅샷 방식을 도입한 후 실시간 데이터 분석이나 상태 복원이 필요한 상황에서 큰 도움을 받았습니다.

그러나 모든 상황에 적합한 방식은 아니므로, 프로젝트의 특성과 요구사항을 잘 고려하여 선택하는 것이 중요합니다.

이 글이 NestJS 개발을 하시는 분들께 조금이나마 도움이 되었으면 좋겠습니다.

질문이나 의견이 있으시면 댓글로 남겨주세요. 함께 고민하고 배우는 과정이 되었으면 합니다.

감사합니다!(좋아요는 큰 힘이 됩니다)

 

 

참고 자료

https://github.com/samchon/bbs-backend

https://github.com/samchon/bbs-backend/blob/master/docs/ERD.md

 

bbs-backend/docs/ERD.md at master · samchon/bbs-backend

Simple Bullet-in Board System Backend. Contribute to samchon/bbs-backend development by creating an account on GitHub.

github.com

 

 

GitHub - samchon/bbs-backend: Simple Bullet-in Board System Backend

Simple Bullet-in Board System Backend. Contribute to samchon/bbs-backend development by creating an account on GitHub.

github.com

 

728x90
댓글
250x250
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday