애플리케이션에서 대량의 데이터를 한 번에 DB에 저장해야 하는 경우 발생한다는 가정!
단순 repository.save() 반복의 문제
- 각 save 호출마다 TypeORM 내부 처리 -> Entity 존재 여부 확인, 이벤트 리스너 실행 등 모두 작동
- 잦은 트랜잭션 커밋 -> DB 부하 증가
TypeORM 환경에서 대용량 Insert 작업을 효율적이고 빠르게 처리하는 다양한 전략을 알아보자..
일단 먼저,
비효율적인 방법: 하나씩 save() 호출하기 (Anti-Pattern)
가장 직관적이지만 가장 비효율적인 방법
// 안티패턴 예시
for (const item of largeDataArray) {
await userRepository.save(item); // 매우 비효율적!
}
제 프로젝트의 개인적인 로직이 들어가 있어 processedRows만 봐주시면 됩니다!
약 45000건을 저장하는 무려 2분이나 걸린다.
async insertUser(user: User): Promise<User> {
try {
return this.userRepository.save(user);
} catch (error) {
this.logger.error(`[insertUser] 오류 발생: ${error}`);
throw error;
}
}
효율적인 방법 1: repository.insert() 활용하기
- insert() 메서드의 특징:
- 엔티티 존재 여부 확인 안 함
- Cascade, Listener, Subscriber 등 대부분의 ORM 기능 비활성화
- 주어진 값 그대로 순수한 INSERT SQL 쿼리 생성 시도 -> 성능 향상
위의 코드에서 save 부분을 insert로 변경
2m 0.96s -> 1m 51.42s 로 단축
여전히 오래걸린다.
* bulkInsert 하는 법 => 저장할 데이터를 배열로 묶은 다음 insert 진행
const usersData = [
{ firstName: '한솔', lastName: '김', isActive: true },
{ firstName: '보람', lastName: '이', isActive: true },
{ firstName: '지훈', lastName: '박', isActive: false },
// ... 수천, 수만 개의 사용자 데이터가 있다고 가정 ...
{ firstName: '수현', lastName: '최', isActive: true },
];
console.log(`Attempting to insert ${usersData.length} users using repository.insert()...`);
const insertResult = await userRepository.insert(usersData);
DB 작업 시간이 무려 1m 51s -> 1.5s 로 단축
좀 더 많은 데이터에 대해서 생각해보자.
현재는 약 5만건의 데이터이지만 만약 50만건 500만건 이라면?
- 데이터베이스의 패킷 사이즈 제한 초과 가능성
- 애플리케이션/데이터베이스 메모리 부족 위험
- 매우 긴 트랜잭션으로 인한 문제 발생 가능성
해결책: 전체 데이터를 일정 크기(Chunk)로 나누어 순차적으로 Insert
적절한 Chunk 크기 선정의 중요성 (예: 500개 ~ 10000개, 환경에 따라 벤치마킹 필요)
const queryRunner = dataSource.createQueryRunner();
try {
await queryRunner.connect(); // 커넥션 연결
await queryRunner.startTransaction(); // 트랜잭션 시작
const chunkSize = 1000;
console.log(`Starting chunk insert process with chunk size ${chunkSize}...`);
for (let i = 0; i < largeDataArray.length; i += chunkSize) {
const chunk = largeDataArray.slice(i, i + chunkSize);
console.log(`Processing chunk ${i / chunkSize + 1}: inserting ${chunk.length} records...`);
await queryRunner.manager.insert(User, chunk);
console.log(`Chunk ${i / chunkSize + 1} inserted successfully.`);
}
await queryRunner.commitTransaction();
console.log('Transaction committed successfully.');
} catch (err) {
console.error('Error during chunk insert transaction:', err);
await queryRunner.rollbackTransaction();
console.log('Transaction rolled back due to error.');
} finally {
await queryRunner.release();
console.log('QueryRunner released.');
}
실제 chunking을 해서 데이터를 insert 하고 만약 도중에 오류가 난 다면 해당 chunk 부분을 rollback 하는 전략이다.
궁금한 부분 있으면 댓글 남겨주세요!
감사합니다.