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 인바운드 대칭 짝.
개요
흔한 오해 두 가지를 먼저 정리한다.
오해 (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/RESTResponseV2API 표면은 최근 릴리스 전반에 안정적이지만, 타임아웃 속성 기본값·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 & 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_id | Application | 해당 없음 | ✅ 낮음 |
setAuthenticationProfile(‘oauth2’, sysId) | OAuth profile sys_id | Application | access 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 이 발생할 수 있다.
| HTTP status 범위 | haveError() | 처리 방향 |
|---|---|---|
| 2xx | false | getBody() 파싱, 성공 처리 |
| 4xx | false | 요청 문제 — 로깅, 재시도 안 함 |
| 5xx | false | 서버 문제 — 재시도 가능, backoff |
| 0 (전송실패) | true | getErrorMessage() / 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 동작도 인스턴스·버전에 따라 달라질 수 있다.
마무리
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_rules429 적용 없음 — inbound 전용 - MID Server relay = ecc_queue(RESTProbe) 경유, 사내망 타깃 도달
관련 글: Scripted REST API 심화(inbound 대칭) · Flow Designer 실행 라이프사이클 · CMDB IRE·Discovery·Reconciliation · BR 무한 루프.
다음으로 어떤 방향이 궁금하신가요? IntegrationHub 스포크 vs RESTMessageV2 스크립팅 선택 기준, SOAPMessageV2 아웃바운드, 또는 webhook 인바운드 수신 패턴을 이어서 다룰 수 있습니다.