Salesforce에서 배치 Apex를 사용한 대량 데이터 처리

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()만 실행되므로, 배치 크기를 조절해 전체 로직이 테스트되도록 해야 합니다.
  • 실제 운영 환경과 달리, 테스트 중에는 모든 청크가 즉시 처리됩니다.

태그: salesforce apex batch-processing database.batchable governor-limits

6월 8일 16:39에 게시됨