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

ServiceNow RESTMessageV2 아웃바운드 통합 심화 — SN 이 HTTP 클라이언트가 될 때의 설계·인증·async·에러·MID Server

sn_ws.RESTMessageV2/RESTResponseV2 로 외부 API 를 호출하는 발신자 관점 심화. 2인자 생성자(두 번째 인자는 HTTP verb 가 아닌 함수 레코드명)와 recordless 생성자 차이, setStringParameter XML 이스케이프 vs setStringParameterNoEscape JSON 비대칭, setHttpTimeout 밀리초 vs waitForResponse 초 단위 혼동, 아웃바운드 인증 setAuthenticationProfile·setBasicAuth·setMutualAuth 와 Connection Credential Alias 이관 안전, haveError 전송실패 vs getStatusCode HTTP 두 레이어, execute 동기 블로킹 semaphore와 30초 캡, executeAsync ECC Queue 비동기와 직후 waitForResponse 안티패턴, MID Server 아웃바운드 relay 까지. cycle 23 인바운드 대칭 짝.

· note integration scripting
검증 인스턴스: OOTB Australia · 검증일 2026-08-04

개요

흔한 오해 두 가지를 먼저 정리한다.

오해 (1) — “new sn_ws.RESTMessageV2(name, method) 의 두 번째 인자는 HTTP verb 다”

아니다. 두 번째 인자는 sys_rest_message_fn 테이블에 저장된 HTTP Method 함수 레코드의 이름이다. 대소문자를 구분한다. ‘GET’ 이 먹히는 것처럼 보이는 건, 관례상 함수명을 verb 와 동일하게 짓기 때문이다. 함수 레코드명이 ‘GetIncident’ 라면 'GetIncident' 를 그대로 넘겨야 한다.

오해 (2) — “executeAsync() 를 쓰면 비동기라 스레드를 블록하지 않는다”

아니다. executeAsync() 직후에 waitForResponse() / getBody() / getStatusCode() 를 호출하는 순간, initiating 스레드는 async 스레드의 완료를 대기한다. 이는 execute() 동기 호출과 사실상 동일하므로 async 이점이 소멸된다.

이 글은 Scripted REST API 심화(cycle 23)의 대칭 짝이다. 그 글은 inbound(외부→SN, SN=수신자·검증자·429 발행자)를 다뤘다. 이 글은 outbound(SN→외부, SN=발신자·자격증명 제시자·429 소비자)를 다룬다.

OOTB 권장은 Flow Designer + IntegrationHub 스포크 우선이다. RESTMessageV2 스크립팅은 복잡한 조건·반복 로직·대용량 고볼륨·스케줄 잡 직접 트리거일 때 유리하다. Flow REST Step 에러는 Flow Designer 실행 라이프사이클로, BR 트랜잭션은 BR 무한 루프로 위임한다.

OOTB(Out-of-the-Box, 기본 제공) Australia 릴리스 기준입니다. sn_ws.RESTMessageV2/RESTResponseV2 API 표면은 최근 릴리스 전반에 안정적이지만, 타임아웃 속성 기본값·OAuth grant type·MID Server 동작·business rule 명은 인스턴스 버전·플러그인에 따라 다를 수 있으니 대상 인스턴스에서 확인을 권장합니다.


§1 — 두 생성자와 요청 빌드

2인자 생성자 new sn_ws.RESTMessageV2('<REST Message명>', '<HTTP Method 함수명>')sys_rest_message + sys_rest_message_fn 레코드를 로드해 base endpoint·인증·헤더를 상속한다. 두 인자 모두 대소문자를 구분한다.

recordless 무인자 생성자 new sn_ws.RESTMessageV2() 는 레코드 없이 인라인으로 요청을 구성한다. setEndpoint(url)setHttpMethod(verb) 가 필수다. getRequestBody() 는 null 을 반환하는 것이 문서화된 제한이다(전송에는 영향 없음).

저장 테이블: sys_rest_message(정의)·sys_rest_message_fn(HTTP Method 함수)·sys_rest_message_fn_headers·sys_rest_message_fn_parameters. 첫 저장 시 default HTTP Method 함수가 자동 생성되는 경우가 일반적이다.

변수 치환 비대칭: setStringParameter(name, value) 는 선언된 변수를 치환하되 XML 예약문자를 이스케이프한다. JSON body 에 쓰면 앰퍼샌드·꺽쇠가 XML 엔티티로 변환돼 JSON 이 깨진다. JSON body 변수 치환은 반드시 setStringParameterNoEscape 다.

setQueryParameter(name, value) 는 사전 선언 없이 URL 에 name=value 를 append 한다 — setStringParameter 의 placeholder 치환과 혼동 주의.

