System.LimitException: Too many SOQL queries: 101 해결 및 예방 가이드

세일즈포스 개발을 하다 보면 누구나 한 번쯤은 맞닥뜨리는 공포의 메시지가 있습니다. 바로 System.LimitException: Too many SOQL queries: 101입니다. 잘 작동하던 기능이 대량의 데이터를 처리할 때 갑자기 멈춰버리는 이 에러는 세일즈포스 입문자와 숙련자를 가르는 기준점이 되기도 합니다. 오늘은 이 에러가 왜 발생하는지, 그리고 실무에서 이를 어떻게 완벽하게 해결하고 예방할 수 있는지 심층적으로 다루어 보겠습니다.

101 에러, 왜 나만 괴롭히는 걸까?

세일즈포스는 ‘멀티 테넌트(Multi-tenant)’ 환경입니다. 즉, 하나의 물리적 서버 자원을 여러 기업이 나누어 씁니다. 만약 한 명의 개발자가 효율적이지 못한 코드로 서버 자원을 독점하면 다른 사용자들에게 피해가 가기 때문에, 세일즈포스는 하나의 트랜잭션 내에서 실행할 수 있는 SOQL 쿼리 수를 최대 100개로 엄격히 제한하고 있습니다. 101 에러는 바로 이 ‘100개’라는 마법의 숫자를 넘겼을 때 발생합니다.

대부분의 경우 이 에러는 코드의 논리적 흐름이 반복문(For Loop) 안에서 쿼리를 호출하고 있을 때 발생합니다. 데이터가 한두 건일 때는 문제가 없다가, 실제 운영 환경에서 수십 건 이상의 레코드가 한꺼번에 처리되는 순간 쿼리 횟수가 100회를 초과하며 시스템이 셧다운되는 것입니다.

실무 사례로 보는 101 에러의 주범과 해결책

가장 흔한 실수 사례를 통해 코드를 어떻게 개선해야 하는지 살펴보겠습니다.

안 좋은 예시 (Anti-Pattern)

아래 코드는 계정(Account) 리스트를 돌면서 각 계정에 속한 연락처(Contact)를 매번 쿼리합니다. 만약 계정이 101개라면 즉시 에러가 발생합니다.

Apex

// 치명적인 실수: 루프 내부의 쿼리
for (Account acc : Trigger.new) {
    List<Contact> cons = [SELECT Id FROM Contact WHERE AccountId = :acc.Id];
    // 비즈니스 로직 수행...
}

권장 예시 (Bulkification 전략)

해결 방법은 쿼리를 루프 밖으로 꺼내는 ‘벌크화(Bulkification)’입니다. 쿼리는 단 한 번만 수행하고, 결과 데이터를 Map에 담아 루프 안에서 꺼내 쓰는 방식입니다.

Apex

// 해결책: 쿼리를 한 번으로 최적화
Set<Id> accIds = Trigger.newMap.keySet();
List<Contact> allContacts = [SELECT Id, AccountId FROM Contact WHERE AccountId IN :accIds];

// 효율적인 데이터 접근을 위한 Map 활용
Map<Id, List<Contact>> accToConMap = new Map<Id, List<Contact>>();
for (Contact con : allContacts) {
    if (!accToConMap.containsKey(con.AccountId)) {
        accToConMap.put(con.AccountId, new List<Contact>());
    }
    accToConMap.get(con.AccountId).add(con);
}

// 루프 안에서는 쿼리 없이 Map에서 데이터를 가져옴
for (Account acc : Trigger.new) {
    List<Contact> cons = accToConMap.get(acc.Id);
    // 비즈니스 로직 수행...
}

이 방식을 사용하면 처리해야 할 레코드가 1,000건이든 10,000건이든 쿼리는 단 1회만 실행됩니다. 이것이 세일즈포스 개발의 정석입니다.

내 코드에는 쿼리가 없는데 왜 101 에러가 날까?

