Skip to main content

대량 Apex 트리거

학습 목표

이 유닛을 완료하면 다음을 수행할 수 있습니다.

  • sObject 컬렉션에서 작동하는 트리거를 작성할 수 있습니다.
  • 효율적인 SOQL 및 DML 작업을 수행하는 트리거를 작성할 수 있습니다.
참고

참고

한국어로 학습하시겠어요? Trailhead playground에서 한국어로 실습 과제를 시작하고, 괄호 안에 제공된 번역을 사용해 탐색해 보세요. 영어 데이터를 기반으로 실습 과제 검증이 이루어지므로 영문으로 표시된 값만 복사해 붙여 넣습니다. 한국어 조직에서 실습 과제를 통과하지 못한 경우, (1) 로캘을 미국을 바꾸고 (2) 여기에 제시된 지침에 따라 언어를 영어로 바꾼 후 (3) "Check Challenge(과제 확인)" 버튼을 눌러 다시 진행해 보세요.

원하는 언어로 Trailhead 사용하기 뱃지를 확인해 현지화된 Trailhead 경험을 활용하는 방법에 대해 자세히 알아보세요.

대량 트리거 디자인 패턴

Apex 트리거는 대량으로 작동하는 데 최적화되어 있습니다. 트리거에서 레코드를 처리하기 위한 대량 디자인 패턴을 사용하는 것이 좋습니다. 대량 디자인 패턴을 사용할 경우 트리거의 성능이 향상되고 서버 리소스 사용량이 줄어 플랫폼 제한을 초과할 가능성이 낮아집니다.

코드를 대량으로 처리할 경우 대량으로 처리된 코드가 많은 수의 레코드를 효율적으로 처리하고 Lightning Platform의 거버너 제한 내에서 실행할 수 있다는 이점이 있습니다. 이러한 거버너 제한은 런어웨이 코드가 다중 테넌트 플랫폼에서 리소스를 독점하지 않도록 방지하기 위한 것입니다.

다음 섹션에서는 트리거의 모든 레코드에서 작동하고 한 번에 단일 sObject 대신 sObject 컬렉션에서 SOQL 및 DML을 수행하는 등 트리거에서 Apex 코드를 대량으로 처리하는 주요 방법을 보여줍니다. SOQL 및 DML 대량 모범 사례는 클래스의 SOQL 및 DML을 포함하여 모든 Apex 코드에 적용됩니다. 주어진 예제는 트리거를 기반으로 하며 Trigger.new 컨텍스트 변수를 사용합니다.

레코드 집합에 대해 작업하기

먼저 트리거에서 가장 기본적인 대량 디자인 개념을 살펴보겠습니다. 대량으로 처리된 트리거는 트리거 컨텍스트의 모든 sObject에서 작동합니다. 일반적으로 트리거를 실행한 작업이 사용자 인터페이스에서 시작된 경우 트리거는 하나의 레코드에서 작동합니다. 그러나 작업의 출처가 대량 DML 또는 API인 경우 트리거는 하나의 레코드가 아닌 레코드 집합에서 작동합니다. 예를 들어 API를 통해 많은 레코드를 가져올 경우 트리거는 전체 레코드 집합에서 작동합니다. 따라서 적절한 프로그래밍 방법은 트리거가 모든 상황에서 작동하도록 레코드 컬렉션에서 작동한다고 항상 가정하는 것입니다.

다음 트리거(MyTriggerNotBulk)는 오로지 하나의 레코드만 트리거를 작동한다고 가정합니다. 이 트리거는 동일한 트랜잭션에 여러 레코드가 삽입된 경우 전체 레코드 집합에서 작동하지 않습니다. 다음 예제에서는 대량으로 처리된 버전을 보여줍니다.

trigger MyTriggerNotBulk on Account(before insert) {
    Account a = Trigger.new[0];
    a.Description = 'New description';
}

이 예제(MyTriggerBulk)는 MyTriggerNotBulk의 수정된 버전입니다. for 루프를 사용하여 사용 가능한 모든 sObject를 반복합니다. 이 루프는 Trigger.new에 하나의 sObject 또는 여러 개의 sObject가 포한된 경우 작동합니다.

trigger MyTriggerBulk on Account(before insert) {
    for(Account a : Trigger.new) {
        a.Description = 'New description';
    }
}

대량 SOQL 수행하기