Content-Type 은 자동 설정되지 않는다. JSON POST/PUT 은 setRequestHeader('Content-Type', 'application/json') 을 명시해야 한다. setRequestBody()setRequestBodyFromAttachment() 는 상호배타적이다.

// ✅ recordless 생성자 — 전체 흐름
var rm = new sn_ws.RESTMessageV2();
rm.setEndpoint('https://api.example.com/items');
rm.setHttpMethod('POST');
rm.setRequestHeader('Content-Type', 'application/json');
rm.setRequestHeader('Accept', 'application/json');

var payload = { name: 'Incident-001', priority: 1 };
rm.setRequestBody(JSON.stringify(payload));

var response = rm.execute();
gs.info('status: ' + response.getStatusCode());
// ⚠ JSON body 에 setStringParameter — XML 이스케이프로 JSON 깨짐
var rm = new sn_ws.RESTMessageV2('My REST Message', 'POST');
rm.setStringParameter('description', 'A & B');   // ⚠ "A &amp; B" 로 이스케이프됨
rm.setRequestBody('{"desc":"${description}"}');   // JSON 이 무효가 됨

// ✅ JSON body 변수 치환은 setStringParameterNoEscape
rm.setStringParameterNoEscape('description', 'A & B');  // 그대로 치환

§2 — 아웃바운드 인증: SN 이 자격증명 제시

inbound 에서 SN 은 WHO 를 검증하는 수신자였다. outbound 에서 SN 은 클라이언트로서 자격증명을 제시한다. 검증·ACL 인가 설계는 Scripted REST API 심화에 위임한다.

setAuthenticationProfile(type, profileId) 는 type 인자로 'basic' 또는 'oauth2' 를 받고, profileId 에 해당 auth profile 레코드의 sys_id 를 넘긴다. REST Message 레코드의 Auth Type 과 profileId 가 일치해야 올바르게 적용된다 — 불일치 시 동작은 인스턴스에서 확인을 권장한다.

setBasicAuth(userName, userPass) 는 스크립트에서 직접 자격증명을 지정해 레코드 설정을 override 한다. 비밀번호가 평문으로 노출되므로 하드코딩 안티패턴이다. 프로덕션 코드에서는 auth profile 또는 Connection Credential Alias 를 쓰는 것이 원칙이다.

OAuth 2.0: Application Registry 에서 “Connect to a third-party OAuth Provider” 로 등록하면 SN 이 OAuth 클라이언트가 된다. OAuth Entity Profile 의 grant type(Client Credentials — 서버 간, Authorization Code — 사용자 동의 등)을 설정한다. access token 만료 시 refresh token 으로 자동 갱신이 일반적이다. 단 refresh token 자체는 자동 갱신되지 않는다. 유효 기간은 provider·설정 의존이므로 만료 대비 재인증 또는 갱신 스케줄 잡을 직접 구성한다.

setMutualAuth(profileName) 은 mTLS/클라이언트 인증서 인증이다. Protocol Profile 과 Certificates 설정을 참조하며, TLS 핸드셰이크(소켓 레벨)에서 평가된다 — payload 전달 이전 단계다. Basic/OAuth 의 application 레벨 인증과 동작 층이 다르다. 중요한 함정: 인자가 sys_id 가 아닌 profile 이름(String) 이다. setAuthenticationProfile 이 sys_id 를 받는 것과 비대칭이다.

API key 는 별도 Auth Type 이 아니다. setRequestHeader('x-api-key', apiKey) 같은 커스텀 헤더로 처리한다.

메서드인자레이어자동 갱신하드코딩 위험
setBasicAuth(user, pass)문자열 직접 지정Application해당 없음⚠ 높음 — 안티패턴
setAuthenticationProfile(‘basic’, sysId)auth profile sys_idApplication해당 없음✅ 낮음
setAuthenticationProfile(‘oauth2’, sysId)OAuth profile sys_idApplicationaccess token ✅ / refresh token ⚠✅ 낮음
setMutualAuth(profileName)profile 이름(String) — sys_id 아님Socket(TLS)해당 없음✅ 낮음

§3 — Connection & Credential Alias: 이관 안전

Alias 는 endpoint·credential 을 앱 메타데이터에서 분리하는 논리 포인터다. 코드와 REST Message 레코드는 alias 만 참조하고, 런타임에 Active Connection 레코드를 조회해 실제 endpoint/credential 을 해석한다.

세 컴포넌트: Alias 레코드(sys_alias, sys_metadata 확장 → Update Set 캡처 가능), Connection 레코드(인스턴스별 개별 구성), 선택적 Credential 레코드. 기본 도메인당 Active Connection 하나.

환경 분리: Update Set 에는 Alias 만 이관, Connection/Credential 은 Dev·Test·Prod 인스턴스별 별도 구성. 코드는 alias 이름 상수만 참조하고, 런타임에 Active Connection 을 조회해 실제 endpoint/credential 을 해석한다.

