Skip to content

벌크 삭제: 퀴즈 서비스 삭제 기능 성능 최적화

nowChae edited this page Dec 3, 2024 · 1 revision

🚀 도전 목표

퀴즈 서비스 삭제 기능 성능 최적화


💡 핵심 과정 및 결과

YouQuiz? 서비스에서 하나의 클래스(Class)는 다수의 퀴즈(Quiz)를 포함하고 있다.

  • 각 퀴즈는 여러 개의 선택지(Choice)를 보유
  • 계층 구조: Class(1) → Quiz(N) → Choice(M)

많은 퀴즈가 생성되는 만큼 많은 퀴즈도 삭제될 것이다.

  • 클래스 삭제 시 연관된 모든 퀴즈와 선택지도 함께 삭제 필요
  • 클래스 당 평균 10개 이상의 퀴즈와 각 퀴즈당 4개 이상의 선택지 예상
  • 향후 서비스 확장 시 데이터 증가로 인한 성능 저하 우려

따라서 클래스 삭제 기능의 성능을 파악하고, 개선사항을 트러블 슈팅해보았다.

퀴즈 서비스의 경우 한 게임 당 문제의 개수는 10개가 넘어가는 경우가 많아질 것이라 보았다.

삭제하는 방법은 다음과 같이 두 개로 나뉘어진다.

순차적 삭제 vs 벌크 삭제

그렇기에, 순차적 삭제의 성능과 벌크 삭제의 성능 분석이 필요했다.

벌크 삭제란?

데이터베이스에서 여러 레코드를 한 번의 쿼리로 삭제하는 방식이다. 기존의 순차적 삭제 방식과 달리, 서브쿼리를 활용하여 연관된 데이터를 한 번에 처리할 수 있다.

기능과 벌크 삭제의 적절성

벌크 삭제의 주요 단점으로는 대량의 로그 생성을 인한 디스크 I/O가 증가 될 수 있으며 트랜잭션 롤백 시 복구 비용이 증가한다는 문제가 있다. 그리고 여러 행을 잠그기 때문에 락(Lock) 경합 가능성이 증가한다.

하지만 우리 프로젝트의 삭제 단위는 클래스 별로 이루어지기 때문에 락 경합 가능성은 덜하다고 판단이 되었다. 물론 디스크 I/O와 트랜잭션 롤백 문제는 여전히 문제가 될 수 있다.

그럼에도 불구하고 추후에 서술할 성능 분석에 따른 결과가 이러한 단점을 상쇄하고도 벌크 삭제를 선택할 수 있었다.

순차적 삭제 코드

  async deleteWithRelations(classEntity: Class): Promise<void> {
    const queryRunner = this.dataSource.createQueryRunner();
    await queryRunner.connect();
    await queryRunner.startTransaction();

    try {
      if (classEntity.quizzes) {
        for (const quiz of classEntity.quizzes) {
          if (quiz.choices && quiz.choices.length > 0) {
            for (const choice of quiz.choices) {
              await queryRunner.manager.delete(Choice, {
                id: choice.id,
              });
            }
          }
        }
      }

      if (classEntity.quizzes && classEntity.quizzes.length > 0) {
        for (const quiz of classEntity.quizzes) {
          await queryRunner.manager.delete(Quiz, {
            id: quiz.id,
          });
        }
      }

      await queryRunner.manager.delete(Class, {
        id: classEntity.id,
      });

      await queryRunner.commitTransaction();
    } catch (error) {
      console.error('Failed to delete class with relations:', error);
      await queryRunner.rollbackTransaction();
      throw error;
    } finally {
      await queryRunner.release();
    }
  }

벌크 삭제 코드

async deleteWithRelations(id: number): Promise<void> {
  const queryRunner = this.dataSource.createQueryRunner();
  await queryRunner.connect();
  await queryRunner.startTransaction();

  try {
    await queryRunner.query(
      `DELETE FROM choice WHERE quiz_id IN (SELECT id FROM quiz WHERE class_id = ?)`,
      [id],
    );
    
    await queryRunner.query(`DELETE FROM quiz WHERE class_id = ?`, [id]);
    await queryRunner.query(`DELETE FROM class WHERE id = ?`, [id]);

    await queryRunner.commitTransaction();
  } catch (error) {
    await queryRunner.rollbackTransaction();
    throw new InternalServerErrorException('Failed to delete');
  }
}

성능 분석을 위한 테스트 진행

  1. 성능 측정 코드 구현
const startTime = performance.now();
// 삭제 로직 실행
const endTime = performance.now();
console.log(`삭제 실행 시간: ${endTime - startTime}ms`);

테스트 절차

  • 각 시나리오별 테스트 데이터 생성
  • 순차적 삭제와 벌크 삭제 각각 실행
  • 실행 시간 기록 및 비교

검증 방법

  • 실행 전후 데이터베이스 상태 확인

테스트 결과

소규모 데이터 (2 퀴즈, 6 선택지)

  • 벌크 삭제: 30.99ms
  • 순차적 삭제: 28.85ms
  • 성능 차이 미미
image image

이때만 해도 둘의 차이가 크지않다는 것을 알 수 있다.

하지만 벌크 삭제의 성능은 삭제할 데이터의 개수가 증가할수록 빛을 발한다.

소-중규모 데이터 (5 퀴즈, 8 선택지)

  • 벌크 삭제: 32.48ms
  • 순차적 삭제: 46.51ms
  • 벌크 삭제가 1.43배 더 빠름
image image

중규모 데이터 (10 퀴즈, 20 선택지)

  • 벌크 삭제: 37.02ms
  • 순차적 삭제: 59.91ms
  • 벌크 삭제가 1.62배 더 빠름
image image

대규모 데이터 (15 퀴즈, 46 선택지)

  • 벌크 삭제: 34.40ms
  • 순차적 삭제: 77.14ms
  • 벌크 삭제가 2.24배 더 빠름
image image

데이터의 규모가 커질수록 성능의 차이가 커지는 것을 알 수 있다. 이러한 큰 차이는 이전에 말했던 디스크 I/O와 트랜잭션 시 롤백 문제를 상쇄하고도 남을 장점이라고 판단되었기에 벌크 삭제를 선택하게 되었다.

성능 개선 내역

  • 성능 최적화
    • 대규모 데이터(15퀴즈, 46선택지) 기준 2.24배 성능 향상
    • 데이터 증가에 따른 선형적 성능 저하 방지
    • 실행 시간 예측 가능성 확보
  • 코드 품질 개선
    • 중첩 루프 제거로 복잡도 감소 (O(N*M) → O(1))
    • 트랜잭션 처리 시간 단축
    • 유지보수성 향상
  • 리소스 관리
    • 데이터베이스 커넥션 사용 시간 감소
    • 메모리 사용량 최적화
    • 시스템 리소스 효율적 활용

🔎 개선 사항

위에 나온 테스트는 로컬 환경에서 이루어진 단적인 경험이기에 서비스 운영 간의 실제로 이뤄지는 성능을 기록해야한다.

실제 서비스 단계에서 발생하는 문제점을 파악하고자 인프라에 추가적인 설정이 필요하다.

인덱스 전략 최적화

  • quiz_id, class_id에 대한 복합 인덱스 검토
  • 삭제 성능 추가 향상 기대

성능 모니터링 도입

  • 실제 서비스 환경에서의 삭제 성능 추적
  • 데이터 규모별 성능 지표 수집

장애 대응 체계

  • 트랜잭션 실패 시 복구 전략 수립
  • 롤백 상황 모니터링
Clone this wiki locally