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

ServiceNow Reference Qualifier — Simple/Dynamic/Advanced 차이, cmdb_ci 트리 함정, 순환 참조

Reference Qualifier 3가지 모드의 정확한 차이, sys_reference_qual 재사용 패턴, cmdb_ci 같은 트리 구조에서의 부모-자식 필터링, 그리고 순환 참조·N+1·Now Experience 제약까지 정리.

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

개요

Reference Qualifier 는 reference 필드(레코드를 다른 테이블 레코드와 연결하는 필드)에서 선택 가능한 항목을 제한하는 필터 메커니즘이다. 3가지 모드 — Simple(정적 encoded query), Dynamic(javascript: prefix + Script Include 호출), Advanced(full JS 스크립트 직접 작성) — 는 유연성과 성능 사이의 트레이드오프가 분명히 다르다. 대부분의 케이스는 Simple 로 충분하고, current/parent 같은 컨텍스트 객체가 필요할 때만 Dynamic 이나 Advanced 를 선택하는 것이 원칙이다.

cmdb_ci 같은 트리 구조 테이블에서 부모-자식 관계를 qualifier 로 필터링할 때는 GlideRecord 재귀 호출이 쌓이는 N+1 쿼리 함정이 존재한다. 트리 깊이 N 에 비례해 DB 쿼리가 N+1 회 발생하며, qualifier 는 폼 필드 포커스 / 타이핑마다 재실행되는 핫 패스이므로 지연이 사용자 경험에 직접 노출된다.

Now Experience(UI Builder · Workspace 기반 UX)에서 dynamic qualifier 의 form context 의존(current.field 참조) 패턴은 일부 깨질 수 있다. 동일 qualifier 를 참조하는 reference 필드가 여럿 있으면 N×M 쿼리 폭주로 이어지고, A qualifier → B 참조 → B qualifier → A 참조 구조는 순환 참조 무한 루프를 만든다.

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


§1 — 3가지 모드 (Simple / Dynamic / Advanced)

Simple

단일 encoded query 문자열만 입력한다. active=true^company=javascript:current.company 같은 형태로 쿼리를 직접 적는다. 서버에서 별도 스크립트 실행 없이 쿼리만 적용하므로 성능이 가장 빠르다. current 참조 없이 정적 조건으로 필터링이 가능한 케이스라면 항상 Simple 을 우선해야 한다.

Dynamic

javascript: prefix 뒤에 Script Include 메서드 호출 표현식을 넣는다. javascript:new MyRefQuals().getActiveUsers() 형태가 대표적이다. 서버 사이드 JS 가 실행되므로 current, parent 같은 컨텍스트 객체를 활용해 현재 폼의 다른 필드 값을 기반으로 필터를 동적으로 생성할 수 있다. Script Include 를 분리하기 때문에 재사용·코드 리뷰·테스트가 Advanced 보다 용이하다.

Advanced

qualifier 필드에 JS 코드를 직접 작성한다. GlideRecord 나 GlideAggregate 쿼리를 qualifier 안에서 직접 구동하는 것이 가능해 가장 유연하다. 그러나 qualifier 는 폼 로드·필드 변경·타이핑마다 재실행되는 핫 패스이므로, Advanced 안에서 무거운 쿼리를 실행하면 폼 응답 지연이 바로 사용자에게 노출된다. 성능 함정이 3가지 모드 중 가장 크다.

Simple

정적 encoded query — 서버 스크립트 없음, 가장 빠름

컨텍스트 불필요할 때 항상 우선 선택

Dynamic

Script Include 호출 — current/parent 컨텍스트 활용 가능

재사용·테스트가 필요할 때, Advanced 전에 먼저 고려

Advanced

full JS 직접 작성 — GlideRecord 직접 구동, 가장 유연하나 성능 함정 최대

Simple/Dynamic 으로 해결 안 될 때만 최후 수단


§2 — Dynamic Qualifier javascript: 패턴

Script Include 분리 권장

Dynamic qualifier 에서 javascript: 뒤에 바로 로직을 쓰는 것보다 Script Include 메서드 하나를 호출하는 형태가 훨씬 낫다. 재사용, 코드 리뷰, 단위 테스트 모두 Script Include 분리 없이는 사실상 불가능하다.

// Reference Qualifier 필드에 입력
javascript:new CmdbRefQuals().getActiveServers()
// Script Include: CmdbRefQuals
var CmdbRefQuals = Class.create();
CmdbRefQuals.prototype = {
    initialize: function() {},

    // qualifier 반환값은 encoded query 문자열
    getActiveServers: function() {
        return 'sys_class_name=cmdb_ci_server^operational_status=1';
    },

    type: 'CmdbRefQuals'
};