오해 정정 (clone): “기본 preserver 가 Connection/Credential 을 보존한다”는 틀렸다. 실제는 Connection/Credential 이 clone 에서 기본 제외(미포함)돼 target 의 기존 값이 남는 것이다. 보존이 필요하다면 별도 Clone Data Preserver 를 명시 구성해야 한다.

setEndpoint(String) 으로 endpoint 를 스크립트에 하드코딩하는 대신 alias/connection 이 해석하게 두는 것이 이관 안전 설계다.

⚠ 안티패턴 — 하드코딩

endpoint URL 과 credential 을 스크립트에 직접 박음. 이관·clone 시 환경 분리 불가, 값 변경 시 코드 배포 필요.

✅ 권장 — Alias 참조

코드·REST Message 는 alias 이름 상수만 참조. Alias 만 Update Set 이관. Connection/Credential 은 인스턴스별 별도 구성. clone 동작은 Clone Data Preserver 설정 확인 필수.


§4 — 응답과 에러: haveError(전송) vs getStatusCode(HTTP) 두 레이어

execute() / executeAsync() 는 모두 sn_ws.RESTResponseV2 객체를 반환한다. 주요 읽기 메서드: getBody()(String), getStatusCode()(Number), getHeader(name), getAllHeaders()(deprecated getHeaders() 대신 권장), getErrorMessage(), getErrorCode(), haveError().

두 독립 레이어가 핵심이다.

haveError()전송·연결 레벨 실패를 Boolean 으로 표시한다. connection refused, 타임아웃, hostname 미해석, MID Server 오프라인 같은 사유다. 이때 getStatusCode()0 이다. 반대로 HTTP 400/404/500 은 서버가 응답을 돌려준 것이므로 haveError()false 이고 getStatusCode() 에 실제 HTTP 상태 코드가 담긴다. 두 레이어를 별도로 처리해야 한다.

getErrorCode() 는 HTTP 상태 코드가 아닌 SN 플랫폼 내부 코드다(예: 1 = socket timeout). 혼동 주의.

getBody() 가 에러 시 null 을 반환하는지는 버전에 따라 다를 수 있다. 에러 분기에서는 getBody() 에 의존하지 말고 getErrorMessage() / getErrorCode() 를 우선 사용한다.

429 소비자 대비: inbound 에서 SN 이 429 를 발행했다면(cycle 23 위임), outbound 에서 SN 은 외부 서버의 429 를 소비한다. RESTMessageV2 는 네이티브 자동 retry 기능을 제공하지 않는 것으로 알려져 있다 — 재시도·backoff 로직은 개발자가 직접 구현해야 한다. Retry-After 헤더는 getHeader('Retry-After') 로 읽는다.

대용량 응답은 saveResponseBodyAsAttachment() 로 첨부 저장을 고려한다. getBody() 가 매우 큰 payload 를 다룰 때 truncation 이 발생할 수 있다.

execute()try/catch 로 감싸야 함catch(e)예외 — 네트워크/플랫폼haveError() true전송실패 · status 0getStatusCode()HTTP 레이어 (2xx/4xx/5xx)haveError() false서버 응답 수신 성공
HTTP status 범위haveError()처리 방향
2xxfalsegetBody() 파싱, 성공 처리
4xxfalse요청 문제 — 로깅, 재시도 안 함
5xxfalse서버 문제 — 재시도 가능, backoff
0 (전송실패)truegetErrorMessage() / getErrorCode() 확인

§5 — 동기 execute() vs executeAsync()/ECC Queue

execute 동기 블로킹: 응답/타임아웃까지 트랜잭션 스레드를 블록하며, in-flight 요청 하나당 노드 semaphore 하나를 점유한다. 느린 API 를 다수 동기 호출하면 semaphore 풀이 고갈될 수 있다 — ServiceNow best-practice 경고 지점.

executeAsync 비동기: 별도 스레드, 응답 대기 없이 즉시 반환. 항상 ecc_queue 경유. ServiceNow 권장 방향은 async 선호다.

30초 캡: glide.http.outbound.max_timeout.enabled=true(기본)면 setHttpTimeout() 값과 30초 중 작은 값이 적용된다. 캡을 늘리려면 property 를 false 로 해야 하지만 공유 인스턴스에서는 비권장이다.

타임아웃 단위 혼동: setHttpTimeout(ms)밀리초, waitForResponse(timeoutSecs)다. 교환 불가. setHttpTimeout(60) 은 60ms(즉시 타임아웃). 30초는 setHttpTimeout(30000).

아웃바운드에는 sys_rate_limit_rules 429 발행이 적용되지 않는다 — inbound 전용. 아웃바운드 동시성은 thread/semaphore·ECC Queue 용량으로 통제된다. IntegrationHub 사용 시에만 licensing 기반 아웃바운드 quota 가 별도로 존재한다.

