Salesforce로 개발을 하기 위해 기능 설계를 하는 단계에서, 무엇보다도 가장 중요하고 고려해 보아야 할 것 중 하나가 Salesforce의 Governor Limits라는 것입니다.

 

대량의 데이터를 처리해야 하는 기능을 개발하는 단계에 있어, 이 제한이 가장 짜증나는 부분이며 예상치 못하고 있다간, 이 제한으로 인해 로직을 변경해야 하는 일이 발생합니다.

 

이번 포스팅에서는 Gorvernor Limits에 대해 설명하겠습니다.


# 목차

  • Governor Limits 란?

  • 남은 제한 확인 방법

  • 회피 방법 예시


 

# Governor Limits 란?

Gorvernor Limits이란, 멀티 테넌트 환경에서 실행하기 위해, Apex 런타임 엔진이 Apex 코드 또는 프로세스가 공유 자원을 독점하지 못하도록 강제로 제한하는 것을 말합니다.

즉, Salesforce 측에서 지나친 독점을 막기 위해 지정해 둔 제한이라고 할 수 있습니다.

(물론, 라이센스 단계에 따라 한계치가 바뀌기도 합니다.)

 

Salesforce의 거의 모든 부분에 있어서 이 제한이 걸려있다고 할 수 있기 때문에, 설계나 개발 단계에서 항상 고려해야 하는 부분입니다.

 ex) 개체 등록 가능 수, 레코드 등록 가능 수, 1트랜잭션 에서 SELECT 가능한 데이터 수, Apex 클래스 등록 가능수, Apex 코드 문자수 등등

 

Gorvernor Limits에 대한 상세내용은 다음을 참고해주세요.

Salesforce Developer Guide - Apex Governor Limits

Salesforce Developer Guide - Execution Governors and Limits


 

# 남은 제한 확인 방법

Apex에는 Gorvernor Limits을 확인하기 위한 "Limits" 클래스가 존재합니다.

 

Limits 클래스에 대한 상세내용은 다음을 참고해주세요.

Salesforce Developer Guide - Limits

 

Apex 코드 상에서는 이 Limit을 통해 남아 있는 제한 수를 확인하는 것이 가능합니다.가끔 확인을 위해 SOQL 쿼리의 실행 수나 스텝 수를 확인하는 메소드를 주로 사용했지만, 이 외에도 여러가지로 많았기 때문에 전부 확인해 보았습니다.ex) 샘플 코드

