본문으로 건너뛰기
Paul's Dev Notes

Business Rule 무한 루프 — current.update() 의 함정과 setWorkflow(false) 우회

BR 메커니즘(when × on × order), current.update() 가 같은 BR 을 재트리거하는 무한 루프, setWorkflow(false) 가 정확히 무엇을 차단하는지, async snapshot 의 stale read 함정까지 정리.

· note scripting
검증 인스턴스: OOTB Australia · 검증일 2026-05-20

개요

Business Rule(BR, 서버 사이드 자동화 규칙)은 when(Before / After / Async / Display) × on(Insert / Update / Delete / Query) × order 세 축으로 실행 시점이 결정된다.

after update 단계에서 current.update() 를 호출하면 같은 BR 이 재귀적으로 재트리거되어 무한 루프가 발생한다. setWorkflow(false) 는 다음 save 호출 시 다른 Business Rule·Flow Designer·Notification 류 자동화를 차단한다 — 다만 ACL·클라이언트 스크립트·자기 자신의 재귀 호출은 차단하지 못한다.

async BR 의 current 는 큐잉 시점의 snapshot(스냅샷, 그 순간 레코드 사본) 이므로, 처리 지연 사이에 레코드가 변경되면 stale read(낡은 값 읽기) 가 발생한다.

OOTB(Out-of-the-Box, 기본 제공) Australia 기준입니다. 인스턴스 버전·플러그인 구성에 따라 동작이 달라질 수 있습니다.


§1 — BR 메커니즘 전제

클라이언트 사이드 함정은 별도다 — Client Script onSubmit 에서 GlideAjax 를 동기처럼 막는 법 글에서 return false 차단 모델과 async/await 충돌을 다룬다. 본 글은 서버 사이드 BR 의 무한 루프 메커니즘에 집중한다.

When × On 매트릭스

Business Rule 의 실행 시점은 WhenOn 의 조합으로 결정된다. When 은 DB 쓰기 라이프사이클 상의 위치를, On 은 어떤 DML 이벤트에 반응할지를 정의한다.

When \ OnInsertUpdateDeleteQuery
BeforeDB 쓰기 전 (setValue 로 변경)DB 쓰기 전 (setValue 로 변경)삭제 전 (setAbortAction)조회 결과 반환 전 (필터 추가)
AfterDB 쓰기 후 (변경 시 update() 필요)DB 쓰기 후 (⚠ 무한 루프 위험)삭제 후 (참조 정리)— (미지원)
Async비동기 큐 (snapshot 주의)비동기 큐 (snapshot 주의)비동기 큐 (snapshot 주의)— (미지원)
Display표시 전 (읽기 전용)표시 전 (읽기 전용)— (미지원)— (미지원)

Before vs After — current 의 의미 차이

Before BR 에서 currentDB 에 아직 기록되지 않은 상태의 레코드를 가리킨다. 이 시점에는 current.setValue('field', value) 만으로 값을 변경해도 DB 쓰기에 반영된다. current.update() 를 추가로 호출할 필요가 없다.

After BR 에서 currentDB 에 이미 저장된 레코드를 가리킨다. 이 단계에서 필드 값을 바꾸려면 current.setValue() 후 반드시 current.update() 를 호출해야 변경이 저장된다. 바로 여기서 무한 루프의 씨앗이 심어진다.

Before사용자 Save트랜잭션 시작Before BR 실행setValue 만으로 충분DB 쓰기current 값 그대로 저장After BR (있다면)DB 쓰기 후 처리After사용자 Save트랜잭션 시작DB 쓰기레코드 저장 완료After BR 실행변경 시 update() 추가 필요트랜잭션 종료커밋

Order 와 실행 순서

동일한 When/On 조합을 가진 BR 이 여러 개 있을 때, Order 필드 값이 낮을수록 먼저 실행된다. Order 가 같으면 레코드 생성(created) 순서가 실행 순서를 결정한다. 하나의 BR 안에서 current.update() 를 잘못 호출하면 이 순서 전체가 꼬인다.

Scoped vs Global BR

Scoped Application 의 BR 은 해당 스코프 안의 테이블에만 기본 접근 권한을 가지며, Global BR 과의 실행 격리가 존재한다. setWorkflow(false) 가 다른 스코프의 BR 에 미치는 영향은 인스턴스 구성에 따라 달라질 수 있으며, 본인 인스턴스에서 직접 확인을 권장한다.


§2 — 무한 루프 문제

재트리거 시나리오

가장 흔한 무한 루프 패턴은 이렇다. after update BR 에서, 레코드의 특정 필드를 감지해 audit 필드(누가·언제 변경했는지 추적용 메타 필드)를 자동으로 갱신하는 로직을 작성했다. 값을 바꾼 뒤 current.update() 로 저장하면 — 그 save 가 다시 after update 이벤트를 발화시키고, 같은 BR 이 다시 실행되어 current.update() 를 다시 호출하는 사이클이 반복된다.