Async BR 로 outbound 위임 시 current 는 트리거 시점 snapshot 이다 — stale 참조 함정은 BR 무한 루프를 참고한다.

// ⚠ executeAsync 직후 waitForResponse — async 이점 소멸, 사실상 동기
var rm = new sn_ws.RESTMessageV2('My API', 'POST');
var response = rm.executeAsync();
response.waitForResponse(30);          // ⚠ initiating 스레드가 여기서 대기
var body = response.getBody();         // 동기 execute() 와 차이 없음

// ✅ fire-and-forget — 응답이 불필요할 때
var rm2 = new sn_ws.RESTMessageV2('Webhook', 'POST');
rm2.setStringParameterNoEscape('payload', JSON.stringify(data));
rm2.executeAsync();                    // 즉시 반환, 응답 수신 불필요
// 이후 코드는 외부 API 응답과 무관하게 계속 실행됨

execute() — 동기

  • 스레드 블록 — semaphore 점유
  • 30초 캡 (glide.http.outbound.max_timeout)
  • ECC Queue 미경유 (직접 연결)
  • 고볼륨·느린 API 에서 노드 contention

executeAsync() — 비동기

  • 별도 스레드 — 즉시 반환
  • 항상 ecc_queue 경유
  • 직후 waitForResponse() 호출 시 동기화 됨 — 이점 소멸
  • 진짜 non-blocking 은 fire-and-forget 또는 out-of-band

§6 — MID Server 경유 아웃바운드 relay

MID Server 개념·CMDB Discovery 측면은 CMDB IRE·Discovery·Reconciliation에 위임한다. 여기서는 outbound relay 각도만 다룬다.

MID Server 를 아웃바운드 relay 로 쓰는 주된 이유: 사내망(온프렘) 타깃 도달, 앱 노드 HTTP 부하 오프로드. 라우팅은 REST Message Function 의 “Use MID Server” 필드 또는 런타임 setMIDServer(String) 으로 지정한다. MID Server 는 REST capability 가 활성화돼 있어야 한다.

ECC Queue 왕복: MID Server 경유 시 요청은 ecc_queue 에 output probe(topic: RESTProbe)로 기록된다. MID Server 가 polling 해 실제 HTTP 호출을 수행하고, 응답은 ecc_queue input record 로 복귀한다. 직접 연결은 이 ECC Queue 왕복을 우회한다.

MID Server + OAuth 동시 사용은 컨텍스트에 따라 지원 여부가 달라질 수 있다 — IntegrationHub 경로에는 별도 설정이 존재하므로 대상 인스턴스에서 확인을 권장한다. cluster failover 동작도 인스턴스·버전에 따라 달라질 수 있다.

SN 인스턴스RESTMessageV2ecc_queueoutput probe RESTProbeMID Server사내망/온프렘온프렘 타깃방화벽 내부 API출력복귀HTTP

마무리

RESTMessageV2 아웃바운드 통합의 설계 체크리스트입니다.

요청·인증·이관

  • 2인자 생성자 두 번째 인자 = HTTP verb 아님, 함수 레코드명(대소문자 구분)
  • JSON body 변수 치환 = setStringParameterNoEscape
  • Content-Type 자동 설정 없음 — JSON 은 수동 setRequestHeader
  • setAuthenticationProfile(type, sys_id) — type: ‘basic’/‘oauth2’
  • setMutualAuth(profileName) — 인자는 sys_id 아닌 이름
  • OAuth refresh token 자동 갱신 안 됨 — 만료 대비 구성 필요
  • Alias 로 환경 분리, clone 은 Clone Data Preserver 별도 확인

에러·실행·MID

  • haveError() = 전송실패(status 0) / getStatusCode() = HTTP — 두 레이어 별도 처리
  • try/catch + haveError + status 3레이어 방어 필수
  • execute() = 스레드 블록·semaphore·30초 캡
  • executeAsync() 직후 waitForResponse() = 동기화됨, 이점 소멸
  • setHttpTimeout = ms / waitForResponse = 초 — 단위 교환 불가
  • 아웃바운드에 sys_rate_limit_rules 429 적용 없음 — inbound 전용
  • MID Server relay = ecc_queue(RESTProbe) 경유, 사내망 타깃 도달

관련 글: Scripted REST API 심화(inbound 대칭) · Flow Designer 실행 라이프사이클 · CMDB IRE·Discovery·Reconciliation · BR 무한 루프.

다음으로 어떤 방향이 궁금하신가요? IntegrationHub 스포크 vs RESTMessageV2 스크립팅 선택 기준, SOAPMessageV2 아웃바운드, 또는 webhook 인바운드 수신 패턴을 이어서 다룰 수 있습니다.