System.debug('limits.getAggregateQueries()                 = ' + limits.getAggregateQueries());                     // SOQL 쿼리 문이 처리 된 집계 쿼리의 수
System.debug('limits.getLimitAggregateQueries()            = ' + limits.getLimitAggregateQueries());                // SOQL 쿼리 문에서 처리 할 수 있는 집계 쿼리의 총 수
System.debug('limits.getCallouts()                         = ' + limits.getCallouts());                             // 처리 된 Web 서비스의 수
System.debug('limits.getLimitCallouts()                    = ' + limits.getLimitCallouts());                        // 처리 할 수있는 Web 서비스의 총 수
System.debug('limits.getCpuTime()                          = ' + limits.getCpuTime());                              // 현재의 트랜잭션에서 사용 된 Salesforce 서버의 CPU 시간 (밀리 초)
System.debug('limits.getLimitCpuTime()                     = ' + limits.getLimitCpuTime());                         // 트랜잭션에서 사용 가능한 최대 CPU 시간 (밀리 초)
System.debug('limits.getDMLRows()                          = ' + limits.getDMLRows());                              // DML 제한에 포함되는 모든 문을 사용하여 처리 된 레코드의 수
System.debug('limits.getLimitDMLRows()                     = ' + limits.getLimitDMLRows());                         // DML 제한에 포함되는 모든 문을 사용하여 처리 할 수있는 총 레코드 수
System.debug('limits.getDMLStatements()                    = ' + limits.getDMLStatements());                        // 호출 된 DML 문 (insert,update또는 database.EmptyRecycleBin 메소드 등)의 수
System.debug('limits.getLimitDMLStatements()               = ' + limits.getLimitDMLStatements());                   // 호출 할 수있는 DML 문 또는database.EmptyRecycleBin 메소드의 총 수
System.debug('limits.getEmailInvocations()                 = ' + limits.getEmailInvocations());                     // 호출 된 메일 호출 수(sendEmail 등) 
System.debug('limits.getLimitEmailInvocations()            = ' + limits.getLimitEmailInvocations());                // 호출 할 수있는 메일 호출의 총 개수(sendEmail 등)
System.debug('limits.getFindSimilarCalls()                 = ' + limits.getFindSimilarCalls());                     // getSoslQueries 같은 값을 반환
System.debug('limits.getLimitFindSimilarCalls()            = ' + limits.getLimitFindSimilarCalls());                // getLimitSoslQueries 같은 값을 반환
System.debug('limits.getFutureCalls()                      = ' + limits.getFutureCalls());                          // 실행 된 future 주석이 있는 메소드의 수
System.debug('limits.getLimitFutureCalls()                 = ' + limits.getLimitFutureCalls());                     // 실행할 수 future 주석이 있는 메소드의 총 개수
System.debug('limits.getHeapSize()                         = ' + limits.getHeapSize());                             // 힙에 사용 된 메모리의 대략의 용량 (Byte 단위)
System.debug('limits.getLimitHeapSize()                    = ' + limits.getLimitHeapSize());                        // 힙을 사용할 수 있는 메모리의 총 용량 (Byte 단위)
System.debug('limits.getLimitMobilePushApexCalls()         = ' + limits.getLimitMobilePushApexCalls());             // 모바일 푸시 알림으로 1 트랜잭션 당 허용되는 Apex 총 통화 수
System.debug('limits.getQueries()                          = ' + limits.getQueries());                              // 발행 된 SOQL 쿼리의 수
System.debug('limits.getLimitQueries()                     = ' + limits.getLimitQueries());                         // 발급 할 SOQL 쿼리의 총 수
System.debug('limits.getQueryLocatorRows()                 = ' + limits.getQueryLocatorRows());                     // Database.getQueryLocator 메소드에서 반환 된 레코드 수
System.debug('limits.getLimitQueryLocatorRows()            = ' + limits.getLimitQueryLocatorRows());                // Database.getQueryLocator 메소드에서 반환 할 수있는 총 레코드 수
System.debug('limits.getQueryRows()                        = ' + limits.getQueryRows());                            // SOQL 쿼리의 발행에서 반환 된 레코드 수
System.debug('limits.getLimitQueryRows()                   = ' + limits.getLimitQueryRows());                       // SOQL 쿼리의 발행에 반환 할 수있는 총 레코드 수
System.debug('limits.getQueueableJobs()                    = ' + limits.getQueueableJobs());                        // 트랜잭션마다 추가된 큐 중,  큐 처리가 가능한 작업 수
System.debug('limits.getLimitQueueableJobs()               = ' + limits.getLimitQueueableJobs());                   // 트랜잭션마다 추가된 큐 중,  큐 처리가 가능한 작업의 최대수
System.debug('limits.getRunAs()                            = ' + limits.getRunAs());                                // getDMLStatements 같은 값을 반환
System.debug('limits.getLimitRunAs()                       = ' + limits.getLimitRunAs());                           // getLimitDMLStatements 같은 값을 반환
System.debug('limits.getSavepointRollbacks()               = ' + limits.getSavepointRollbacks());                   // getDMLStatements 같은 값을 반환
System.debug('limits.getLimitSavepointRollbacks()          = ' + limits.getLimitSavepointRollbacks());              // getLimitDMLStatements 같은 값을 반환
System.debug('limits.getSavepoints()                       = ' + limits.getSavepoints());                           // getDMLStatements 같은 값을 반환
System.debug('limits.getLimitSavepoints()                  = ' + limits.getLimitSavepoints());                      // getLimitDMLStatements 같은 값을 반환
System.debug('limits.getSoslQueries()                      = ' + limits.getSoslQueries());                          // 발행 된 SOSL 쿼리의 수
System.debug('limits.getLimitSoslQueries()                 = ' + limits.getLimitSoslQueries());                     // 발급 할 SOSL 쿼리의 총 수
//System.debug('limits.getAggregateQueries()                 = ' + limits.getChildRelationshipsDescribes());          // 반환 된 자식 관계 개체의 수

 

