Business Rule 무한 루프 — current.update() 의 함정과 setWorkflow(false) 우회
BR 메커니즘(when × on × order), current.update() 가 같은 BR 을 재트리거하는 무한 루프, setWorkflow(false) 가 정확히 무엇을 차단하는지, async snapshot 의 stale read 함정까지 정리.
개요
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 의 실행 시점은 When 과 On 의 조합으로 결정된다. When 은 DB 쓰기 라이프사이클 상의 위치를, On 은 어떤 DML 이벤트에 반응할지를 정의한다.
| When \ On | Insert | Update | Delete | Query |
|---|---|---|---|---|
| Before | DB 쓰기 전 (setValue 로 변경) | DB 쓰기 전 (setValue 로 변경) | 삭제 전 (setAbortAction) | 조회 결과 반환 전 (필터 추가) |
| After | DB 쓰기 후 (변경 시 update() 필요) | DB 쓰기 후 (⚠ 무한 루프 위험) | 삭제 후 (참조 정리) | — (미지원) |
| Async | 비동기 큐 (snapshot 주의) | 비동기 큐 (snapshot 주의) | 비동기 큐 (snapshot 주의) | — (미지원) |
| Display | 표시 전 (읽기 전용) | 표시 전 (읽기 전용) | — (미지원) | — (미지원) |
Before vs After — current 의 의미 차이
Before BR 에서 current 는 DB 에 아직 기록되지 않은 상태의 레코드를 가리킨다. 이 시점에는 current.setValue('field', value) 만으로 값을 변경해도 DB 쓰기에 반영된다. current.update() 를 추가로 호출할 필요가 없다.
After BR 에서 current 는 DB 에 이미 저장된 레코드를 가리킨다. 이 단계에서 필드 값을 바꾸려면 current.setValue() 후 반드시 current.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 이 재실행된다. 이 사이클은 플랫폼이 최대 재귀 깊이에 도달해 오류를 발생시키거나 트랜잭션을 강제 종료할 때까지 반복된다.
콜 스택 시각화
왜 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 의 암묵적 실행 순서 의존도를 낮춤.