// ⚠ 무한 루프 예시 — after update BR
// 시나리오: 담당자 변경 시 audit 필드 자동 기록
if (current.assigned_to.changes()) {
    current.u_last_assigned_changed = new GlideDateTime();
    current.u_last_assigned_by = gs.getUserName();
    // 아래 update() 가 같은 BR 을 재트리거함
    current.update(); // ← 무한 루프 원인
}

첫 번째 save 이벤트가 BR 을 트리거한다. BR 은 current.update() 를 호출한다. 이 update 가 또 다른 save 를 일으키고 BR 이 재실행된다. 이 사이클은 플랫폼이 최대 재귀 깊이에 도달해 오류를 발생시키거나 트랜잭션을 강제 종료할 때까지 반복된다.

콜 스택 시각화

사용자 SaveAfter Update BR 실행 (1회)assigned_to.changes() = true → update() 호출After Update BR 실행 (2회)assigned_to.changes() = false 이지만 update() 재호출After Update BR 실행 (3회)update() 재호출 → 스택 계속 쌓임플랫폼 최대 재귀 깊이 도달 → 강제 종료

왜 Before 에서는 update() 가 위험한가

before update 에서도 current.update() 를 호출하면 동일한 재트리거 사이클이 발생한다. Before 단계는 DB 쓰기 전이므로 current.setValue() 만으로 값 변경이 DB 에 반영된다. update() 를 부르는 순간 “동일 레코드에 대한 새로운 save” 가 트랜잭션 안에서 발생하고 BR 이 재호출된다. Before BR 안에서 필드 값을 바꾸고 싶다면 setValue 만 사용하는 것이 올바른 패턴이다.


§3 — setWorkflow(false) 의 의미와 정확한 차단 목록

current.setWorkflow(false) 는 해당 GlideRecord 인스턴스에 “다음 save 호출 시 자동화를 건너뛰라”는 플래그를 설정하는 메서드다. 메서드 호출 자체는 저장을 일으키지 않는다. 이후 발생하는 save / update / insert 호출 시점에 차단 플래그가 적용되어, 서버 자동화 체계의 대부분이 건너뛰어진다.

차단되는 것과 차단되지 않는 것

차단됨 (Blocked)

  • 다른 Business Rule — 같은 테이블·레코드를 대상으로 하는 다른 BR (같은 트리거 이벤트)
  • Flow Designer 트리거 — 해당 save 이벤트로 기동되는 Flow (Record-Based Trigger)
  • Legacy Workflow — deprecated 지만 활성 인스턴스에 다수 존재하는 Workflow 엔진 트리거
  • Notification 류 — Email 중심의 자동 알림 (인스턴스·플러그인 구성에 따라 SMS/Push 차이 가능)
  • Approval 자동 생성 — Approval Engine 의 sysapproval_approver 자동 생성 로직 (인스턴스 구성에 따라 차이 가능)
  • History 엔진 기록 (부분적) — sys_history_set / sys_history_line 의 변경 이력 기록 (일반적으로 영향 받음)

차단되지 않음 (Not Blocked)

  • ACL (보안 규칙) — 레코드 접근 권한 평가는 setWorkflow 와 무관하게 동작
  • UI Policy / Client Script — 클라이언트 사이드 로직, 서버 저장 시점과 무관
  • 다른 GlideRecord 경로의 재트리거 — 호출한 GlideRecord 인스턴스의 다음 save 만 차단. 같은 BR 안에서 별도 GlideRecord 객체로 같은 레코드를 update 하면 막지 못함

sys_audit 필드 감사는 dictionary 의 audit 플래그로 별도 제어되어 setWorkflow(false) 와 무관하게 계속 기록되는 것이 일반적이다. 인스턴스 버전·필드 설정에 따라 차이 발생 가능.

결국 무한 루프의 근본 원인은 current.update() 호출 그 자체다. setWorkflow(false) 우회보다 update() 호출을 제거하는 것이 본질적 해법이다.

유의점 — 조용한 실패(Silent Failure)

setWorkflow(false) 를 적용한 뒤 예상했던 Notification 이 오지 않거나, 연계 자동화가 동작하지 않으면 바로 이 메서드가 원인인 경우가 많다. 플랫폼은 차단된 자동화에 대해 별도 오류를 내지 않기 때문에 원인 추적이 어렵다.

디버깅 함정

setWorkflow(false) 가 설정된 상태로 save 가 일어나면 연결된 모든 자동화가 조용히 건너뛰어진다. Notification 이 오지 않는 현상의 첫 번째 의심 대상으로 확인할 것.

의존성 누락 위험