☞실행 결과

 

Apex 코드 내에서 확인하는 방법 이외에, "Workbench" 라는 사이트를 통해서 확인하는 방법도 있습니다.

 

Workbench(워크 벤치)를 이용해서 확인하는 방법은 다음과 같습니다.

먼저 확인 대상이 되는 환경에 로그인 하기 위해, [I agree to the terms of service]에 체크하고 [Login with Salesforce] 버튼을 눌러 로그인을 합니다.

로그인 화면 표시

 

로그인 허용, 환경 로그인

환경에 로그인이 되었다면, [Utilities - REST Explorer]를 선택해주세요.

그 후 표시되는 REST Explorer 화면에서, "/services/data/v48.0/limits" 를 입력하고 [Execute] 버튼을 누르면 현재 사용 가능한 Gorvernor Limits의 수가 종류별로 표시가 됩니다.


 

# 회피 방법 예시

 

Apex 개발을 하다보면, 데이터 처리 등에 있어 Gorvernor Limits이 걸리는 경우, 제한이 걸리지 않게 하기 위해 로직을 변경하여 회피해야 합니다.

 

이번 예시에서는 작성자 본인이 평소 일을 하면서 자주 사용하는 회피 방법 3가지에 대해 설명하겠습니다.

( 혹시 고수분들 중에, 더 나은 방법이라 생각되는 것이 있으시다면 알려주시기 바랍니다. (_o_) )

 

1. Batch를 통한 일괄 처리 방법 

Apex에서는 다량의 데이터(수천~수백만 개의 데이터)를 처리하기 위해, "Database.Batchable" 이라는 인터페이스를 제공하고 있습니다.

이 Batch를 사용하면 해당 플랫폼의 제한을 초과하지 않고, 레코드를 Batch 단위로 나누어, 비동기로 일괄 처리할 수 있습니다. 

그렇기 때문에, 데이터의 정리 및 보관 등, 처리해야 하는 레코드의 수가 많을 경우, Batch를 통한 일괄 처리가 적합합니다.

Apex Batch를 통한 일괄 처리 가이드

Salesforce Trailhead - Apex 일괄처리(Batch)

 

"Database.Batchable" 인터페이스는 다음과 같이 구성되어 있습니다.

global class MyBatchClass implements Database.Batchable<sObject> {
    global (Database.QueryLocator | Iterable<sObject>) start(Database.BatchableContext bc) {
        // execute에 전달하여 일괄 처리할 레코드를 습득
        // 주로, Database.QueryLocator를 통해 레코드 습득
        // QueryLocator를 사용하는 경우 Gorvernor Limits "SOQL 쿼리로 검색된 레코드의 총 개수"가 무시되고 최대 5,000만 레코드까지 조회가능
        // Iterable를 사용하는 경우 Gorvernor Limits "SOQL 쿼리로 검색된 레코드의 총 개수"가 그대로 적용
    }

    global void execute(Database.BatchableContext bc, List<P> records){
        // start로 부터 전달 된 레코드 일괄 처리
        // 기본 Batch 크기는 200 레코드
        // 레코드 배치가 start 메소드로부터 받은 순서대로 실행된다는 보장이 없음
    }

    global void finish(Database.BatchableContext bc){
        // 레코드 일괄 처리 실행 후 처리할 마무리 작업(이메일 전송 등)
        // 모든 배치가 처리 된 후 1회만 호출
    }
}

 

즉, 그림으로 표현하면 아래와 같이 됩니다.

Batch 실행도 

 

Database.Batchable의 사용 예시는 다음과 같습니다.

/**
 * @description 회원명 갱신 Batch
 */