SOQL 쿼리는 강력할 수 있습니다. 단일 쿼리에서 관련 레코드를 검색하고 여러 조건의 조합을 확인할 수 있습니다. SOQL 기능을 사용하면 더 적은 코드를 작성하고 데이터베이스에 대한 쿼리를 더 적게 작성할 수 있습니다. 데이터베이스 쿼리 수를 줄일 경우 동기 Apex의 경우 100개의 SOQL 쿼리 또는 비동기식 Apex의 경우 200개인 쿼리 제한에 도달하는 것을 방지할 수 있습니다.

다음 트리거(SoqlTriggerNotBulk)는 피해야 할 SOQL 쿼리 패턴을 보여줍니다. 예제에서는 for 루프 내에서 SOQL 쿼리를 만들어 각 계정에 대한 관련 기회를 가져오며, 이 쿼리는 Trigger.new의 각 계정 sObject에 대해 한 번 실행됩니다. 계정 목록이 많은 경우 for 루프 내부의 SOQL 쿼리로 인해 SOQL 쿼리가 너무 많이 발생할 수 있습니다. 다음 예제에서 권장되는 접근 방식을 보여줍니다.

trigger SoqlTriggerNotBulk on Account(after update) {
    for(Account a : Trigger.new) {
        // Get child records for each account
        // Inefficient SOQL query as it runs once for each account!
        Opportunity[] opps = [SELECT Id,Name,CloseDate
                             FROM Opportunity WHERE AccountId=:a.Id];
        // Do some other processing
    }
}

다음 예제(SoqlTriggerBulk)는 이전 예제를 수정한 버전으로서 SOQL 쿼리 실행의 모범 사례를 보여줍니다. SOQL 쿼리는 무거운 작업을 수행하고 기본 루프 외부에서 한 번 호출됩니다.

  • SOQL 쿼리는 계정의 내부 쿼리(SELECT Id FROM Opportunities)를 사용하여 계정에 대한 관련 기회를 가져옵니다.
  • SOQL 쿼리는 IN 절을 사용하고 WHERE 절(WHERE Id IN :Trigger.new)의 Trigger.new 변수를 바인딩하여 트리거 컨텍스트 레코드에 연결됩니다. 이 WHERE 조건은 이 트리거를 실행한 레코드로만 계정을 필터링합니다.

쿼리의 두 부분을 결합하면 각 계정의 관련 기회가 있는 이 트리거의 계정에서 호출 한 번으로 원하는 레코드를 생성할 수 있습니다.

해당 레코드와 관련 레코드를 가져온 후 for 루프는 컬렉션 변수(이 경우 acctsWithOpps)를 사용하여 관심있는 레코드를 반복합니다. 컬렉션 변수는 SOQL 쿼리 결과를 보유합니다. 그렇게 할 경우 for 루프는 작업할 레코드에 대해서만 반복합니다. 관련 레코드를 이미 가져왔으므로 루프 내에서 해당 레코드를 가져오기 위한 추가 쿼리는 필요하지 않습니다.

trigger SoqlTriggerBulk on Account(after update) {
    // Perform SOQL query once.
    // Get the accounts and their related opportunities.
    List<Account> acctsWithOpps =
        [SELECT Id,(SELECT Id,Name,CloseDate FROM Opportunities)
         FROM Account WHERE Id IN :Trigger.new];
    // Iterate over the returned accounts
    for(Account a : acctsWithOpps) {
        Opportunity[] relatedOpps = a.Opportunities;
        // Do some other processing
    }
}

또는 계정 상위 레코드가 필요하지 않은 경우 이 트리거 컨텍스트 내에서 계정과 관련된 기회만 검색할 수 있습니다. 이 목록은 Trigger.new(WHERE AccountId IN :Trigger.new)에서 계정 ID에 대한 기회의 AccountId 필드와 일치함으로써 WHERE 절에서 지정됩니다. 반환된 기회는 특정 계정이 아니라 이 트리거 컨텍스트의 모든 계정에 대한 기회입니다. 다음 예제에서는 모든 관련 기회를 가져오는 데 사용되는 쿼리를 보여줍니다.

trigger SoqlTriggerBulk on Account(after update) {
    // Perform SOQL query once.
    // Get the related opportunities for the accounts in this trigger.
    List<Opportunity> relatedOpps = [SELECT Id,Name,CloseDate FROM Opportunity
        WHERE AccountId IN :Trigger.new];
    // Iterate over the related opportunities
    for(Opportunity opp : relatedOpps) {
        // Do some other processing
    }
}

하나의 명령문인 SOQL for 루프에서 SOQL 쿼리를 for 루프와 결합하여 이전 예제의 크기를 줄일 수 있습니다. 다음은 SOQL for 루프를 사용하는 이 대량 트리거의 다른 버전입니다.