때로는 내 Apex 클래스 안에는 루프 쿼리가 없는데도 101 에러가 발생하여 우리를 당황하게 합니다. 이때는 다음 두 가지 가능성을 의심해야 합니다.

  1. 트리거 재귀(Recursion): 레코드를 업데이트할 때 트리거가 다시 자기 자신을 호출하거나 다른 트리거를 연쇄적으로 깨워 쿼리 횟수가 누적되는 경우입니다. Static 변수를 활용하여 트리거 재귀를 방지하는 로직이 필요합니다.
  2. 선언적 도구(Flow/Process Builder)의 영향: 세일즈포스는 코드뿐만 아니라 해당 트랜잭션에서 실행되는 플로우나 프로세스 빌더의 쿼리 사용량도 합산합니다. 특히 플로우 내에서 ‘Get Records’ 요소가 루프 안에 들어가 있다면 101 에러의 강력한 용의자가 됩니다.

101 에러 예방을 위한 3단계 체크리스트

시스템의 안정성을 높이기 위해 개발 과정에서 반드시 지켜야 할 체크리스트입니다.

  • 쿼리 수집(Collection): 모든 SOQL은 WHERE 필드 IN :컬렉션 형태를 갖추고 루프 외부에서 실행되는가?
  • Map 기반 아키텍처: 관계가 있는 데이터를 매칭할 때 중첩 For문을 돌리는 대신 Map을 사용하여 CPU 시간과 쿼리 효율성을 챙겼는가?
  • 트랜잭션 가시성: 디버그 로그(Debug Log)의 LIMIT_USAGE_FOR_NS 섹션을 확인하여 현재 트랜잭션의 쿼리 사용량이 위험 수준(예: 80회 이상)에 도달하지 않았는지 모니터링하는가?

101 에러에 대해 자주 묻는 질문 (FAQ)

Q1. 에러가 발생한 위치를 어떻게 정확히 찾을 수 있나요?

세일즈포스 Setup > Debug Logs에서 로그를 생성한 뒤 실행해 보세요. 로그 파일에서 SOQL_EXECUTION_BEGIN 키워드를 검색하면 어떤 쿼리가 반복적으로 실행되고 있는지 추적할 수 있습니다. 또한 로그 하단의 Cumulative Limit Usage 섹션을 보면 어느 시점에 쿼리 수가 급증했는지 한눈에 파악할 수 있습니다.

Q2. 쿼리 결과가 5만 건을 넘으면 어떻게 하나요?

101 에러는 쿼리의 ‘횟수’에 대한 제한이고, 5만 건 제한은 쿼리로 가져오는 ‘레코드 수’에 대한 제한입니다. 만약 5만 건 이상의 대량 데이터를 처리해야 한다면 Batch Apex를 사용해야 합니다. 배치 클래스는 데이터를 200개(기본값) 단위의 작은 묶음으로 나누어 여러 개의 독립된 트랜잭션으로 처리하므로 101 에러와 5만 건 제한을 동시에 회피할 수 있습니다.

Q3. 플로우(Flow)에서도 101 에러를 피하는 방법이 있나요?

플로우 역시 벌크화 원칙이 동일하게 적용됩니다. 루프 안에서 ‘Get Records’나 ‘Update Records’ 요소를 절대 배치하지 마세요. 대신 루프 시작 전에 필요한 데이터를 모두 가져오고, 루프 안에서는 ‘Assignment’ 요소를 이용해 컬렉션 변수에 데이터를 모은 뒤, 루프가 끝난 다음에 한꺼번에 ‘Update’ 요소를 실행해야 합니다.

마치며: 리밋은 제약이 아닌 아키텍처의 가이드라인

Too many SOQL queries: 101 에러는 결코 무서운 적이 아닙니다. 오히려 우리에게 “이 코드는 비효율적이니 더 나은 구조로 개선하라”고 알려주는 친절한 가이드라인에 가깝습니다. 벌크화와 Map 활용을 습관화한다면 리밋의 공포에서 벗어나 훨씬 더 견고한 세일즈포스 시스템을 구축할 수 있을 것입니다.

오늘 정리한 해결법이 여러분의 디버깅 시간을 줄여주고, 더 나은 코드를 작성하는 데 도움이 되기를 바랍니다. 다음에는 CPU 타임아웃 리밋을 줄이는 고도의 최적화 기법에 대해 다루어 보겠습니다.

댓글 남기기