global class UpdateMemberName implements Database.Batchable<sObject>, Database.Stateful {

    /**
     * @description 일괄 처리 대상 레코드 SELECT
     */
    global Database.QueryLocator start(Database.BatchableContext bc) {

        // 회원 레코드 SELECT, return으로 execute에 레코드 전달
        return Database.getQueryLocator(
            'SELECT '
            + ' ID '
            + ', Name '
            + 'FROM '
            + ' Member__c '
        );
    }

    /**
     * @description 레코드 일괄 처리 메서드(비동기)
     */
    global void execute(Database.BatchableContext bc, List<Member__c> memberList){
        
        for (Member__c member : memberList) {

            // 회원 이름 뒤에 "님" 추가
            member.Name = member.Name + ' 님 ';
        }

        // 회원 레코드 UPDATE
        update memberList;
    }    
    global void finish(Database.BatchableContext bc){

        // 현재 처리중인 Batch 의 Job 정보 표시(단순 확인용)
        AsyncApexJob job = [SELECT Id, Status, NumberOfErrors, 
            JobItemsProcessed,
            TotalJobItems, CreatedBy.Email
            FROM AsyncApexJob
            WHERE Id = :bc.getJobId()];
        System.debug(job);

        // execute 일괄 처리 이후의 회원 정보 확인
        List<Member__c> memberList = [SELECT Id, Name FROM Member__c];
        System.debug(memberList); 
    }    
}

 

Batch 클래스를 작성하였다면, 작성한 Batch를 실행해보겠습니다.

지금은 개발자 콘솔에서 실행하겠습니다만, 실제 개발에서는 Apex 코드 내에서 실행하시면 되겠습니다.

 

[개발자 콘솔 - Open Execute Anonymous Window]로 Apex 코드 입력창을 표시합니다.

그 후, 아래의 코드를 작성한 후 [Execute]버튼을 눌러 Batch를 실행합니다.

// 실행 대상 Batch 클래스 정의
UpdateMemberName umnBatch = new UpdateMemberName(); 

// Database.executeBatch를 통해 Batch 실행
// prm1 : 실행 대상 Batch
// prm2 : 1개의 Batch 내에서의 일괄 처리 레코드 수
Id batchId = Database.executeBatch(umnBatch, 200);

 

2. 1개의 트랜잭션에서 실행 가능한 SOQL 제한을 회피하는 방법

Gorvernor Limits 가이드를 보셨다면 아마 아시겠지만, 1개의 트랜잭션에서 실행 가능한 SOQL의 쿼리 수에 제한이 있습니다.

즉, 가능한 반복문에서는 SOQL 쿼리를 사용하지 않는 것이 중요하다는 의미가 됩니다.

 

혹시, 반복문 내에서 다른 데이터의 정보를 습득해서 수정하는 처리 등을 해야할 필요가 있을 경우에는 로직을 통해 회피하는 수 밖에 없습니다.

 

여기서는 주로 제가 사용하는 방법에 대해 설명하겠습니다.

(아마 경험 많은 개발자 분들이면 충분히 사용하고 있을 법한 로직이 되겠습니다만..)

 

간단히 설명하면 다음과 같은 방법이 되겠습니다.

  ① 반복문에 들어가기 이전에 필요로 하는 레코드를 전부 SELECT 해서 Map에 저장

    ※ 여기서도 1개의 SOQL 쿼리로 SELECT 가능한 Gorvernor Limits(50,000개)이 있기 떄문에, 필요에 따라 WHERE로 필요한 레코드만 SELECT

  ② 반복문에서는 그 Map으로부터 데이터를 습득해서 사용

 

그럼 샘플 코드로 확인해보겠습니다.

/**
 * @description 도서 대여중인 회원의 이름
 */
private void getMemberNameByBookLender() {

    // 모든 회원을 SELECT해서 Map에 추가 (Key:레코드ID, Value: 회원 개체)
    Map<ID, Member__c> allMemberMap = (Map<ID, Member__c>)[SELECT Id, Name FROM Member__c];
    List<Book__c> allBookList = [SELECT Id, Name, Lender__c FROM Book__c];

    for (Book__c book : allBookList) {

        if (String.isBlank(book.Lender__c) || allMemberMap.containsKey(book.Lender__c)) {
            // 도서가 대여중이 아닌 경우 또는 대여 중인 회원 정보를 찾지 못한 경우, 다음 도서로 넘어감

            continue;
        }

        // 회원Map에서 회원ID가 일치하는 회원 정보를 검색
        Member__c lender = allMemberMap.get(book.Lender__c);

        System.debug('도서명 : ' + book.Name + ' 대여 중인 회원명 : ' + lender.Name);
    }
}

 

