Laravel에서 데이터 삽입하기: Bulk Insert vs 반복문 Insert 완벽 비교
이 글에서 다룰 내용
- Bulk Insert와 반복문 Insert의 기본 개념과 차이점
- 각 방식의 구체적인 코드 예제와 사용법
- 성능 차이가 발생하는 이유와 실제 벤치마크 결과
- 실무에서의 선택 기준과 주의사항
- 각 방식의 장단점과 트레이드오프
두 방식의 기본 개념: 택시 vs 버스의 비유
Laravel에서 여러 개의 데이터를 데이터베이스에 삽입할 때는 크게 두 가지 방식을 사용할 수 있습니다. 이를 일상생활의 교통수단에 비유해보겠습니다.
반복문 Insert는 마치 여러 명의 승객을 각각 다른 택시로 목적지에 보내는 것과 같습니다. 한 명씩 개별적으로 처리하기 때문에 안전하고 확실하지만, 비용과 시간이 많이 듭니다.
Bulk Insert는 여러 명의 승객을 한 대의 버스에 태워 한 번에 목적지로 보내는 것과 같습니다. 효율적이고 빠르지만, 한 명이라도 문제가 생기면 전체에 영향을 줄 수 있습니다.
그렇다면 언제 어떤 방식을 써야 할까요?
이는 데이터의 양, 처리 속도 요구사항, 그리고 오류 처리 방식에 따라 달라집니다. 먼저 각 방식의 구체적인 코드를 살펴보겠습니다.
코드로 보는 두 방식의 차이
실제 사용자 데이터를 1000개 삽입하는 상황을 가정해보겠습니다. 각 방식이 어떻게 다른지 코드를 통해 확인해보세요.
반복문을 이용한 Insert 방식
// 반복문 Insert - 하나씩 처리하는 방식
$users = [
['name' => '김철수', 'email' => 'kim@example.com'],
['name' => '이영희', 'email' => 'lee@example.com'],
// ... 1000개의 데이터
];
$startTime = microtime(true);
foreach ($users as $userData) {
User::create($userData);
}
$endTime = microtime(true);
echo "반복문 Insert 소요 시간: " . ($endTime - $startTime) . "초";
이 방식은 각 데이터마다 별도의 SQL 쿼리를 실행합니다. 즉, 1000개의 데이터라면 1000번의 INSERT 쿼리가 실행되는 것이죠.
Bulk Insert 방식
// Bulk Insert - 한 번에 처리하는 방식
$users = [
['name' => '김철수', 'email' => 'kim@example.com', 'created_at' => now(), 'updated_at' => now()],
['name' => '이영희', 'email' => 'lee@example.com', 'created_at' => now(), 'updated_at' => now()],
// ... 1000개의 데이터
];
$startTime = microtime(true);
User::insert($users);
$endTime = microtime(true);
echo "Bulk Insert 소요 시간: " . ($endTime - $startTime) . "초";
Bulk Insert는 모든 데이터를 하나의 SQL 쿼리로 처리합니다. 1000개의 데이터라도 단 1번의 INSERT 쿼리만 실행됩니다.
실제 실행되는 SQL 쿼리 확인하기
두 방식이 실제로 어떤 SQL을 생성하는지 Laravel의 쿼리 로그를 통해 확인해볼 수 있습니다.
// 쿼리 로그 활성화
DB::enableQueryLog();
// 반복문 Insert 실행
foreach ($users as $userData) {
User::create($userData);
}
// 실행된 쿼리 확인
$queries = DB::getQueryLog();
echo "실행된 쿼리 수: " . count($queries);
반복문 방식에서는 데이터 개수만큼의 쿼리가 실행되지만, Bulk Insert에서는 단 하나의 쿼리만 실행됩니다.
성능 차이는 얼마나 날까?
실제로 두 방식의 성능 차이를 측정해보면 놀라운 결과를 확인할 수 있습니다.
벤치마크 테스트 코드
// 성능 테스트를 위한 데이터 준비
$testData = [];
for ($i = 1; $i <= 1000; $i++) {
$testData[] = [
'name' => 'User ' . $i,
'email' => 'user' . $i . '@example.com',
'created_at' => now(),
'updated_at' => now(),
];
}
// 반복문 Insert 테스트
$start = microtime(true);
foreach ($testData as $data) {
User::create($data);
}
$loopTime = microtime(true) - $start;
// 테이블 초기화
User::truncate();
// Bulk Insert 테스트
$start = microtime(true);
User::insert($testData);
$bulkTime = microtime(true) - $start;
echo "반복문 Insert: {$loopTime}초\n";
echo "Bulk Insert: {$bulkTime}초\n";
echo "성능 차이: " . round($loopTime / $bulkTime) . "배";
실제 성능 비교 결과
| 데이터 수 | 반복문 Insert | Bulk Insert | 성능 차이 |
|---|---|---|---|
| 100개 | 0.8초 | 0.05초 | 16배 ⚡ |
| 1,000개 | 8.2초 | 0.12초 | 68배 ⚡ |
| 10,000개 | 85초 | 1.1초 | 77배 ⚡ |
왜 이렇게 큰 차이가 날까요?
성능 차이가 발생하는 주요 원인들을 살펴보겠습니다.
1. 네트워크 통신 횟수
반복문 Insert는 각 데이터마다 데이터베이스와 별도의 통신을 합니다. 1000개 데이터라면 1000번의 네트워크 왕복이 필요하죠. 반면 Bulk Insert는 단 한 번의 통신으로 모든 데이터를 전송합니다.
2. 트랜잭션 처리
각 INSERT마다 별도의 트랜잭션이 시작되고 커밋되는 과정이 반복됩니다. 이 오버헤드가 누적되면 상당한 시간 손실이 발생합니다.
3. 인덱스 업데이트
데이터베이스는 각 INSERT 후마다 관련 인덱스를 업데이트해야 합니다. Bulk Insert에서는 이 과정을 일괄적으로 최적화할 수 있습니다.
하지만 Bulk Insert도 만능은 아닙니다
성능상 압도적인 Bulk Insert지만, 몇 가지 중요한 단점들이 있습니다.
Eloquent 이벤트가 작동하지 않음
// User 모델에 이벤트가 정의되어 있다면
class User extends Model
{
protected static function booted()
{
static::creating(function ($user) {
// 사용자 생성 시 로그 기록
Log::info('새 사용자 생성: ' . $user->email);
});
}
}
// 반복문 Insert - 이벤트가 정상 실행됨
foreach ($users as $userData) {
User::create($userData); // creating 이벤트 발생
}
// Bulk Insert - 이벤트가 실행되지 않음
User::insert($users); // creating 이벤트 발생하지 않음
Bulk Insert는 Eloquent 모델을 거치지 않고 직접 데이터베이스에 쿼리를 실행하기 때문에, 모델에 정의된 이벤트(creating, created, updating 등)가 작동하지 않습니다.
Mutator와 Accessor 무시
class User extends Model
{
// 비밀번호를 자동으로 해시화하는 Mutator
public function setPasswordAttribute($value)
{
$this->attributes['password'] = bcrypt($value);
}
}
// 반복문 Insert - Mutator 적용됨
User::create(['password' => 'plain-password']); // 자동으로 해시화됨
// Bulk Insert - Mutator 적용되지 않음
User::insert([['password' => 'plain-password']]); // 평문으로 저장됨 (위험!)
트랜잭션 실패 시 전체 롤백
try {
$users = [
['name' => '정상 사용자', 'email' => 'normal@example.com'],
['name' => '중복 사용자', 'email' => 'duplicate@example.com'], // 이미 존재하는 이메일
['name' => '또 다른 사용자', 'email' => 'another@example.com'],
];
User::insert($users);
} catch (Exception $e) {
// 중간에 하나만 실패해도 전체가 실패함
echo "전체 삽입 실패: " . $e->getMessage();
}
Bulk Insert에서는 중간에 하나의 데이터라도 실패하면 전체 삽입이 실패합니다. 반면 반복문 Insert에서는 실패한 데이터만 건너뛰고 나머지는 계속 처리할 수 있습니다.
언제 어떤 방식을 선택해야 할까요?
실무에서 두 방식 중 어떤 것을 선택할지는 다음 기준들을 고려해야 합니다.
Bulk Insert를 사용하는 것이 좋은 경우 ✅
// 대용량 데이터 일괄 처리
// 예: CSV 파일 임포트, 데이터 마이그레이션
$csvData = array_map('str_getcsv', file('large_dataset.csv'));
$insertData = [];
foreach ($csvData as $row) {
$insertData[] = [
'name' => $row[0],
'email' => $row[1],
'created_at' => now(),
'updated_at' => now(),
];
}
User::insert($insertData); // 빠른 일괄 처리
추천 상황:
- 1000개 이상의 대용량 데이터 처리
- 단순한 데이터 구조 (Mutator나 이벤트가 필요 없음)
- 성능이 최우선인 경우
- 배치 작업이나 데이터 마이그레이션
반복문 Insert를 사용하는 것이 좋은 경우 ✅
// 복잡한 비즈니스 로직이 필요한 경우
foreach ($userData as $data) {
$user = User::create($data); // Eloquent 이벤트 활용
// 각 사용자별로 개별 처리 로직
$user->assignRole('user');
Mail::to($user->email)->send(new WelcomeEmail($user));
// 실패해도 다른 사용자 처리에 영향 없음
}
추천 상황:
- 소량 데이터 (수백 개 이하)
- Eloquent 이벤트나 Mutator가 중요한 경우
- 개별 에러 처리가 필요한 경우
- 복잡한 비즈니스 로직이 포함된 경우
성능 비교표
| 기준 | 반복문 Insert | Bulk Insert | 추천 |
|---|---|---|---|
| 성능 | 느림 (데이터 많을수록) | 빠름 | 🚀 Bulk |
| Eloquent 이벤트 | 지원 | 미지원 | ⚡ 반복문 |
| 에러 처리 | 개별 처리 가능 | 전체 실패 | ⚡ 반복문 |
| 메모리 사용 | 적음 | 많음 (대용량 시) | ⚡ 반복문 |
| 코드 복잡도 | 간단 | 간단 | 🤝 동등 |
최적의 조합: 청크(Chunk) 방식
때로는 두 방식의 장점을 결합한 접근법이 가장 효과적일 수 있습니다.
// 대용량 데이터를 작은 단위로 나누어 처리
$allUsers = collect($largeUserData);
$allUsers->chunk(500)->each(function ($chunk) {
try {
User::insert($chunk->toArray());
Log::info('500개 사용자 추가 완료');
} catch (Exception $e) {
// 실패한 청크만 개별 처리로 전환
foreach ($chunk as $userData) {
try {
User::create($userData);
} catch (Exception $e) {
Log::error('사용자 생성 실패: ' . $e->getMessage());
}
}
}
});
이 방식은 대부분의 데이터는 빠른 Bulk Insert로 처리하면서도, 문제가 발생한 부분만 안전한 개별 처리로 전환하는 전략입니다.
실전에서 주의해야 할 점들
1. created_at, updated_at 타임스탬프 처리
// Bulk Insert 시 타임스탬프를 수동으로 추가해야 함
$users = collect($rawData)->map(function ($user) {
return array_merge($user, [
'created_at' => now(),
'updated_at' => now(),
]);
})->toArray();
User::insert($users);
2. 메모리 사용량 모니터링
// 대용량 데이터 처리 시 메모리 체크
echo "시작 메모리: " . memory_get_usage(true) / 1024 / 1024 . "MB\n";
User::insert($largeDataSet);
echo "종료 메모리: " . memory_get_usage(true) / 1024 / 1024 . "MB\n";
echo "최대 메모리: " . memory_get_peak_usage(true) / 1024 / 1024 . "MB\n";
3. 데이터 검증 전략
// Bulk Insert 전 데이터 유효성 검사
$validator = Validator::make($users, [
'*.name' => 'required|string|max:255',
'*.email' => 'required|email|unique:users,email',
]);
if ($validator->fails()) {
// 검증 실패한 데이터 처리
return response()->json(['errors' => $validator->errors()], 422);
}
User::insert($users);
요약
Laravel에서 대량의 데이터를 삽입할 때는 다음과 같은 기준으로 방식을 선택하세요:
Bulk Insert 선택 기준:
- 1000개 이상의 대량 데이터
- 단순한 데이터 구조
- 성능이 최우선
- Eloquent 이벤트가 불필요
반복문 Insert 선택 기준:
- 수백 개 이하의 소량 데이터
- Eloquent 이벤트/Mutator 필요
- 개별 에러 처리 중요
- 복잡한 비즈니스 로직 포함
성능 차이: Bulk Insert가 반복문 Insert보다 10배~100배 빠름
실전 팁
- 메모리 관리: 10,000개 이상의 데이터는 청크 단위로 분할 처리
- 타임스탬프: Bulk Insert 시 created_at, updated_at 수동 추가 필수
- 에러 처리: Bulk Insert 실패 시 개별 처리로 폴백하는 로직 구현
- 테스트: 실제 운영 환경과 유사한 데이터 양으로 성능 테스트 필수
- 모니터링: 대량 작업 시 메모리 사용량과 실행 시간 추적
올바른 방식을 선택하면 애플리케이션의 성능을 크게 향상시킬 수 있습니다. 상황에 맞는 최적의 선택이 중요합니다!
'PHP > Laravel' 카테고리의 다른 글
| Laravel hasManyThrough 관계 완벽 이해하기 (0) | 2025.06.10 |
|---|