trigger SoqlTriggerBulk on Account(after update) {
    // Perform SOQL query once.
    // Get the related opportunities for the accounts in this trigger,
    // and iterate over those records.
    for(Opportunity opp : [SELECT Id,Name,CloseDate FROM Opportunity
        WHERE AccountId IN :Trigger.new]) {
        // Do some other processing
    }
}





참고

트리거는 한 번에 200개의 레코드 배치에서 실행됩니다. 따라서 400개의 레코드로 인해 트리거가 실행될 경우 트리거는 200개 레코드마다 한 번씩 두 번 실행됩니다. 이러한 이유로 트리거는 레코드도 일괄 처리하므로 트리거에서 레코드 일괄 처리에 대한 SOQL for 루프의 이점을 얻지 못합니다. 이 예제에서는 SOQL for 루프가 두 번 호출되지만 독립 실행형 SOQL 쿼리도 두 번 호출됩니다. 그러나 SOQL for 루프는 컬렉션 변수를 반복하는 것보다 여전히 편리해 보입니다.

대량 DML 수행하기

트리거 또는 클래스에서 DML 호출을 수행할 때 가능한 경우 sObject 컬렉션에 대해 DML 호출을 수행합니다. 각 sObject에서 DML을 개별적으로 수행하면 리소스가 비효율적으로 사용됩니다. Apex 런타임은 하나의 트랜잭션에서 최대 150개의 DML 호출을 허용합니다.

이 트리거(DmlTriggerNotBulk)는 관련 기회들에 대하여 반복되는 for 루프 내에서 업데이트 호출을 수행합니다. 특정 조건이 충족될 경우 트리거가 기회 설명을 업데이트합니다. 이 예제에서 update 문은 각 기회에 대해 한 번씩 비효율적으로 호출됩니다. 대량 계정 업데이트 작업이 트리거를 실행한 경우 많은 계정이 있을 수 있습니다. 각 계정에 하나 또는 두 개의 기회가 있는 경우 150개 이상의 기회로 쉽게 끝날 수 있습니다. DML 문 제한은 150개 호출입니다.

trigger DmlTriggerNotBulk on Account(after update) {
    // Get the related opportunities for the accounts in this trigger.
    List<Opportunity> relatedOpps = [SELECT Id,Name,Probability FROM Opportunity
        WHERE AccountId IN :Trigger.new];
    // Iterate over the related opportunities
    for(Opportunity opp : relatedOpps) {
        // Update the description when probability is greater
        // than 50% but less than 100%
        if ((opp.Probability >= 50) && (opp.Probability < 100)) {
            opp.Description = 'New description for opportunity.';
            // Update once for each opportunity -- not efficient!
            update opp;
        }
    }
}

다음 예제(DmlTriggerBulk)는 기회 목록에서 단 하나의 DML 호출로 DML을 효율적으로 대량으로 수행하는 방법을 보여줍니다. 이 예제에서는 루프에서 기회(oppsToUpdate) 목록에 업데이트할 Opportunity sObject를 추가합니다. 그런 다음 트리거는 모든 기회가 목록에 추가된 후 이 목록의 루프 외부에서 DML 호출을 수행합니다. 이 패턴은 업데이트되는 sObject의 개수에 관계없이 하나의 DML 호출만 사용합니다.

trigger DmlTriggerBulk on Account(after update) {
    // Get the related opportunities for the accounts in this trigger.
    List<Opportunity> relatedOpps = [SELECT Id,Name,Probability FROM Opportunity
        WHERE AccountId IN :Trigger.new];
    List<Opportunity> oppsToUpdate = new List<Opportunity>();
    // Iterate over the related opportunities
    for(Opportunity opp : relatedOpps) {
        // Update the description when probability is greater
        // than 50% but less than 100%
        if ((opp.Probability >= 50) && (opp.Probability < 100)) {
            opp.Description = 'New description for opportunity.';
            oppsToUpdate.add(opp);
        }
    }
    // Perform DML on a collection
    update oppsToUpdate;
}

계정 관련 기회에 액세스하는 트리거를 작성하여 학습한 디자인 패턴을 적용해 보겠습니다. 이전 유닛에서 AddRelatedRecord 트리거에 대한 트리거 예제를 수정합니다. AddRelatedRecord 트리거는 대량으로 작동하지만 이전 유닛의 트리거는 Trigger.NewsObject 레코드를 모두 반복하므로 가능한 한 효율적이지 않습니다. 다음 예제에서는 관심있는 레코드만 가져온 다음 해당 레코드를 반복하도록 트리거 코드와 SOQL 쿼리를 모두 수정합니다. 이 트리거를 생성하지 않은 경우라도 걱정하지 않으셔도 됩니다. 이 섹션에서 트리거를 생성할 수 있습니다.

