-
Notifications
You must be signed in to change notification settings - Fork 2
벌크 삭제: 퀴즈 서비스 삭제 기능 성능 최적화
퀴즈 서비스 삭제 기능 성능 최적화
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');
}
}
- 성능 측정 코드 구현
const startTime = performance.now();
// 삭제 로직 실행
const endTime = performance.now();
console.log(`삭제 실행 시간: ${endTime - startTime}ms`);
테스트 절차
- 각 시나리오별 테스트 데이터 생성
- 순차적 삭제와 벌크 삭제 각각 실행
- 실행 시간 기록 및 비교
검증 방법
- 실행 전후 데이터베이스 상태 확인
소규모 데이터 (2 퀴즈, 6 선택지)
- 벌크 삭제: 30.99ms
- 순차적 삭제: 28.85ms
- 성능 차이 미미
![image](https://private-user-images.githubusercontent.com/52828205/392086655-1ee9a83f-d8dc-4a50-854c-d169b2ecf995.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3Mzk2MzMxODYsIm5iZiI6MTczOTYzMjg4NiwicGF0aCI6Ii81MjgyODIwNS8zOTIwODY2NTUtMWVlOWE4M2YtZDhkYy00YTUwLTg1NGMtZDE2OWIyZWNmOTk1LnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTAyMTUlMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUwMjE1VDE1MjEyNlomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTNiNzY5ZDMwZTk5MzA2ZTc3NjRjYTk2Mjg5YzdlZTg2N2FjODIzYThlNDdhNzkwZmFlMDVhMDBjYjcwYTU4OWUmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.s4_s-y2CXyZLWI7V5DZrFJbEQF4ZAql2xWw9YYkz-Zg)
![image](https://private-user-images.githubusercontent.com/52828205/392086548-297ad07c-1612-44db-b511-a52d01ef1ae2.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3Mzk2MzMxODYsIm5iZiI6MTczOTYzMjg4NiwicGF0aCI6Ii81MjgyODIwNS8zOTIwODY1NDgtMjk3YWQwN2MtMTYxMi00NGRiLWI1MTEtYTUyZDAxZWYxYWUyLnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTAyMTUlMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUwMjE1VDE1MjEyNlomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTMxN2IzMDUzNDY5ZjU5OTliZTcyNzMyN2NhM2ViYWExZmQ5YmE3NDQwNjhlZTEyMTA5OGFiN2E0N2FiM2M2ZTUmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.JASE6uyIpCKLJoC4IolW2yxWnOQ1PoF8N653A8R0qDg)
이때만 해도 둘의 차이가 크지않다는 것을 알 수 있다.
하지만 벌크 삭제의 성능은 삭제할 데이터의 개수가 증가할수록 빛을 발한다.
소-중규모 데이터 (5 퀴즈, 8 선택지)
- 벌크 삭제: 32.48ms
- 순차적 삭제: 46.51ms
- 벌크 삭제가 1.43배 더 빠름
![image](https://private-user-images.githubusercontent.com/52828205/392085965-7a805b44-e436-405e-b2f5-bde4ec0fabc1.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3Mzk2MzMxODYsIm5iZiI6MTczOTYzMjg4NiwicGF0aCI6Ii81MjgyODIwNS8zOTIwODU5NjUtN2E4MDViNDQtZTQzNi00MDVlLWIyZjUtYmRlNGVjMGZhYmMxLnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTAyMTUlMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUwMjE1VDE1MjEyNlomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTliOTFkNzIyY2NlMzI5MjNjZDc3M2UzYzlhMDIzYjk4NmU3ZDY0M2FiZGZlMjAzYzk4YmI1NWYzMTA3Zjk5ODMmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.cXF4Uwo5Q49wL9jeg4vghwQzD5MlNOEIyClYS9hzUuw)
![image](https://private-user-images.githubusercontent.com/52828205/392085639-32379ca9-dbba-4516-9548-ee0afdda2901.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3Mzk2MzMxODYsIm5iZiI6MTczOTYzMjg4NiwicGF0aCI6Ii81MjgyODIwNS8zOTIwODU2MzktMzIzNzljYTktZGJiYS00NTE2LTk1NDgtZWUwYWZkZGEyOTAxLnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTAyMTUlMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUwMjE1VDE1MjEyNlomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTlmZjk0NjA1OTc5ZGRlZTFiOWY1YTNkZWFlMDAxMmEyNjNhYmNjNjI0ODI2MWQ1ZTQ3YjdhNjg0NDFlYWQzNjImWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.kO39YsdGadCGeuHwV7VYF0RsxPfDt20ARrVKcBbmlZQ)
중규모 데이터 (10 퀴즈, 20 선택지)
- 벌크 삭제: 37.02ms
- 순차적 삭제: 59.91ms
- 벌크 삭제가 1.62배 더 빠름
![image](https://private-user-images.githubusercontent.com/52828205/392086134-bf0a124e-f87e-4252-ad01-fb8aae3c42c6.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3Mzk2MzMxODYsIm5iZiI6MTczOTYzMjg4NiwicGF0aCI6Ii81MjgyODIwNS8zOTIwODYxMzQtYmYwYTEyNGUtZjg3ZS00MjUyLWFkMDEtZmI4YWFlM2M0MmM2LnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTAyMTUlMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUwMjE1VDE1MjEyNlomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTU2YmRlN2RlYmEzZWE5ZTU0YWY4MDEzMzM0NDYyZDZkMjE1NTVlMmNkZTdiNjlkODZlMjQ5OThiYjdmYWRkMTUmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.SA-FIjRp-Zi0k3KHSmr4ZY4Ez2zjZSAs6yPVbqK9pE4)
![image](https://private-user-images.githubusercontent.com/52828205/392086274-a613a067-9f4c-4a66-9172-1856243d9474.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3Mzk2MzMxODYsIm5iZiI6MTczOTYzMjg4NiwicGF0aCI6Ii81MjgyODIwNS8zOTIwODYyNzQtYTYxM2EwNjctOWY0Yy00YTY2LTkxNzItMTg1NjI0M2Q5NDc0LnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTAyMTUlMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUwMjE1VDE1MjEyNlomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPWQ2ZTI3YjQ2ZDRjMWY1N2M5MDAzNDAyYTllMGMwNjllNTdhNzI0MTg0NTEwMzgyY2RiYTc1MTdlYmJkYjcwZDkmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.PaPDn7_DM53Eyg0CEzMv8lB7osjFcZ5T1-4bmv6oYAo)
대규모 데이터 (15 퀴즈, 46 선택지)
- 벌크 삭제: 34.40ms
- 순차적 삭제: 77.14ms
- 벌크 삭제가 2.24배 더 빠름
![image](https://private-user-images.githubusercontent.com/52828205/392086447-d092c7fe-c6a5-4c2d-8232-90645902b68c.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3Mzk2MzMxODYsIm5iZiI6MTczOTYzMjg4NiwicGF0aCI6Ii81MjgyODIwNS8zOTIwODY0NDctZDA5MmM3ZmUtYzZhNS00YzJkLTgyMzItOTA2NDU5MDJiNjhjLnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTAyMTUlMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUwMjE1VDE1MjEyNlomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPWI3ZjUwNWJmZGNhODYwOWZmOWFjMDVmMWY1NDdiZDNjNTU3ZjhhMDc3YTc3NmJhYTY5NmM0Y2ZlMDQ2NDE5ZDEmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.wZ6s7LP-e9tHczigWT5Ms7b0O7VkfXyP4pu0wvd5u0g)
![image](https://private-user-images.githubusercontent.com/52828205/392086363-a6c73dee-9c58-4901-9d58-8703b0014f85.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3Mzk2MzMxODYsIm5iZiI6MTczOTYzMjg4NiwicGF0aCI6Ii81MjgyODIwNS8zOTIwODYzNjMtYTZjNzNkZWUtOWM1OC00OTAxLTlkNTgtODcwM2IwMDE0Zjg1LnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTAyMTUlMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUwMjE1VDE1MjEyNlomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTViZGNkZmI0YzRiZDc5YzQ4OGQ1NmZiNDhhOTJjMzMzYmM2ZDZmOWU3ZjJlNjU4NTA2YTY1NmM1ZTk4NjVjODImWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.WZe7v3XQo1ljxY3ACGFNlFLbsMVCr1ILWxjtCMusB_8)
데이터의 규모가 커질수록 성능의 차이가 커지는 것을 알 수 있다. 이러한 큰 차이는 이전에 말했던 디스크 I/O와 트랜잭션 시 롤백 문제를 상쇄하고도 남을 장점이라고 판단되었기에 벌크 삭제를 선택하게 되었다.
-
성능 최적화
- 대규모 데이터(15퀴즈, 46선택지) 기준 2.24배 성능 향상
- 데이터 증가에 따른 선형적 성능 저하 방지
- 실행 시간 예측 가능성 확보
-
코드 품질 개선
- 중첩 루프 제거로 복잡도 감소 (O(N*M) → O(1))
- 트랜잭션 처리 시간 단축
- 유지보수성 향상
-
리소스 관리
- 데이터베이스 커넥션 사용 시간 감소
- 메모리 사용량 최적화
- 시스템 리소스 효율적 활용
위에 나온 테스트는 로컬 환경에서 이루어진 단적인 경험이기에 서비스 운영 간의 실제로 이뤄지는 성능을 기록해야한다.
실제 서비스 단계에서 발생하는 문제점을 파악하고자 인프라에 추가적인 설정이 필요하다.
인덱스 전략 최적화
- quiz_id, class_id에 대한 복합 인덱스 검토
- 삭제 성능 추가 향상 기대
성능 모니터링 도입
- 실제 서비스 환경에서의 삭제 성능 추적
- 데이터 규모별 성능 지표 수집
장애 대응 체계
- 트랜잭션 실패 시 복구 전략 수립
- 롤백 상황 모니터링