반환값은 항상 encoded query 문자열이어야 한다. GlideRecord 객체를 반환하거나 빈 문자열을 반환하면 플랫폼이 qualifier 를 무시하거나 전체 테이블을 노출하는 등 예상치 못한 동작이 발생할 수 있다.

sys_reference_qual 재사용 패턴

sys_reference_qual 테이블은 qualifier 정의를 별도 레코드로 저장해 여러 필드에서 참조해 재사용하는 메커니즘을 제공한다. 인스턴스 구성과 버전에 따라 동작 방식에 차이가 있을 수 있으므로, 실제 재사용 전 본인 인스턴스에서 동작을 확인하는 것을 권장한다.

form context 의존성 함정

Dynamic qualifier 에서 current.field_name 을 참조하면 폼 컨텍스트에 의존한다. 폼에서 레코드를 열어둔 상태라면 current 는 현재 레코드를 정상적으로 가리키지만, 리스트 뷰에서 reference 필드를 직접 편집하거나 referrer 경로가 다른 경우 current 가 부재하거나 예상치 못한 값을 가질 수 있다.

Form context 있음폼에서 레코드 열기current = 현재 레코드qualifier 실행current.company 정상 참조필터 정상 적용company 기준 후보 목록 반환Form context 없음리스트 인라인 편집referrer 경로 상이qualifier 실행current 부재 또는 null필터 미적용 또는 오류전체 후보 노출 또는 빈 목록

이 함정을 피하려면 Script Include 안에서 current 를 참조하기 전에 null 체크를 추가하거나, context 에 의존하지 않는 형태로 qualifier 를 분리하는 것이 안전하다.


§3 — cmdb_ci 트리 패턴 (부모-자식 필터링)

parent 필드와 sys_class_path