이러한 형식으로 반복문에서 SOQL 쿼리를 사용하지 않고도 필요한 데이터를 습득하고 사용할 수 있습니다.

특히, Map을 잘 사용할 수 있도록 익혀두면, 여러 방면에서 유용하게 사용할 수 있습니다.

 

3. 1개 컬렉션으로 표시 가능한 데이터 제한 수

어떻게 보면, Gorvernor Limits으로 인해 가장 짜증나는 경우가 이 경우일 수 있습니다.

Gorvernor Limits 중, 1개의 컬렉션에서 표시 가능한 데이터 수가 있는데 쉽게 말하면, 

검색 화면에서 데이터를 검색했을 때, 검색 결과가 제한 수인 1000개 이상이 되면 에러가 발생한다는 것입니다.

 

 

이 부분에 대해서는 딱히 1000개 이상을 표시하기 위한 회피 방법이 없으므로, 로직상에서 1000개 이하가 되도록 데이터에 제한을 걸고, 화면에 INFO 메시지로 "최대 표시 가능한 데이터 수를 초과했으므로, 선두 1000개만 표시합니다." 라던가, "검색 조건을 통해 결과를 좁혀주세요." 라는 등의 방법을 사용하고 있습니다.

( 좋은 방법 있으면 알려주시면 감사하겠습니다. )

 

샘플 코드

final integer VF_COLLECTION_ITEM_LIMIT = 1000;
if (VF_COLLECTION_ITEM_LIMIT < memberList.size()) {
    // 검색 결과가 1개 컬렉션에서 표시 가능한 데이터 제한수(1000개)를 초과한 경우

    // 화면에 메시지 표시
    ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.INFO, '표시 가능한 데이터 수 초과, 검색 조건을 지정해 대상을 좁히시오.'));

    while (VF_COLLECTION_ITEM_LIMIT < memberList.size()) {

        // 제한 수에 도달할 때 까지, 맨 뒤의 요소를 삭제
        memberList.remove(memberList.size() - 1);
    }
}

 

이 외에 가장 큰 문제가 되는 부분이 바로 csv출력 입니다.

csv를 출력할 때에도 출력에 사용되는 컬렉션에 1000개 이상의 데이터가 있는 경우, 위와 동일한 에러가 발생하게 됩니다.

 

작성자의 경우, 이 문제에 대해서는 정적으로 2개 이상의 컬렉션를 정의 해놓은 다음, 각각의 컬렉션을 순서대로 출력하는 방법을 사용하고 있습니다.

허나, 이 방법을 사용할 때의 문제점으로는 각 컬렉션에 분할에서 넣기 위한 로직이 필요하다는 것입니다.

( 이 부분에 대해서도 좋은 방법 있으면 공유 해주시면 감사하겠습니다. )


 

이것으로, Salesforce로 개발을 할 때에 가장 신경써야 하는 부분 중 하나인 Gorvernor Limits에 대해 알아보았습니다.

 

개인적으로 이 Gorvernor Limits 때문에 Salesforce를 때려치고 싶다는 생각이 들 정도로 개발을 하다보면 상당히 불편한 부분입니다.

그렇기 때문에, 설계 단계에서 우선적으로 조사한 후, 거기에 맞춰서 설계를 해놓을 필요가 있습니다.

 

 

 

 

 

 

 

참고1) https://trailhead.salesforce.com/ja/content/learn/modules/asynchronous_apex/async_apex_batch - Apex Batch 일괄 처리

참고2) developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_methods_system_limits.htm - Limits Class

참고3) https://mokochi.tistory.com/manage/newpost/?type=post&returnURL=%2Fmanage%2Fposts - Governor Limits

+ Recent posts