Batch Apex란?
Salesforce에서 대용량 데이터를 효율적으로 처리해야 할 경우, 일반적인 트리거나 동기화된 코드로는 Governor Limits에 쉽게 도달하게 됩니다. 이를 해결하기 위해 Salesforce는 Database.Batchable 인터페이스를 제공하며, 이를 구현한 클래스를 통해 비동기적으로 데이터를 청크 단위로 나누어 처리할 수 있습니다.
다음은 기본적인 배치 클래스의 구조입니다:
public class AccountDataProcessor implements Database.Batchable<SObject> {
public Database.QueryLocator start(Database.BatchableContext context) {
String soql = 'SELECT Id, Name FROM Account WHERE CreatedDate = TODAY';
return Database.getQueryLocator(soql);
}
public void execute(Database.BatchableContext context, List<Account> records) {
for (Account acc : records) {
acc.Name = acc.Name + ' - Updated';
}
update records;
}
public void finish(Database.BatchableContext context) {
// 후속 작업 또는 알림 처리
}
}
- 클래스는 반드시 public 또는 global 접근 제어자를 가져야 합니다.
- 세 가지 메서드:
start(),execute(),finish()를 반드시 구현해야 합니다.
1. start() 메서드
배치 작업의 시작점으로, 처리할 레코드 집합을 반환합니다. 주로 SOQL 쿼리를 기반으로 Database.QueryLocator 객체를 리턴합니다.
public Database.QueryLocator start(Database.BatchableContext context) {
return Database.getQueryLocator('SELECT Id FROM Contact WHERE Active__c = true');
}
QueryLocator는 최대 5천만 건까지 지원하지만, 이를 초과하면 작업이 즉시 실패(Failed 상태)로 전환됩니다.- 서브쿼리는 가능하면 피하고, 필요한 추가 정보는
execute()내에서 재조회하는 것이 성능상 유리합니다. - 매 배치 실행 시 한 번만 호출됩니다.
2. execute() 메서드
실제 데이터 처리가 이루어지는 핵심 메서드입니다. start()에서 반환된 결과가 지정된 배치 크기에 따라 분할되어 전달됩니다.
public void execute(Database.BatchableContext context, List<Contact> scope) {
// 동시성 문제 방지를 위해 FOR UPDATE로 락킹
List<Id> ids = new List<Id>();
for (Contact c : scope) {
ids.add(c.Id);
}
List<Contact> lockedContacts = [SELECT Id, Email FROM Contact WHERE Id IN :ids FOR UPDATE];
// 업데이트 로직 수행
for (Contact c : lockedContacts) {
c.Email = 'updated_' + System.currentTimeMillis() + '@example.com';
}
update lockedContacts;
}
- 각 청크는 별도의 트랜잭션으로 실행되며, 하나의 청크에서 발생한 오류는 다른 청크에 영향을 주지 않습니다.
- 처리 순서는 보장되지 않으며, 롤백은 실패한 트랜잭션 내에서만 적용됩니다.
- 데이터 충돌을 막기 위해 FOR UPDATE를 사용한 재조회를 권장합니다.
3. finish() 메서드
모든 배치 청크가 완료된 후 실행되는 메서드로, 정리 작업이나 알림 발송에 활용됩니다.
public void finish(Database.BatchableContext context) {
AsyncApexJob job = [
SELECT Id, Status, NumberOfErrors, TotalJobItems
FROM AsyncApexJob
WHERE Id = :context.getJobId()
];
if (job.NumberOfErrors == 0) {
Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage();
mail.setToAddresses(new List<String>{'admin@company.com'});
mail.setSubject('배치 작업 완료');
mail.setPlainTextBody(job.TotalJobItems + '건의 레코드가 성공적으로 처리되었습니다.');
Messaging.sendEmail(new List<Messaging.SingleEmailMessage>{mail});
}
}
- 여기서 또 다른 배치를 실행하거나, 스케줄링할 수 있습니다.
- 비동기 작업 종료 후 호출되므로 장시간 실행되는 로직은 주의 필요.
배치 실행 방법
즉시 실행
Database.executeBatch()를 사용하여 큐에 배치 작업을 제출합니다.
Id jobId = Database.executeBatch(new AccountDataProcessor(), 100); // 배치 사이즈 100
- 두 번째 인자는 배치 크기로, 기본값은 200, 최대값은 2000입니다.
- 작업은 비동기로 처리되며, 실제 실행 시점은 시스템 부하에 따라 지연될 수 있습니다.
- 반환값은
AsyncApexJob오브젝트의 ID입니다.
예약 실행
특정 시간 후에 실행되도록 예약할 수 있습니다.
String cronId = System.scheduleBatch(
new AccountDataProcessor(),
'오늘 오후 업데이트',
60, // 60분 후 실행
100 // 배치 사이즈
);
- 반환값은
CronTrigger오브젝트의 ID이며, 실행 후 자동 삭제됩니다.
Database.Stateful 인터페이스
기본적으로 각 배치 청크는 독립된 트랜잭션으로 실행되며, 변수 상태가 유지되지 않습니다. 하지만 Database.Stateful을 구현하면 인스턴스 변수의 값을 across batches 간 유지할 수 있습니다.
public class CountProcessedRecords implements Database.Batchable<SObject>, Database.Stateful {
private Integer recordCount = 0;
public Database.QueryLocator start(Database.BatchableContext ctx) {
return Database.getQueryLocator('SELECT Id FROM Opportunity');
}
public void execute(Database.BatchableContext ctx, List<Opportunity> opportunities) {
recordCount += opportunities.size();
// 추가 처리 로직
}
public void finish(Database.BatchableContext ctx) {
System.debug('총 처리된 레코드 수: ' + recordCount);
}
}
- 집계, 카운팅, 로그 수집 등에 유용합니다.
- 정적(static) 변수는 여전히 유지되지 않으므로 주의가 필요합니다.
테스트 작성 시 고려사항
배치 클래스의 테스트 코드는 다음과 같은 특성을 고려해야 합니다.
@isTest
static void testBatchExecution() {
// 테스트 데이터 생성
List<Account> accounts = new List<Account>();
for (Integer i = 0; i < 10; i++) {
accounts.add(new Account(Name = 'Test Acc ' + i));
}
insert accounts;
Test.startTest();
AccountDataProcessor batch = new AccountDataProcessor();
Database.executeBatch(batch, 5); // 2번의 execute 호출
Test.stopTest(); // 비동기 작업 완료 대기
// 결과 검증
List<Account> updatedAccounts = [SELECT Name FROM Account];
System.assertEquals(10, updatedAccounts.size());
}
Test.startTest()와Test.stopTest()사이에executeBatch()를 호출하여 비동기 작업이 동기적으로 완료되도록 해야 합니다.- 테스트 컨텍스트에서는 최대 1회
execute()만 실행되므로, 배치 크기를 조절해 전체 로직이 테스트되도록 해야 합니다. - 실제 운영 환경과 달리, 테스트 중에는 모든 청크가 즉시 처리됩니다.