cmdb_ci 는 Configuration Item(구성 항목)을 저장하는 테이블로, 계층적 트리 구조를 가진다. parent 필드가 상위 CI 를 참조하는 단일 단계 관계를 표현하고, sys_class_path 는 상속 계층 경로를 저장한다. qualifier 에서 특정 CI 의 자식만 표시하고 싶을 때, parent=<sys_id> 조건으로 한 단계 바로 아래 자식만 필터링하는 것은 간단하다. 그러나 다단계 ancestor(조상) 하위를 모두 포함하려면 재귀 탐색이 필요하고 여기서 N+1 함정이 시작된다. (sys_class_path 의 STARTSWITH 매칭 패턴은 CMDB Deep Dive #2 — Class Hierarchy 와 sys_class_path 에서 깊이 다룬다.)

GlideRecord 재귀 쿼리의 N+1 함정

아래는 직관적이지만 위험한 재귀 패턴이다.

// ⚠ N+1 함정 — qualifier 안 재귀 패턴
function getChildSysIds(parentId) {
    var ids = [];
    var gr = new GlideRecord('cmdb_ci');
    gr.addQuery('parent', parentId);
    gr.query();
    while (gr.next()) {
        ids.push(gr.getUniqueValue());
        // 각 자식에 대해 재귀 호출 → 트리 깊이만큼 추가 쿼리 발생
        ids = ids.concat(getChildSysIds(gr.getUniqueValue()));
    }
    return ids;
}

트리 깊이가 N 이면 이 함수는 최소 N+1 회 DB 쿼리를 실행한다. qualifier 는 사용자 타이핑 중에도 재실행될 수 있으므로, 한 번의 필드 포커스에 수십 번의 DB 쿼리가 폭발적으로 발생할 수 있다.

평면 lookup 대안: sys_class_path 를 활용해 단일 쿼리로 모든 하위 클래스를 한 번에 조회하거나, 관심 클래스 목록을 미리 정의해 sys_class_nameIN<list> 조건으로 flat 하게 조회하는 것이 훨씬 안전하다.

// 평면 lookup — 단일 쿼리로 처리
getActiveServersFlat: function() {
    // 대상 클래스를 미리 열거 — 재귀 없이 단일 쿼리
    return 'sys_class_nameINcmdb_ci_server,cmdb_ci_win_server,cmdb_ci_linux_server^operational_status=1';
},
cmdb_ci 트리 구조cmdb_ci (루트)cmdb_ci_servercmdb_ci_storagewin_serverlinux_serverqualifier 방식 비교재귀 GlideRecord트리 깊이 N → N+1 쿼리 폭주쿼리 1 → 쿼리 2 → …깊이마다 추가 DB 왕복평면 class list단일 IN 쿼리 — DB 왕복 1회성능 안전 — qualifier 핫 패스 적합

§4 — 순환 참조 + N+1 함정

순환 참조 시나리오

Reference Qualifier 에서 순환 참조는 생각보다 쉽게 발생한다. 대표적인 패턴은 다음과 같다.

  • 테이블 A 의 reference 필드 X 에 qualifier 가 붙어 있고, 그 qualifier 가 테이블 B 의 특정 레코드를 조회한다.
  • 테이블 B 의 reference 필드 Y 에 qualifier 가 붙어 있고, 그 qualifier 가 다시 테이블 A 의 current 값을 참조한다.

이 구조에서 사용자가 A 의 X 필드를 채우려 lookup 창을 열면 qualifier 가 B 를 조회하고, B qualifier 실행 중 A 의 current 컨텍스트를 다시 참조하는 루프가 발생할 수 있다. 인스턴스·버전·레코드 상태에 따라 무한 루프로 이어지거나 오류 없이 조용히 잘못된 결과를 반환하는 경우가 있으므로, 교차 참조 qualifier 구조를 설계할 때는 반드시 사이클 여부를 검토해야 한다.

폼에 reference 필드 다수 + 공유 qualifier → N×M 폭주

폼에 reference 필드가 여러 개 있고 모두 동일한 Script Include 메서드를 qualifier 로 참조하는 경우, 폼 로드 시 각 필드가 독립적으로 qualifier 를 호출한다. 필드 N 개 × qualifier 내 쿼리 M 개 = N×M 회 DB 쿼리가 폼 로드 한 번에 발생한다.

테이블 A필드 X — qualifier → B 조회테이블 B필드 Y — qualifier → A 참조A qualifier 가 B 를 조회B qualifier 가 A current 참조순환 참조 → 무한 루프 위험

workaround

  • 캐싱: Script Include 안에서 GlideSystem.putProperty 또는 세션 캐시 활용해 같은 qualifier 가 폼 로드 중 여러 번 호출될 때 결과를 재사용. 다만 세션 캐시 공유 방식은 인스턴스 구성에 따라 다를 수 있으므로 단정하기 어렵다.
  • 순환 감지 플래그: Script Include 안에 정적 변수나 실행 카운터를 두어 재진입을 막는 방어 코드 추가.
  • Script Include 분리 + 의존 관계 문서화: qualifier 가 어떤 테이블·필드를 참조하는지를 명시적으로 문서화해, 설계 단계에서 사이클을 발견할 수 있도록 한다.

§5 — Now Experience (UI Builder · Workspace) 제약

Now Experience 기반 UX(UI Builder 로 구성한 Workspace 화면 등)에서 dynamic qualifier 의 current.field 참조 패턴은 클래식 폼과 다르게 동작할 수 있다. Now Experience 컴포넌트 렌더링 방식에서는 폼 컨텍스트 제공 방식이 클래식 UI 와 다르고, current 객체가 예상한 값을 가지지 않거나 부재하는 케이스가 보고되고 있다. 다만 이는 인스턴스 버전·UI Builder 구성·Workspace 템플릿에 따라 차이가 있으므로, 본인 인스턴스에서 직접 검증하는 것을 권장한다.

우회 방법으로는 qualifier 에서 current 에 의존하는 대신 GlideAjax 와 client_callable Script Include 를 조합해 클라이언트에서 명시적으로 컨텍스트를 전달하는 패턴을 고려할 수 있다. Script Include 메서드가 파라미터를 받아 필터를 구성하면, 호출 측에서 필요한 컨텍스트 값을 명시적으로 넘겨줄 수 있어 form context 의존을 끊을 수 있다. client_callable 설정 필요 여부는 인스턴스 구성에 따라 다를 수 있으므로 별도 확인이 필요하다.


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

모드 선택 기준

  • 컨텍스트 불필요 → Simple 우선
  • current/parent 참조 필요 → Dynamic + Script Include
  • 복합 쿼리 필수 → Advanced (성능 검증 필수)

Script Include 분리

  • qualifier 로직은 Script Include 메서드로 분리 — 재사용·테스트 가능
  • 반환값은 항상 encoded query 문자열 확인
  • current null 체크 방어 코드 추가

cmdb 트리 쿼리

  • 재귀 GlideRecord 호출 회피 — N+1 쿼리 폭주
  • 대상 클래스 열거 후 단일 IN 쿼리로 처리
  • qualifier 는 핫 패스 — DB 쿼리 최소화 원칙

다수 reference 필드 캐싱

  • 같은 qualifier 를 공유하는 필드 N 개 → 폼 로드 시 N 회 호출
  • Script Include 안에서 결과 캐싱으로 중복 쿼리 방지
  • 교차 참조 qualifier 는 사이클 여부 설계 단계에서 확인

학습 허브

이 글은 ServiceNow CMDB 완전 정리 학습 허브의 실전 쿼리 편이다. CMDB 의 클래스 계층·관계·IRE 를 이해한 뒤 cmdb_ci 트리를 안전하게 필터링·쿼리하는 단계이며, 전체 경로를 허브에서 따라갈 수 있다.


참조