다른 BR 이 이 레코드의 save 이벤트를 기다리고 있다면, 그 BR 도 함께 차단된다. 자동화 체인 전체가 끊길 수 있음을 염두에 두어야 한다.

History 누락 위험

자동화 체인이 차단되면 history 기반 변경 이력 추적도 함께 끊길 수 있다. SOX·ISO 27001 등 감사·이력 요구사항이 있는 코드 경로에서는 반드시 영향 범위를 확인해야 한다.

스코프 경계

Scoped BR 에서 setWorkflow(false) 를 호출했을 때 Global BR 에 미치는 영향은 인스턴스 설정에 따라 달라질 수 있다. 일반화하기 어려운 영역이므로 본인 인스턴스에서 직접 검증을 권장한다.


§4 — async snapshot 함정

when: After, Advanced: true (Async) 로 설정된 Async BR 은 동기 After BR 과 달리 비동기 큐를 통해 처리된다. 핵심은 current 가 큐에 등록된 시점(큐잉 시점)의 레코드 snapshot 이라는 점이다. 실제 처리는 큐 대기열이 처리되는 시점에 이루어지므로, 큐잉과 실행 사이에 시간 간격이 존재한다.

이 시간 간격 동안 다른 사용자 또는 자동화 프로세스가 같은 레코드를 변경했다면, Async BR 이 바라보는 current 는 이미 현재 상태와 다른 stale snapshot(낡은 스냅샷) 이 된다.

흔한 사례를 하나 들면, 우선순위(priority) 변경 시 외부 시스템에 알림을 보내는 Async BR 이 있다고 하자. 사용자 A 가 priority 를 P1 으로 올렸다. Async BR 이 큐에 들어갔다. 처리 전에 사용자 B 가 priority 를 다시 P2 로 내렸다. 이제 Async BR 이 실행될 때 current.priority 는 P1 (큐잉 시점 값) 이지만 실제 레코드는 P2 다. 외부 시스템에는 P1 이라는 잘못된 값이 전달된다.

Workaround: Async BR 안에서 현재 레코드 상태를 신뢰해야 한다면, current.sys_id 를 키로 명시 re-fetch 해서 최신 값을 읽어야 한다.

// Async BR 안에서 fresh read
var fresh = new GlideRecord('incident');
if (fresh.get(current.sys_id)) {
    // fresh 가 실제 현재 상태를 반영
    var currentPriority = fresh.getValue('priority');
    // 외부 시스템 호출 등
}

단, re-fetch 시점과 실제 처리 시점 사이에도 추가 변경이 발생할 수 있다. stale read 문제가 심각한 비즈니스 로직이라면 Async BR 대신 Flow Designer 의 Flow 로 옮겨 오케스트레이션을 명시화하는 것을 고려할 만하다.


§5 — 권장 패턴 체크리스트

setWorkflow(false) 를 써도 되는 경우

  • Silent data migration: 사용자 알림이 불필요한 일괄 데이터 갱신. Notification 이 발생하면 오히려 혼란을 줄 때.
  • Bulk import 중 자동화·history 누락 허용: 대량 레코드 삽입 시 Notification·history 기록 등 부수 자동화가 컴플라이언스 요구사항이 아닌 환경 (sys_audit 필드 감사는 일반적으로 별도 영향 없음).
  • 임시 상태 플래그 토글: lock 필드처럼 내부 로직용 필드 갱신. 연계 자동화 체인 없음.

피해야 하는 경우

  • Notification 이 비즈니스 가치인 경우: Incident assignment 변경 알림처럼 담당자·팀이 실시간으로 받아야 하는 알림이 있을 때.
  • 감사 추적이 컴플라이언스 요구사항: SOX, ISO 27001, HIPAA 등 규제 환경에서 변경 이력 보존이 필수인 경우.
  • 다른 BR 의존성이 있는 자동화 체인: 이 레코드의 save 이벤트를 기다리는 다른 BR 이나 Flow 가 있는 경우.

대안 패턴

  • Before BR + setValue 만 사용: 필드 변경이 목적이라면 Before 단계에서 setValue 로만 처리. update() 불필요 — 무한 루프 원천 차단.
  • 조건부 실행 플래그 필드 추가: u_skip_automation 같은 boolean 필드를 추가해, BR 첫 줄에서 이 값을 체크해 재진입을 막는 방법. setWorkflow(false) 없이 무한 루프만 방지 가능.
  • After BR + sys_id 기반 fresh GlideRecord: 최신 레코드 상태가 필요하다면, current 에 의존하지 않고 별도 GlideRecord 조회로 읽는 패턴.
  • Flow Designer 로 이전: 복잡한 자동화 체인은 Flow Designer 의 비주얼 오케스트레이션으로 옮겨 실행 순서·조건·에러 처리를 명시화. BR 의 암묵적 실행 순서 의존도를 낮춤.

참조