AddRelatedRecord 트리거에 대한 요구 사항을 검토해보겠습니다. 트리거는 계정이 삽입되거나 업데이트된 후에 실행됩니다. 트리거는 아직 기회가 없는 모든 계정에 대해 기본 기회를 추가합니다. 

먼저 새로 삽입된 계정에는 기본 기회가 없으므로 계정을 추가해야 합니다. 하지만 업데이트된 계정의 경우 관련된 기회가 있는지 확인해야 합니다. 이제 Trigger.operationType 컨텍스트 변수에 switch 문을 사용하여 삽입과 업데이트 작업을 처리하는 방법을 구분해 보겠습니다. 그런 다음 toProcess 변수로 처리해야 하는 계정을 추적합니다. 다음의 예를 확인해 보세요.

List<Account> toProcess = null;
switch on Trigger.operationType {
    when AFTER_INSERT {
        // do stuff
    }
    when AFTER_UPDATE {
        // do stuff
    }
}

모든 계정 삽입의 경우 새 계정을 toProcess 목록에 할당하면 됩니다.

when AFTER_INSERT {
     toProcess = Trigger.New;
}

업데이트하려면 이 트리거의 기존 계정에 관련 기회가 없는지 확인해야 합니다. 이 트리거는 after 트리거이므로 데이터베이스에서 영향을 받는 레코드를 쿼리할 수 있습니다. 다음은 toProcess 목록에 할당하는 SOQL 문입니다.

when AFTER_UPDATE {
     toProcess = [SELECT Id,Name FROM Account
                  WHERE Id IN :Trigger.New AND
                  Id NOT IN (SELECT AccountId FROM Opportunity WHERE AccountId in :Trigger.New)];
}

이제 for 루프를 사용하여 toProcess 계정 목록을 반복하고 oppList에 관련 기본 기회를 추가합니다. 완료되면 insert DML 문을 사용하여 기회 목록을 일괄 추가합니다.  전체 트리거를 생성하거나 업데이트하는 방법은 다음과 같습니다.

  1. 이미 이전 유닛의 AddRelatedRecord 트리거를 생성한 경우 해당 내용을 다음 트리거로 변경하여 해당 트리거를 수정합니다. 그렇지 않을 경우 Developer Console을 사용하여 다음 트리거를 추가하고 트리거 이름에 AddRelatedRecord를 입력합니다.


    trigger AddRelatedRecord on Account(after insert, after update) {
        List<Opportunity> oppList = new List<Opportunity>();
        // Add an opportunity for each account if it doesn't already have one.
        // Iterate over accounts that are in this trigger but that don't have opportunities.
        List<Account> toProcess = null;
        switch on Trigger.operationType {
            when AFTER_INSERT {
            // All inserted Accounts will need the Opportunity, so there is no need to perform the query
                toProcess = Trigger.New;
            }
            when AFTER_UPDATE {
                toProcess = [SELECT Id,Name FROM Account
                             WHERE Id IN :Trigger.New AND
                             Id NOT IN (SELECT AccountId FROM Opportunity WHERE AccountId in :Trigger.New)];
            }
        }
        for (Account a : toProcess) {
            // Add a default opportunity for this account
            oppList.add(new Opportunity(Name=a.Name + ' Opportunity',
                                        StageName='Prospecting',
                                        CloseDate=System.today().addMonths(1),
                                        AccountId=a.Id));
        }
        if (oppList.size() > 0) {
            insert oppList;
        }
    }
  2. 트리거를 테스트하려면 Salesforce 사용자 인터페이스에서 계정을 만들고 Lions & Cats(사자 및 고양이)로 이름을 지정합니다.
  3. 계정 페이지의 기회 관련 목록에서 새 기회 Lions & Cats를 찾습니다. 트리거가 기회를 자동으로 추가했습니다.

리소스

Salesforce 도움말에서 Trailhead 피드백을 공유하세요.

Trailhead에 관한 여러분의 의견에 귀 기울이겠습니다. 이제 Salesforce 도움말 사이트에서 언제든지 새로운 피드백 양식을 작성할 수 있습니다.

자세히 알아보기 의견 공유하기